diff --git a/.github/.patch_files b/.github/.patch_files index cf0b1787..346d63b3 100644 --- a/.github/.patch_files +++ b/.github/.patch_files @@ -1,14 +1,18 @@ .github/.patch_files -.github/.syncignore -.github/CODEOWNERS -.github/dependabot.yml .github/labels.yml -.github/workflows/check-pr-labels.yml +.github/CODEOWNERS +.github/workflows +.github/workflows/approve-bot-pr.yml .github/workflows/codeql-analysis.yml -.github/workflows/label-pr.yml .github/workflows/lint.yml -.github/workflows/synchronize-labels.yml +.github/workflows/update-github-config.yml +.github/workflows/create-draft-release.yml .github/workflows/test-pull-request.yml +.github/workflows/lint-yaml.yml +.github/workflows/synchronize-labels.yml +.github/workflows/label-pr.yml +.github/.syncignore +.github/dependabot.yml .gitignore LICENSE NOTICE diff --git a/.github/.syncignore b/.github/.syncignore new file mode 100644 index 00000000..7c0127a9 --- /dev/null +++ b/.github/.syncignore @@ -0,0 +1 @@ +CODEOWNERS diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d921d0ff..bf49a9d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ +--- version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily - open-pull-requests-limit: 10 diff --git a/.github/labels.yml b/.github/labels.yml index bace6e20..9a230526 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -16,3 +16,27 @@ - name: documentation description: This issue relates to writing documentation color: D4C5F9 +- name: help wanted + description: Extra attention is needed + color: 008672 +- name: semver:major + description: A change requiring a major version bump + color: 6b230e +- name: semver:minor + description: A change requiring a minor version bump + color: cc6749 +- name: semver:patch + description: A change requiring a patch version bump + color: f9d0c4 +- name: good first issue + description: A good first issue to get started with + color: d3fc03 +- name: "failure:release" + description: An issue filed automatically when a release workflow run fails + color: f00a0a +- name: "failure:update-github-config" + description: An issue filed automatically when a github config update workflow run fails + color: f00a0a +- name: "failure:approve-bot-pr" + description: An issue filed automatically when a PR auto-approve workflow run fails + color: f00a0a diff --git a/.github/workflows/approve-bot-pr.yml b/.github/workflows/approve-bot-pr.yml new file mode 100644 index 00000000..3f45a3d7 --- /dev/null +++ b/.github/workflows/approve-bot-pr.yml @@ -0,0 +1,88 @@ +name: Approve Bot PRs and Enable Auto-Merge + +on: + workflow_run: + workflows: ["Test Pull Request"] + types: + - completed + +jobs: + download: + name: Download PR Artifact + if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-22.04 + outputs: + pr-author: ${{ steps.pr-data.outputs.author }} + pr-number: ${{ steps.pr-data.outputs.number }} + steps: + - name: 'Download artifact' + uses: paketo-buildpacks/github-config/actions/pull-request/download-artifact@main + with: + name: "event-payload" + repo: ${{ github.repository }} + run_id: ${{ github.event.workflow_run.id }} + workspace: "/github/workspace" + token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} + - id: pr-data + run: | + echo "author=$(cat event.json | jq -r '.pull_request.user.login')" >> "$GITHUB_OUTPUT" + echo "number=$(cat event.json | jq -r '.pull_request.number')" >> "$GITHUB_OUTPUT" + + approve: + name: Approve Bot PRs + needs: download + if: ${{ needs.download.outputs.pr-author == 'paketo-bot' || needs.download.outputs.pr-author == 'dependabot[bot]' }} + runs-on: ubuntu-22.04 + steps: + - name: Check Commit Verification + id: unverified-commits + uses: paketo-buildpacks/github-config/actions/pull-request/check-unverified-commits@main + with: + token: ${{ secrets.PAKETO_BOT_REVIEWER_GITHUB_TOKEN }} + repo: ${{ github.repository }} + number: ${{ needs.download.outputs.pr-number }} + + - name: Check for Human Commits + id: human-commits + uses: paketo-buildpacks/github-config/actions/pull-request/check-human-commits@main + with: + token: ${{ secrets.PAKETO_BOT_REVIEWER_GITHUB_TOKEN }} + repo: ${{ github.repository }} + number: ${{ needs.download.outputs.pr-number }} + + - name: Checkout + if: steps.human-commits.outputs.human_commits == 'false' && steps.unverified-commits.outputs.unverified_commits == 'false' + uses: actions/checkout@v3 + + - name: Approve + if: steps.human-commits.outputs.human_commits == 'false' && steps.unverified-commits.outputs.unverified_commits == 'false' + uses: paketo-buildpacks/github-config/actions/pull-request/approve@main + with: + token: ${{ secrets.PAKETO_BOT_REVIEWER_GITHUB_TOKEN }} + number: ${{ needs.download.outputs.pr-number }} + + - name: Enable Auto-Merge + if: steps.human-commits.outputs.human_commits == 'false' && steps.unverified-commits.outputs.unverified_commits == 'false' + run: | + gh pr merge ${{ needs.download.outputs.pr-number }} --auto --rebase + env: + GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} + + failure: + name: Alert on Failure + runs-on: ubuntu-22.04 + needs: [download, approve] + if: ${{ always() && needs.download.result == 'failure' || needs.approve.result == 'failure' }} + steps: + - name: File Failure Alert Issue + uses: paketo-buildpacks/github-config/actions/issue/file@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + repo: ${{ github.repository }} + label: "failure:approve-bot-pr" + comment_if_exists: true + issue_title: "Failure: Approve bot PR workflow" + issue_body: | + Approve bot PR workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). + comment_body: | + Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} diff --git a/.github/workflows/check-pr-labels.yml b/.github/workflows/check-pr-labels.yml deleted file mode 100644 index c4f10eb1..00000000 --- a/.github/workflows/check-pr-labels.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Validate PR Labels -on: - pull_request: - branches: - - main - types: - - synchronize - - opened - - reopened - - labeled - - unlabeled - -concurrency: pr_labels - -jobs: - semver: - name: Ensure Minimal Semver Labels - runs-on: ubuntu-latest - steps: - - uses: mheap/github-action-required-labels@v1 - with: - count: 1 - labels: semver:major, semver:minor, semver:patch - mode: exactly diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index acff4add..f7c35e62 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,16 +2,20 @@ name: "CodeQL" on: push: - branches: [ main ] + branches: + - main + - v* pull_request: - branches: [ main ] + branches: + - main + - v* schedule: - - cron: '0 0 * * *' # Once a day at midnight + - cron: '24 18 * * *' # daily at 18:24 UTC jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -21,15 +25,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-draft-release.yml similarity index 51% rename from .github/workflows/create-release.yml rename to .github/workflows/create-draft-release.yml index 74d1ffec..07b903cc 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-draft-release.yml @@ -1,9 +1,13 @@ -name: Create Release +name: Create or Update Draft Release on: push: branches: - main + - v* + - '!v*-*' + repository_dispatch: + types: [ version-bump ] workflow_dispatch: inputs: version: @@ -15,29 +19,30 @@ concurrency: release jobs: unit: name: Unit Tests - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Setup Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v3 with: - go-version: 1.16 + go-version: 'stable' - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Run Unit Tests - run: go test -v -count=1 ./... + run: ./scripts/unit.sh release: name: Release - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: unit steps: - name: Setup Go - uses: actions/setup-go@v2.1.3 + uses: actions/setup-go@v3 with: - go-version: 1.16.x + go-version: 'stable' - name: Checkout - uses: actions/checkout@v2 - - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true + uses: actions/checkout@v3 + with: + fetch-tags: true - name: Reset Draft Release id: reset uses: paketo-buildpacks/github-config/actions/release/reset-draft@main @@ -51,6 +56,7 @@ jobs: with: repo: ${{ github.repository }} token: ${{ github.token }} + ref-name: ${{ github.ref_name }} - name: Set Release Tag id: tag run: | @@ -58,8 +64,8 @@ jobs: if [ -z "${tag}" ]; then tag="${{ steps.semver.outputs.tag }}" fi - echo "::set-output name=tag::${tag}" - - name: Create Draft Release + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + - name: Create Release uses: paketo-buildpacks/github-config/actions/release/create@main with: repo: ${{ github.repository }} @@ -68,3 +74,22 @@ jobs: target_commitish: ${{ github.sha }} name: v${{ steps.tag.outputs.tag }} draft: true + + failure: + name: Alert on Failure + runs-on: ubuntu-22.04 + needs: [ unit, release ] + if: ${{ always() && needs.unit.result == 'failure' || needs.release.result == 'failure' }} + steps: + - name: File Failure Alert Issue + uses: paketo-buildpacks/github-config/actions/issue/file@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + repo: ${{ github.repository }} + label: "failure:release" + comment_if_exists: true + issue_title: "Failure: Create Draft Release workflow" + issue_body: | + Create Draft Release workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). + comment_body: | + Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index a1767af1..36472c6d 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -1,17 +1,34 @@ -name: Auto-label PR +name: Set / Validate PR Labels on: - pull_request: + pull_request_target: branches: - main + - v* + types: + - synchronize + - opened + - reopened + - labeled + - unlabeled -concurrency: pr_labels +concurrency: pr_labels_${{ github.event.number }} jobs: - semver-label: - name: Semver Auto-Label - runs-on: ubuntu-latest + autolabel: + name: Ensure Minimal Semver Labels + runs-on: ubuntu-22.04 steps: + - name: Check Minimal Semver Labels + uses: mheap/github-action-required-labels@v3 + with: + count: 1 + labels: semver:major, semver:minor, semver:patch + mode: exactly + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Auto-label Semver + if: ${{ failure() }} uses: paketo-buildpacks/github-config/actions/pull-request/auto-semver-label@main env: GITHUB_TOKEN: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/lint-yaml.yml b/.github/workflows/lint-yaml.yml new file mode 100644 index 00000000..e6f4a8b1 --- /dev/null +++ b/.github/workflows/lint-yaml.yml @@ -0,0 +1,30 @@ +name: Lint Workflows + +on: + pull_request: + paths: + - '.github/**.yml' + - '.github/**.yaml' + +jobs: + lintYaml: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + + - name: Checkout github-config + uses: actions/checkout@v3 + with: + repository: paketo-buildpacks/github-config + path: github-config + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Install yamllint + run: pip install yamllint + + - name: Lint YAML files + run: yamllint ./.github -c github-config/.github/.yamllint diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 49e27383..c0a16278 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,17 +4,27 @@ on: push: branches: - main + - v* pull_request: branches: - main + - v* jobs: golangci: name: lint - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 - - name: golangci-lint - uses: golangci/golangci-lint-action@v2.3.0 - with: - version: v1.32.2 + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: 'stable' + + - name: Checkout + uses: actions/checkout@v3 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + args: --timeout 3m0s diff --git a/.github/workflows/synchronize-labels.yml b/.github/workflows/synchronize-labels.yml index 1c04394a..0f8c59b2 100644 --- a/.github/workflows/synchronize-labels.yml +++ b/.github/workflows/synchronize-labels.yml @@ -1,17 +1,21 @@ name: Synchronize Labels -"on": - push: - branches: - - main - paths: - - .github/labels.yml + +on: + push: + branches: + - main + - v* + paths: + - .github/labels.yml + workflow_dispatch: {} + jobs: - synchronize: - name: Synchronize Labels - runs-on: - - ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: micnncim/action-label-syncer@v1 - env: - GITHUB_TOKEN: ${{ github.token }} + synchronize: + name: Synchronize Labels + runs-on: + - ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: micnncim/action-label-syncer@v1 + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index ffa9bbea..f9e9877e 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -4,17 +4,30 @@ on: pull_request: branches: - main + - v* jobs: unit: name: Unit Tests - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Setup Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v3 with: - go-version: 1.16 + go-version: 'stable' + - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - name: Run Unit Tests - run: go test -v -count=1 ./... + run: ./scripts/unit.sh + + upload: + name: Upload Workflow Event Payload + runs-on: ubuntu-22.04 + steps: + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: event-payload + path: ${{ github.event_path }} diff --git a/.github/workflows/update-github-config.yml b/.github/workflows/update-github-config.yml new file mode 100644 index 00000000..8c100eb1 --- /dev/null +++ b/.github/workflows/update-github-config.yml @@ -0,0 +1,82 @@ +name: Update shared github-config + +on: + schedule: + - cron: '16 19 * * *' # daily at 19:16 UTC + workflow_dispatch: {} + +concurrency: github_config_update + +jobs: + build: + name: Create PR to update shared files + runs-on: ubuntu-22.04 + steps: + + - name: Checkout + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} + + - name: Checkout github-config + uses: actions/checkout@v3 + with: + repository: paketo-buildpacks/github-config + path: github-config + + - name: Checkout Branch + uses: paketo-buildpacks/github-config/actions/pull-request/checkout-branch@main + with: + branch: automation/github-config/update + + - name: Run the sync action + uses: paketo-buildpacks/github-config/actions/sync@main + with: + workspace: /github/workspace + config: /github/workspace/github-config/library + + - name: Cleanup + run: rm -rf github-config + + - name: Commit + id: commit + uses: paketo-buildpacks/github-config/actions/pull-request/create-commit@main + with: + message: "Updating github-config" + pathspec: "." + keyid: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY_ID }} + key: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY }} + + - name: Push Branch + if: ${{ steps.commit.outputs.commit_sha != '' }} + uses: paketo-buildpacks/github-config/actions/pull-request/push-branch@main + with: + branch: automation/github-config/update + + - name: Open Pull Request + if: ${{ steps.commit.outputs.commit_sha != '' }} + uses: paketo-buildpacks/github-config/actions/pull-request/open@main + with: + token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} + title: "Updates github-config" + branch: automation/github-config/update + base: ${{ github.event.repository.default_branch }} + + failure: + name: Alert on Failure + runs-on: ubuntu-22.04 + needs: [build] + if: ${{ always() && needs.build.result == 'failure' }} + steps: + - name: File Failure Alert Issue + uses: paketo-buildpacks/github-config/actions/issue/file@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + repo: ${{ github.repository }} + label: "failure:update-github-config" + comment_if_exists: true + issue_title: "Failure: Update GitHub config workflow" + issue_body: | + Update GitHub config workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). + comment_body: | + Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} diff --git a/.github/workflows/update-go-mod-version.yml b/.github/workflows/update-go-mod-version.yml new file mode 100644 index 00000000..6bbfc5ef --- /dev/null +++ b/.github/workflows/update-go-mod-version.yml @@ -0,0 +1,93 @@ +name: Update Go version + +on: + schedule: + - cron: '19 3 * * MON' # every monday at 3:19 UTC + workflow_dispatch: + +concurrency: update-go + +jobs: + update-go: + name: Update go toolchain in go.mod + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Checkout PR Branch + uses: paketo-buildpacks/github-config/actions/pull-request/checkout-branch@main + with: + branch: automation/go-mod-update/update-main + - name: Setup Go + id: setup-go + uses: actions/setup-go@v5 + with: + go-version: 'stable' + - name: Get current go toolchain version + id: current-go-version + uses: paketo-buildpacks/github-config/actions/update-go-mod-version@main + with: + go-version: ${{ steps.setup-go.outputs.go-version }} + - name: Go mod tidy + run: | + #!/usr/bin/env bash + set -euo pipefail + shopt -s inherit_errexit + + echo "Before running go mod tidy" + echo "head -n10 go.mod " + head -n10 go.mod + + echo "git diff" + git diff + + echo "Running go mod tidy" + go mod tidy + + echo "After running go mod tidy" + echo "head -n10 go.mod " + head -n10 go.mod + + echo "git diff" + git diff + - name: Commit + id: commit + uses: paketo-buildpacks/github-config/actions/pull-request/create-commit@main + with: + message: "Updates go mod version to ${{ steps.setup-go.outputs.go-version }}" + pathspec: "." + keyid: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY_ID }} + key: ${{ secrets.PAKETO_BOT_GPG_SIGNING_KEY }} + + - name: Push Branch + if: ${{ steps.commit.outputs.commit_sha != '' }} + uses: paketo-buildpacks/github-config/actions/pull-request/push-branch@main + with: + branch: automation/go-mod-update/update-main + + - name: Open Pull Request + if: ${{ steps.commit.outputs.commit_sha != '' }} + uses: paketo-buildpacks/github-config/actions/pull-request/open@main + with: + token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} + title: "Updates go mod version to ${{ steps.setup-go.outputs.go-version }}" + branch: automation/go-mod-update/update-main + + failure: + name: Alert on Failure + runs-on: ubuntu-22.04 + needs: [update-go] + if: ${{ always() && needs.update-go.result == 'failure' }} + steps: + - name: File Failure Alert Issue + uses: paketo-buildpacks/github-config/actions/issue/file@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + repo: ${{ github.repository }} + label: "failure:update-go-version" + comment_if_exists: true + issue_title: "Failure: Update Go Mod Version workflow" + issue_body: | + Update Go Mod Version workflow [failed](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}). + comment_body: | + Another failure occurred: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} diff --git a/.gitignore b/.gitignore index ec05de3b..0bfc21a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store .idea coverage.out +**/*.tar diff --git a/README.md b/README.md index 7995c4a5..55eb6927 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # packit -[![GoDoc](https://img.shields.io/badge/pkg.go.dev-doc-blue)](http://pkg.go.dev/github.com/paketo-buildpacks/packit) +[![GoDoc](https://img.shields.io/badge/pkg.go.dev-doc-blue)](http://pkg.go.dev/github.com/paketo-buildpacks/packit/v2) Package packit provides primitives for implementing a Cloud Native Buildpack according to the specification: @@ -29,7 +29,7 @@ import ( "os" "path/filepath" - "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/v2" ) func main() { @@ -103,7 +103,7 @@ application source code. ```go package main -import "github.com/paketo-buildpacks/packit" +import "github.com/paketo-buildpacks/packit/v2" func main() { // The build phase includes the yarn cli in a new layer that is made @@ -171,7 +171,7 @@ example that combines a simple detect and build into a single main program. ```go package main -import "github.com/paketo-buildpacks/packit" +import "github.com/paketo-buildpacks/packit/v2" func main() { detect := func(context packit.DetectContext) (packit.DetectResult, error) { @@ -213,17 +213,19 @@ the types and functions declared herein. * [matchers](./matchers) +* [paketosbom](./paketosbom): Package paketosbom implements a standardized SBoM format that can be used in Paketo Buildpacks. + * [pexec](./pexec): Package pexec provides a mechanism for invoking a program executable with a varying set of arguments. * [postal](./postal): Package postal provides a service for resolving and installing dependencies for a buildpack. -* [scribe](./scribe) +* [sbom](./sbom): Package sbom implements standardized SBoM tooling that allows multiple SBoM formats to be generated from the same scanning information. -* [vacation](./vacation): Package vacation provides a set of functions that enable input stream decompression logic from several popular decompression formats. +* [scribe](./scribe): Package scribe provides a set of interfaces to allow buildpack authors to control their logs on varying levels of granularity. -## `jam` CLI +* [servicebindings](./servicebindings): Package servicebindings provides a service for inspecting and retrieving data from service binding. -The `jam` CLI has been moved into its [own dedicated repository](https://github.com/paketo-buildpacks/jam). For new `jam` releases, please visit the `jam` repositories [release page](https://github.com/paketo-buildpacks/jam/releases). +* [vacation](./vacation): Package vacation provides a set of functions that enable input stream decompression logic from several popular decompression formats. --- Readme created from Go doc with [goreadme](https://github.com/posener/goreadme) diff --git a/build.go b/build.go index d1f7ffac..aafb9079 100644 --- a/build.go +++ b/build.go @@ -3,14 +3,17 @@ package packit import ( "errors" "fmt" + "math" "os" "path/filepath" "sort" "strings" + "github.com/paketo-buildpacks/packit/v2/fs" + "github.com/BurntSushi/toml" "github.com/Masterminds/semver/v3" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" ) // BuildFunc is the definition of a callback that can be invoked when the Build @@ -94,18 +97,17 @@ func Build(f BuildFunc, options ...Option) { config = option(config) } - var ( - layersPath = config.args[1] - platformPath = config.args[2] - planPath = config.args[3] - ) - pwd, err := os.Getwd() if err != nil { config.exitHandler.Error(err) return } + planPath, ok := os.LookupEnv("CNB_BP_PLAN_PATH") + if !ok { + planPath = config.args[3] + } + var plan BuildpackPlan _, err = toml.DecodeFile(planPath, &plan) if err != nil { @@ -118,6 +120,16 @@ func Build(f BuildFunc, options ...Option) { cnbPath = filepath.Clean(strings.TrimSuffix(config.args[0], filepath.Join("bin", "build"))) } + layersPath, ok := os.LookupEnv("CNB_LAYERS_DIR") + if !ok { + layersPath = config.args[1] + } + + platformPath, ok := os.LookupEnv("CNB_PLATFORM_DIR") + if !ok { + platformPath = config.args[2] + } + var buildpackInfo struct { APIVersion string `toml:"api"` Buildpack BuildpackInfo `toml:"buildpack"` @@ -131,6 +143,8 @@ func Build(f BuildFunc, options ...Option) { apiV05, _ := semver.NewVersion("0.5") apiV06, _ := semver.NewVersion("0.6") + apiV08, _ := semver.NewVersion("0.8") + apiV09, _ := semver.NewVersion("0.9") apiVersion, err := semver.NewVersion(buildpackInfo.APIVersion) if err != nil { config.exitHandler.Error(err) @@ -233,6 +247,30 @@ func Build(f BuildFunc, options ...Option) { return } } + + if (apiVersion.GreaterThan(apiV05) || apiVersion.Equal(apiV05)) && len(layer.ExecD) > 0 { + execdDir := filepath.Join(layer.Path, "exec.d") + err = os.MkdirAll(execdDir, os.ModePerm) + if err != nil { + config.exitHandler.Error(err) + return + } + + lexicalWidth := 1 + int(math.Log10(float64(len(layer.ExecD)))) + + for i, exe := range layer.ExecD { + err = fs.Copy(exe, filepath.Join(execdDir, fmt.Sprintf("%0*d-%s", lexicalWidth, i, filepath.Base(exe)))) + + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = fmt.Errorf("file %s does not exist. Be sure to include it in the buildpack.toml", exe) + } + + config.exitHandler.Error(err) + return + } + } + } } if !result.Launch.isEmpty() { @@ -247,13 +285,27 @@ func Build(f BuildFunc, options ...Option) { } var launch struct { - Processes []Process `toml:"processes"` - Slices []Slice `toml:"slices"` - Labels []label `toml:"labels"` - BOM []BOMEntry `toml:"bom"` + Processes []Process `toml:"processes,omitempty"` + DirectProcesses []DirectProcess `toml:"processes,omitempty"` + Slices []Slice `toml:"slices"` + Labels []label `toml:"labels"` + BOM []BOMEntry `toml:"bom"` + } + + if apiVersion.LessThan(apiV09) { + if result.Launch.DirectProcesses != nil { + config.exitHandler.Error(errors.New("direct processes can only be used with Buildpack API v0.9 or higher")) + return + } + launch.Processes = result.Launch.Processes + } else { + if result.Launch.Processes != nil { + config.exitHandler.Error(errors.New("non direct processes can only be used with Buildpack API v0.8 or lower")) + return + } + launch.DirectProcesses = result.Launch.DirectProcesses } - launch.Processes = result.Launch.Processes if apiVersion.LessThan(apiV06) { for _, process := range launch.Processes { if process.Default { @@ -263,6 +315,15 @@ func Build(f BuildFunc, options ...Option) { } } + if apiVersion.LessThan(apiV08) { + for _, process := range launch.Processes { + if process.WorkingDirectory != "" { + config.exitHandler.Error(errors.New("processes can only have a specific working directory with Buildpack API v0.8 or higher")) + return + } + } + } + launch.Slices = result.Launch.Slices launch.BOM = result.Launch.BOM if len(result.Launch.Labels) > 0 { diff --git a/build_test.go b/build_test.go index cca3f3ad..a6326697 100644 --- a/build_test.go +++ b/build_test.go @@ -9,12 +9,12 @@ import ( "testing" "testing/iotest" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/fakes" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/fakes" "github.com/sclevine/spec" . "github.com/onsi/gomega" - . "github.com/paketo-buildpacks/packit/matchers" + . "github.com/paketo-buildpacks/packit/v2/matchers" ) func testBuild(t *testing.T, context spec.G, it spec.S) { @@ -27,7 +27,6 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { layersDir string planPath string cnbDir string - envCnbDir string binaryPath string exitHandler *fakes.ExitHandler ) @@ -70,11 +69,8 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { cnbDir, err = os.MkdirTemp("", "cnb") Expect(err).NotTo(HaveOccurred()) - envCnbDir, err = os.MkdirTemp("", "envCnb") - Expect(err).NotTo(HaveOccurred()) - bpTOML := []byte(` -api = "0.7" +api = "0.8" [buildpack] id = "some-id" name = "some-name" @@ -90,7 +86,6 @@ api = "0.7" uri = "some-license-uri" `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) binaryPath = filepath.Join(cnbDir, "bin", "build") @@ -168,8 +163,6 @@ api = "0.4" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - }) it("updates the buildpack plan.toml with any changes", func() { @@ -216,7 +209,7 @@ api = "0.4" return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", Build: true, @@ -263,7 +256,7 @@ api = "0.5" return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", Build: true, @@ -299,7 +292,7 @@ cache = true return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", SBOM: packit.SBOMFormats{ @@ -337,7 +330,6 @@ api = "0.6" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) }) it("throws an error", func() { @@ -347,7 +339,7 @@ api = "0.6" return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: layerPath, Name: "some-layer", SBOM: packit.SBOMFormats{ @@ -461,7 +453,7 @@ api = "0.6" context("when the CNB_BUILDPACK_DIR environment variable is set", func() { it.Before(func() { - os.Setenv("CNB_BUILDPACK_DIR", envCnbDir) + os.Setenv("CNB_BUILDPACK_DIR", cnbDir) }) it.After(func() { @@ -475,10 +467,126 @@ api = "0.6" context = ctx return packit.BuildResult{}, nil - }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) + }, packit.WithArgs([]string{"env-var-override", layersDir, platformDir, planPath})) + + Expect(context).To(Equal(packit.BuildContext{ + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + Stack: "some-stack", + WorkingDir: tmpDir, + Plan: packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "some-entry", + Metadata: map[string]interface{}{ + "version": "some-version", + "some-key": "some-value", + }, + }, + }, + }, + Layers: packit.Layers{ + Path: layersDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + Homepage: "some-homepage", + Description: "some-description", + Keywords: []string{"some-keyword"}, + SBOMFormats: []string{"some-sbom-format", "some-other-sbom-format"}, + Licenses: []packit.BuildpackInfoLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + })) + }) + }) + + context("when the CNB_LAYERS_DIR environment variable is set", func() { + it.Before(func() { + os.Setenv("CNB_LAYERS_DIR", layersDir) + }) + + it.After(func() { + os.Unsetenv("CNB_LAYERS_DIR") + }) + + it("sets the correct value for layers dir in the Build context", func() { + var context packit.BuildContext + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + context = ctx + + return packit.BuildResult{}, nil + }, packit.WithArgs([]string{binaryPath, "env-var-override", platformDir, planPath})) + + Expect(context).To(Equal(packit.BuildContext{ + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + Stack: "some-stack", + WorkingDir: tmpDir, + Plan: packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "some-entry", + Metadata: map[string]interface{}{ + "version": "some-version", + "some-key": "some-value", + }, + }, + }, + }, + Layers: packit.Layers{ + Path: layersDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + Homepage: "some-homepage", + Description: "some-description", + Keywords: []string{"some-keyword"}, + SBOMFormats: []string{"some-sbom-format", "some-other-sbom-format"}, + Licenses: []packit.BuildpackInfoLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + })) + }) + }) + + context("when the CNB_PLATFORM_DIR environment variable is set", func() { + it.Before(func() { + os.Setenv("CNB_PLATFORM_DIR", platformDir) + }) + + it.After(func() { + os.Unsetenv("CNB_PLATFORM_DIR") + }) + + it("sets the correct value for platform dir in the Build context", func() { + var context packit.BuildContext + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + context = ctx + + return packit.BuildResult{}, nil + }, packit.WithArgs([]string{binaryPath, layersDir, "env-var-override", planPath})) Expect(context).To(Equal(packit.BuildContext{ - CNBPath: envCnbDir, + CNBPath: cnbDir, Platform: packit.Platform{ Path: platformDir, }, @@ -517,6 +625,76 @@ api = "0.6" }) }) + context("when the CNB_BP_PLAN_PATH environment variable is set", func() { + it.Before(func() { + os.Setenv("CNB_BP_PLAN_PATH", planPath) + }) + + it.After(func() { + os.Unsetenv("CNB_BP_PLAN_PATH") + }) + + it("sets the correct value for platform dir in the Build context", func() { + var context packit.BuildContext + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + context = ctx + + return packit.BuildResult{}, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, "env-var-override"})) + + Expect(context).To(Equal(packit.BuildContext{ + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + Stack: "some-stack", + WorkingDir: tmpDir, + Plan: packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "some-entry", + Metadata: map[string]interface{}{ + "version": "some-version", + "some-key": "some-value", + }, + }, + }, + }, + Layers: packit.Layers{ + Path: layersDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + Homepage: "some-homepage", + Description: "some-description", + Keywords: []string{"some-keyword"}, + SBOMFormats: []string{"some-sbom-format", "some-other-sbom-format"}, + Licenses: []packit.BuildpackInfoLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + })) + + contents, err := os.ReadFile(planPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` +[[entries]] + name = "some-entry" + +[entries.metadata] + version = "some-version" + some-key = "some-value" +`)) + }) + }) + context("when there are sbom entries in the build metadata", func() { it("writes them to their specified locations", func() { packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { @@ -556,7 +734,6 @@ api = "0.6" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) }) it("throws an error", func() { @@ -621,7 +798,6 @@ api = "0.4" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) }) it("throws an error", func() { @@ -685,8 +861,6 @@ api = "0.4" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - }) it("throws an error", func() { @@ -749,7 +923,6 @@ api = "0.6" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) }) it("throws an error", func() { @@ -865,6 +1038,38 @@ api = "0.6" }) }) + context("when the process specifies a working directory", func() { + it("persists a launch.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + Processes: []packit.Process{ + { + Type: "some-type", + Command: "some-command", + Args: []string{"some-arg"}, + Direct: true, + WorkingDirectory: "some-working-dir", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) + + contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[processes]] + type = "some-type" + command = "some-command" + args = ["some-arg"] + direct = true + working-directory = "some-working-dir" + `)) + }) + }) + context("when the api version is less than 0.6", func() { it.Before(func() { Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` @@ -897,6 +1102,174 @@ api = "0.5" Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("processes can only be marked as default with Buildpack API v0.6 or higher"))) }) }) + + context("when the api version is less than 0.8", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +api = "0.7" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`), 0600)).To(Succeed()) + }) + + it("errors", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + Processes: []packit.Process{ + { + Type: "some-type", + Command: "some-command", + Args: []string{"some-arg"}, + Direct: true, + Default: true, + WorkingDirectory: "some-working-dir", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("processes can only have a specific working directory with Buildpack API v0.8 or higher"))) + }) + }) + + context("when the api version is less than 0.9", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +api = "0.8" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`), 0600)).To(Succeed()) + }) + + it("persists a launch.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + Processes: []packit.Process{ + { + Type: "some-type", + Command: "some-command", + Args: []string{"some-arg"}, + Direct: false, + Default: true, + WorkingDirectory: "some-working-dir", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[processes]] + args = ["some-arg"] + command = "some-command" + direct = false + default = true + type = "some-type" + working-directory = "some-working-dir" + `)) + }) + + context("failure cases", func() { + it("throws a specific error when new style proccesses are used", func() { + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + DirectProcesses: []packit.DirectProcess{ + { + Type: "some-type", + Command: []string{"some-command"}, + Args: []string{"some-arg"}, + Default: false, + WorkingDirectory: workingDir, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("direct processes can only be used with Buildpack API v0.9 or higher")) + }) + }) + }) + + context("when the api version is 0.9", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), []byte(` +api = "0.9" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`), 0600)).To(Succeed()) + }) + + it("persists a launch.toml", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + DirectProcesses: []packit.DirectProcess{ + { + Type: "some-type", + Command: []string{"some-command"}, + Args: []string{"some-arg"}, + Default: true, + WorkingDirectory: "some-working-dir", + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + contents, err := os.ReadFile(filepath.Join(layersDir, "launch.toml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` + [[processes]] + args = ["some-arg"] + command = ["some-command"] + default = true + type = "some-type" + working-directory = "some-working-dir" + `)) + }) + context("failure cases", func() { + it("throws a specific error when old style proccesses are used", func() { + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Launch: packit.LaunchMetadata{ + Processes: []packit.Process{ + { + Type: "some-type", + Command: "some-command", + Args: []string{"some-arg"}, + Direct: false, + Default: false, + WorkingDirectory: workingDir, + }, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("non direct processes can only be used with Buildpack API v0.8 or lower")) + }) + }) + }) }) context("when there are slices in the result", func() { @@ -1079,6 +1452,138 @@ api = "0.5" }) }) + context("when layers have Exec.D executables", func() { + var ( + exe0 string + ) + + it.Before(func() { + temp, err := os.CreateTemp(cnbDir, "exec-d") + Expect(err).NotTo(HaveOccurred()) + exe0 = temp.Name() + }) + + context("when the api version is greater than 0.4", func() { + it.Before(func() { + bpTOML := []byte(` +api = "0.5" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false + `) + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + Expect(os.Chmod(planPath, 0444)).To(Succeed()) + }) + + it("puts the Exec.D executables in the exec.d directory", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Layers: []packit.Layer{ + { + Path: filepath.Join(ctx.Layers.Path, "layer-with-exec-d-stuff"), + ExecD: []string{exe0}, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) + + Expect(filepath.Join(layersDir, "layer-with-exec-d-stuff", "exec.d", fmt.Sprintf("0-%s", filepath.Base(exe0)))).To(BeARegularFile()) + }) + + it("does not create an exec.d directory when ExecD is empty", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Layers: []packit.Layer{ + { + Path: filepath.Join(ctx.Layers.Path, "layer-with-exec-d-stuff"), + ExecD: []string{}, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) + + Expect(filepath.Join(layersDir, "layer-with-exec-d-stuff", "exec.d")).NotTo(BeARegularFile()) + }) + + it("prepends a padded integer for lexical ordering", func() { + N := 101 + + var exes []string + for i := 0; i < N; i++ { + command, err := os.CreateTemp(cnbDir, "command") + Expect(err).NotTo(HaveOccurred()) + + exes = append(exes, command.Name()) + } + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Layers: []packit.Layer{ + { + Path: filepath.Join(ctx.Layers.Path, "layer-with-exec-d-stuff"), + ExecD: exes, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) + + Expect(filepath.Join(layersDir, "layer-with-exec-d-stuff", "exec.d", fmt.Sprintf("000-%s", filepath.Base(exes[0])))).To(BeARegularFile()) + Expect(filepath.Join(layersDir, "layer-with-exec-d-stuff", "exec.d", fmt.Sprintf("010-%s", filepath.Base(exes[10])))).To(BeARegularFile()) + Expect(filepath.Join(layersDir, "layer-with-exec-d-stuff", "exec.d", fmt.Sprintf("100-%s", filepath.Base(exes[100])))).To(BeARegularFile()) + }) + }) + + context("when the api version is less than 0.5", func() { + it.Before(func() { + bpTOML := []byte(` +api = "0.4" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false + `) + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) + Expect(os.Chmod(planPath, 0444)).To(Succeed()) + }) + + it("should not do anything", func() { + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Layers: []packit.Layer{ + { + Path: filepath.Join(ctx.Layers.Path, "layer-with-exec-d-stuff"), + ExecD: []string{exe0}, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath})) + + Expect(filepath.Join(layersDir, "layer-with-exec-d-stuff", "exec.d")).NotTo(BeARegularFile()) + }) + }) + + context("failure cases", func() { + it("throws a specific error when executable not found", func() { + + packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { + return packit.BuildResult{ + Layers: []packit.Layer{ + { + Path: filepath.Join(ctx.Layers.Path, "layer-with-exec-d-stuff"), + ExecD: []string{"foobar"}, + }, + }, + }, nil + }, packit.WithArgs([]string{binaryPath, layersDir, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("file foobar does not exist. Be sure to include it in the buildpack.toml")) + }) + }) + }) + context("failure cases", func() { context("when the buildpack plan.toml is malformed", func() { it.Before(func() { @@ -1131,7 +1636,6 @@ api = "0.4" clear-env = false `) Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(envCnbDir, "buildpack.toml"), bpTOML, 0600)).To(Succeed()) Expect(os.Chmod(planPath, 0444)).To(Succeed()) }) @@ -1157,7 +1661,7 @@ api = "0.4" packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: filepath.Join(layersDir, "some-layer"), Name: "some-layer", }, @@ -1174,7 +1678,7 @@ api = "0.4" packit.Build(func(ctx packit.BuildContext) (packit.BuildResult, error) { return packit.BuildResult{ Layers: []packit.Layer{ - packit.Layer{ + { Path: filepath.Join(layersDir, "some-layer"), Name: "some-layer", SBOM: packit.SBOMFormats{ diff --git a/buildpack_info.go b/buildpack_info.go index 8f0bdfce..d6961cef 100644 --- a/buildpack_info.go +++ b/buildpack_info.go @@ -1,9 +1,13 @@ package packit -// BuildpackInfo is a representation of the basic information for a buildpack +// BuildpackInfo +// Deprecated: use Info instead +type BuildpackInfo = Info + +// Info is a representation of the basic information for a buildpack // provided in its buildpack.toml file as described in the specification: // https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml. -type BuildpackInfo struct { +type Info struct { // ID is the identifier specified in the `buildpack.id` field of the // buildpack.toml. ID string `toml:"id"` @@ -30,17 +34,21 @@ type BuildpackInfo struct { // Licenses are the list of licenses specified in the `buildpack.licenses` // fields of the buildpack.toml. - Licenses []BuildpackInfoLicense + Licenses []InfoLicense // SBOMFormats is the list of Software Bill of Materials media types that the buildpack // produces (e.g. "application/spdx+json"). SBOMFormats []string `toml:"sbom-formats"` } -// BuildpackInfoLicense is a representation of a license specified in the +// type BuildpackInfoLicense +// Deprecated: use InfoLicense instead +type BuildpackInfoLicense = InfoLicense + +// InfoLicense is a representation of a license specified in the // buildpack.toml as described in the specification: // https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml. -type BuildpackInfoLicense struct { +type InfoLicense struct { // Type is the identifier specified in the `buildpack.licenses.type` field of // the buildpack.toml. Type string `toml:"type"` diff --git a/cargo/buildpack_parser_test.go b/cargo/buildpack_parser_test.go index 39a12784..4ac277f5 100644 --- a/cargo/buildpack_parser_test.go +++ b/cargo/buildpack_parser_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/paketo-buildpacks/packit/cargo" + "github.com/paketo-buildpacks/packit/v2/cargo" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/cargo/checksum.go b/cargo/checksum.go new file mode 100644 index 00000000..25c767ae --- /dev/null +++ b/cargo/checksum.go @@ -0,0 +1,42 @@ +package cargo + +import ( + "strings" +) + +// Checksum represents a checksum algorithm and hash pair formatted as +// algorithm:hash. +type Checksum string + +// Algorithm returns the algorithm portion of the checksum string. If that +// portion is missing, it defaults to "sha256". +func (c Checksum) Algorithm() string { + algorithm, _, found := strings.Cut(string(c), ":") + if !found { + return "sha256" + } + + return algorithm +} + +// Hash returns the hexidecimal encoded hash portion of the checksum string. +func (c Checksum) Hash() string { + _, hash, found := strings.Cut(string(c), ":") + if !found { + hash = string(c) + } + + return hash +} + +// EqualTo returns true only when the given checksum algorithms and hashes +// match. +func (c Checksum) Match(o Checksum) bool { + return strings.EqualFold(c.Algorithm(), o.Algorithm()) && c.Hash() == o.Hash() +} + +// EqualTo returns true only when the given checksum formatted string +// algorithms and hashes match. +func (c Checksum) MatchString(o string) bool { + return c.Match(Checksum(o)) +} diff --git a/cargo/checksum_test.go b/cargo/checksum_test.go new file mode 100644 index 00000000..3e10c1b2 --- /dev/null +++ b/cargo/checksum_test.go @@ -0,0 +1,54 @@ +package cargo_test + +import ( + "fmt" + "testing" + + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testChecksum(t *testing.T, context spec.G, it spec.S) { + Expect := NewWithT(t).Expect + + context("Matching", func() { + type testCaseType struct { + c1 string + c2 string + result bool + } + + for _, tc := range []testCaseType{ + {"", "", true}, + {"c", "c", true}, + {"sha256:c", "c", true}, + {"c", "sha256:c", true}, + {"md5:c", "md5:c", true}, + {"md5:c", ":c", false}, + {":", ":", true}, + {":c", ":c", true}, + {"", "c", false}, + {"c", "", false}, + {"c", "z", false}, + {"md5:c", "sha256:c", false}, + {"md5:c", "md5:d", false}, + {"md5:c:d", "md5:c:d", true}, + {"md5:c", "md5:c:d", false}, + {":", "::", false}, + {":", ":::", false}, + } { + + // NOTE: we need to keep a "loop-local" variable to use in the "it + // function closure" below, otherwise the value of tc will simply be the + // last element in the slice every time the test is evaluated. + ca, cb, sb, result := cargo.Checksum(tc.c1), cargo.Checksum(tc.c2), tc.c2, tc.result + + it(fmt.Sprintf("will check result %q == %q", ca, cb), func() { + Expect(ca.Match(cb)).To(Equal(result)) + Expect(ca.MatchString(sb)).To(Equal(result)) + }) + } + }) +} diff --git a/cargo/config.go b/cargo/config.go index 6100a90c..657068d0 100644 --- a/cargo/config.go +++ b/cargo/config.go @@ -49,16 +49,19 @@ type ConfigMetadata struct { } type ConfigMetadataDependency struct { + Checksum string `toml:"checksum" json:"checksum,omitempty"` CPE string `toml:"cpe" json:"cpe,omitempty"` - PURL string `toml:"purl" json:"purl,omitempty"` + PURL string `toml:"purl" json:"purl,omitempty"` DeprecationDate *time.Time `toml:"deprecation_date" json:"deprecation_date,omitempty"` ID string `toml:"id" json:"id,omitempty"` Licenses []interface{} `toml:"licenses" json:"licenses,omitempty"` Name string `toml:"name" json:"name,omitempty"` SHA256 string `toml:"sha256" json:"sha256,omitempty"` Source string `toml:"source" json:"source,omitempty"` + SourceChecksum string `toml:"source-checksum" json:"source-checksum,omitempty"` SourceSHA256 string `toml:"source_sha256" json:"source_sha256,omitempty"` Stacks []string `toml:"stacks" json:"stacks,omitempty"` + StripComponents int `toml:"strip-components" json:"strip-components,omitempty"` URI string `toml:"uri" json:"uri,omitempty"` Version string `toml:"version" json:"version,omitempty"` } @@ -96,12 +99,17 @@ func EncodeConfig(writer io.Writer, config Config) error { return err } + c, err = convertStripComponents(config.Metadata.Dependencies, c) + if err != nil { + return err + } + return toml.NewEncoder(writer).Encode(c) } func DecodeConfig(reader io.Reader, config *Config) error { var c map[string]interface{} - _, err := toml.DecodeReader(reader, &c) + _, err := toml.NewDecoder(reader).Decode(&c) if err != nil { return err } @@ -213,8 +221,8 @@ func (cd ConfigMetadataDependency) HasStack(stack string) bool { // Unmarshal stores json numbers in float64 types, adding an unnecessary decimal point to the patch in the final toml. // convertPatches converts this float64 into an int and returns a new map that contains an integer value for patches -func convertPatches(constraints []ConfigMetadataDependencyConstraint, c map[string]interface{}) (map[string]interface{}, error) { - if len(constraints) > 0 { +func convertPatches(cons []ConfigMetadataDependencyConstraint, c map[string]interface{}) (map[string]interface{}, error) { + if len(cons) > 0 { metadata, ok := c["metadata"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("failure to assert type: unexpected data in metadata") @@ -240,3 +248,32 @@ func convertPatches(constraints []ConfigMetadataDependencyConstraint, c map[stri } return c, nil } + +// Accomplishes the same this as the convertPatches function but for strip components in the dependencies list. +func convertStripComponents(deps []ConfigMetadataDependency, c map[string]interface{}) (map[string]interface{}, error) { + if len(deps) > 0 { + metadata, ok := c["metadata"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failure to assert type: unexpected data in metadata") + } + + dependencies, ok := metadata["dependencies"].([]interface{}) + if !ok { + return nil, fmt.Errorf("failure to assert type: unexpected data in constraints") + } + + for _, dependency := range dependencies { + stripComponents, ok := dependency.(map[string]interface{})["strip-components"] + if !ok { + continue + } + + floatStripComponents, ok := stripComponents.(float64) + if !ok { + return nil, fmt.Errorf("failure to assert type: unexpected data") + } + dependency.(map[string]interface{})["strip-components"] = int(floatStripComponents) + } + } + return c, nil +} diff --git a/cargo/config_test.go b/cargo/config_test.go index f5c1c595..beae8a37 100644 --- a/cargo/config_test.go +++ b/cargo/config_test.go @@ -7,11 +7,11 @@ import ( "testing" "time" - "github.com/paketo-buildpacks/packit/cargo" + "github.com/paketo-buildpacks/packit/v2/cargo" "github.com/sclevine/spec" . "github.com/onsi/gomega" - . "github.com/paketo-buildpacks/packit/matchers" + . "github.com/paketo-buildpacks/packit/v2/matchers" ) func testConfig(t *testing.T, context spec.G, it spec.S) { @@ -66,6 +66,7 @@ func testConfig(t *testing.T, context spec.G, it spec.S) { PrePackage: "some-pre-package-script.sh", Dependencies: []cargo.ConfigMetadataDependency{ { + Checksum: "sha256:some-sum", CPE: "some-cpe", PURL: "some-purl", DeprecationDate: &deprecationDate, @@ -74,8 +75,10 @@ func testConfig(t *testing.T, context spec.G, it spec.S) { Name: "Some Dependency", SHA256: "shasum", Source: "source", + SourceChecksum: "sha256:source-shasum", SourceSHA256: "source-shasum", Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + StripComponents: 1, URI: "http://some-url", Version: "1.2.3", }, @@ -132,6 +135,7 @@ api = "0.6" some-dependency = "1.2.x" [[metadata.dependencies]] + checksum = "sha256:some-sum" cpe = "some-cpe" purl = "some-purl" deprecation_date = "2020-06-01T00:00:00Z" @@ -140,8 +144,10 @@ api = "0.6" name = "Some Dependency" sha256 = "shasum" source = "source" + source-checksum = "sha256:source-shasum" source_sha256 = "source-shasum" stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + strip-components = 1 uri = "http://some-url" version = "1.2.3" @@ -209,6 +215,7 @@ api = "0.6" PrePackage: "some-pre-package-script.sh", Dependencies: []cargo.ConfigMetadataDependency{ { + Checksum: "sha256:some-sum", CPE: "some-cpe", PURL: "some-purl", DeprecationDate: &deprecationDate, @@ -223,13 +230,14 @@ api = "0.6" URI: "some-license-uri", }, }, - Name: "Some Dependency", - SHA256: "shasum", - Source: "source", - SourceSHA256: "source-shasum", - Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, - URI: "http://some-url", - Version: "1.2.3", + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", }, }, DependencyConstraints: []cargo.ConfigMetadataDependencyConstraint{ @@ -280,6 +288,7 @@ api = "0.6" some-dependency = "1.2.x" [[metadata.dependencies]] + checksum = "sha256:some-sum" cpe = "some-cpe" purl = "some-purl" deprecation_date = "2020-06-01T00:00:00Z" @@ -287,6 +296,7 @@ api = "0.6" name = "Some Dependency" sha256 = "shasum" source = "source" + source-checksum = "sha256:source-shasum" source_sha256 = "source-shasum" stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] uri = "http://some-url" @@ -329,6 +339,43 @@ api = "0.6" }) + context("when not all metadata.dependencies include strip-components", func() { + it("correctly encodes all elements", func() { + err := cargo.EncodeConfig(buffer, cargo.Config{ + API: "0.6", + Buildpack: cargo.ConfigBuildpack{ + ID: "some-buildpack-id", + }, + Metadata: cargo.ConfigMetadata{ + Dependencies: []cargo.ConfigMetadataDependency{ + { + ID: "some-dependency", + }, + { + ID: "other-dependency", + StripComponents: 1, + }, + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(MatchTOML(` +api = "0.6" + +[buildpack] + id = "some-buildpack-id" + +[[metadata.dependencies]] + id = "some-dependency" + +[[metadata.dependencies]] + id = "other-dependency" + strip-components = 1 +`)) + }) + + }) + context("failure cases", func() { context("when the Config cannot be marshalled to json", func() { it("returns an error", func() { @@ -393,15 +440,18 @@ api = "0.6" key = "value" [[metadata.dependencies]] + checksum = "sha256:some-sum" cpe = "some-cpe" purl = "some-purl" id = "some-dependency" licenses = ["fancy-license", "fancy-license-2"] name = "Some Dependency" sha256 = "shasum" - source = "source" + source = "source" + source-checksum = "sha256:source-shasum" source_sha256 = "source-shasum" stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + strip-components = 1 uri = "http://some-url" version = "1.2.3" @@ -466,17 +516,20 @@ api = "0.6" PrePackage: "some-pre-package-script.sh", Dependencies: []cargo.ConfigMetadataDependency{ { - CPE: "some-cpe", - PURL: "some-purl", - ID: "some-dependency", - Licenses: []interface{}{"fancy-license", "fancy-license-2"}, - Name: "Some Dependency", - SHA256: "shasum", - Source: "source", - SourceSHA256: "source-shasum", - Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, - URI: "http://some-url", - Version: "1.2.3", + Checksum: "sha256:some-sum", + CPE: "some-cpe", + PURL: "some-purl", + ID: "some-dependency", + Licenses: []interface{}{"fancy-license", "fancy-license-2"}, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + StripComponents: 1, + URI: "http://some-url", + Version: "1.2.3", }, }, DependencyConstraints: []cargo.ConfigMetadataDependencyConstraint{ @@ -535,12 +588,14 @@ api = "0.2" key = "value" [[metadata.dependencies]] + checksum = "sha256:some-sum" cpe = "some-cpe" purl = "some-purl" id = "some-dependency" name = "Some Dependency" sha256 = "shasum" - source = "source" + source = "source" + source-checksum = "sha256:source-shasum" source_sha256 = "source-shasum" stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] uri = "http://some-url" @@ -611,9 +666,10 @@ api = "0.2" PrePackage: "some-pre-package-script.sh", Dependencies: []cargo.ConfigMetadataDependency{ { - CPE: "some-cpe", - PURL: "some-purl", - ID: "some-dependency", + Checksum: "sha256:some-sum", + CPE: "some-cpe", + PURL: "some-purl", + ID: "some-dependency", Licenses: []interface{}{ map[string]interface{}{ "type": "fancy-license", @@ -624,13 +680,14 @@ api = "0.2" "uri": "some-license-uri", }, }, - Name: "Some Dependency", - SHA256: "shasum", - Source: "source", - SourceSHA256: "source-shasum", - Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, - URI: "http://some-url", - Version: "1.2.3", + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", }, }, DependencyConstraints: []cargo.ConfigMetadataDependencyConstraint{ diff --git a/cargo/directory_duplicator.go b/cargo/directory_duplicator.go index eeda0f8c..94d1ba56 100644 --- a/cargo/directory_duplicator.go +++ b/cargo/directory_duplicator.go @@ -1,6 +1,6 @@ package cargo -import "github.com/paketo-buildpacks/packit/fs" +import "github.com/paketo-buildpacks/packit/v2/fs" type DirectoryDuplicator struct{} diff --git a/cargo/directory_duplicator_test.go b/cargo/directory_duplicator_test.go index 65eb79f0..a97ed728 100644 --- a/cargo/directory_duplicator_test.go +++ b/cargo/directory_duplicator_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/cargo" + "github.com/paketo-buildpacks/packit/v2/cargo" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/cargo/extension_config.go b/cargo/extension_config.go new file mode 100644 index 00000000..c7e9d595 --- /dev/null +++ b/cargo/extension_config.go @@ -0,0 +1,98 @@ +package cargo + +import ( + "encoding/json" + "io" + + "github.com/BurntSushi/toml" +) + +type ExtensionConfig struct { + API string `toml:"api" json:"api,omitempty"` + Extension ConfigExtension `toml:"extension" json:"extension,omitempty"` + Metadata ConfigExtensionMetadata `toml:"metadata" json:"metadata,omitempty"` +} + +type ConfigExtensionMetadata struct { + IncludeFiles []string `toml:"include-files" json:"include-files,omitempty"` + PrePackage string `toml:"pre-package" json:"pre-package,omitempty"` + DefaultVersions map[string]string `toml:"default-versions" json:"default-versions,omitempty"` + Dependencies []ConfigExtensionMetadataDependency `toml:"dependencies" json:"dependencies,omitempty"` + Configurations []ConfigExtensionMetadataConfiguration `toml:"configurations" json:"configurations,omitempty"` +} + +type ConfigExtensionMetadataDependency struct { + Checksum string `toml:"checksum" json:"checksum,omitempty"` + ID string `toml:"id" json:"id,omitempty"` + Licenses []interface{} `toml:"licenses" json:"licenses,omitempty"` + Name string `toml:"name" json:"name,omitempty"` + SHA256 string `toml:"sha256" json:"sha256,omitempty"` + Source string `toml:"source" json:"source,omitempty"` + SourceChecksum string `toml:"source-checksum" json:"source-checksum,omitempty"` + SourceSHA256 string `toml:"source_sha256" json:"source_sha256,omitempty"` + Stacks []string `toml:"stacks" json:"stacks,omitempty"` + URI string `toml:"uri" json:"uri,omitempty"` + Version string `toml:"version" json:"version,omitempty"` +} +type ConfigExtensionMetadataConfiguration struct { + Default string `toml:"default" json:"default,omitempty"` + Launch bool `toml:"launch" json:"launch,omitempty"` + Description string `toml:"description" json:"description,omitempty"` + Build bool `toml:"build" json:"build,omitempty"` + Name string `toml:"name" json:"name,omitempty"` +} +type ConfigExtension struct { + ID string `toml:"id" json:"id,omitempty"` + Name string `toml:"name" json:"name,omitempty"` + Version string `toml:"version" json:"version,omitempty"` + Homepage string `toml:"homepage,omitempty" json:"homepage,omitempty"` + Description string `toml:"description,omitempty" json:"description,omitempty"` + Keywords []string `toml:"keywords,omitempty" json:"keywords,omitempty"` + Licenses []ConfigExtensionLicense `toml:"licenses,omitempty" json:"licenses,omitempty"` + SBOMFormats []string `toml:"sbom-formats,omitempty" json:"sbom-formats,omitempty"` +} + +type ConfigExtensionLicense struct { + Type string `toml:"type" json:"type"` + URI string `toml:"uri" json:"uri"` +} + +func EncodeExtensionConfig(writer io.Writer, extensionConfig ExtensionConfig) error { + content, err := json.Marshal(extensionConfig) + if err != nil { + return err + } + + c := map[string]interface{}{} + err = json.Unmarshal(content, &c) + if err != nil { + return err + } + + return toml.NewEncoder(writer).Encode(c) +} + +func DecodeExtensionConfig(reader io.Reader, extensionConfig *ExtensionConfig) error { + var c map[string]interface{} + _, err := toml.NewDecoder(reader).Decode(&c) + if err != nil { + return err + } + + content, err := json.Marshal(c) + if err != nil { + return err + } + + return json.Unmarshal(content, extensionConfig) +} + +func (cd ConfigExtensionMetadataDependency) HasStack(stack string) bool { + for _, s := range cd.Stacks { + if s == stack { + return true + } + } + + return false +} diff --git a/cargo/extension_config_test.go b/cargo/extension_config_test.go new file mode 100644 index 00000000..896b9543 --- /dev/null +++ b/cargo/extension_config_test.go @@ -0,0 +1,449 @@ +package cargo_test + +import ( + "bytes" + + "strings" + "testing" + + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" + . "github.com/paketo-buildpacks/packit/v2/matchers" +) + +func testExtensionConfig(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + buffer *bytes.Buffer + ) + + it.Before(func() { + buffer = bytes.NewBuffer(nil) + }) + + context("EncodeExtensionConfig", func() { + it("encodes the extension config to TOML", func() { + + err := cargo.EncodeExtensionConfig(buffer, cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Description: "some-extension-description", + Keywords: []string{"some-extension-keyword"}, + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{"fancy-license", "fancy-license-2"}, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + Configurations: []cargo.ConfigExtensionMetadataConfiguration{ + { + Default: "0", + Description: "some-metadata-configuration-description", + Launch: true, + Name: "SOME_METADATA_CONFIGURATION_NAME", + Build: true, + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(MatchTOML(` +api = "0.7" + +[extension] + description = "some-extension-description" + homepage = "some-extension-homepage" + id = "some-extension-id" + keywords = ["some-extension-keyword"] + name = "some-extension-name" + version = "some-extension-version" + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + + [[metadata.configurations]] + build = true + default = "0" + description = "some-metadata-configuration-description" + launch = true + name = "SOME_METADATA_CONFIGURATION_NAME" + [metadata.default-versions] + some-dependency = "1.2.x" + + [[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + licenses = ["fancy-license", "fancy-license-2"] + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" +`)) + }) + + context("when the config dependency licenses are structured like ConfigExtensionLicenses", func() { + it("encodes the config to TOML", func() { + + err := cargo.EncodeExtensionConfig(buffer, cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Description: "some-extension-description", + Keywords: []string{"some-extension-keyword"}, + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{ + cargo.ConfigBuildpackLicense{ + Type: "fancy-license", + URI: "some-license-uri", + }, + cargo.ConfigBuildpackLicense{ + Type: "fancy-license-2", + URI: "some-license-uri", + }, + }, Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + Configurations: []cargo.ConfigExtensionMetadataConfiguration{ + { + Default: "0", + Description: "some-metadata-configuration-description", + Launch: true, + Name: "SOME_METADATA_CONFIGURATION_NAME", + Build: true, + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(MatchTOML(` +api = "0.7" + +[extension] + description = "some-extension-description" + homepage = "some-extension-homepage" + id = "some-extension-id" + keywords = ["some-extension-keyword"] + name = "some-extension-name" + version = "some-extension-version" + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + + [[metadata.configurations]] + build = true + default = "0" + description = "some-metadata-configuration-description" + launch = true + name = "SOME_METADATA_CONFIGURATION_NAME" + [metadata.default-versions] + some-dependency = "1.2.x" + + [[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" + + [[metadata.dependencies.licenses]] + type = "fancy-license" + uri = "some-license-uri" + + [[metadata.dependencies.licenses]] + type = "fancy-license-2" + uri = "some-license-uri" +`)) + }) + }) + }) + + context("DecodeExtensionConfig", func() { + it("decodes TOML to Extensionconfig", func() { + tomlBuffer := strings.NewReader(` +api = "0.7" + +[extension] +id = "some-extension-id" +name = "some-extension-name" +version = "some-extension-version" +homepage = "some-extension-homepage" +description = "some-extension-description" +keywords = [ "some-extension-keyword" ] + +[[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + +[metadata.default-versions] + some-dependency = "1.2.x" + +[[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + licenses = ["fancy-license", "fancy-license-2"] + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" + +[[metadata.configurations]] + default = "0" + description = "some-metadata-configuration-description" + launch = true + name = "SOME_METADATA_CONFIGURATION_NAME" + build = true +`) + + var config cargo.ExtensionConfig + Expect(cargo.DecodeExtensionConfig(tomlBuffer, &config)).To(Succeed()) + Expect(config).To(Equal(cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Description: "some-extension-description", + Keywords: []string{"some-extension-keyword"}, + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{"fancy-license", "fancy-license-2"}, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + Configurations: []cargo.ConfigExtensionMetadataConfiguration{ + { + Default: "0", + Description: "some-metadata-configuration-description", + Launch: true, + Name: "SOME_METADATA_CONFIGURATION_NAME", + Build: true, + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + })) + }) + + context("dependency license are not a list of IDs", func() { + it("decodes TOML to extensionConfig", func() { + tomlBuffer := strings.NewReader(` +api = "0.2" + +[extension] + id = "some-extension-id" + name = "some-extension-name" + version = "some-extension-version" + homepage = "some-extension-homepage" + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + +[metadata.default-versions] + some-dependency = "1.2.x" + +[[metadata.some-map]] + key = "value" + +[[metadata.dependencies]] + checksum = "sha256:some-sum" + id = "some-dependency" + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source-checksum = "sha256:source-shasum" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" + + [[metadata.dependencies.licenses]] + type = "fancy-license" + uri = "some-license-uri" + + [[metadata.dependencies.licenses]] + type = "fancy-license-2" + uri = "some-license-uri" +`) + + var config cargo.ExtensionConfig + Expect(cargo.DecodeExtensionConfig(tomlBuffer, &config)).To(Succeed()) + Expect(config).To(Equal(cargo.ExtensionConfig{ + API: "0.2", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + Homepage: "some-extension-homepage", + Licenses: []cargo.ConfigExtensionLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + Checksum: "sha256:some-sum", + ID: "some-dependency", + Licenses: []interface{}{ + map[string]interface{}{ + "type": "fancy-license", + "uri": "some-license-uri", + }, + map[string]interface{}{ + "type": "fancy-license-2", + "uri": "some-license-uri", + }, + }, + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceChecksum: "sha256:source-shasum", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + DefaultVersions: map[string]string{ + "some-dependency": "1.2.x", + }, + }, + })) + }) + + }) + + context("failure cases", func() { + context("when a bad reader is passed in", func() { + it("returns an error", func() { + err := cargo.DecodeExtensionConfig(errorReader{}, &cargo.ExtensionConfig{}) + Expect(err).To(MatchError(ContainSubstring("failed to read"))) + }) + }) + }) + }) +} diff --git a/cargo/extension_parser.go b/cargo/extension_parser.go new file mode 100644 index 00000000..9860f15b --- /dev/null +++ b/cargo/extension_parser.go @@ -0,0 +1,24 @@ +package cargo + +import "os" + +type ExtensionParser struct{} + +func NewExtensionParser() ExtensionParser { + return ExtensionParser{} +} + +func (p ExtensionParser) Parse(path string) (ExtensionConfig, error) { + file, err := os.Open(path) + if err != nil { + return ExtensionConfig{}, err + } + + var config ExtensionConfig + err = DecodeExtensionConfig(file, &config) + if err != nil { + return ExtensionConfig{}, err + } + + return config, nil +} diff --git a/cargo/extension_parser_test.go b/cargo/extension_parser_test.go new file mode 100644 index 00000000..271b5479 --- /dev/null +++ b/cargo/extension_parser_test.go @@ -0,0 +1,116 @@ +package cargo_test + +import ( + "os" + "testing" + + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testExtensionParser(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + path string + parser cargo.ExtensionParser + ) + + it.Before(func() { + file, err := os.CreateTemp("", "extension.toml") + Expect(err).NotTo(HaveOccurred()) + + _, err = file.WriteString(`api = "0.7" +[extension] +id = "some-extension-id" +name = "some-extension-name" +version = "some-extension-version" + +[metadata] + include-files = ["some-include-file", "other-include-file"] + pre-package = "some-pre-package-script.sh" + +[[metadata.some-map]] + key = "value" + +[[metadata.dependencies]] + id = "some-dependency" + name = "Some Dependency" + sha256 = "shasum" + source = "source" + source_sha256 = "source-shasum" + stacks = ["io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"] + uri = "http://some-url" + version = "1.2.3" +`) + Expect(err).NotTo(HaveOccurred()) + + Expect(file.Close()).To(Succeed()) + + path = file.Name() + + parser = cargo.NewExtensionParser() + }) + + it.After(func() { + Expect(os.RemoveAll(path)).To(Succeed()) + }) + + context("Parse", func() { + it("parses a given extension.toml", func() { + config, err := parser.Parse(path) + Expect(err).NotTo(HaveOccurred()) + Expect(config).To(Equal(cargo.ExtensionConfig{ + API: "0.7", + Extension: cargo.ConfigExtension{ + ID: "some-extension-id", + Name: "some-extension-name", + Version: "some-extension-version", + }, + Metadata: cargo.ConfigExtensionMetadata{ + IncludeFiles: []string{ + "some-include-file", + "other-include-file", + }, + PrePackage: "some-pre-package-script.sh", + Dependencies: []cargo.ConfigExtensionMetadataDependency{ + { + ID: "some-dependency", + Name: "Some Dependency", + SHA256: "shasum", + Source: "source", + SourceSHA256: "source-shasum", + Stacks: []string{"io.buildpacks.stacks.bionic", "org.cloudfoundry.stacks.tiny"}, + URI: "http://some-url", + Version: "1.2.3", + }, + }, + }, + })) + }) + + context("when the extension.toml does not exist", func() { + it.Before(func() { + Expect(os.Remove(path)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := parser.Parse(path) + Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) + }) + }) + + context("when the extension.toml is malformed", func() { + it.Before(func() { + Expect(os.WriteFile(path, []byte("%%%"), 0644)).To(Succeed()) + }) + + it("returns an error", func() { + _, err := parser.Parse(path) + Expect(err).To(MatchError(ContainSubstring("expected '.' or '=', but got '%' instead"))) + }) + }) + }) +} diff --git a/cargo/init_test.go b/cargo/init_test.go index 74f5afaf..4c4545b2 100644 --- a/cargo/init_test.go +++ b/cargo/init_test.go @@ -11,10 +11,13 @@ import ( func TestUnitCargo(t *testing.T) { suite := spec.New("cargo", spec.Report(report.Terminal{})) suite("BuildpackParser", testBuildpackParser) + suite("ExtensionParser", testExtensionParser) suite("Config", testConfig) + suite("ExtensionConfig", testExtensionConfig) suite("DirectoryDuplicator", testDirectoryDuplicator) suite("Transport", testTransport) suite("ValidatedReader", testValidatedReader) + suite("Checksum", testChecksum) suite.Run(t) } diff --git a/cargo/transport.go b/cargo/transport.go index 16c82fce..f6ddbf78 100644 --- a/cargo/transport.go +++ b/cargo/transport.go @@ -35,5 +35,10 @@ func (t Transport) Drop(root, uri string) (io.ReadCloser, error) { return nil, fmt.Errorf("failed to make request: %s", err) } + if response.StatusCode >= 400 { + response.Body.Close() + return nil, fmt.Errorf("unexpected status code %d while fetching %q", response.StatusCode, uri) + } + return response.Body, nil } diff --git a/cargo/transport_test.go b/cargo/transport_test.go index 269f419d..241f9034 100644 --- a/cargo/transport_test.go +++ b/cargo/transport_test.go @@ -9,7 +9,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/cargo" + "github.com/paketo-buildpacks/packit/v2/cargo" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -74,6 +74,13 @@ func testTransport(t *testing.T, context spec.G, it spec.S) { Expect(err).To(MatchError(ContainSubstring("connection refused"))) }) }) + + context("when the http status indicates an error", func() { + it("returns an error", func() { + _, err := transport.Drop("", fmt.Sprintf("%s/some-bundle-that-does-not-exist", server.URL)) + Expect(err).To(MatchError(ContainSubstring("unexpected status code 404 while fetching"))) + }) + }) }) }) diff --git a/cargo/validated_reader.go b/cargo/validated_reader.go index 3f73b351..ce4cc9c9 100644 --- a/cargo/validated_reader.go +++ b/cargo/validated_reader.go @@ -3,8 +3,10 @@ package cargo import ( "bytes" "crypto/sha256" + "crypto/sha512" "encoding/hex" "errors" + "fmt" "hash" "io" ) @@ -13,19 +15,41 @@ var ChecksumValidationError = errors.New("validation error: checksum does not ma type ValidatedReader struct { reader io.Reader - checksum string + checksum Checksum hash hash.Hash } -func NewValidatedReader(reader io.Reader, checksum string) ValidatedReader { +type errorHash struct { + hash.Hash + + err error +} + +func NewValidatedReader(reader io.Reader, sum string) ValidatedReader { + var hash hash.Hash + checksum := Checksum(sum) + + switch checksum.Algorithm() { + case "sha256": + hash = sha256.New() + case "sha512": + hash = sha512.New() + default: + return ValidatedReader{hash: errorHash{err: fmt.Errorf("unsupported algorithm %q: the following algorithms are supported [sha256, sha512]", checksum.Algorithm())}} + } + return ValidatedReader{ reader: reader, checksum: checksum, - hash: sha256.New(), + hash: hash, } } func (vr ValidatedReader) Read(p []byte) (int, error) { + if errHash, ok := vr.hash.(errorHash); ok { + return 0, errHash.err + } + var done bool n, err := vr.reader.Read(p) if err != nil { @@ -44,7 +68,7 @@ func (vr ValidatedReader) Read(p []byte) (int, error) { if done { sum := hex.EncodeToString(vr.hash.Sum(nil)) - if sum != vr.checksum { + if sum != vr.checksum.Hash() { return n, ChecksumValidationError } diff --git a/cargo/validated_reader_test.go b/cargo/validated_reader_test.go index 64a778ab..3118a2e8 100644 --- a/cargo/validated_reader_test.go +++ b/cargo/validated_reader_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/paketo-buildpacks/packit/cargo" + "github.com/paketo-buildpacks/packit/v2/cargo" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -15,30 +15,35 @@ import ( func testValidatedReader(t *testing.T, context spec.G, it spec.S) { var ( Expect = NewWithT(t).Expect - vr cargo.ValidatedReader ) - it.Before(func() { - vr = cargo.NewValidatedReader(strings.NewReader("some-contents"), "6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") - }) - context("Read", func() { + var buffer *bytes.Buffer + it.Before(func() { + buffer = bytes.NewBuffer(nil) + }) it("reads the contents of the internal reader", func() { - buffer := bytes.NewBuffer(nil) + vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "sha256:6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") _, err := io.Copy(buffer, vr) Expect(err).NotTo(HaveOccurred()) Expect(buffer.String()).To(Equal("some-contents")) }) - context("when the checksum does not match", func() { - it.Before(func() { - vr = cargo.NewValidatedReader(strings.NewReader("some-contents"), "this checksum does not match") + context("when running with a different algorithm", func() { + it("reads the contents of the internal reader", func() { + vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "sha512:b7b2b9e0a4d7f84985a720d1273166bb00132a60ac45388a7d3090a7d4c9692f38d019f807a02750f810f52c623362f977040231c2bbf5947170fe83686cfd9d") + + _, err := io.Copy(buffer, vr) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(Equal("some-contents")) }) + }) + context("when the checksum does not match", func() { it("returns an error", func() { - buffer := bytes.NewBuffer(nil) + vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "sha256:this checksum does not match") _, err := io.Copy(buffer, vr) Expect(err).To(MatchError("validation error: checksum does not match")) @@ -46,22 +51,31 @@ func testValidatedReader(t *testing.T, context spec.G, it spec.S) { }) context("when the internal reader cannot be read", func() { - it.Before(func() { - vr = cargo.NewValidatedReader(errorReader{}, "6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") - }) - it("returns an error", func() { - buffer := bytes.NewBuffer(nil) + vr := cargo.NewValidatedReader(errorReader{}, "sha256:6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") _, err := io.Copy(buffer, vr) Expect(err).To(MatchError("failed to read")) }) }) + + context("failure cases", func() { + context("there is an unsupported algorithm", func() { + it("returns an error", func() { + vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "magic:6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") + + _, err := io.Copy(buffer, vr) + Expect(err).To(MatchError(`unsupported algorithm "magic": the following algorithms are supported [sha256, sha512]`)) + }) + }) + }) }) context("Valid", func() { context("when the checksums match", func() { it("returns true", func() { + vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "sha256:6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") + ok, err := vr.Valid() Expect(err).NotTo(HaveOccurred()) Expect(ok).To(BeTrue()) @@ -69,11 +83,9 @@ func testValidatedReader(t *testing.T, context spec.G, it spec.S) { }) context("when the checksums do not match", func() { - it.Before(func() { - vr = cargo.NewValidatedReader(strings.NewReader("some-contents"), "this checksum does not match") - }) - it("returns false", func() { + vr := cargo.NewValidatedReader(strings.NewReader("some-contents"), "sha256:this checksum does not match") + ok, err := vr.Valid() Expect(err).NotTo(HaveOccurred()) Expect(ok).To(BeFalse()) @@ -82,11 +94,9 @@ func testValidatedReader(t *testing.T, context spec.G, it spec.S) { context("failure cases", func() { context("when the internal reader cannot be read", func() { - it.Before(func() { - vr = cargo.NewValidatedReader(errorReader{}, "6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") - }) - it("returns an error", func() { + vr := cargo.NewValidatedReader(errorReader{}, "sha256:6e32ea34db1b3755d7dec972eb72c705338f0dd8e0be881d966963438fb2e800") + ok, err := vr.Valid() Expect(err).To(MatchError("failed to read")) Expect(ok).To(BeFalse()) diff --git a/chronos/clock_test.go b/chronos/clock_test.go index 6583cfc0..72610bd2 100644 --- a/chronos/clock_test.go +++ b/chronos/clock_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/paketo-buildpacks/packit/chronos" + "github.com/paketo-buildpacks/packit/v2/chronos" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/chronos/doc.go b/chronos/doc.go index 76557e6b..dbbf6441 100644 --- a/chronos/doc.go +++ b/chronos/doc.go @@ -9,7 +9,7 @@ // import ( // "os" // -// "github.com/paketo-buildpacks/packit/chronos" +// "github.com/paketo-buildpacks/packit/v2/chronos" // ) // // func main() { diff --git a/detect.go b/detect.go index db20dbe2..966aff52 100644 --- a/detect.go +++ b/detect.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/BurntSushi/toml" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" ) // DetectFunc is the definition of a callback that can be invoked when the @@ -31,9 +31,14 @@ type DetectContext struct { // https://github.com/buildpacks/spec/blob/main/buildpack.md#detection Platform Platform - // BuildpackInfo includes the details of the buildpack parsed from the - // buildpack.toml included in the buildpack contents. - BuildpackInfo BuildpackInfo + // BuildpackInfo + // Deprecated: Use Info instead + BuildpackInfo Info + + // Info includes the details of the buildpack (or extension) parsed + // from the buildpack.toml (or extension.tom) included in the buildpack + // (or extension) contents. + Info Info // Stack is the value of the chosen stack. This value is populated from the // $CNB_STACK_ID environment variable. @@ -70,26 +75,47 @@ func Detect(f DetectFunc, options ...Option) { } cnbPath, ok := os.LookupEnv("CNB_BUILDPACK_DIR") + isExtension := false + if !ok { + cnbPath, ok = os.LookupEnv("CNB_EXTENSION_DIR") + isExtension = ok + } if !ok { cnbPath = filepath.Clean(strings.TrimSuffix(config.args[0], filepath.Join("bin", "detect"))) } - var buildpackInfo struct { - Buildpack BuildpackInfo `toml:"buildpack"` + info := Info{} + if isExtension { + _, err = toml.DecodeFile(filepath.Join(cnbPath, "extension.toml"), &struct { + Extension *Info `toml:"extension"` + }{ + Extension: &info, + }) + } else { + _, err = toml.DecodeFile(filepath.Join(cnbPath, "buildpack.toml"), &struct { + Buildpack *Info `toml:"buildpack"` + }{ + Buildpack: &info, + }) } - _, err = toml.DecodeFile(filepath.Join(cnbPath, "buildpack.toml"), &buildpackInfo) if err != nil { config.exitHandler.Error(err) return } + platformPath, ok := os.LookupEnv("CNB_PLATFORM_DIR") + if !ok { + platformPath = config.args[1] + } + result, err := f(DetectContext{ WorkingDir: dir, Platform: Platform{ - Path: config.args[1], + Path: platformPath, }, CNBPath: cnbPath, - BuildpackInfo: buildpackInfo.Buildpack, + BuildpackInfo: info, + Info: info, Stack: os.Getenv("CNB_STACK_ID"), }) if err != nil { @@ -97,7 +123,12 @@ func Detect(f DetectFunc, options ...Option) { return } - file, err := os.OpenFile(config.args[2], os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + planPath, ok := os.LookupEnv("CNB_BUILD_PLAN_PATH") + if !ok { + planPath = config.args[2] + } + + file, err := os.OpenFile(planPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) if err != nil { config.exitHandler.Error(err) return diff --git a/detect_test.go b/detect_test.go index 9d58cd33..ae1f13ed 100644 --- a/detect_test.go +++ b/detect_test.go @@ -7,24 +7,23 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/fakes" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/fakes" + "github.com/paketo-buildpacks/packit/v2/internal" "github.com/sclevine/spec" . "github.com/onsi/gomega" - . "github.com/paketo-buildpacks/packit/matchers" + . "github.com/paketo-buildpacks/packit/v2/matchers" ) func testDetect(t *testing.T, context spec.G, it spec.S) { var ( - Expect = NewWithT(t).Expect - + Expect = NewWithT(t).Expect + err error workingDir string tmpDir string platformDir string cnbDir string - cnbEnvDir string binaryPath string stackID string planDir string @@ -34,7 +33,6 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { ) it.Before(func() { - var err error workingDir, err = os.Getwd() Expect(err).NotTo(HaveOccurred()) @@ -55,28 +53,8 @@ func testDetect(t *testing.T, context spec.G, it spec.S) { stackID = "io.packit.test.stack" Expect(os.Setenv("CNB_STACK_ID", stackID)).To(Succeed()) - //Separate, but valid CNB dir for testing env parsing - cnbEnvDir, err = os.MkdirTemp("", "cnbEnv") - Expect(err).NotTo(HaveOccurred()) - binaryPath = filepath.Join(cnbDir, "bin", "detect") - bpTOMLContent := []byte(` -api = "0.5" -[buildpack] - id = "some-id" - name = "some-name" - version = "some-version" - clear-env = false -`) - Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOMLContent, 0600)).To(Succeed()) - Expect(os.WriteFile(filepath.Join(cnbEnvDir, "buildpack.toml"), bpTOMLContent, 0600)).To(Succeed()) - - planDir, err = os.MkdirTemp("", "buildplan.toml") - Expect(err).NotTo(HaveOccurred()) - - planPath = filepath.Join(planDir, "buildplan.toml") - exitHandler = &fakes.ExitHandler{} }) @@ -84,61 +62,146 @@ api = "0.5" Expect(os.Chdir(workingDir)).To(Succeed()) Expect(os.RemoveAll(tmpDir)).To(Succeed()) Expect(os.RemoveAll(cnbDir)).To(Succeed()) - Expect(os.RemoveAll(planDir)).To(Succeed()) Expect(os.RemoveAll(platformDir)).To(Succeed()) Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) }) - context("when providing the detect context to the given DetectFunc", func() { - it("succeeds", func() { - var context packit.DetectContext + context("when detect within an extension", func() { - packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { - context = ctx + it.Before(func() { + extTOMLContent := []byte(` +api = "0.9" +[extension] + id = "some-id" + name = "some-name" + version = "some-version" +`) + Expect(os.WriteFile(filepath.Join(cnbDir, "extension.toml"), extTOMLContent, 0600)).To(Succeed()) + Expect(os.Setenv("CNB_EXTENSION_DIR", cnbDir)) + planDir, err = os.MkdirTemp("", "buildplan.toml") + Expect(err).NotTo(HaveOccurred()) - return packit.DetectResult{}, nil - }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) + planPath = filepath.Join(planDir, "buildplan.toml") - Expect(context).To(Equal(packit.DetectContext{ - WorkingDir: tmpDir, - CNBPath: cnbDir, - Platform: packit.Platform{ - Path: platformDir, - }, - BuildpackInfo: packit.BuildpackInfo{ - ID: "some-id", - Name: "some-name", - Version: "some-version", - }, - Stack: stackID, - })) + }) + + it.After(func() { + Expect(os.RemoveAll(planDir)).To(Succeed()) + Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed()) + }) + + context("when providing the detect context to the given DetectFunc", func() { + it("succeeds", func() { + var context packit.DetectContext + + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + context = ctx + + return packit.DetectResult{}, nil + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + Expect(context).To(Equal(packit.DetectContext{ + WorkingDir: tmpDir, + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Info: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Stack: stackID, + })) + }) }) }) - it("writes out the buildplan.toml", func() { - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { - return packit.DetectResult{ - Plan: packit.BuildPlan{ - Provides: []packit.BuildPlanProvision{ - {Name: "some-provision"}, + context("when detect within a buildpack", func() { + it.Before(func() { + bpTOMLContent := []byte(` +api = "0.5" +[buildpack] + id = "some-id" + name = "some-name" + version = "some-version" + clear-env = false +`) + Expect(os.WriteFile(filepath.Join(cnbDir, "buildpack.toml"), bpTOMLContent, 0600)).To(Succeed()) + + planDir, err = os.MkdirTemp("", "buildplan.toml") + Expect(err).NotTo(HaveOccurred()) + + planPath = filepath.Join(planDir, "buildplan.toml") + }) + + it.After(func() { + Expect(os.RemoveAll(planDir)).To(Succeed()) + }) + + context("when providing the detect context to the given DetectFunc", func() { + it("succeeds", func() { + var context packit.DetectContext + + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + context = ctx + + return packit.DetectResult{}, nil + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + Expect(context).To(Equal(packit.DetectContext{ + WorkingDir: tmpDir, + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", }, - Requires: []packit.BuildPlanRequirement{ - { - Name: "some-requirement", - Metadata: map[string]string{ - "version": "some-version", - "some-key": "some-value", + Info: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Stack: stackID, + })) + }) + }) + + it("writes out the buildplan.toml", func() { + packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{ + Plan: packit.BuildPlan{ + Provides: []packit.BuildPlanProvision{ + {Name: "some-provision"}, + }, + Requires: []packit.BuildPlanRequirement{ + { + Name: "some-requirement", + Metadata: map[string]string{ + "version": "some-version", + "some-key": "some-value", + }, }, }, }, - }, - }, nil - }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) + }, nil + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) - contents, err := os.ReadFile(planPath) - Expect(err).NotTo(HaveOccurred()) + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + contents, err := os.ReadFile(planPath) + Expect(err).NotTo(HaveOccurred()) - Expect(string(contents)).To(MatchTOML(` + Expect(string(contents)).To(MatchTOML(` [[provides]] name = "some-provision" @@ -149,62 +212,63 @@ api = "0.5" version = "some-version" some-key = "some-value" `)) - }) + }) - it("writes out the buildplan.toml with multiple plans", func() { - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { - return packit.DetectResult{ - Plan: packit.BuildPlan{ - Provides: []packit.BuildPlanProvision{ - {Name: "some-provision"}, - }, - Requires: []packit.BuildPlanRequirement{ - { - Name: "some-requirement", - Metadata: map[string]string{ - "version": "some-version", - "some-key": "some-value", - }, + it("writes out the buildplan.toml with multiple plans", func() { + packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{ + Plan: packit.BuildPlan{ + Provides: []packit.BuildPlanProvision{ + {Name: "some-provision"}, }, - }, - Or: []packit.BuildPlan{ - { - Provides: []packit.BuildPlanProvision{ - {Name: "some-other-provision"}, - }, - Requires: []packit.BuildPlanRequirement{ - { - Name: "some-other-requirement", - Metadata: map[string]string{ - "version": "some-other-version", - "some-other-key": "some-other-value", - }, + Requires: []packit.BuildPlanRequirement{ + { + Name: "some-requirement", + Metadata: map[string]string{ + "version": "some-version", + "some-key": "some-value", }, }, }, - { - Provides: []packit.BuildPlanProvision{ - {Name: "some-another-provision"}, + Or: []packit.BuildPlan{ + { + Provides: []packit.BuildPlanProvision{ + {Name: "some-other-provision"}, + }, + Requires: []packit.BuildPlanRequirement{ + { + Name: "some-other-requirement", + Metadata: map[string]string{ + "version": "some-other-version", + "some-other-key": "some-other-value", + }, + }, + }, }, - Requires: []packit.BuildPlanRequirement{ - { - Name: "some-another-requirement", - Metadata: map[string]string{ - "version": "some-another-version", - "some-another-key": "some-another-value", + { + Provides: []packit.BuildPlanProvision{ + {Name: "some-another-provision"}, + }, + Requires: []packit.BuildPlanRequirement{ + { + Name: "some-another-requirement", + Metadata: map[string]string{ + "version": "some-another-version", + "some-another-key": "some-another-value", + }, }, }, }, }, }, - }, - }, nil - }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) + }, nil + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) - contents, err := os.ReadFile(planPath) - Expect(err).NotTo(HaveOccurred()) + contents, err := os.ReadFile(planPath) + Expect(err).NotTo(HaveOccurred()) - Expect(string(contents)).To(MatchTOML(` + Expect(string(contents)).To(MatchTOML(` [[provides]] name = "some-provision" @@ -238,95 +302,103 @@ api = "0.5" version = "some-another-version" some-another-key = "some-another-value" `)) - }) - - context("when CNB_BUILDPACK_DIR is set", func() { - it.Before(func() { - Expect(os.Setenv("CNB_BUILDPACK_DIR", cnbEnvDir)).To(Succeed()) }) - it.After(func() { - Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) - }) + context("when CNB_BUILDPACK_DIR is set", func() { + it.Before(func() { + Expect(os.Setenv("CNB_BUILDPACK_DIR", cnbDir)).To(Succeed()) + }) - it("the Detect context receives the correct value", func() { - var context packit.DetectContext + it.After(func() { + Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed()) + }) - packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { - context = ctx + it("the Detect context receives the correct value", func() { + var context packit.DetectContext - return packit.DetectResult{}, nil - }, packit.WithArgs([]string{binaryPath, platformDir, planPath})) + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + context = ctx - Expect(context).To(Equal(packit.DetectContext{ - WorkingDir: tmpDir, - CNBPath: cnbEnvDir, - Platform: packit.Platform{ - Path: platformDir, - }, - BuildpackInfo: packit.BuildpackInfo{ - ID: "some-id", - Name: "some-name", - Version: "some-version", - }, - Stack: stackID, - })) + return packit.DetectResult{}, nil + }, packit.WithArgs([]string{"env-var-override", platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + Expect(context).To(Equal(packit.DetectContext{ + WorkingDir: tmpDir, + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + BuildpackInfo: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Info: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Stack: stackID, + })) + }) }) - }) - context("when the DetectFunc returns an error", func() { - it("calls the ExitHandler with that error", func() { - packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { - return packit.DetectResult{}, errors.New("failed to detect") - }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + context("when CNB_PLATFORM_DIR is set", func() { + it.Before(func() { + Expect(os.Setenv("CNB_PLATFORM_DIR", platformDir)).To(Succeed()) + }) - Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("failed to detect")) - }) - }) + it.After(func() { + Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed()) + }) - context("when the DetectFunc fails", func() { - it("calls the ExitHandler with the correct exit code", func() { - var exitCode int - buffer := bytes.NewBuffer(nil) - - packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { - return packit.DetectResult{}, packit.Fail.WithMessage("failure message") - }, - packit.WithArgs([]string{binaryPath, platformDir, planPath}), - packit.WithExitHandler( - internal.NewExitHandler( - internal.WithExitHandlerExitFunc(func(code int) { - exitCode = code - }), - internal.WithExitHandlerStderr(buffer), - ), - ), - ) + it("the Detect context receives the correct value", func() { + var context packit.DetectContext - Expect(exitCode).To(Equal(100)) - Expect(buffer.String()).To(Equal("failure message\n")) - }) - }) + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + context = ctx - context("failure cases", func() { - context("when the buildpack.toml cannot be read", func() { - it("returns an error", func() { - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { return packit.DetectResult{}, nil - }, packit.WithArgs([]string{binaryPath, platformDir, "/no/such/plan/path"}), packit.WithExitHandler(exitHandler)) - - Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("no such file or directory"))) + }, packit.WithArgs([]string{binaryPath, "env-var-override", planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + Expect(context).To(Equal(packit.DetectContext{ + WorkingDir: tmpDir, + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Info: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Stack: stackID, + })) }) }) - context("when the buildplan.toml cannot be opened", func() { + context("when CNB_BUILD_PLAN_PATH is set", func() { it.Before(func() { - _, err := os.OpenFile(planPath, os.O_CREATE|os.O_RDWR, 0000) - Expect(err).NotTo(HaveOccurred()) + Expect(os.Setenv("CNB_BUILD_PLAN_PATH", planPath)).To(Succeed()) + }) + + it.After(func() { + Expect(os.Unsetenv("CNB_BUILD_PLAN_PATH")).To(Succeed()) }) - it("returns an error", func() { - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { + it("the Detect context receives the correct value", func() { + var context packit.DetectContext + + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + context = ctx + return packit.DetectResult{ Plan: packit.BuildPlan{ Provides: []packit.BuildPlanProvision{ @@ -343,31 +415,140 @@ api = "0.5" }, }, }, nil + }, packit.WithArgs([]string{binaryPath, platformDir, "env-var-override"}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + Expect(context).To(Equal(packit.DetectContext{ + WorkingDir: tmpDir, + CNBPath: cnbDir, + Platform: packit.Platform{ + Path: platformDir, + }, + BuildpackInfo: packit.BuildpackInfo{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Info: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + }, + Stack: stackID, + })) + + contents, err := os.ReadFile(planPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(contents)).To(MatchTOML(` +[[provides]] + name = "some-provision" + +[[requires]] + name = "some-requirement" + +[requires.metadata] + version = "some-version" + some-key = "some-value" +`)) + }) + }) + + context("when the DetectFunc returns an error", func() { + it("calls the ExitHandler with that error", func() { + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{}, errors.New("failed to detect") }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) - Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("failed to detect")) }) }) - context("when the buildplan.toml cannot be encoded", func() { - it("returns an error", func() { - packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { - return packit.DetectResult{ - Plan: packit.BuildPlan{ - Provides: []packit.BuildPlanProvision{ - {Name: "some-provision"}, + context("when the DetectFunc fails", func() { + it("calls the ExitHandler with the correct exit code", func() { + var exitCode int + buffer := bytes.NewBuffer(nil) + + packit.Detect(func(ctx packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{}, packit.Fail.WithMessage("failure message") + }, + packit.WithArgs([]string{binaryPath, platformDir, planPath}), + packit.WithExitHandler( + internal.NewExitHandler( + internal.WithExitHandlerExitFunc(func(code int) { + exitCode = code + }), + internal.WithExitHandlerStderr(buffer), + ), + ), + ) + + Expect(exitCode).To(Equal(100)) + Expect(buffer.String()).To(Equal("failure message\n")) + }) + }) + + context("failure cases", func() { + context("when the buildpack.toml cannot be read", func() { + it("returns an error", func() { + packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{}, nil + }, packit.WithArgs([]string{binaryPath, platformDir, "/no/such/plan/path"}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("no such file or directory"))) + }) + }) + + context("when the buildplan.toml cannot be opened", func() { + it.Before(func() { + _, err := os.OpenFile(planPath, os.O_CREATE|os.O_RDWR, 0000) + Expect(err).NotTo(HaveOccurred()) + }) + + it("returns an error", func() { + packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{ + Plan: packit.BuildPlan{ + Provides: []packit.BuildPlanProvision{ + {Name: "some-provision"}, + }, + Requires: []packit.BuildPlanRequirement{ + { + Name: "some-requirement", + Metadata: map[string]string{ + "version": "some-version", + "some-key": "some-value", + }, + }, + }, }, - Requires: []packit.BuildPlanRequirement{ - { - Name: "some-requirement", - Metadata: map[int]int{}, + }, nil + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("permission denied"))) + }) + }) + + context("when the buildplan.toml cannot be encoded", func() { + it("returns an error", func() { + packit.Detect(func(packit.DetectContext) (packit.DetectResult, error) { + return packit.DetectResult{ + Plan: packit.BuildPlan{ + Provides: []packit.BuildPlanProvision{ + {Name: "some-provision"}, + }, + Requires: []packit.BuildPlanRequirement{ + { + Name: "some-requirement", + Metadata: map[int]int{}, + }, }, }, - }, - }, nil - }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) + }, nil + }, packit.WithArgs([]string{binaryPath, platformDir, planPath}), packit.WithExitHandler(exitHandler)) - Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("cannot encode a map with non-string key type"))) + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("cannot encode a map with non-string key type"))) + }) }) }) }) diff --git a/doc.go b/doc.go index 43863ac0..7f2f9568 100644 --- a/doc.go +++ b/doc.go @@ -24,7 +24,7 @@ // "os" // "path/filepath" // -// "github.com/paketo-buildpacks/packit" +// "github.com/paketo-buildpacks/packit/v2" // ) // // func main() { @@ -96,7 +96,7 @@ // // package main // -// import "github.com/paketo-buildpacks/packit" +// import "github.com/paketo-buildpacks/packit/v2" // // func main() { // // The build phase includes the yarn cli in a new layer that is made @@ -162,7 +162,7 @@ // // package main // -// import "github.com/paketo-buildpacks/packit" +// import "github.com/paketo-buildpacks/packit/v2" // // func main() { // detect := func(context packit.DetectContext) (packit.DetectResult, error) { diff --git a/draft/example_test.go b/draft/example_test.go index 4d81cfd9..c933a36a 100644 --- a/draft/example_test.go +++ b/draft/example_test.go @@ -4,8 +4,8 @@ import ( "fmt" "regexp" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/draft" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/draft" ) func ExamplePlanner_Resolve() { diff --git a/draft/planner.go b/draft/planner.go index 6ea188b4..8ae212a1 100644 --- a/draft/planner.go +++ b/draft/planner.go @@ -7,7 +7,7 @@ import ( "regexp" "sort" - "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/v2" ) // A Planner sorts buildpack plan entries using a given list of priorities. A diff --git a/draft/planner_test.go b/draft/planner_test.go index 63d7a7cd..077789b8 100644 --- a/draft/planner_test.go +++ b/draft/planner_test.go @@ -4,8 +4,8 @@ import ( "regexp" "testing" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/draft" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/draft" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/environment_test.go b/environment_test.go index 912c62a1..ed9b1c0f 100644 --- a/environment_test.go +++ b/environment_test.go @@ -3,7 +3,7 @@ package packit_test import ( "testing" - "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/v2" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/fail.go b/fail.go index ae69c0bd..09905838 100644 --- a/fail.go +++ b/fail.go @@ -1,6 +1,6 @@ package packit -import "github.com/paketo-buildpacks/packit/internal" +import "github.com/paketo-buildpacks/packit/v2/internal" // Fail is a sentinal value that can be used to indicate a failure to detect // during the detect phase. Fail implements the Error interface and should be diff --git a/fakes/some-executable/main.go b/fakes/some-executable/main.go index 8407edf8..b17eaf7f 100644 --- a/fakes/some-executable/main.go +++ b/fakes/some-executable/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "os" ) @@ -12,6 +13,14 @@ func main() { fmt.Fprintf(os.Stderr, "Output on stderr\n") fmt.Printf("Arguments: %v\n", os.Args) + stdin, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Printf("Input on stdin\n%s\n", stdin) + pwd, _ := os.Getwd() fmt.Printf("PWD=%s\n", pwd) diff --git a/fs/checksum_calculator_test.go b/fs/checksum_calculator_test.go index 2240cc22..e0bfdd62 100644 --- a/fs/checksum_calculator_test.go +++ b/fs/checksum_calculator_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/fs" + "github.com/paketo-buildpacks/packit/v2/fs" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/fs/copy_test.go b/fs/copy_test.go index 669ac955..6802cf4d 100644 --- a/fs/copy_test.go +++ b/fs/copy_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/fs" + "github.com/paketo-buildpacks/packit/v2/fs" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/fs/exists_test.go b/fs/exists_test.go index 8c6bcafa..ca55e817 100644 --- a/fs/exists_test.go +++ b/fs/exists_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/fs" + "github.com/paketo-buildpacks/packit/v2/fs" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/fs/is_empty_dir_test.go b/fs/is_empty_dir_test.go index 7f278c75..1398e2dd 100644 --- a/fs/is_empty_dir_test.go +++ b/fs/is_empty_dir_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/fs" + "github.com/paketo-buildpacks/packit/v2/fs" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/fs/move_test.go b/fs/move_test.go index fa317186..f7cd8b42 100644 --- a/fs/move_test.go +++ b/fs/move_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/fs" + "github.com/paketo-buildpacks/packit/v2/fs" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/generate.go b/generate.go new file mode 100644 index 00000000..241a573e --- /dev/null +++ b/generate.go @@ -0,0 +1,159 @@ +package packit + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/paketo-buildpacks/packit/v2/internal" +) + +// GenerateFunc is the definition of a callback that can be invoked when the Generate +// function is executed. Extension authors should implement a GenerateFunc that +// performs the specific generate phase operations for that extension. +type GenerateFunc func(GenerateContext) (GenerateResult, error) + +// GenerateContext provides the contextual details that are made available by the +// extension lifecycle during the generate phase. This context is populated by the +// Generate function and passed to GenerateFunc during execution. +type GenerateContext struct { + // Info includes the details of the buildpack parsed from the + // extension.toml included in the extension contents. + Info Info + + // CNBPath is the absolute path location of the buildpack contents. + // This path is useful for finding the buildpack.toml or any other + // files included in the buildpack. + CNBPath string + + // Platform includes the platform context according to the specification: + // https://github.com/buildpacks/spec/blob/main/buildpack.md#build + Platform Platform + + // Plan includes the BuildpackPlan provided by the lifecycle as specified in + // the specification: + // https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpack-plan-toml. + Plan BuildpackPlan + + // Stack is the value of the chosen stack. This value is populated from the + // $CNB_STACK_ID environment variable. + Stack string + + // WorkingDir is the location of the application source code as provided by + // the lifecycle. + WorkingDir string +} + +// GenerateResult allows extension authors to indicate the result of the generate +// phase for a given extension. This result, returned in a GenerateFunc callback, +// will be parsed and persisted by the Generate function and returned to the +// lifecycle at the end of the generate phase execution. +type GenerateResult struct { + // ExtendConfig contains the config of an extension + ExtendConfig ExtendConfig + + // BuildDockerfile the Dockerfile to define the build image + BuildDockerfile io.Reader + // RunDockerfile the Dockerfile to define the run image + RunDockerfile io.Reader +} + +type ExtendConfig struct { + Build ExtendImageConfig `toml:"build"` +} + +type ExtendImageConfig struct { + Args []ExtendImageConfigArg `toml:"args"` +} + +type ExtendImageConfigArg struct { + Name string `toml:"name"` + Value string `toml:"value"` +} + +// Generate is an implementation of the generate phase according to the Cloud Native +// Buildpacks specification. Calling this function with a GenerateFunc will +// perform the generate phase process of an extension. +func Generate(f GenerateFunc, options ...Option) { + config := OptionConfig{ + exitHandler: internal.NewExitHandler(), + args: os.Args, + tomlWriter: internal.NewTOMLWriter(), + envWriter: internal.NewEnvironmentWriter(), + fileWriter: internal.NewFileWriter(), + } + + for _, option := range options { + config = option(config) + } + + pwd, err := os.Getwd() + if err != nil { + config.exitHandler.Error(err) + return + } + + planPath := os.Getenv("CNB_BP_PLAN_PATH") + + var plan BuildpackPlan + _, err = toml.DecodeFile(planPath, &plan) + if err != nil { + config.exitHandler.Error(err) + return + } + + cnbPath := os.Getenv("CNB_EXTENSION_DIR") + outputPath := os.Getenv("CNB_OUTPUT_DIR") + platformPath := os.Getenv("CNB_PLATFORM_DIR") + + var info struct { + APIVersion string `toml:"api"` + Info Info `toml:"extension"` + } + + extensionTOML := filepath.Join(cnbPath, "extension.toml") + _, err = toml.DecodeFile(extensionTOML, &info) + if err != nil { + config.exitHandler.Error(fmt.Errorf("could not parse %q: %w", extensionTOML, err)) + return + } + + result, err := f(GenerateContext{ + CNBPath: cnbPath, + Platform: Platform{ + Path: platformPath, + }, + Stack: os.Getenv("CNB_STACK_ID"), + WorkingDir: pwd, + Plan: plan, + Info: info.Info, + }) + if err != nil { + config.exitHandler.Error(err) + return + } + + if result.BuildDockerfile != nil { + err = config.fileWriter.Write(filepath.Join(outputPath, "build.Dockerfile"), result.BuildDockerfile) + if err != nil { + config.exitHandler.Error(err) + return + } + } + if result.RunDockerfile != nil { + err = config.fileWriter.Write(filepath.Join(outputPath, "run.Dockerfile"), result.RunDockerfile) + if err != nil { + config.exitHandler.Error(err) + return + } + } + + err = config.tomlWriter.Write(filepath.Join(outputPath, "extend-config.toml"), result.ExtendConfig) + if err != nil { + config.exitHandler.Error(err) + return + } + +} diff --git a/generate_test.go b/generate_test.go new file mode 100644 index 00000000..72e15f0d --- /dev/null +++ b/generate_test.go @@ -0,0 +1,193 @@ +package packit_test + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/fakes" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testGenerate(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + workingDir string + platformDir string + tmpDir string + planPath string + cnbDir string + binaryPath string + exitHandler *fakes.ExitHandler + ) + + it.Before(func() { + var err error + workingDir, err = os.Getwd() + Expect(err).NotTo(HaveOccurred()) + + tmpDir, err = os.MkdirTemp("", "working-dir") + Expect(err).NotTo(HaveOccurred()) + + tmpDir, err = filepath.EvalSymlinks(tmpDir) + Expect(err).NotTo(HaveOccurred()) + + Expect(os.Chdir(tmpDir)).To(Succeed()) + + platformDir, err = os.MkdirTemp("", "platform") + Expect(err).NotTo(HaveOccurred()) + + file, err := os.CreateTemp("", "plan.toml") + Expect(err).NotTo(HaveOccurred()) + defer file.Close() + + _, err = file.WriteString(` +[[entries]] + name = "some-entry" + +[entries.metadata] + version = "some-version" + some-key = "some-value" +`) + Expect(err).NotTo(HaveOccurred()) + + planPath = file.Name() + + cnbDir, err = os.MkdirTemp("", "cnb") + Expect(err).NotTo(HaveOccurred()) + + extTOML := []byte(` +api = "0.9" +[extension] + id = "some-id" + name = "some-name" + version = "some-version" + homepage = "some-homepage" + description = "some-description" + keywords = ["some-keyword"] + + [[extension.licenses]] + type = "some-license-type" + uri = "some-license-uri" +`) + Expect(os.WriteFile(filepath.Join(cnbDir, "extension.toml"), extTOML, 0600)).To(Succeed()) + + binaryPath = filepath.Join(cnbDir, "bin", "generate") + + Expect(os.Setenv("CNB_STACK_ID", "some-stack")).To(Succeed()) + Expect(os.Setenv("CNB_BP_PLAN_PATH", planPath)).To(Succeed()) + Expect(os.Setenv("CNB_PLATFORM_DIR", platformDir)).To(Succeed()) + Expect(os.Setenv("CNB_EXTENSION_DIR", cnbDir)).To(Succeed()) + + exitHandler = &fakes.ExitHandler{} + exitHandler.ErrorCall.Stub = func(err error) { + Expect(err).NotTo(HaveOccurred()) + } + + }) + + it.After(func() { + Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed()) + Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed()) + Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed()) + Expect(os.Unsetenv("CNB_EXTENSION_DIR")).To(Succeed()) + + Expect(os.Chdir(workingDir)).To(Succeed()) + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + Expect(os.RemoveAll(platformDir)).To(Succeed()) + }) + + it("provides the generate context to the given GenerateFunc", func() { + var context packit.GenerateContext + packit.Generate(func(ctx packit.GenerateContext) (packit.GenerateResult, error) { + context = ctx + + return packit.GenerateResult{}, nil + }, packit.WithArgs([]string{binaryPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) + Expect(context).To(Equal(packit.GenerateContext{ + CNBPath: cnbDir, + Stack: "some-stack", + Platform: packit.Platform{ + Path: platformDir, + }, + WorkingDir: tmpDir, + Plan: packit.BuildpackPlan{ + Entries: []packit.BuildpackPlanEntry{ + { + Name: "some-entry", + Metadata: map[string]interface{}{ + "version": "some-version", + "some-key": "some-value", + }, + }, + }, + }, + Info: packit.Info{ + ID: "some-id", + Name: "some-name", + Version: "some-version", + Homepage: "some-homepage", + Description: "some-description", + Keywords: []string{"some-keyword"}, + Licenses: []packit.BuildpackInfoLicense{ + { + Type: "some-license-type", + URI: "some-license-uri", + }, + }, + }, + })) + }) + + context("failure cases", func() { + context("when the buildpack plan.toml is malformed", func() { + it.Before(func() { + err := os.WriteFile(planPath, []byte("%%%"), 0600) + Expect(err).NotTo(HaveOccurred()) + exitHandler.ErrorCall.Stub = nil + }) + + it("calls the exit handler", func() { + packit.Generate(func(ctx packit.GenerateContext) (packit.GenerateResult, error) { + return packit.GenerateResult{}, nil + }, packit.WithArgs([]string{binaryPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("expected '.' or '=', but got '%' instead"))) + }) + }) + + context("when the generate func returns an error", func() { + it("calls the exit handler", func() { + exitHandler.ErrorCall.Stub = nil + packit.Generate(func(ctx packit.GenerateContext) (packit.GenerateResult, error) { + return packit.GenerateResult{}, errors.New("generate failed") + }, packit.WithArgs([]string{binaryPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("generate failed")) + }) + }) + + context("when the exension.toml is malformed", func() { + it.Before(func() { + err := os.WriteFile(filepath.Join(cnbDir, "extension.toml"), []byte("%%%"), 0600) + Expect(err).NotTo(HaveOccurred()) + exitHandler.ErrorCall.Stub = nil + }) + + it("calls the exit handler", func() { + packit.Generate(func(ctx packit.GenerateContext) (packit.GenerateResult, error) { + return packit.GenerateResult{}, nil + }, packit.WithArgs([]string{binaryPath}), packit.WithExitHandler(exitHandler)) + + Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError(ContainSubstring("expected '.' or '=', but got '%' instead"))) + }) + }) + }) +} diff --git a/go.mod b/go.mod index 7f8a2061..f1387300 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,125 @@ -module github.com/paketo-buildpacks/packit +module github.com/paketo-buildpacks/packit/v2 -go 1.16 +go 1.23.0 require ( - github.com/BurntSushi/toml v0.4.1 - github.com/Masterminds/semver/v3 v3.1.1 - github.com/anchore/syft v0.31.0 - github.com/cheggaaa/pb/v3 v3.0.8 + github.com/BurntSushi/toml v1.4.0 + github.com/Masterminds/semver/v3 v3.2.1 + 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-20230412183729-8602f1afc574 + github.com/anchore/syft v0.80.0 + github.com/apex/log v1.9.0 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 - github.com/gabriel-vasile/mimetype v1.4.0 - github.com/onsi/gomega v1.17.0 - github.com/pelletier/go-toml v1.9.4 + github.com/gabriel-vasile/mimetype v1.4.5 + github.com/google/uuid v1.6.0 + github.com/onsi/gomega v1.34.1 + github.com/pelletier/go-toml v1.9.5 github.com/sclevine/spec v1.4.0 - github.com/ulikunitz/xz v0.5.10 + github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e + github.com/sergi/go-diff v1.3.1 + github.com/spdx/tools-golang v0.5.0 + github.com/stretchr/testify v1.9.0 + github.com/ulikunitz/xz v0.5.12 ) -// TODO: remove once a new release is cut with the CycloneDX format (probably v0.32.0) -replace github.com/anchore/syft => github.com/anchore/syft v0.31.1-0.20211204010623-5374a1dc6ff6 +require ( + github.com/CycloneDX/cyclonedx-go v0.7.1 // indirect + github.com/DataDog/zstd v1.4.5 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/acobaugh/osrelease v0.1.0 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 // indirect + github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect + github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/becheran/wildmatch-go v1.0.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect + github.com/cloudflare/circl v1.1.0 // indirect + github.com/containerd/containerd v1.7.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect + github.com/docker/cli v23.0.1+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v23.0.5+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-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/facebookincubator/nvdtools v0.1.5 // indirect + github.com/go-git/gcfg v1.5.0 // indirect + github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-git/go-git/v5 v5.6.1 // indirect + github.com/go-restruct/restruct v1.2.0-alpha // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-containerregistry v0.14.0 // indirect + github.com/google/licensecheck v0.3.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jinzhu/copier v0.3.5 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.16.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b // indirect + github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mholt/archiver/v3 v3.5.1 // indirect + github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sassoftware/go-rpmutils v0.2.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/skeema/knownhosts v1.1.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/sylabs/sif/v2 v2.8.1 // indirect + github.com/sylabs/squashfs v0.6.1 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/vbatts/go-mtree v0.5.3 // indirect + github.com/vbatts/tar-split v0.11.2 // indirect + github.com/vifraa/gopom v0.2.1 // indirect + github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 // indirect + github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.23.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd // indirect + google.golang.org/grpc v1.54.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index cada2c88..cf6692d9 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -16,6 +16,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -26,6 +27,8 @@ cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aD cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -34,7 +37,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -44,129 +47,102 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +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 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CycloneDX/cyclonedx-go v0.4.0 h1:Wz4QZ9B4RXGWIWTypVLEOVJgOdFfy5mcS5PGNzUkZxU= -github.com/CycloneDX/cyclonedx-go v0.4.0/go.mod h1:rmRcf//gT7PIzovatusbWi377xqCg1FS4jyST0GH20E= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= -github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= -github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ= -github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= +github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk= -github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa h1:mDLUAkgXsV5Z8D0EEj8eS6FBekolV/A+Xxbs9054bPw= -github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa/go.mod h1:29jwxTSAS6pBcrmuwf1U3r1Tqp1o1XpuiOJ0NT9NoGg= -github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63 h1:C9W/LAydEz/qdUhx1MdjO9l8NEcFKYknkxDVyo9LAoM= -github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63/go.mod h1:6qH8c6U/3CBVvDDDBZnPSTbTINq3cIdADUYTaVf75EM= +github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 h1:imgMA0gN0TZx7PSa/pdWqXadBvrz8WsN6zySzCe4XX0= +github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8/go.mod h1:+gPap4jha079qzRTUaehv+UZ6sSdaNwkH0D3b6zhTuk= +github.com/anchore/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= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8= github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= -github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk= -github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0= -github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89 h1:pcMFN7wjIQIxdZm8NA0R3JiRRFn5Eyx9mhagHOVS4Bw= -github.com/anchore/stereoscope v0.0.0-20211203160213-5a5e323a5c89/go.mod h1:FNm1rtauEjkC3elA3jr3bJkU+/4QiovApwJdPnHQ9x0= -github.com/anchore/syft v0.31.1-0.20211204010623-5374a1dc6ff6 h1:FmUWw+gcHKK5x3238eMRmOvJFcW3j1tUs+ab7hpq6V4= -github.com/anchore/syft v0.31.1-0.20211204010623-5374a1dc6ff6/go.mod h1:6tuVZBaHohcTuX8S0G6S80o/6PmzoF7sHbjxDUJaLjU= +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-20230412183729-8602f1afc574 h1:VFX+FD9EH6am+tfqwr1KeCAmabAknSJQX95aIY3QJJI= +github.com/anchore/stereoscope v0.0.0-20230412183729-8602f1afc574/go.mod h1:2GGFHkHry/xDlEQgBrVGcarq+z7Z6hLnHdyhcKB2lfQ= +github.com/anchore/syft v0.80.0 h1:2KCRjkxqscMUKLQsR7RTw39YQrxzOipVY1Db+/2Y7Qs= +github.com/anchore/syft v0.80.0/go.mod h1:5zBFVARBz0+C/zwSLibQowriqC2CCca/K38QDfqfo2Y= 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/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= +github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= +github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= +github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= +github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= +github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= -github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= -github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA= -github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= -github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +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.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= -github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= -github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -174,160 +150,43 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= -github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= -github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw= -github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/stargz-snapshotter/estargz v0.10.0 h1:glqzafvxBBAMo+x2w2sdDjUDZeTqqLJmqZPY05qehCU= -github.com/containerd/stargz-snapshotter/estargz v0.10.0/go.mod h1:aE5PCyhFMwR8sbrErO5eM2GcvkyXTTJremG883D4qF0= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg= +github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 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.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= 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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v20.10.10+incompatible h1:kcbwdgWbrBOH8QwQzaJmyriHwF7XIl4HT1qh0HTRys4= -github.com/docker/cli v20.10.10+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo= -github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= -github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= +github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M= +github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= +github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= +github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v23.0.5+incompatible h1:DaxtlTJjFSnLOXVNUBU1+6kXGz2lpDoEAH6QoxaSg8k= +github.com/docker/docker v23.0.5+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-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/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= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -336,70 +195,63 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/facebookincubator/nvdtools v0.1.4 h1:x1Ucw9+bSkMd8DJJN4jNQ1Lk4PSFlJarGOxp9D6WUMo= -github.com/facebookincubator/nvdtools v0.1.4/go.mod h1:0/FIVnSEl9YHXLq3tKBPpKaI0iUceDhdSHPlIwIX44Y= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= +github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ= +github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gabriel-vasile/mimetype v1.3.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -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/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +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.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ= +github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= +github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= +github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-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 v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= -github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +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/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -425,11 +277,13 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -443,12 +297,15 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-containerregistry v0.7.0 h1:u0onUUOcyoCDHEiJoyR1R1gx5er1+r06V5DBhUU5ndk= -github.com/google/go-containerregistry v0.7.0/go.mod h1:2zaoelrL0d08gGbpdP3LqyUuBmhWbpD6IOe2s9nLS2k= +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.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw= +github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= +github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -462,412 +319,338 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/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= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= -github.com/gookit/color v1.2.7 h1:4qePMNWZhrmbfYJDix+J4V2l0iVW+6jQGjicELlN14E= -github.com/gookit/color v1.2.7/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE= +github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/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/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w= -github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU= +github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b h1:boYyvL3tbUuKcMN029mpCl7oYYJ7yIXujLj+fiW4Alc= +github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 h1:tQRHcLQwnwrPq2j2Qra/NnyjyESBGwdeBeVdAE9kXYg= +github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= 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/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2-0.20210730191737-8e42a01fb1b7/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 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/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sassoftware/go-rpmutils v0.2.0 h1:pKW0HDYMFWQ5b4JQPiI3WI12hGsVoW0V8+GMoZiI/JE= +github.com/sassoftware/go-rpmutils v0.2.0/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= -github.com/scylladb/go-set v1.0.2 h1:SkvlMCKhP0wyyct6j+0IHJkBkSZL+TDzZ4E7f7BCcRE= -github.com/scylladb/go-set v1.0.2/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= +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/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +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/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= +github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.1.0 h1:iDMNEPqQk6CdiDj6eWDIDw85j0wQ3IR3pH9p0X05TSQ= -github.com/spdx/tools-golang v0.1.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spdx/tools-golang v0.5.0 h1:/fqihV2Jna7fmow65dHpgKNsilgLK7ICpd2tkCnPEyY= +github.com/spdx/tools-golang v0.5.0/go.mod h1:kkGlrSXXfHwuSzHQZJRV3aKu9ZXCq/MSf2+xyiJH1lM= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/sylabs/sif/v2 v2.8.1 h1:whr4Vz12RXfLnYyVGHoD/rD/hbF2g9OW7BJHa+WIqW8= +github.com/sylabs/sif/v2 v2.8.1/go.mod h1:LQOdYXC9a8i7BleTKRw9lohi0rTbXkJOeS9u0ebvgyM= +github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= +github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc= +github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= +github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= +github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= -github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vbatts/go-mtree v0.5.3 h1:S/jYlfG8rZ+a0bhZd+RANXejy7M4Js8fq9U+XoWTd5w= +github.com/vbatts/go-mtree v0.5.3/go.mod h1:eXsdoPMdL2jcJx6HweWi9lYQxBsTp4lNhqqAjgkZUg8= github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= -github.com/vifraa/gopom v0.1.0 h1:v897eVxf6lflkEXzPmKbo4YhX2oS/LGjz7cqjWnSmCU= -github.com/vifraa/gopom v0.1.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= +github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM= +github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8= github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= -github.com/wagoodman/go-progress v0.0.0-20200621122631-1a2120f0695a/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= -github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 h1:r6BlIP7CVZtMlxUQhT40h1IE1TzEgKVqwmsVGuscvdk= -github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= -github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb/go.mod h1:nDi3BAC5nEbVbg+WSJDHLbjHv0ZToq8nMPA97XMxF3E= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +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/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/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -876,29 +659,35 @@ 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.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +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/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 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= @@ -909,6 +698,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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= @@ -922,6 +713,7 @@ 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= @@ -933,32 +725,29 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -969,12 +758,10 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -984,11 +771,18 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211111160137-58aab5ef257a h1:c83jeVQW0KGKNaKBRfelNYNHaev+qawl9yaA825s8XE= -golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 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= @@ -1000,11 +794,11 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1016,14 +810,15 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1032,37 +827,22 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/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-20190616124812-15dcb6c0061f/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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1071,34 +851,24 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1106,12 +876,33 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 h1:WecRHqgE09JBkh/584XIE6PMz5KKE/vER4izNUi30AQ= -golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 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= @@ -1119,35 +910,34 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/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-20181030221726-6c7e314b6563/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= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1178,21 +968,25 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 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= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1214,14 +1008,17 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1229,13 +1026,11 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -1244,7 +1039,6 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -1264,11 +1058,12 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1287,17 +1082,21 @@ google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211111162719-482062a4217b h1:qvEQEwKjZRAg6rjY/jqfJ7T8/w/D7jTIFJGcaSka96k= -google.golang.org/genproto v0.0.0-20211111162719-482062a4217b/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU= +google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1319,8 +1118,10 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1334,30 +1135,22 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1368,11 +1161,11 @@ 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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= @@ -1382,37 +1175,16 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= +modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/init_test.go b/init_test.go index dc5a7fd8..85682d85 100644 --- a/init_test.go +++ b/init_test.go @@ -11,6 +11,7 @@ func TestUnitPackit(t *testing.T) { suite := spec.New("packit", spec.Report(report.Terminal{})) suite("Build", testBuild) suite("Detect", testDetect) + suite("Generate", testGenerate) suite("Environment", testEnvironment) suite("Layer", testLayer) suite("Layers", testLayers) diff --git a/internal/environment_writer.go b/internal/environment_writer.go index 4ef659c8..674f8b08 100644 --- a/internal/environment_writer.go +++ b/internal/environment_writer.go @@ -1,8 +1,11 @@ package internal import ( + "fmt" "os" "path/filepath" + "regexp" + "strings" ) type EnvironmentWriter struct{} @@ -21,7 +24,15 @@ func (w EnvironmentWriter) Write(dir string, env map[string]string) error { return err } + // this regex checks that map keys contain valid env var name characters, + // per https://pubs.opengroup.org/onlinepubs/9699919799/ + validEnvVarRegex := regexp.MustCompile(`^[a-zA-Z_]{1,}[a-zA-Z0-9_]*$`) + for key, value := range env { + parts := strings.SplitN(key, ".", 2) + if !validEnvVarRegex.MatchString(parts[0]) { + return fmt.Errorf("invalid environment variable name '%s'", parts[0]) + } err := os.WriteFile(filepath.Join(dir, key), []byte(value), 0644) if err != nil { return err diff --git a/internal/environment_writer_test.go b/internal/environment_writer_test.go index 82b38024..646123c0 100644 --- a/internal/environment_writer_test.go +++ b/internal/environment_writer_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -35,18 +35,23 @@ func testEnvironmentWriter(t *testing.T, context spec.G, it spec.S) { it("writes the given environment to a directory", func() { err := writer.Write(tmpDir, map[string]string{ - "some-name": "some-content", - "other-name": "other-content", + "some_name": "some-content", + "OTHER_NAME": "other-content", + "ANOTHER.override": "more-content", }) Expect(err).NotTo(HaveOccurred()) - content, err := os.ReadFile(filepath.Join(tmpDir, "some-name")) + content, err := os.ReadFile(filepath.Join(tmpDir, "some_name")) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal("some-content")) - content, err = os.ReadFile(filepath.Join(tmpDir, "other-name")) + content, err = os.ReadFile(filepath.Join(tmpDir, "OTHER_NAME")) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal("other-content")) + + content, err = os.ReadFile(filepath.Join(tmpDir, "ANOTHER.override")) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(Equal("more-content")) }) it("writes does not create a directory of the env map is empty", func() { @@ -64,8 +69,8 @@ func testEnvironmentWriter(t *testing.T, context spec.G, it spec.S) { it("returns an error", func() { err := writer.Write(filepath.Join(tmpDir, "sub-dir"), map[string]string{ - "some-name": "some-content", - "other-name": "other-content", + "some_name": "some-content", + "OTHER_NAME": "other-content", }) Expect(err).To(MatchError(ContainSubstring("permission denied"))) }) @@ -78,11 +83,20 @@ func testEnvironmentWriter(t *testing.T, context spec.G, it spec.S) { it("returns an error", func() { err := writer.Write(tmpDir, map[string]string{ - "some-name": "some-content", - "other-name": "other-content", + "some_name": "some-content", + "OTHER_NAME": "other-content", }) Expect(err).To(MatchError(ContainSubstring("permission denied"))) }) }) + + context("when env var name is invalid", func() { + it("returns an error", func() { + err := writer.Write(tmpDir, map[string]string{ + "INVA=*LID.override": "more-content", + }) + Expect(err).To(MatchError(ContainSubstring("invalid environment variable name 'INVA=*LID'"))) + }) + }) }) } diff --git a/internal/exit_handler_test.go b/internal/exit_handler_test.go index c61587c0..a7e55d16 100644 --- a/internal/exit_handler_test.go +++ b/internal/exit_handler_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/internal/fail_test.go b/internal/fail_test.go index 055aeb65..16a898e0 100644 --- a/internal/fail_test.go +++ b/internal/fail_test.go @@ -3,7 +3,7 @@ package internal_test import ( "testing" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/internal/file_writer_test.go b/internal/file_writer_test.go index 1af53b79..3622876b 100644 --- a/internal/file_writer_test.go +++ b/internal/file_writer_test.go @@ -8,7 +8,7 @@ import ( "testing" "testing/iotest" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/internal/toml_writer_test.go b/internal/toml_writer_test.go index e557c1ec..6630f5ab 100644 --- a/internal/toml_writer_test.go +++ b/internal/toml_writer_test.go @@ -5,11 +5,11 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" "github.com/sclevine/spec" . "github.com/onsi/gomega" - . "github.com/paketo-buildpacks/packit/matchers" + . "github.com/paketo-buildpacks/packit/v2/matchers" ) func testTOMLWriter(t *testing.T, context spec.G, it spec.S) { diff --git a/launch_metadata.go b/launch_metadata.go index 2642f2c4..dc6fe08e 100644 --- a/launch_metadata.go +++ b/launch_metadata.go @@ -8,6 +8,10 @@ type LaunchMetadata struct { // be executed during the launch phase. Processes []Process + // DirectProcesses is a list of processes that will be returned to the lifecycle to + // be executed directly during the launch phase. + DirectProcesses []DirectProcess + // Slices is a list of slices that will be returned to the lifecycle to be // exported as separate layers during the export phase. Slices []Slice @@ -31,5 +35,5 @@ func (l LaunchMetadata) isEmpty() bool { sbom = l.SBOM.Formats() } - return len(sbom)+len(l.Processes)+len(l.Slices)+len(l.Labels)+len(l.BOM) == 0 + return len(sbom)+len(l.Processes)+len(l.DirectProcesses)+len(l.Slices)+len(l.Labels)+len(l.BOM) == 0 } diff --git a/layer.go b/layer.go index 3637b02b..4ce04eb3 100644 --- a/layer.go +++ b/layer.go @@ -68,6 +68,25 @@ type Layer struct { // SBOM is a type that implements SBOMFormatter and declares the formats that // bill-of-materials should be output for the layer SBoM. SBOM SBOMFormatter + + // ExecD references Exec.D scripts or executables by fully-qualified file path. + // These will be executed in `alphabetically ascending order by file name` as per the buildpack launch mechanism. + // See https://github.com/buildpacks/spec/blob/main/buildpack.md#launch. + // The listed executables will be given a numerical prefix so that they will be executed in the slice order. + // E.g. []string{"/helper", "/bin/command", "/${cnbPath}/other-process"} will result in: + // - /${Name}/exec.d/0-helper + // - /${Name}/exec.d/1-command + // - /${Name}/exec.d/2-other-process + // Do not forget to add these files to the `buildpack.toml` so they are available. + // If enough executables are given, this function will pad the prefix with zeros so that they are in the correct + // alphabetically ascending order. + // E.g. []string{"/cmd0", ... , "/cmd10", ..., "/cmd100"} will result in: + // - /${Name}/exec.d/000-cmd0 + // - /${Name}/exec.d/010-cmd10 + // - /${Name}/exec.d/100-cmd100 + // ExecD is only recognized when the buildpack API is v0.5+ + // https://buildpacks.io/docs/reference/spec/migration/buildpack-api-0.4-0.5/#execd + ExecD []string } // Reset clears the state of a layer such that the layer can be replaced with diff --git a/layer_test.go b/layer_test.go index ee06efde..0d3d639c 100644 --- a/layer_test.go +++ b/layer_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/v2" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/layers_test.go b/layers_test.go index 503d0b1c..e1039511 100644 --- a/layers_test.go +++ b/layers_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit" + "github.com/paketo-buildpacks/packit/v2" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/paketosbom/sbom.go b/paketosbom/sbom.go index 3c8d7eb9..a0d929b8 100644 --- a/paketosbom/sbom.go +++ b/paketosbom/sbom.go @@ -60,7 +60,7 @@ func GetBOMChecksumAlgorithm(alg string) (algorithm, error) { } } - return "", fmt.Errorf("failed to get supported BOM checksum algorithm: %s is not valid", alg) + return UNKNOWN, fmt.Errorf("failed to get supported BOM checksum algorithm: %s is not valid", alg) } const ( @@ -76,4 +76,5 @@ const ( BLAKE2B512 algorithm = "BLAKE2b-512" BLAKE3 algorithm = "BLAKE3" MD5 algorithm = "MD5" + UNKNOWN algorithm = "UNKNOWN" ) diff --git a/paketosbom/sbom_test.go b/paketosbom/sbom_test.go index ea3f9e72..e44f49ff 100644 --- a/paketosbom/sbom_test.go +++ b/paketosbom/sbom_test.go @@ -8,7 +8,7 @@ import ( . "github.com/onsi/gomega" //nolint Ignore SA1019, usage of deprecated package within a deprecated test case - "github.com/paketo-buildpacks/packit/paketosbom" + "github.com/paketo-buildpacks/packit/v2/paketosbom" ) func testPaketoSBOM(t *testing.T, context spec.G, it spec.S) { @@ -41,7 +41,8 @@ func testPaketoSBOM(t *testing.T, context spec.G, it spec.S) { context("failure cases", func() { context("when the attempted BOM checksum algorithm is not supported", func() { it("persists a build.toml", func() { - _, err := paketosbom.GetBOMChecksumAlgorithm("RANDOM-ALG") + alg, err := paketosbom.GetBOMChecksumAlgorithm("RANDOM-ALG") + Expect(alg).To(Equal(paketosbom.UNKNOWN)) Expect(err).To(MatchError("failed to get supported BOM checksum algorithm: RANDOM-ALG is not valid")) }) }) diff --git a/pexec/doc.go b/pexec/doc.go index 19f54a88..0ffc26b8 100644 --- a/pexec/doc.go +++ b/pexec/doc.go @@ -8,7 +8,7 @@ // import ( // "os" // -// "github.com/paketo-buildpacks/packit/pexec" +// "github.com/paketo-buildpacks/packit/v2/pexec" // ) // // func main() { diff --git a/pexec/executable.go b/pexec/executable.go index 74bda181..67a60305 100644 --- a/pexec/executable.go +++ b/pexec/executable.go @@ -57,6 +57,7 @@ func (e Executable) Execute(execution Execution) error { cmd.Stdout = execution.Stdout cmd.Stderr = execution.Stderr + cmd.Stdin = execution.Stdin return cmd.Run() } @@ -81,4 +82,7 @@ type Execution struct { // Stderr is where the output of stderr will be written during the execution. Stderr io.Writer + + // Stdin is where the input of stdin will be read during the execution. + Stdin io.Reader } diff --git a/pexec/executable_test.go b/pexec/executable_test.go index 1fec2ace..4f3931ab 100644 --- a/pexec/executable_test.go +++ b/pexec/executable_test.go @@ -5,12 +5,12 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" - "github.com/sclevine/spec" - "github.com/onsi/gomega/gexec" - "github.com/paketo-buildpacks/packit/pexec" + "github.com/paketo-buildpacks/packit/v2/pexec" + "github.com/sclevine/spec" . "github.com/onsi/gomega" ) @@ -50,7 +50,7 @@ func testPexec(t *testing.T, context spec.G, it spec.S) { Stdout: stdout, }) Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(ContainSubstring(fmt.Sprintf("Arguments: [%s something]", fakeCLI))) + Expect(stdout.String()).To(ContainSubstring(fmt.Sprintf("Arguments: [%s something]", fakeCLI))) }) context("when given a execution directory", func() { @@ -60,7 +60,7 @@ func testPexec(t *testing.T, context spec.G, it spec.S) { Stdout: stdout, }) Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(ContainSubstring(fmt.Sprintf("PWD=%s", tmpDir))) + Expect(stdout.String()).To(ContainSubstring(fmt.Sprintf("PWD=%s", tmpDir))) }) }) @@ -71,7 +71,7 @@ func testPexec(t *testing.T, context spec.G, it spec.S) { Stdout: stdout, }) Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(ContainSubstring("SOME_KEY=some-value")) + Expect(stdout.String()).To(ContainSubstring("SOME_KEY=some-value")) }) }) @@ -83,16 +83,22 @@ func testPexec(t *testing.T, context spec.G, it spec.S) { }) Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(ContainSubstring("Output on stdout")) - Expect(stderr).To(ContainSubstring("Output on stderr")) + Expect(stdout.String()).To(ContainSubstring("Output on stdout")) + Expect(stderr.String()).To(ContainSubstring("Output on stderr")) }) }) context("when the executable is on the PATH given as an argument", func() { + var path string it.Before(func() { + path = os.Getenv("PATH") os.Setenv("PATH", "some-path") }) + it.After(func() { + os.Setenv("PATH", path) + }) + it("executes the given arguments against the executable", func() { err := executable.Execute(pexec.Execution{ Args: []string{"something"}, @@ -105,6 +111,17 @@ func testPexec(t *testing.T, context spec.G, it spec.S) { }) }) + context("when given a reader for stdin", func() { + it("pipes that reader to stdout", func() { + err := executable.Execute(pexec.Execution{ + Stdin: strings.NewReader("something on stdin"), + Stdout: stdout, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(ContainSubstring("Input on stdin\nsomething on stdin\n")) + }) + }) + context("failure cases", func() { context("when the executable cannot be found on the path", func() { it.Before(func() { @@ -127,7 +144,7 @@ func testPexec(t *testing.T, context spec.G, it spec.S) { Expect(os.Setenv("PATH", existingPath)).To(Succeed()) var err error - errorCLI, err = gexec.Build("github.com/paketo-buildpacks/packit/fakes/some-executable", "-ldflags", "-X main.fail=true") + errorCLI, err = gexec.Build("github.com/paketo-buildpacks/packit/v2/fakes/some-executable", "-ldflags", "-X main.fail=true") Expect(err).NotTo(HaveOccurred()) path = os.Getenv("PATH") diff --git a/pexec/init_test.go b/pexec/init_test.go index c5de5924..5712f309 100644 --- a/pexec/init_test.go +++ b/pexec/init_test.go @@ -17,14 +17,14 @@ var ( fakeCLI string ) -func TestUnitExec(t *testing.T) { +func TestUnitPexec(t *testing.T) { var Expect = NewWithT(t).Expect suite := spec.New("packit/pexec", spec.Report(report.Terminal{})) suite("pexec", testPexec) var err error - fakeCLI, err = gexec.Build("github.com/paketo-buildpacks/packit/fakes/some-executable") + fakeCLI, err = gexec.Build("github.com/paketo-buildpacks/packit/v2/fakes/some-executable") Expect(err).NotTo(HaveOccurred()) existingPath = os.Getenv("PATH") diff --git a/postal/buildpack.go b/postal/buildpack.go index 82b168c3..df3d5498 100644 --- a/postal/buildpack.go +++ b/postal/buildpack.go @@ -6,16 +6,31 @@ import ( "time" "github.com/BurntSushi/toml" + "github.com/paketo-buildpacks/packit/v2/cargo" ) +type Checksum = cargo.Checksum + // Dependency is a representation of a buildpack dependency. type Dependency struct { - // CPE is the Common Platform Enumerator for the dependency. + // CPE is the Common Platform Enumerator for the dependency. Used in legacy + // image label SBOM (GenerateBillOfMaterials). + // + // Deprecated: use CPEs instead. CPE string `toml:"cpe"` + // CPEs are the Common Platform Enumerators for the dependency. Used in Syft + // and SPDX JSON SBOMs. If unset, falls back to CPE. + CPEs []string `toml:"cpes"` + // DeprecationDate is the data upon which this dependency is considered deprecated. DeprecationDate time.Time `toml:"deprecation_date"` + // Checksum is a string that includes an algorithm and the hex-encoded hash + // of the built dependency separated by a colon. Example + // sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. + Checksum string `toml:"checksum"` + // ID is the identifier used to specify the dependency. ID string `toml:"id"` @@ -29,12 +44,22 @@ type Dependency struct { PURL string `toml:"purl"` // SHA256 is the hex-encoded SHA256 checksum of the built dependency. + // + // Deprecated: use Checksum instead. SHA256 string `toml:"sha256"` // Source is the uri location of the source-code representation of the dependency. Source string `toml:"source"` - // SourceSHA256 is the hex-encoded SHA256 checksum of the source-code representation of the dependency. + // SourceChecksum is a string that includes an algorithm and the hex-encoded + // hash of the source representation of the dependency separated by a colon. + // Example sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. + SourceChecksum string `toml:"source-checksum"` + + // SourceSHA256 is the hex-encoded SHA256 checksum of the source-code + // representation of the dependency. + // + // Deprecated: use SourceChecksum instead. SourceSHA256 string `toml:"source_sha256"` // Stacks is a list of stacks for which the dependency is built. @@ -63,7 +88,7 @@ func parseBuildpack(path, name string) ([]Dependency, string, error) { Dependencies []Dependency `toml:"dependencies"` } `toml:"metadata"` } - _, err = toml.DecodeReader(file, &buildpack) + _, err = toml.NewDecoder(file).Decode(&buildpack) if err != nil { return nil, "", fmt.Errorf("failed to parse buildpack.toml: %w", err) } @@ -73,7 +98,7 @@ func parseBuildpack(path, name string) ([]Dependency, string, error) { func stacksInclude(stacks []string, stack string) bool { for _, s := range stacks { - if s == stack { + if s == stack || s == "*" { return true } } diff --git a/postal/doc.go b/postal/doc.go index 42353198..9b29ac12 100644 --- a/postal/doc.go +++ b/postal/doc.go @@ -8,8 +8,8 @@ // import ( // "log" // -// "github.com/paketo-buildpacks/packit/cargo" -// "github.com/paketo-buildpacks/packit/postal" +// "github.com/paketo-buildpacks/packit/v2/cargo" +// "github.com/paketo-buildpacks/packit/v2/postal" // ) // // func main() { diff --git a/postal/fakes/mapping_resolver.go b/postal/fakes/mapping_resolver.go index 09a8b0a5..36f300b0 100644 --- a/postal/fakes/mapping_resolver.go +++ b/postal/fakes/mapping_resolver.go @@ -7,7 +7,7 @@ type MappingResolver struct { mutex sync.Mutex CallCount int Receives struct { - SHA256 string + Checksum string PlatformDir string } Returns struct { @@ -22,7 +22,7 @@ func (f *MappingResolver) FindDependencyMapping(param1 string, param2 string) (s f.FindDependencyMappingCall.mutex.Lock() defer f.FindDependencyMappingCall.mutex.Unlock() f.FindDependencyMappingCall.CallCount++ - f.FindDependencyMappingCall.Receives.SHA256 = param1 + f.FindDependencyMappingCall.Receives.Checksum = param1 f.FindDependencyMappingCall.Receives.PlatformDir = param2 if f.FindDependencyMappingCall.Stub != nil { return f.FindDependencyMappingCall.Stub(param1, param2) diff --git a/postal/fakes/mirror_resolver.go b/postal/fakes/mirror_resolver.go new file mode 100644 index 00000000..19c83bfd --- /dev/null +++ b/postal/fakes/mirror_resolver.go @@ -0,0 +1,31 @@ +package fakes + +import "sync" + +type MirrorResolver struct { + FindDependencyMirrorCall struct { + mutex sync.Mutex + CallCount int + Receives struct { + Uri string + PlatformDir string + } + Returns struct { + String string + Error error + } + Stub func(string, string) (string, error) + } +} + +func (f *MirrorResolver) FindDependencyMirror(param1 string, param2 string) (string, error) { + f.FindDependencyMirrorCall.mutex.Lock() + defer f.FindDependencyMirrorCall.mutex.Unlock() + f.FindDependencyMirrorCall.CallCount++ + f.FindDependencyMirrorCall.Receives.Uri = param1 + f.FindDependencyMirrorCall.Receives.PlatformDir = param2 + if f.FindDependencyMirrorCall.Stub != nil { + return f.FindDependencyMirrorCall.Stub(param1, param2) + } + return f.FindDependencyMirrorCall.Returns.String, f.FindDependencyMirrorCall.Returns.Error +} diff --git a/postal/internal/dependency_mappings.go b/postal/internal/dependency_mappings.go index f54e71b5..e9d7aed8 100644 --- a/postal/internal/dependency_mappings.go +++ b/postal/internal/dependency_mappings.go @@ -2,7 +2,10 @@ package internal import ( "fmt" - "github.com/paketo-buildpacks/packit/servicebindings" + "strings" + + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/paketo-buildpacks/packit/v2/servicebindings" ) //go:generate faux --interface BindingResolver --output fakes/binding_resolver.go @@ -21,15 +24,39 @@ func NewDependencyMappingResolver(bindingResolver BindingResolver) DependencyMap } // FindDependencyMapping looks up if there is a matching dependency mapping -func (d DependencyMappingResolver) FindDependencyMapping(sha256, platformDir string) (string, error) { +// If the binding is given in the form of `hash`, assume it is of algorithm `sha256` +// If the binding is given in the form of `algorithm:hash`, compare it to the full `checksum` input +func (d DependencyMappingResolver) FindDependencyMapping(checksum, platformDir string) (string, error) { bindings, err := d.bindingResolver.Resolve("dependency-mapping", "", platformDir) if err != nil { return "", fmt.Errorf("failed to resolve 'dependency-mapping' binding: %w", err) } + hash := cargo.Checksum(checksum).Hash() + for _, binding := range bindings { - if uri, ok := binding.Entries[sha256]; ok { - return uri.ReadString() + // binding provided in the form `hash` (no algorithm provided) + // assumed to be of `sha256` algorithm + if uri, ok := binding.Entries[hash]; ok && cargo.Checksum(checksum).Algorithm() == "sha256" { + content, err := uri.ReadString() + if err != nil { + return "", err + } + return strings.TrimSpace(content), nil + // binding provided in the form `algorithm:hash` + } else if uri, ok := binding.Entries[checksum]; ok { + content, err := uri.ReadString() + if err != nil { + return "", err + } + return strings.TrimSpace(content), nil + // binding provided in the form `algorithm_hash` + } else if uri, ok := binding.Entries[strings.Replace(checksum, ":", "_", 1)]; ok { + content, err := uri.ReadString() + if err != nil { + return "", err + } + return strings.TrimSpace(content), nil } } diff --git a/postal/internal/dependency_mappings_test.go b/postal/internal/dependency_mappings_test.go index dc7ae033..57978c5d 100644 --- a/postal/internal/dependency_mappings_test.go +++ b/postal/internal/dependency_mappings_test.go @@ -5,9 +5,9 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/postal/internal" - "github.com/paketo-buildpacks/packit/postal/internal/fakes" - "github.com/paketo-buildpacks/packit/servicebindings" + "github.com/paketo-buildpacks/packit/v2/postal/internal" + "github.com/paketo-buildpacks/packit/v2/postal/internal/fakes" + "github.com/paketo-buildpacks/packit/v2/servicebindings" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -25,7 +25,7 @@ func testDependencyMappings(t *testing.T, context spec.G, it spec.S) { it.Before(func() { tmpDir, err = os.MkdirTemp("", "dependency-mappings") Expect(err).NotTo(HaveOccurred()) - Expect(os.WriteFile(filepath.Join(tmpDir, "entry-data"), []byte("dependency-mapping-entry.tgz"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "entry-data"), []byte("\n\tdependency-mapping-entry.tgz\n"), os.ModePerm)) bindingResolver = &fakes.BindingResolver{} resolver = internal.NewDependencyMappingResolver(bindingResolver) @@ -49,10 +49,18 @@ func testDependencyMappings(t *testing.T, context spec.G, it spec.S) { }, { Name: "other-binding", - Path: "other-path", + Path: "other-", Type: "dependency-mapping", Entries: map[string]*servicebindings.Entry{ - "other-sha": servicebindings.NewEntry("some-entry-path"), + "sha512:other-sha": servicebindings.NewEntry(filepath.Join(tmpDir, "entry-data")), + }, + }, + { + Name: "other-binding-with-hyphen", + Path: "hypen-another-", + Type: "dependency-mapping", + Entries: map[string]*servicebindings.Entry{ + "sha-512_other-sha-underscore": servicebindings.NewEntry(filepath.Join(tmpDir, "entry-data")), }, }, { @@ -66,13 +74,43 @@ func testDependencyMappings(t *testing.T, context spec.G, it spec.S) { context("given a set of bindings and a dependency", func() { it("finds a matching dependency mappings in the platform bindings if there is one", func() { - boundDependency, err := resolver.FindDependencyMapping("some-sha", "some-platform-dir") + boundDependency, err := resolver.FindDependencyMapping("sha256:some-sha", "some-platform-dir") Expect(err).ToNot(HaveOccurred()) Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mapping")) Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) Expect(boundDependency).To(Equal("dependency-mapping-entry.tgz")) }) + + context("the binding is of format :", func() { + it("finds a matching dependency mappings in the platform bindings if there is one", func() { + boundDependency, err := resolver.FindDependencyMapping("sha512:other-sha", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mapping")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("dependency-mapping-entry.tgz")) + }) + }) + + context("the binding is of format _", func() { + it("finds a matching dependency mappings in the platform bindings if there is one", func() { + boundDependency, err := resolver.FindDependencyMapping("sha-512:other-sha-underscore", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mapping")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("dependency-mapping-entry.tgz")) + }) + }) + + context("the binding does not contain an algorithm", func() { + it("does not find matching dependency mapping when input isn't of sha256 algorithm", func() { + boundDependency, err := resolver.FindDependencyMapping("sha512:some-sha", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("")) + }) + }) }) context("given a set of bindings and a dependency", func() { diff --git a/postal/internal/dependency_mirror.go b/postal/internal/dependency_mirror.go new file mode 100644 index 00000000..f8653c77 --- /dev/null +++ b/postal/internal/dependency_mirror.go @@ -0,0 +1,138 @@ +package internal + +import ( + "fmt" + "net/url" + "os" + "strings" +) + +type DependencyMirrorResolver struct { + bindingResolver BindingResolver +} + +func NewDependencyMirrorResolver(bindingResolver BindingResolver) DependencyMirrorResolver { + return DependencyMirrorResolver{ + bindingResolver: bindingResolver, + } +} + +func formatAndVerifyMirror(mirror, uri string) (string, error) { + mirrorURL, err := url.Parse(mirror) + if err != nil { + return "", err + } + + uriURL, err := url.Parse(uri) + if err != nil { + return "", err + } + + if strings.ToLower(mirrorURL.Scheme) != "https" && strings.ToLower(mirrorURL.Scheme) != "file" { + return "", fmt.Errorf("invalid mirror scheme") + } + + mirrorURL.Path = strings.Replace(mirrorURL.Path, "{originalHost}", uriURL.Hostname(), 1) + uriURL.Path + return mirrorURL.String(), nil +} + +func (d DependencyMirrorResolver) FindDependencyMirror(uri, platformDir string) (string, error) { + mirror, err := d.findMirrorFromEnv(uri) + if err != nil { + return "", err + } + + if mirror != "" { + return formatAndVerifyMirror(mirror, uri) + } + + mirror, err = d.findMirrorFromBinding(uri, platformDir) + if err != nil { + return "", err + } + + if mirror != "" { + return formatAndVerifyMirror(mirror, uri) + } + + return "", nil +} + +func (d DependencyMirrorResolver) findMirrorFromEnv(uri string) (string, error) { + const DefaultMirror = "BP_DEPENDENCY_MIRROR" + const NonDefaultMirrorPrefix = "BP_DEPENDENCY_MIRROR_" + mirrors := make(map[string]string) + environmentVariables := os.Environ() + for _, ev := range environmentVariables { + pair := strings.SplitN(ev, "=", 2) + key := pair[0] + value := pair[1] + + if !strings.Contains(key, DefaultMirror) { + continue + } + + if key == DefaultMirror { + mirrors["default"] = value + continue + } + + // convert key + hostname := strings.SplitN(key, NonDefaultMirrorPrefix, 2)[1] + hostname = strings.ReplaceAll(strings.ReplaceAll(hostname, "__", "-"), "_", ".") + hostname = strings.ToLower(hostname) + mirrors[hostname] = value + + if !strings.Contains(uri, hostname) { + continue + } + + return value, nil + } + + if mirrorUri, ok := mirrors["default"]; ok { + return mirrorUri, nil + } + + return "", nil +} + +func (d DependencyMirrorResolver) findMirrorFromBinding(uri, platformDir string) (string, error) { + bindings, err := d.bindingResolver.Resolve("dependency-mirror", "", platformDir) + if err != nil { + return "", fmt.Errorf("failed to resolve 'dependency-mirror' binding: %w", err) + } + + if len(bindings) > 1 { + return "", fmt.Errorf("cannot have multiple bindings of type 'dependency-mirror'") + } + + if len(bindings) == 0 { + return "", nil + } + + mirror := "" + entries := bindings[0].Entries + for hostname, entry := range entries { + if hostname == "default" { + mirror, err = entry.ReadString() + if err != nil { + return "", err + } + continue + } + + if !strings.Contains(uri, hostname) { + continue + } + + mirror, err = entry.ReadString() + if err != nil { + return "", err + } + + return mirror, nil + } + + return mirror, nil +} diff --git a/postal/internal/dependency_mirror_test.go b/postal/internal/dependency_mirror_test.go new file mode 100644 index 00000000..3efa27fa --- /dev/null +++ b/postal/internal/dependency_mirror_test.go @@ -0,0 +1,305 @@ +package internal_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/paketo-buildpacks/packit/v2/postal/internal" + "github.com/paketo-buildpacks/packit/v2/postal/internal/fakes" + "github.com/paketo-buildpacks/packit/v2/servicebindings" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testDependencyMirror(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + tmpDir string + resolver internal.DependencyMirrorResolver + bindingResolver *fakes.BindingResolver + err error + ) + + context("FindDependencyMirror", func() { + context("via binding", func() { + it.Before(func() { + tmpDir, err = os.MkdirTemp("", "dependency-mirror") + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(filepath.Join(tmpDir, "default"), []byte("https://mirror.example.org/{originalHost}"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "type"), []byte("dependency-mirror"), os.ModePerm)) + + bindingResolver = &fakes.BindingResolver{} + resolver = internal.NewDependencyMirrorResolver(bindingResolver) + + bindingResolver.ResolveCall.Returns.BindingSlice = []servicebindings.Binding{ + { + Name: "some-binding", + Path: "some-path", + Type: "dependency-mirror", + Entries: map[string]*servicebindings.Entry{ + "default": servicebindings.NewEntry(filepath.Join(tmpDir, "default")), + }, + }, + } + }) + + it.After(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + }) + + context("given a default mirror binding", func() { + it("finds a matching dependency mirror in the platform bindings if there is one", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mirror")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("https://mirror.example.org/some-uri.com/dep.tgz")) + }) + }) + + context("given default mirror and specific hostname bindings", func() { + it.Before(func() { + Expect(os.WriteFile(filepath.Join(tmpDir, "github.com"), []byte("https://mirror.example.org/public-github"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "nodejs.org"), []byte("https://mirror.example.org/node-dist"), os.ModePerm)) + + bindingResolver.ResolveCall.Returns.BindingSlice[0].Entries = map[string]*servicebindings.Entry{ + "default": servicebindings.NewEntry(filepath.Join(tmpDir, "default")), + "github.com": servicebindings.NewEntry(filepath.Join(tmpDir, "github.com")), + "nodejs.org": servicebindings.NewEntry(filepath.Join(tmpDir, "nodejs.org")), + } + }) + + it("finds the default mirror when given uri does not match a specific hostname", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mirror")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("https://mirror.example.org/some-uri.com/dep.tgz")) + }) + + it("finds the mirror matching the specific hostname in the given uri", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-github.com-uri/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mirror")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("https://mirror.example.org/public-github/dep.tgz")) + }) + }) + + context("given a specific hostname binding and no default mirror binding", func() { + it.Before(func() { + Expect(os.Remove(filepath.Join(tmpDir, "default"))) + Expect(os.WriteFile(filepath.Join(tmpDir, "github.com"), []byte("https://mirror.example.org/public-github"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "nodejs.org"), []byte("https://mirror.example.org/node-dist"), os.ModePerm)) + + bindingResolver.ResolveCall.Returns.BindingSlice[0].Entries = map[string]*servicebindings.Entry{ + "github.com": servicebindings.NewEntry(filepath.Join(tmpDir, "github.com")), + "nodejs.org": servicebindings.NewEntry(filepath.Join(tmpDir, "nodejs.org")), + } + }) + + it("return empty string for non specific hostnames", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mirror")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("")) + }) + + it("finds the mirror matching the specific hostname in the given uri", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-nodejs.org-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(bindingResolver.ResolveCall.Receives.Typ).To(Equal("dependency-mirror")) + Expect(bindingResolver.ResolveCall.Receives.Provider).To(BeEmpty()) + Expect(bindingResolver.ResolveCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(boundDependency).To(Equal("https://mirror.example.org/node-dist/dep.tgz")) + }) + }) + }) + + context("via environment variables", func() { + it.Before(func() { + Expect(os.Setenv("BP_DEPENDENCY_MIRROR", "https://mirror.example.org/{originalHost}")) + + bindingResolver = &fakes.BindingResolver{} + resolver = internal.NewDependencyMirrorResolver(bindingResolver) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR")) + }) + + context("given the default mirror environment variable is set", func() { + it("finds the matching dependency mirror", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("https://mirror.example.org/some-uri.com/dep.tgz")) + }) + }) + + context("given environment variables for a default mirror and specific hostname mirrors", func() { + it.Before(func() { + Expect(os.Setenv("BP_DEPENDENCY_MIRROR_GITHUB_COM", "https://mirror.example.org/public-github")) + Expect(os.Setenv("BP_DEPENDENCY_MIRROR_TESTING_123__ABC", "https://mirror.example.org/testing")) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR_GITHUB_COM")) + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR_TESTING_123__ABC")) + }) + + it("finds the default mirror when given uri does not match a specific hostname", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("https://mirror.example.org/some-uri.com/dep.tgz")) + }) + + it("finds the mirror matching the specific hostname in the given uri", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-github.com-uri/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("https://mirror.example.org/public-github/dep.tgz")) + }) + + it("properly decodes the hostname", func() { + boundDependency, err := resolver.FindDependencyMirror("https://testing.123-abc-uri/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("https://mirror.example.org/testing/dep.tgz")) + }) + }) + + context("given environment variables for a specific hostname and none for a default mirror", func() { + it.Before(func() { + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR")) + Expect(os.Setenv("BP_DEPENDENCY_MIRROR_GITHUB_COM", "https://mirror.example.org/public-github")) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR_GITHUB_COM")) + }) + + it("return empty string for non specific hostnames", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("")) + }) + + it("finds the mirror matching the specific hostname in the given uri", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-github.com-uri/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("https://mirror.example.org/public-github/dep.tgz")) + }) + }) + }) + + context("when mirror is provided by both bindings and environment variables", func() { + it.Before(func() { + tmpDir, err = os.MkdirTemp("", "dependency-mirror") + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(filepath.Join(tmpDir, "default"), []byte("https://mirror.example.org/{originalHost}"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "type"), []byte("dependency-mirror"), os.ModePerm)) + + Expect(os.Setenv("BP_DEPENDENCY_MIRROR", "https://mirror.other-example.org/{originalHost}")) + }) + + it.After(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR")) + }) + + it("defaults to environment variable and ignores binding", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).NotTo(Equal("https://mirror.example.org/some-uri.com/dep.tgz")) + Expect(boundDependency).To(Equal("https://mirror.other-example.org/some-uri.com/dep.tgz")) + + }) + }) + + context("without originalHost placeholder", func() { + it.Before(func() { + Expect(os.Setenv("BP_DEPENDENCY_MIRROR", "https://mirror.example.org")) + + bindingResolver = &fakes.BindingResolver{} + resolver = internal.NewDependencyMirrorResolver(bindingResolver) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR")) + }) + + it("sets mirror excluding original hostname", func() { + boundDependency, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).ToNot(HaveOccurred()) + Expect(boundDependency).To(Equal("https://mirror.example.org/dep.tgz")) + }) + + }) + + context("failure cases", func() { + context("when more than one dependency mirror binding exists", func() { + it.Before(func() { + tmpDir, err = os.MkdirTemp("", "dependency-mirror") + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(filepath.Join(tmpDir, "default"), []byte("https://mirror.example.org/{originalHost}"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "github.com"), []byte("https://mirror.example.org/public-github"), os.ModePerm)) + Expect(os.WriteFile(filepath.Join(tmpDir, "type"), []byte("dependency-mirror"), os.ModePerm)) + + bindingResolver = &fakes.BindingResolver{} + resolver = internal.NewDependencyMirrorResolver(bindingResolver) + }) + + it.After(func() { + Expect(os.RemoveAll(tmpDir)).To(Succeed()) + }) + + it("returns an error", func() { + bindingResolver.ResolveCall.Returns.BindingSlice = []servicebindings.Binding{ + { + Name: "some-binding", + Path: "some-path", + Type: "dependency-mirror", + Entries: map[string]*servicebindings.Entry{ + "default": servicebindings.NewEntry(filepath.Join(tmpDir, "default")), + }, + }, + { + Name: "some-other-binding", + Path: "some-other-path", + Type: "dependency-mirror", + Entries: map[string]*servicebindings.Entry{ + "github.com": servicebindings.NewEntry(filepath.Join(tmpDir, "github.com")), + }, + }, + } + + _, err = resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).To(MatchError(ContainSubstring("cannot have multiple bindings of type 'dependency-mirror'"))) + }) + }) + + context("when mirror contains invalid scheme", func() { + it.Before(func() { + Expect(os.Setenv("BP_DEPENDENCY_MIRROR", "http://mirror.example.org/{originalHost}")) + + bindingResolver = &fakes.BindingResolver{} + resolver = internal.NewDependencyMirrorResolver(bindingResolver) + }) + + it.After(func() { + Expect(os.Unsetenv("BP_DEPENDENCY_MIRROR")) + }) + + it("returns an error", func() { + _, err := resolver.FindDependencyMirror("https://some-uri.com/dep.tgz", "some-platform-dir") + Expect(err).To(MatchError(ContainSubstring("invalid mirror scheme"))) + }) + }) + }) + }) +} diff --git a/postal/internal/fakes/binding_resolver.go b/postal/internal/fakes/binding_resolver.go index 8edce015..cbc8de37 100644 --- a/postal/internal/fakes/binding_resolver.go +++ b/postal/internal/fakes/binding_resolver.go @@ -3,7 +3,7 @@ package fakes import ( "sync" - "github.com/paketo-buildpacks/packit/servicebindings" + "github.com/paketo-buildpacks/packit/v2/servicebindings" ) type BindingResolver struct { diff --git a/postal/internal/init_test.go b/postal/internal/init_test.go index 08b32ef1..56969873 100644 --- a/postal/internal/init_test.go +++ b/postal/internal/init_test.go @@ -10,6 +10,7 @@ import ( func TestUnitPostalInternal(t *testing.T) { suite := spec.New("packit/postal/internal", spec.Report(report.Terminal{})) suite("DependencyMappings", testDependencyMappings) + suite("DependencyMirror", testDependencyMirror) suite.Run(t) } diff --git a/postal/service.go b/postal/service.go index cf172d3f..3147b059 100644 --- a/postal/service.go +++ b/postal/service.go @@ -1,6 +1,7 @@ package postal import ( + "errors" "fmt" "io" "path/filepath" @@ -10,14 +11,14 @@ import ( "time" "github.com/Masterminds/semver/v3" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/cargo" - "github.com/paketo-buildpacks/packit/postal/internal" - "github.com/paketo-buildpacks/packit/servicebindings" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/cargo" + "github.com/paketo-buildpacks/packit/v2/postal/internal" + "github.com/paketo-buildpacks/packit/v2/servicebindings" + "github.com/paketo-buildpacks/packit/v2/vacation" //nolint Ignore SA1019, usage of deprecated package within a deprecated test case - "github.com/paketo-buildpacks/packit/paketosbom" + "github.com/paketo-buildpacks/packit/v2/paketosbom" ) //go:generate faux --interface Transport --output fakes/transport.go @@ -28,11 +29,40 @@ type Transport interface { Drop(root, uri string) (io.ReadCloser, error) } -//go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go // MappingResolver serves as the interface that looks up platform binding provided // dependency mappings given a SHA256 +// +//go:generate faux --interface MappingResolver --output fakes/mapping_resolver.go type MappingResolver interface { - FindDependencyMapping(SHA256, platformDir string) (string, error) + FindDependencyMapping(checksum, platformDir string) (string, error) +} + +// MirrorResolver serves as the interface that looks for a dependency mirror via +// environment variable or binding +// +//go:generate faux --interface MirrorResolver --output fakes/mirror_resolver.go +type MirrorResolver interface { + FindDependencyMirror(uri, platformDir string) (string, error) +} + +// ErrNoDeps is a typed error indicating that no dependencies were resolved during Service.Resolve() +// +// errors can be tested against this type with: errors.As() +type ErrNoDeps struct { + id string + version string + stack string + supportedVersions []string +} + +// Error implements the error.Error interface +func (e *ErrNoDeps) Error() string { + return fmt.Sprintf("failed to satisfy %q dependency version constraint %q: no compatible versions on %q stack. Supported versions are: [%s]", + e.id, + e.version, + e.stack, + strings.Join(e.supportedVersions, ", "), + ) } // Service provides a mechanism for resolving and installing dependencies given @@ -40,6 +70,7 @@ type MappingResolver interface { type Service struct { transport Transport mappingResolver MappingResolver + mirrorResolver MirrorResolver } // NewService creates an instance of a Service given a Transport. @@ -49,6 +80,9 @@ func NewService(transport Transport) Service { mappingResolver: internal.NewDependencyMappingResolver( servicebindings.NewResolver(), ), + mirrorResolver: internal.NewDependencyMirrorResolver( + servicebindings.NewResolver(), + ), } } @@ -57,6 +91,11 @@ func (s Service) WithDependencyMappingResolver(mappingResolver MappingResolver) return s } +func (s Service) WithDependencyMirrorResolver(mirrorResolver MirrorResolver) Service { + s.mirrorResolver = mirrorResolver + return s +} + // Resolve will pick the best matching dependency given a path to a // buildpack.toml file, and the id, version, and stack value of a dependency. // The version value is treated as a SemVer constraint and will pick the @@ -121,39 +160,117 @@ func (s Service) Resolve(path, id, version, stack string) (Dependency, error) { } if len(compatibleVersions) == 0 { - return Dependency{}, fmt.Errorf( - "failed to satisfy %q dependency version constraint %q: no compatible versions on %q stack. Supported versions are: [%s]", - id, - version, - stack, - strings.Join(supportedVersions, ", "), - ) + return Dependency{}, &ErrNoDeps{id, version, stack, supportedVersions} + } + + stacksForVersion := map[string][]string{} + + for _, dep := range compatibleVersions { + stacksForVersion[dep.Version] = append(stacksForVersion[dep.Version], dep.Stacks...) + } + + for version, stacks := range stacksForVersion { + count := stringSliceElementCount(stacks, "*") + if count > 1 { + return Dependency{}, fmt.Errorf("multiple dependencies support wildcard stack for version: %q", version) + } } sort.Slice(compatibleVersions, func(i, j int) bool { - iVersion := semver.MustParse(compatibleVersions[i].Version) - jVersion := semver.MustParse(compatibleVersions[j].Version) - return iVersion.GreaterThan(jVersion) + iDep := compatibleVersions[i] + jDep := compatibleVersions[j] + + jVersion := semver.MustParse(jDep.Version) + iVersion := semver.MustParse(iDep.Version) + + if !iVersion.Equal(jVersion) { + return iVersion.GreaterThan(jVersion) + } + + iStacks := iDep.Stacks + jStacks := jDep.Stacks + + // If either dependency supports the wildcard stack, it has lower + // priority than a dependency that only supports a more specific stack. + // This is true regardless of whether or not the dependency with + // wildcard stack support also supports other stacks + // + // If is an error to have multiple dependencies with the same version + // and wildcard stack support. + // This is tested for above, and we would not enter this sort function + // in this case + + if stringSliceContains(iStacks, "*") { + return false + } + + if stringSliceContains(jStacks, "*") { + return true + } + + // As mentioned above, this isn't a valid path to encounter because + // only one dependency may have support for wildcard stacks for a given + // version. We could panic, but it is preferable to return an invalid + // sort order instead. + // + // This is untested as this path is not possible to encounter. + return true }) return compatibleVersions[0], nil } +func stringSliceContains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + + return false +} + +func stringSliceElementCount(slice []string, str string) int { + count := 0 + for _, s := range slice { + if s == str { + count++ + } + } + + return count +} + // Deliver will fetch and expand a dependency into a layer path location. The // location of the CNBPath is given so that dependencies that may be included // in a buildpack when packaged for offline consumption can be retrieved. If // there is a dependency mapping for the specified dependency, Deliver will use -// the given dependency mapping URI to fetch the dependency. The dependency is -// validated against the checksum value provided on the Dependency and will -// error if there are inconsistencies in the fetched result. +// the given dependency mapping URI to fetch the dependency. If there is a +// dependency mirror for the specified dependency, Deliver will use the mirror +// URI to fetch the dependency. If both a dependency mapping and mirror are BOTH +// present, the mapping will take precedence over the mirror.The dependency is +// validated against the checksum value provided on the Dependency and will error +// if there are inconsistencies in the fetched result. func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath string) error { - dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependency.SHA256, platformPath) + dependencyChecksum := dependency.Checksum + if dependency.SHA256 != "" { + dependencyChecksum = fmt.Sprintf("sha256:%s", dependency.SHA256) + } + + dependencyMirrorURI, err := s.mirrorResolver.FindDependencyMirror(dependency.URI, platformPath) + if err != nil { + return fmt.Errorf("failure checking for dependency mirror: %s", err) + } + + dependencyMappingURI, err := s.mappingResolver.FindDependencyMapping(dependencyChecksum, platformPath) if err != nil { return fmt.Errorf("failure checking for dependency mappings: %s", err) } if dependencyMappingURI != "" { dependency.URI = dependencyMappingURI + } else if dependencyMirrorURI != "" { + dependency.URI = dependencyMirrorURI } bundle, err := s.transport.Drop(cnbPath, dependency.URI) @@ -162,7 +279,7 @@ func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath } defer bundle.Close() - validatedReader := cargo.NewValidatedReader(bundle, dependency.SHA256) + validatedReader := cargo.NewValidatedReader(bundle, dependencyChecksum) name := dependency.Name if name == "" { @@ -179,7 +296,7 @@ func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath } if !ok { - return fmt.Errorf("checksum does not match: %s", err) + return errors.New("failed to validate dependency: checksum does not match") } return nil @@ -192,17 +309,44 @@ func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath func (s Service) GenerateBillOfMaterials(dependencies ...Dependency) []packit.BOMEntry { var entries []packit.BOMEntry for _, dependency := range dependencies { + + checksum := Checksum(dependency.SHA256) + if len(dependency.Checksum) > 0 { + checksum = Checksum(dependency.Checksum) + } + + hash := checksum.Hash() + paketoSbomAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(checksum.Algorithm()) + // GetBOMChecksumAlgorithm will set algorithm to UNKNOWN if there is an error + if err != nil || hash == "" { + paketoSbomAlgorithm = paketosbom.UNKNOWN + hash = "" + } + + sourceChecksum := Checksum(dependency.SourceSHA256) + if len(dependency.Checksum) > 0 { + sourceChecksum = Checksum(dependency.SourceChecksum) + } + + sourceHash := sourceChecksum.Hash() + paketoSbomSrcAlgorithm, err := paketosbom.GetBOMChecksumAlgorithm(sourceChecksum.Algorithm()) + // GetBOMChecksumAlgorithm will set algorithm to UNKNOWN if there is an error + if err != nil || sourceHash == "" { + paketoSbomSrcAlgorithm = paketosbom.UNKNOWN + sourceHash = "" + } + paketoBomMetadata := paketosbom.BOMMetadata{ Checksum: paketosbom.BOMChecksum{ - Algorithm: paketosbom.SHA256, - Hash: dependency.SHA256, + Algorithm: paketoSbomAlgorithm, + Hash: hash, }, URI: dependency.URI, Version: dependency.Version, Source: paketosbom.BOMSource{ Checksum: paketosbom.BOMChecksum{ - Algorithm: paketosbom.SHA256, - Hash: dependency.SourceSHA256, + Algorithm: paketoSbomSrcAlgorithm, + Hash: sourceHash, }, URI: dependency.Source, }, diff --git a/postal/service_test.go b/postal/service_test.go index 23af1a58..25e54b2c 100644 --- a/postal/service_test.go +++ b/postal/service_test.go @@ -5,6 +5,7 @@ import ( "bytes" "compress/gzip" "crypto/sha256" + "crypto/sha512" "encoding/hex" "errors" "fmt" @@ -14,13 +15,13 @@ import ( "testing" "time" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/postal" - "github.com/paketo-buildpacks/packit/postal/fakes" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/postal" + "github.com/paketo-buildpacks/packit/v2/postal/fakes" "github.com/sclevine/spec" //nolint Ignore SA1019, usage of deprecated package within a deprecated test case - "github.com/paketo-buildpacks/packit/paketosbom" + "github.com/paketo-buildpacks/packit/v2/paketosbom" . "github.com/onsi/gomega" ) @@ -33,6 +34,7 @@ func testService(t *testing.T, context spec.G, it spec.S) { transport *fakes.Transport mappingResolver *fakes.MappingResolver + mirrorResolver *fakes.MirrorResolver service postal.Service ) @@ -45,6 +47,8 @@ func testService(t *testing.T, context spec.G, it spec.S) { _, err = file.WriteString(` [[metadata.dependencies]] deprecation_date = 2022-04-01T00:00:00Z +cpe = "some-cpe" +cpes = ["some-cpe", "other-cpe"] id = "some-entry" sha256 = "some-sha" stacks = ["some-stack"] @@ -53,6 +57,7 @@ version = "1.2.3" [[metadata.dependencies]] id = "some-other-entry" +cpes = ["some-cpe", "other-cpe"] sha256 = "some-other-sha" stacks = ["some-stack"] uri = "some-uri" @@ -67,6 +72,8 @@ version = "1.2.5" [[metadata.dependencies]] id = "some-random-entry" +cpe = "some-cpe" +cpes = ["some-cpe", "other-cpe"] sha256 = "some-random-sha" stacks = ["other-random-stack"] uri = "some-uri" @@ -86,6 +93,14 @@ stacks = ["some-stack"] uri = "some-uri" version = "4.5.6" strip-components = 1 + +[[metadata.dependencies]] +id = "some-other-entry" +sha256 = "some-sha" +stacks = ["*"] +uri = "some-uri" +version = "4.5.6" +strip-components = 1 `) Expect(err).NotTo(HaveOccurred()) @@ -95,7 +110,11 @@ strip-components = 1 mappingResolver = &fakes.MappingResolver{} - service = postal.NewService(transport).WithDependencyMappingResolver(mappingResolver) + mirrorResolver = &fakes.MirrorResolver{} + + service = postal.NewService(transport). + WithDependencyMappingResolver(mappingResolver). + WithDependencyMirrorResolver(mirrorResolver) }) context("Resolve", func() { @@ -106,6 +125,8 @@ strip-components = 1 dependency, err := service.Resolve(path, "some-entry", "1.2.*", "some-stack") Expect(err).NotTo(HaveOccurred()) Expect(dependency).To(Equal(postal.Dependency{ + CPE: "some-cpe", + CPEs: []string{"some-cpe", "other-cpe"}, DeprecationDate: deprecationDate, ID: "some-entry", Stacks: []string{"some-stack"}, @@ -115,6 +136,21 @@ strip-components = 1 })) }) + context("when the dependency has a wildcard stack", func() { + it("is compatible with all stack ids", func() { + dependency, err := service.Resolve(path, "some-other-entry", "", "random-stack") + Expect(err).NotTo(HaveOccurred()) + Expect(dependency).To(Equal(postal.Dependency{ + ID: "some-other-entry", + Stacks: []string{"*"}, + URI: "some-uri", + SHA256: "some-sha", + Version: "4.5.6", + StripComponents: 1, + })) + }) + }) + context("when there is NOT a default version", func() { context("when the entry version is empty", func() { it("picks the dependency with the highest semantic version number", func() { @@ -155,6 +191,8 @@ strip-components = 1 Expect(err).NotTo(HaveOccurred()) Expect(dependency).To(Equal(postal.Dependency{ DeprecationDate: deprecationDate, + CPE: "some-cpe", + CPEs: []string{"some-cpe", "other-cpe"}, ID: "some-entry", Stacks: []string{"some-stack"}, URI: "some-uri", @@ -172,6 +210,8 @@ strip-components = 1 dependency, err := service.Resolve(path, "some-entry", "~> 1.1", "some-stack") Expect(err).NotTo(HaveOccurred()) Expect(dependency).To(Equal(postal.Dependency{ + CPE: "some-cpe", + CPEs: []string{"some-cpe", "other-cpe"}, DeprecationDate: deprecationDate, ID: "some-entry", Stacks: []string{"some-stack"}, @@ -190,6 +230,8 @@ strip-components = 1 dependency, err := service.Resolve(path, "some-entry", "~> 1", "some-stack") Expect(err).NotTo(HaveOccurred()) Expect(dependency).To(Equal(postal.Dependency{ + CPE: "some-cpe", + CPEs: []string{"some-cpe", "other-cpe"}, DeprecationDate: deprecationDate, ID: "some-entry", Stacks: []string{"some-stack"}, @@ -268,6 +310,55 @@ version = "4.5.6" }) }) + context("when both a wildcard stack constraint and a specific stack constraint exist for the same dependency version", func() { + it.Before(func() { + err := os.WriteFile(path, []byte(` +[metadata] +[[metadata.dependencies]] +id = "some-entry" +sha256 = "some-sha" +stacks = ["some-stack"] +uri = "some-uri-specific-stack" +version = "1.2.1" + +[[metadata.dependencies]] +id = "some-entry" +sha256 = "some-sha" +stacks = ["*"] +uri = "some-uri-only-wildcard" +version = "1.2.1" + +[[metadata.dependencies]] +id = "some-entry" +sha256 = "some-sha" +stacks = ["some-stack","*"] +uri = "some-uri-only-wildcard" +version = "1.2.3" + +[[metadata.dependencies]] +id = "some-entry" +sha256 = "some-sha" +stacks = ["some-stack"] +uri = "some-uri-specific-stack" +version = "1.2.3" +`), 0600) + + Expect(err).NotTo(HaveOccurred()) + }) + + it("selects the more specific stack constraint", func() { + dependency, err := service.Resolve(path, "some-entry", "*", "some-stack") + Expect(err).NotTo(HaveOccurred()) + Expect(dependency).To(Equal(postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-uri-specific-stack", + SHA256: "some-sha", + Version: "1.2.3", + })) + }) + }) + context("failure cases", func() { context("when the buildpack.toml is malformed", func() { it.Before(func() { @@ -307,9 +398,37 @@ version = "this is super not semver" }) }) + context("when multiple dependencies have a wildcard stack for the same version", func() { + it.Before(func() { + err := os.WriteFile(path, []byte(` +[[metadata.dependencies]] +id = "some-entry" +sha256 = "some-sha-A" +stacks = ["some-stack","*"] +uri = "some-uri-A" +version = "1.2.3" + +[[metadata.dependencies]] +id = "some-entry" +sha256 = "some-sha-B" +stacks = ["some-stack","some-other-stack","*"] +uri = "some-uri-B" +version = "1.2.3" +`), 0600) + Expect(err).NotTo(HaveOccurred()) + }) + + it("returns an error", func() { + _, err := service.Resolve(path, "some-entry", "1.2.3", "some-stack") + Expect(err).To(MatchError(ContainSubstring(`multiple dependencies support wildcard stack for version: "1.2.3"`))) + }) + }) + context("when the entry version constraint cannot be satisfied", func() { - it("returns an error with all the supported versions listed", func() { + it("returns a typed error with all the supported versions listed", func() { + expectedErr := &postal.ErrNoDeps{} _, err := service.Resolve(path, "some-entry", "9.9.9", "some-stack") + Expect(errors.As(err, &expectedErr)).To(BeTrue()) Expect(err).To(MatchError(ContainSubstring("failed to satisfy \"some-entry\" dependency version constraint \"9.9.9\": no compatible versions on \"some-stack\" stack. Supported versions are: [1.2.3, 4.5.6]"))) }) }) @@ -318,9 +437,10 @@ version = "this is super not semver" context("Deliver", func() { var ( - dependencySHA string - layerPath string - deliver func() error + dependencyHash string + hash512 string + layerPath string + deliver func() error ) it.Before(func() { @@ -357,7 +477,10 @@ version = "this is super not semver" Expect(zw.Close()).To(Succeed()) sum := sha256.Sum256(buffer.Bytes()) - dependencySHA = hex.EncodeToString(sum[:]) + dependencyHash = hex.EncodeToString(sum[:]) + + sum512 := sha512.Sum512(buffer.Bytes()) + hash512 = hex.EncodeToString(sum512[:]) transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) @@ -367,7 +490,7 @@ version = "this is super not semver" ID: "some-entry", Stacks: []string{"some-stack"}, URI: "some-entry.tgz", - SHA256: dependencySHA, + SHA256: dependencyHash, Version: "1.2.3", }, "some-cnb-path", @@ -405,6 +528,50 @@ version = "this is super not semver" Expect(info.Mode()).To(Equal(os.FileMode(0755))) }) + context("when using the checksum field", func() { + it.Before(func() { + deliver = func() error { + return service.Deliver( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + Checksum: fmt.Sprintf("sha512:%s", hash512), + Version: "1.2.3", + }, + "some-cnb-path", + layerPath, + "some-platform-dir", + ) + } + }) + + it("downloads the dependency and unpackages it into the path", func() { + err := deliver() + + Expect(err).NotTo(HaveOccurred()) + + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("some-entry.tgz")) + Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + + }) + }) + context("when the dependency has a strip-components value set", func() { it.Before(func() { var err error @@ -440,7 +607,7 @@ version = "this is super not semver" Expect(zw.Close()).To(Succeed()) sum := sha256.Sum256(buffer.Bytes()) - dependencySHA = hex.EncodeToString(sum[:]) + dependencyHash = hex.EncodeToString(sum[:]) transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) @@ -450,7 +617,7 @@ version = "this is super not semver" ID: "some-entry", Stacks: []string{"some-stack"}, URI: "some-entry.tgz", - SHA256: dependencySHA, + SHA256: dependencyHash, Version: "1.2.3", StripComponents: 1, }, @@ -499,7 +666,7 @@ version = "this is super not semver" buffer.WriteString("some-file-contents") sum := sha256.Sum256(buffer.Bytes()) - dependencySHA = hex.EncodeToString(sum[:]) + dependencyHash = hex.EncodeToString(sum[:]) transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) @@ -509,7 +676,7 @@ version = "this is super not semver" ID: "some-entry", Stacks: []string{"some-stack"}, URI: "https://dependencies.example.com/dependencies/some-file-name.txt", - SHA256: dependencySHA, + SHA256: dependencyHash, Version: "1.2.3", }, "some-cnb-path", @@ -540,6 +707,68 @@ version = "this is super not semver" }) }) + context("when there is a dependency mapping via binding", func() { + it.Before(func() { + mappingResolver.FindDependencyMappingCall.Returns.String = "dependency-mapping-entry.tgz" + }) + + context("the dependency has a checksum field", func() { + it("looks up the dependency from the platform binding and downloads that instead", func() { + err := deliver() + + Expect(err).NotTo(HaveOccurred()) + + Expect(mappingResolver.FindDependencyMappingCall.Receives.Checksum).To(Equal("sha256:" + dependencyHash)) + Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mapping-entry.tgz")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + + }) + + context("the dependency has a SHA256 field", func() { + it("looks up the dependency from the platform binding and downloads that instead", func() { + err := deliver() + + Expect(err).NotTo(HaveOccurred()) + + Expect(mappingResolver.FindDependencyMappingCall.Receives.Checksum).To(Equal(fmt.Sprintf("sha256:%s", dependencyHash))) + Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mapping-entry.tgz")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + + }) + }) + context("when there is a dependency mapping via binding", func() { it.Before(func() { mappingResolver.FindDependencyMappingCall.Returns.String = "dependency-mapping-entry.tgz" @@ -550,7 +779,7 @@ version = "this is super not semver" Expect(err).NotTo(HaveOccurred()) - Expect(mappingResolver.FindDependencyMappingCall.Receives.SHA256).To(Equal(dependencySHA)) + Expect(mappingResolver.FindDependencyMappingCall.Receives.Checksum).To(Equal(fmt.Sprintf("sha256:%s", dependencyHash))) Expect(mappingResolver.FindDependencyMappingCall.Receives.PlatformDir).To(Equal("some-platform-dir")) Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mapping-entry.tgz")) @@ -571,7 +800,60 @@ version = "this is super not semver" }) }) + context("when there is a dependency mirror", func() { + it.Before(func() { + mirrorResolver.FindDependencyMirrorCall.Returns.String = "dependency-mirror-url" + }) + + it("downloads dependency from mirror", func() { + err := deliver() + + Expect(err).NotTo(HaveOccurred()) + + Expect(mirrorResolver.FindDependencyMirrorCall.Receives.Uri).To(Equal("some-entry.tgz")) + Expect(mirrorResolver.FindDependencyMirrorCall.Receives.PlatformDir).To(Equal("some-platform-dir")) + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("dependency-mirror-url")) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(layerPath, "first"), + filepath.Join(layerPath, "second"), + filepath.Join(layerPath, "third"), + filepath.Join(layerPath, "some-dir"), + filepath.Join(layerPath, "symlink"), + })) + + info, err := os.Stat(filepath.Join(layerPath, "first")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()).To(Equal(os.FileMode(0755))) + }) + }) + context("failure cases", func() { + context("when dependency mapping resolver fails", func() { + it.Before(func() { + mappingResolver.FindDependencyMappingCall.Returns.Error = fmt.Errorf("some dependency mapping error") + }) + it("fails to find dependency mappings", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("some dependency mapping error"))) + }) + }) + + context("when dependency mirror resolver fails", func() { + it.Before(func() { + mirrorResolver.FindDependencyMirrorCall.Returns.Error = fmt.Errorf("some dependency mirror error") + }) + it("fails to find dependency mirror", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring("some dependency mirror error"))) + }) + }) + context("when the transport cannot fetch a dependency", func() { it.Before(func() { transport.DropCall.Returns.Error = errors.New("there was an error") @@ -584,6 +866,31 @@ version = "this is super not semver" }) }) + context("when there is a problem with the checksum", func() { + it.Before(func() { + deliver = func() error { + return service.Deliver( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "some-entry.tgz", + Checksum: fmt.Sprintf("magic:%s", hash512), + Version: "1.2.3", + }, + "some-cnb-path", + layerPath, + "some-platform-dir", + ) + } + }) + + it("fails to create a validated reader", func() { + err := deliver() + + Expect(err).To(MatchError(ContainSubstring(`unsupported algorithm "magic"`))) + }) + }) + context("when the file contents are empty", func() { it.Before(func() { // This is a FLAC header @@ -591,7 +898,7 @@ version = "this is super not semver" transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) sum := sha256.Sum256(buffer.Bytes()) - dependencySHA = hex.EncodeToString(sum[:]) + dependencyHash = hex.EncodeToString(sum[:]) }) it("fails to create a gzip reader", func() { @@ -616,7 +923,7 @@ version = "this is super not semver" "", ) - Expect(err).To(MatchError(ContainSubstring("checksum does not match"))) + Expect(err).To(MatchError("validation error: checksum does not match")) }) }) @@ -673,7 +980,7 @@ version = "this is super not semver" Expect(os.Symlink("some-file", filepath.Join(layerPath, "symlink"))).To(Succeed()) sum := sha256.Sum256(buffer.Bytes()) - dependencySHA = hex.EncodeToString(sum[:]) + dependencyHash = hex.EncodeToString(sum[:]) transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) }) @@ -684,15 +991,64 @@ version = "this is super not semver" Expect(err).To(MatchError(ContainSubstring("failed to extract symlink"))) }) }) - }) - context("when dependency mapping resolver fails", func() { - it.Before(func() { - mappingResolver.FindDependencyMappingCall.Returns.Error = fmt.Errorf("some dependency mapping error") - }) - it("fails to find dependency mappings", func() { - err := deliver() - Expect(err).To(MatchError(ContainSubstring("some dependency mapping error"))) + context("when the has additional data in the byte stream", func() { + it.Before(func() { + var err error + layerPath, err = os.MkdirTemp("", "path") + Expect(err).NotTo(HaveOccurred()) + + buffer := bytes.NewBuffer(nil) + tw := tar.NewWriter(buffer) + + file := "some-file" + Expect(tw.WriteHeader(&tar.Header{Name: file, Mode: 0755, Size: int64(len(file))})).To(Succeed()) + _, err = tw.Write([]byte(file)) + Expect(err).NotTo(HaveOccurred()) + + Expect(tw.Close()).To(Succeed()) + + sum := sha256.Sum256(buffer.Bytes()) + dependencyHash = hex.EncodeToString(sum[:]) + + // Empty block is tricking tar reader into think that we have reached + // EOF becuase we have surpassed the maximum block header size + var block [1024]byte + _, err = buffer.Write(block[:]) + Expect(err).NotTo(HaveOccurred()) + + _, err = buffer.WriteString("additional data") + Expect(err).NotTo(HaveOccurred()) + + transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer) + + deliver = func() error { + return service.Deliver( + postal.Dependency{ + ID: "some-entry", + Stacks: []string{"some-stack"}, + URI: "https://dependencies.example.com/dependencies/some-file-name.txt", + SHA256: dependencyHash, + Version: "1.2.3", + }, + "some-cnb-path", + layerPath, + "some-platform-dir", + ) + } + }) + + it.After(func() { + Expect(os.RemoveAll(layerPath)).To(Succeed()) + }) + + it("returns an error", func() { + err := deliver() + Expect(err).To(MatchError("failed to validate dependency: checksum does not match")) + + Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path")) + Expect(transport.DropCall.Receives.Uri).To(Equal("https://dependencies.example.com/dependencies/some-file-name.txt")) + }) }) }) }) @@ -701,24 +1057,24 @@ version = "this is super not semver" it("returns a list of BOMEntry values", func() { entries := service.GenerateBillOfMaterials( postal.Dependency{ - ID: "some-entry", - Name: "Some Entry", - SHA256: "some-sha", - Source: "some-source", - SourceSHA256: "some-source-sha", - Stacks: []string{"some-stack"}, - URI: "some-uri", - Version: "1.2.3", + ID: "some-entry", + Name: "Some Entry", + Checksum: "sha256:some-sha", + Source: "some-source", + SourceChecksum: "sha256:some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", }, postal.Dependency{ - ID: "other-entry", - Name: "Other Entry", - SHA256: "other-sha", - Source: "other-source", - SourceSHA256: "other-source-sha", - Stacks: []string{"other-stack"}, - URI: "other-uri", - Version: "4.5.6", + ID: "other-entry", + Name: "Other Entry", + Checksum: "sha256:other-sha", + Source: "other-source", + SourceChecksum: "sha256:other-source-sha", + Stacks: []string{"other-stack"}, + URI: "other-uri", + Version: "4.5.6", }, ) Expect(entries).To(Equal([]packit.BOMEntry{ @@ -767,15 +1123,15 @@ version = "this is super not semver" it("generates a BOM with the CPE", func() { entries := service.GenerateBillOfMaterials( postal.Dependency{ - CPE: "some-cpe", - ID: "some-entry", - Name: "Some Entry", - SHA256: "some-sha", - Source: "some-source", - SourceSHA256: "some-source-sha", - Stacks: []string{"some-stack"}, - URI: "some-uri", - Version: "1.2.3", + CPE: "some-cpe", + ID: "some-entry", + Name: "Some Entry", + Checksum: "sha256:some-sha", + Source: "some-source", + SourceChecksum: "sha256:some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", }, ) @@ -802,6 +1158,48 @@ version = "this is super not semver" }, })) }) + context("and there are CPEs", func() { + it("uses CPE, ignores CPEs, for backward compatibility", func() { + entries := service.GenerateBillOfMaterials( + postal.Dependency{ + CPE: "some-cpe", + CPEs: []string{"some-other-cpe"}, + ID: "some-entry", + Name: "Some Entry", + Checksum: "sha256:some-sha", + Source: "some-source", + SourceChecksum: "sha256:some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", + }, + ) + + Expect(entries).To(Equal([]packit.BOMEntry{ + { + Name: "Some Entry", + Metadata: paketosbom.BOMMetadata{ + CPE: "some-cpe", + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA256, + Hash: "some-sha", + }, + Source: paketosbom.BOMSource{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA256, + Hash: "some-source-sha", + }, + URI: "some-source", + }, + + URI: "some-uri", + Version: "1.2.3", + }, + }, + })) + }) + + }) }) context("when there is a deprecation date", func() { @@ -819,9 +1217,9 @@ version = "this is super not semver" DeprecationDate: deprecationDate, ID: "some-entry", Name: "Some Entry", - SHA256: "some-sha", + Checksum: "sha256:some-sha", Source: "some-source", - SourceSHA256: "some-source-sha", + SourceChecksum: "sha256:some-source-sha", Stacks: []string{"some-stack"}, URI: "some-uri", Version: "1.2.3", @@ -857,15 +1255,15 @@ version = "this is super not semver" it("generates a BOM with the license information", func() { entries := service.GenerateBillOfMaterials( postal.Dependency{ - ID: "some-entry", - Licenses: []string{"some-license"}, - Name: "Some Entry", - SHA256: "some-sha", - Source: "some-source", - SourceSHA256: "some-source-sha", - Stacks: []string{"some-stack"}, - URI: "some-uri", - Version: "1.2.3", + ID: "some-entry", + Licenses: []string{"some-license"}, + Name: "Some Entry", + Checksum: "sha256:some-sha", + Source: "some-source", + SourceChecksum: "sha256:some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", }, ) @@ -896,11 +1294,51 @@ version = "this is super not semver" context("when there is a pURL", func() { it("generates a BOM with the pURL", func() { + entries := service.GenerateBillOfMaterials( + postal.Dependency{ + ID: "some-entry", + Name: "Some Entry", + PURL: "some-purl", + Checksum: "sha256:some-sha", + Source: "some-source", + SourceChecksum: "sha256:some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", + }, + ) + + Expect(entries).To(Equal([]packit.BOMEntry{ + { + Name: "Some Entry", + Metadata: paketosbom.BOMMetadata{ + PURL: "some-purl", + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA256, + Hash: "some-sha", + }, + Source: paketosbom.BOMSource{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA256, + Hash: "some-source-sha", + }, + URI: "some-source", + }, + + URI: "some-uri", + Version: "1.2.3", + }, + }, + })) + }) + }) + + context("when there is a SHA256 instead of Checksum", func() { + it("generates a BOM with the SHA256", func() { entries := service.GenerateBillOfMaterials( postal.Dependency{ ID: "some-entry", Name: "Some Entry", - PURL: "some-purl", SHA256: "some-sha", Source: "some-source", SourceSHA256: "some-source-sha", @@ -914,7 +1352,6 @@ version = "this is super not semver" { Name: "Some Entry", Metadata: paketosbom.BOMMetadata{ - PURL: "some-purl", Checksum: paketosbom.BOMChecksum{ Algorithm: paketosbom.SHA256, Hash: "some-sha", @@ -934,5 +1371,122 @@ version = "this is super not semver" })) }) }) + + context("when there is a checksum and SHA256", func() { + it("generates a BOM with the checksum", func() { + entries := service.GenerateBillOfMaterials( + postal.Dependency{ + ID: "some-entry", + Name: "Some Entry", + Checksum: "sha512:checksum-sha", + SHA256: "some-sha", + Source: "some-source", + SourceChecksum: "sha512:source-checksum-sha", + SourceSHA256: "some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", + }, + ) + + Expect(entries).To(Equal([]packit.BOMEntry{ + { + Name: "Some Entry", + Metadata: paketosbom.BOMMetadata{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA512, + Hash: "checksum-sha", + }, + Source: paketosbom.BOMSource{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA512, + Hash: "source-checksum-sha", + }, + URI: "some-source", + }, + + URI: "some-uri", + Version: "1.2.3", + }, + }, + })) + }) + }) + + context("when the checksum algorithm is unknown", func() { + it("generates a BOM with the empty/unknown checksum", func() { + entries := service.GenerateBillOfMaterials( + postal.Dependency{ + ID: "some-entry", + Name: "Some Entry", + Checksum: "no-such-algo:some-hash", + Source: "some-source", + SourceChecksum: "no-such-algo:some-hash", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", + }, + ) + + Expect(entries).To(Equal([]packit.BOMEntry{ + { + Name: "Some Entry", + Metadata: paketosbom.BOMMetadata{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.UNKNOWN, + Hash: "", + }, + Source: paketosbom.BOMSource{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.UNKNOWN, + Hash: "", + }, + URI: "some-source", + }, + + URI: "some-uri", + Version: "1.2.3", + }, + }, + })) + }) + }) + + context("when there is no checksum or SHA256", func() { + it("generates a BOM with the empty/unknown checksum", func() { + entries := service.GenerateBillOfMaterials( + postal.Dependency{ + ID: "some-entry", + Name: "Some Entry", + Source: "some-source", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", + }, + ) + + Expect(entries).To(Equal([]packit.BOMEntry{ + { + Name: "Some Entry", + Metadata: paketosbom.BOMMetadata{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.UNKNOWN, + Hash: "", + }, + Source: paketosbom.BOMSource{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.UNKNOWN, + Hash: "", + }, + URI: "some-source", + }, + + URI: "some-uri", + Version: "1.2.3", + }, + }, + })) + }) + }) }) } diff --git a/process.go b/process.go index 6c023cac..fd84ab8e 100644 --- a/process.go +++ b/process.go @@ -1,7 +1,7 @@ package packit // Process represents a process to be run during the launch phase as described -// in the specification: +// in the specification lower than v0.9: // https://github.com/buildpacks/spec/blob/main/buildpack.md#launch. The // fields of the process are describe in the specification of the launch.toml // file: @@ -22,4 +22,35 @@ type Process struct { // Default indicates if this process should be the default when launched. Default bool `toml:"default,omitempty"` + + // WorkingDirectory indicates if this process should be run in a working + // directory other than the application directory. This can either be an + // absolute path or one relative to the default application directory. + WorkingDirectory string `toml:"working-directory,omitempty"` +} + +// DirectProcess represents a process to be run during the launch phase as described +// in the specification higher or equal than v0.9: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#launch. The +// fields of the process are describe in the specification of the launch.toml +// file: +// https://github.com/buildpacks/spec/blob/main/buildpack.md#launchtoml-toml. +type DirectProcess struct { + // Type is an identifier to describe the type of process to be executed, eg. + // "web". + Type string `toml:"type"` + + // Command is the start command to be executed at launch. + Command []string `toml:"command"` + + // Args is a list of arguments to be passed to the command at launch. + Args []string `toml:"args"` + + // Default indicates if this process should be the default when launched. + Default bool `toml:"default,omitempty"` + + // WorkingDirectory indicates if this process should be run in a working + // directory other than the application directory. This can either be an + // absolute path or one relative to the default application directory. + WorkingDirectory string `toml:"working-directory,omitempty"` } diff --git a/run.go b/run.go index 23f4b294..4eec78e1 100644 --- a/run.go +++ b/run.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/paketo-buildpacks/packit/internal" + "github.com/paketo-buildpacks/packit/v2/internal" ) // Run combines the invocation of both build and detect into a single entry @@ -26,11 +26,36 @@ func Run(detect DetectFunc, build BuildFunc, options ...Option) { switch phase { case "detect": Detect(detect, options...) - case "build": Build(build, options...) + default: + config.exitHandler.Error(fmt.Errorf("failed to run buildpack: unknown lifecycle phase %q", phase)) + } + +} + +// RunExtension combines the invocation of both generate and detect into a single entry +// point. Calling Run from an executable with a name matching "generate" or +// "detect" will result in the matching DetectFunc or GenerateFunc being called. +func RunExtension(detect DetectFunc, generate GenerateFunc, options ...Option) { + config := OptionConfig{ + exitHandler: internal.NewExitHandler(), + args: os.Args, + } + for _, option := range options { + config = option(config) + } + + phase := filepath.Base(config.args[0]) + + switch phase { + case "detect": + Detect(detect, options...) + case "generate": + Generate(generate, options...) default: config.exitHandler.Error(fmt.Errorf("failed to run buildpack: unknown lifecycle phase %q", phase)) } + } diff --git a/run_test.go b/run_test.go index 9efe0767..dd9430f3 100644 --- a/run_test.go +++ b/run_test.go @@ -5,8 +5,8 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/fakes" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/fakes" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -20,8 +20,21 @@ func testRun(t *testing.T, context spec.G, it spec.S) { tmpDir string cnbDir string exitHandler *fakes.ExitHandler + + buildCalled bool + detectCalled bool ) + build := func(packit.BuildContext) (packit.BuildResult, error) { + buildCalled = true + return packit.BuildResult{}, nil + } + + detect := func(packit.DetectContext) (packit.DetectResult, error) { + detectCalled = true + return packit.DetectResult{}, nil + } + it.Before(func() { var err error workingDir, err = os.Getwd() @@ -48,6 +61,8 @@ clear-env = false `), 0644)).To(Succeed()) exitHandler = &fakes.ExitHandler{} + buildCalled = false + detectCalled = false }) it.After(func() { @@ -73,16 +88,10 @@ clear-env = false }) it("calls the DetectFunc", func() { - var detectCalled bool - - detect := func(packit.DetectContext) (packit.DetectResult, error) { - detectCalled = true - return packit.DetectResult{}, nil - } - - packit.Run(detect, nil, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) + packit.Run(detect, build, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) Expect(detectCalled).To(BeTrue()) + Expect(buildCalled).To(BeFalse()) Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) }) }) @@ -123,16 +132,10 @@ some-key = "some-value" }) it("calls the BuildFunc", func() { - var buildCalled bool - - build := func(packit.BuildContext) (packit.BuildResult, error) { - buildCalled = true - return packit.BuildResult{}, nil - } - - packit.Run(nil, build, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) + packit.Run(detect, build, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) Expect(buildCalled).To(BeTrue()) + Expect(detectCalled).To(BeFalse()) Expect(exitHandler.ErrorCall.CallCount).To(Equal(0)) }) }) @@ -147,6 +150,8 @@ some-key = "some-value" it("returns an error", func() { packit.Run(nil, nil, packit.WithArgs(args), packit.WithExitHandler(exitHandler)) + Expect(buildCalled).To(BeFalse()) + Expect(detectCalled).To(BeFalse()) Expect(exitHandler.ErrorCall.Receives.Error).To(MatchError("failed to run buildpack: unknown lifecycle phase \"something-else\"")) }) }) diff --git a/sbom/formatted_reader.go b/sbom/formatted_reader.go index 2781074a..a4462328 100644 --- a/sbom/formatted_reader.go +++ b/sbom/formatted_reader.go @@ -2,26 +2,46 @@ package sbom import ( "bytes" + "encoding/json" "fmt" "io" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" "sync" + "time" "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/sbom" + "github.com/google/uuid" ) // FormattedReader outputs the SBoM in a specified format. type FormattedReader struct { - m sync.Mutex - sbom SBOM - format Format - reader io.Reader + m sync.Mutex + sbom SBOM + rawFormatID string + format sbom.Format + reader io.Reader } // NewFormattedReader creates an instance of FormattedReader given an SBOM and // Format. func NewFormattedReader(s SBOM, f Format) *FormattedReader { - return &FormattedReader{sbom: s, format: f} + // For backward compatibility, caller can pass f as a format ID like + // "cyclonedx-1.3-json" or as a media type like + // 'application/vnd.cyclonedx+json' + sbomFormat, err := sbomFormatByID(sbom.FormatID(f)) + if err != nil { + sbomFormat, err = sbomFormatByMediaType(string(f)) + if err != nil { + // Defer throwing an error until Read() is called + return &FormattedReader{sbom: s, rawFormatID: string(f), format: nil} + } + } + return &FormattedReader{sbom: s, rawFormatID: string(sbomFormat.ID()), format: sbomFormat} } // Read implements the io.Reader interface to output the contents of the @@ -31,23 +51,98 @@ func (f *FormattedReader) Read(b []byte) (int, error) { defer f.m.Unlock() if f.reader == nil { - var option format.Option - switch f.format { - case CycloneDXFormat: - option = format.CycloneDxJSONOption - case SPDXFormat: - option = format.SPDXJSONOption - case SyftFormat: - option = format.JSONOption - default: - option = format.UnknownFormatOption + if f.format == nil { + return 0, fmt.Errorf("failed to format sbom: '%s' is not a valid SBOM format identifier", f.rawFormatID) } - output, err := syft.Encode(f.sbom.syft, option) + output, err := syft.Encode(f.sbom.syft, f.format) if err != nil { + // not tested return 0, fmt.Errorf("failed to format sbom: %w", err) } + // Makes CycloneDX SBOM more reproducible, see + // https://github.com/paketo-buildpacks/packit/issues/367 for more details. + if f.format.ID() == "cyclonedx-1.3-json" || f.format.ID() == "cyclonedx-json" { + var cycloneDXOutput map[string]interface{} + err = json.Unmarshal(output, &cycloneDXOutput) + if err != nil { + return 0, fmt.Errorf("failed to modify CycloneDX SBOM for reproducibility: %w", err) + } + + if metadata, ok := cycloneDXOutput["metadata"].(map[string]interface{}); ok { + delete(metadata, "timestamp") + cycloneDXOutput["metadata"] = metadata + } + + delete(cycloneDXOutput, "serialNumber") + + // Indent with a two spaces, as they do in CycloneDX: + // https://github.com/CycloneDX/cyclonedx-go/blob/429d353cfcdbfedf367f597cbdde2a840ebf29df/encode.go#L44 + output, err = json.MarshalIndent(cycloneDXOutput, "", " ") + if err != nil { + return 0, fmt.Errorf("failed to modify CycloneDX SBOM for reproducibility: %w", err) + } + } + + // Makes SPDX SBOM more reproducible, see + // https://github.com/paketo-buildpacks/packit/issues/368 for more details. + if f.format.ID() == "spdx-2-json" { + var spdxOutput map[string]interface{} + + err = json.Unmarshal(output, &spdxOutput) + if err != nil { + return 0, fmt.Errorf("failed to modify SPDX SBOM for reproducibility: %w", err) + } + + // Makes the creationInfo reproducible so a hash can be taken for the + // documentNamespace + if creationInfo, ok := spdxOutput["creationInfo"].(map[string]interface{}); ok { + creationInfo["created"] = time.Time{} // This is the zero-valued time + + sourceDateEpoch := os.Getenv("SOURCE_DATE_EPOCH") + if sourceDateEpoch != "" { + sde, err := strconv.ParseInt(sourceDateEpoch, 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse SOURCE_DATE_EPOCH: %w", err) + } + creationInfo["created"] = time.Unix(sde, 0).UTC() + } + spdxOutput["creationInfo"] = creationInfo + } + + if namespace, ok := spdxOutput["documentNamespace"].(string); ok { + delete(spdxOutput, "documentNamespace") + + data, err := json.Marshal(spdxOutput) + if err != nil { + return 0, fmt.Errorf("failed to checksum SPDX document: %w", err) + } + + uri, err := url.Parse(namespace) + if err != nil { + // not tested + return 0, err + } + + uri.Host = "paketo.io" + uri.Path = strings.Replace(uri.Path, "syft", "packit", 1) + oldBase := filepath.Base(uri.Path) + source, _, _ := strings.Cut(oldBase, "-") + newBase := fmt.Sprintf("%s-%s", source, uuid.NewSHA1(uuid.NameSpaceURL, data)) + uri.Path = strings.Replace(uri.Path, oldBase, newBase, 1) + + spdxOutput["documentNamespace"] = uri.String() + } + + // Indent with a single space, as they do in SPDX: + // https://github.com/anchore/syft/blob/1344889766743beb736aafdfb29266910b738fbb/internal/formats/spdx22json/encoder.go#L16 + output, err = json.MarshalIndent(spdxOutput, "", " ") + if err != nil { + return 0, fmt.Errorf("failed to modify SPDX SBOM for reproducibility: %w", err) + } + } + f.reader = bytes.NewBuffer(output) } diff --git a/sbom/formatted_reader_test.go b/sbom/formatted_reader_test.go index 9225576d..0acca104 100644 --- a/sbom/formatted_reader_test.go +++ b/sbom/formatted_reader_test.go @@ -3,11 +3,14 @@ package sbom_test import ( "bytes" "encoding/json" - "fmt" "io" + "os" "testing" + "time" - "github.com/paketo-buildpacks/packit/sbom" + "github.com/anchore/syft/syft" + "github.com/paketo-buildpacks/packit/v2/sbom" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -26,585 +29,268 @@ func testFormattedReader(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) }) - it("writes the SBOM in CycloneDX format", func() { + it("writes the SBOM in the default CycloneDX format", func() { buffer := bytes.NewBuffer(nil) _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.CycloneDXFormat)) Expect(err).NotTo(HaveOccurred()) - var output struct { - SerialNumber string `json:"serialNumber"` - Metadata struct { - Timestamp string `json:"timestamp"` - } `json:"metadata"` - } - err = json.Unmarshal(buffer.Bytes(), &output) + format := syft.IdentifyFormat(buffer.Bytes()) + Expect(format.ID()).To(Equal(syft.CycloneDxJSONFormatID)) + + // Ensures pretty printing + Expect(buffer.String()).To(ContainSubstring(`{ + "bomFormat": "CycloneDX", + "components": [ + {`)) + + var cdxOutput cdxOutput + + err = json.Unmarshal(buffer.Bytes(), &cdxOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + Expect(cdxOutput.BOMFormat).To(Equal("CycloneDX"), buffer.String()) + Expect(cdxOutput.SpecVersion).To(Equal("1.3"), buffer.String()) + Expect(cdxOutput.SerialNumber).To(Equal(""), buffer.String()) + + Expect(cdxOutput.Metadata.Timestamp).To(Equal(""), buffer.String()) + Expect(cdxOutput.Metadata.Component.Type).To(Equal("file"), buffer.String()) + Expect(cdxOutput.Metadata.Component.Type).To(Equal("file"), buffer.String()) + Expect(cdxOutput.Metadata.Component.Name).To(Equal("testdata/"), buffer.String()) + Expect(cdxOutput.Components[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(cdxOutput.Components[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(cdxOutput.Components[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(cdxOutput.Components[3].Name).To(Equal("once"), buffer.String()) + Expect(cdxOutput.Components[4].Name).To(Equal("pump"), buffer.String()) + Expect(cdxOutput.Components[5].Name).To(Equal("wrappy"), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.CycloneDXFormat)) Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) + }) - Expect(buffer.String()).To(MatchJSON(fmt.Sprintf(`{ - "bomFormat": "CycloneDX", - "specVersion": "1.3", - "version": 1, - "serialNumber": "%s", - "metadata": { - "timestamp": "%s", - "tools": [ - { - "vendor": "anchore", - "name": "syft", - "version": "[not provided]" - } - ], - "component": { - "type": "file", - "name": "testdata/", - "version": "" - } - }, - "components": [ - { - "type": "library", - "name": "collapse-white-space", - "version": "2.0.0", - "purl": "pkg:npm/collapse-white-space@2.0.0" - }, - { - "type": "library", - "name": "end-of-stream", - "version": "1.4.4", - "purl": "pkg:npm/end-of-stream@1.4.4" - }, - { - "type": "library", - "name": "insert-css", - "version": "2.0.0", - "purl": "pkg:npm/insert-css@2.0.0" - }, - { - "type": "library", - "name": "once", - "version": "1.4.0", - "purl": "pkg:npm/once@1.4.0" - }, - { - "type": "library", - "name": "pump", - "version": "3.0.0", - "purl": "pkg:npm/pump@3.0.0" - }, - { - "type": "library", - "name": "wrappy", - "version": "1.0.2", - "purl": "pkg:npm/wrappy@1.0.2" - } - ] - }`, output.SerialNumber, output.Metadata.Timestamp))) + it("writes the SBOM in the latest CycloneDX format (1.4)", func() { + buffer := bytes.NewBuffer(nil) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.Format(syft.CycloneDxJSONFormatID))) + Expect(err).NotTo(HaveOccurred()) + + format := syft.IdentifyFormat(buffer.Bytes()) + Expect(format.ID()).To(Equal(syft.CycloneDxJSONFormatID)) + + var cdxOutput cdxOutput + + err = json.Unmarshal(buffer.Bytes(), &cdxOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + Expect(cdxOutput.BOMFormat).To(Equal("CycloneDX"), buffer.String()) + Expect(cdxOutput.SpecVersion).To(Equal("1.4"), buffer.String()) + Expect(cdxOutput.SerialNumber).To(Equal(""), buffer.String()) + + Expect(cdxOutput.Metadata.Timestamp).To(Equal(""), buffer.String()) + Expect(cdxOutput.Metadata.Component.Type).To(Equal("file"), buffer.String()) + Expect(cdxOutput.Metadata.Component.Name).To(Equal("testdata/"), buffer.String()) + Expect(cdxOutput.Components[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(cdxOutput.Components[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(cdxOutput.Components[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(cdxOutput.Components[3].Name).To(Equal("once"), buffer.String()) + Expect(cdxOutput.Components[4].Name).To(Equal("pump"), buffer.String()) + Expect(cdxOutput.Components[5].Name).To(Equal("wrappy"), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.Format(syft.CycloneDxJSONFormatID))) + Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) }) - it("writes the SBOM in SPDX format", func() { + context("writes the SBOM in SPDX format, with fields replaced for reproducibility", func() { + it("produces an SBOM", func() { + buffer := bytes.NewBuffer(nil) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.SPDXFormat)) + Expect(err).NotTo(HaveOccurred()) + + format := syft.IdentifyFormat(buffer.Bytes()) + Expect(format.ID()).To(Equal(syft.SPDXJSONFormatID)) + + // Ensures pretty printing + Expect(buffer.String()).To(ContainSubstring(`{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": {`)) + + var spdxOutput spdxOutput + + err = json.Unmarshal(buffer.Bytes(), &spdxOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + Expect(spdxOutput.SPDXVersion).To(Equal("SPDX-2.2"), buffer.String()) + + Expect(spdxOutput.Packages[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(spdxOutput.Packages[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(spdxOutput.Packages[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(spdxOutput.Packages[3].Name).To(Equal("once"), buffer.String()) + Expect(spdxOutput.Packages[4].Name).To(Equal("pump"), buffer.String()) + Expect(spdxOutput.Packages[5].Name).To(Equal("wrappy"), buffer.String()) + + // Ensure documentNamespace and creationInfo.created have reproducible values + Expect(spdxOutput.DocumentNamespace).To(Equal("https://paketo.io/packit/dir/testdata-e5ba1162-56a7-57ac-8372-3aff3f15e036"), buffer.String()) + Expect(spdxOutput.CreationInfo.Created).To(BeZero(), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.SPDXFormat)) + Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) + }) + + context("when SOURCE_DATE_EPOCH is set", func() { + var original string + + it.Before(func() { + original = os.Getenv("SOURCE_DATE_EPOCH") + Expect(os.Setenv("SOURCE_DATE_EPOCH", "1659551872")).To(Succeed()) + }) + + it.After(func() { + Expect(os.Setenv("SOURCE_DATE_EPOCH", original)).To(Succeed()) + }) + + context("when the timestamp is valid", func() { + it.Before(func() { + Expect(os.Setenv("SOURCE_DATE_EPOCH", "1659551872")).To(Succeed()) + }) + + it("produces an SBOM with the given timestamp", func() { + buffer := bytes.NewBuffer(nil) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.SPDXFormat)) + Expect(err).NotTo(HaveOccurred()) + + var spdxOutput spdxOutput + + err = json.Unmarshal(buffer.Bytes(), &spdxOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + format := syft.IdentifyFormat(buffer.Bytes()) + Expect(format.ID()).To(Equal(syft.SPDXJSONFormatID)) + + Expect(spdxOutput.SPDXVersion).To(Equal("SPDX-2.2"), buffer.String()) + + Expect(spdxOutput.Packages[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(spdxOutput.Packages[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(spdxOutput.Packages[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(spdxOutput.Packages[3].Name).To(Equal("once"), buffer.String()) + Expect(spdxOutput.Packages[4].Name).To(Equal("pump"), buffer.String()) + Expect(spdxOutput.Packages[5].Name).To(Equal("wrappy"), buffer.String()) + + // Ensure documentNamespace and creationInfo.created have reproducible values + Expect(spdxOutput.DocumentNamespace).To(Equal("https://paketo.io/packit/dir/testdata-ef57d584-3f15-5c91-be8c-0f7c011883a8"), buffer.String()) + Expect(spdxOutput.CreationInfo.Created).To(Equal(time.Unix(1659551872, 0).UTC()), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.SPDXFormat)) + Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) + }) + + context("failure cases", func() { + context("when the timestamp is not valid", func() { + it.Before(func() { + Expect(os.Setenv("SOURCE_DATE_EPOCH", "not-a-valid-timestamp")).To(Succeed()) + }) + + it("returns an error", func() { + buffer := bytes.NewBuffer(nil) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.SPDXFormat)) + Expect(err).To(MatchError(ContainSubstring("failed to parse SOURCE_DATE_EPOCH"))) + }) + }) + }) + }) + }) + }, spec.Sequential()) + + it("writes the SBOM in the default syft format", func() { buffer := bytes.NewBuffer(nil) - _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.SPDXFormat)) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.SyftFormat)) + Expect(err).NotTo(HaveOccurred()) + + var syftOutput syftOutput + + err = json.Unmarshal(buffer.Bytes(), &syftOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + Expect(syftOutput.Schema.Version).To(Equal(`3.0.1`), buffer.String()) + + Expect(syftOutput.Source.Type).To(Equal("directory"), buffer.String()) + Expect(syftOutput.Source.Target).To(Equal("testdata/"), buffer.String()) + Expect(syftOutput.Artifacts[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(syftOutput.Artifacts[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(syftOutput.Artifacts[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(syftOutput.Artifacts[3].Name).To(Equal("once"), buffer.String()) + Expect(syftOutput.Artifacts[4].Name).To(Equal("pump"), buffer.String()) + Expect(syftOutput.Artifacts[5].Name).To(Equal("wrappy"), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.SyftFormat)) Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) + }) - var output struct { - CreationInfo struct { - Created string `json:"created"` - } `json:"creationInfo"` - DocumentNamespace string `json:"documentNamespace"` - } - err = json.Unmarshal(buffer.Bytes(), &output) + it("writes the SBOM in Syft 2.0.2 format", func() { + buffer := bytes.NewBuffer(nil) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.Format(syft2.ID))) Expect(err).NotTo(HaveOccurred()) - Expect(buffer.String()).To(MatchJSON(fmt.Sprintf(`{ - "SPDXID": "SPDXRef-DOCUMENT", - "name": "testdata", - "spdxVersion": "SPDX-2.2", - "creationInfo": { - "created": "%s", - "creators": [ - "Organization: Anchore, Inc", - "Tool: syft-[not provided]" - ], - "licenseListVersion": "3.15" - }, - "dataLicense": "CC0-1.0", - "documentNamespace": "%s", - "packages": [ - { - "SPDXID": "SPDXRef-32427d6153854661", - "name": "collapse-white-space", - "licenseConcluded": "NONE", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse-white-space:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse-white-space:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse_white_space:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse_white_space:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse-white:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse-white:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse_white:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse_white:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:collapse:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:npm/collapse-white-space@2.0.0", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "NONE", - "sourceInfo": "acquired package info from installed node module manifest file: testdata/package-lock.json", - "versionInfo": "2.0.0" - }, - { - "SPDXID": "SPDXRef-3a9cd5afdee12f9e", - "name": "end-of-stream", - "licenseConcluded": "NONE", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end-of-stream:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end-of-stream:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end_of_stream:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end_of_stream:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end-of:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end-of:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end_of:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end_of:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:end:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:npm/end-of-stream@1.4.4", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "NONE", - "sourceInfo": "acquired package info from installed node module manifest file: testdata/package-lock.json", - "versionInfo": "1.4.4" - }, - { - "SPDXID": "SPDXRef-1a6a787a32934992", - "name": "insert-css", - "licenseConcluded": "NONE", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:insert-css:insert-css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:insert-css:insert_css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:insert_css:insert-css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:insert_css:insert_css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:insert:insert-css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:insert:insert_css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:insert-css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:insert_css:2.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:npm/insert-css@2.0.0", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "NONE", - "sourceInfo": "acquired package info from installed node module manifest file: testdata/package-lock.json", - "versionInfo": "2.0.0" - }, - { - "SPDXID": "SPDXRef-4705ae55f7cf3d30", - "name": "once", - "licenseConcluded": "NONE", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:once:once:1.4.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:once:1.4.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:npm/once@1.4.0", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "NONE", - "sourceInfo": "acquired package info from installed node module manifest file: testdata/package-lock.json", - "versionInfo": "1.4.0" - }, - { - "SPDXID": "SPDXRef-cbd84c0e95ea71a3", - "name": "pump", - "licenseConcluded": "NONE", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:pump:pump:3.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:pump:3.0.0:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:npm/pump@3.0.0", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "NONE", - "sourceInfo": "acquired package info from installed node module manifest file: testdata/package-lock.json", - "versionInfo": "3.0.0" - }, - { - "SPDXID": "SPDXRef-7f69702d44cabe6", - "name": "wrappy", - "licenseConcluded": "NONE", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:wrappy:wrappy:1.0.2:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:*:wrappy:1.0.2:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:npm/wrappy@1.0.2", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "NONE", - "sourceInfo": "acquired package info from installed node module manifest file: testdata/package-lock.json", - "versionInfo": "1.0.2" - } - ] - }`, output.CreationInfo.Created, output.DocumentNamespace))) + // Ensures pretty printing + Expect(buffer.String()).To(ContainSubstring(`{ + "artifacts": [ + { + "id":`)) + + var syftOutput syftOutput + + err = json.Unmarshal(buffer.Bytes(), &syftOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + Expect(syftOutput.Schema.Version).To(Equal("2.0.2"), buffer.String()) + + Expect(syftOutput.Source.Type).To(Equal("directory"), buffer.String()) + Expect(syftOutput.Source.Target).To(Equal("testdata/"), buffer.String()) + Expect(syftOutput.Artifacts[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(syftOutput.Artifacts[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(syftOutput.Artifacts[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(syftOutput.Artifacts[3].Name).To(Equal("once"), buffer.String()) + Expect(syftOutput.Artifacts[4].Name).To(Equal("pump"), buffer.String()) + Expect(syftOutput.Artifacts[5].Name).To(Equal("wrappy"), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.Format(syft2.ID))) + Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) }) - it("writes the SBOM in Syft format", func() { + it("writes the SBOM in the latest Syft format (7.*)", func() { buffer := bytes.NewBuffer(nil) - _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.SyftFormat)) + _, err := io.Copy(buffer, sbom.NewFormattedReader(bom, sbom.Format(syft.JSONFormatID))) Expect(err).NotTo(HaveOccurred()) - Expect(buffer.String()).To(MatchJSON(`{ - "artifacts": [ - { - "id": "32427d6153854661", - "name": "collapse-white-space", - "version": "2.0.0", - "type": "npm", - "foundBy": "javascript-lock-cataloger", - "locations": [ - { - "path": "testdata/package-lock.json" - } - ], - "licenses": [], - "language": "javascript", - "cpes": [ - "cpe:2.3:a:collapse-white-space:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse-white-space:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse_white_space:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse_white_space:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse-white:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse-white:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse_white:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse_white:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:collapse:collapse_white_space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:*:collapse-white-space:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:*:collapse_white_space:2.0.0:*:*:*:*:*:*:*" - ], - "purl": "pkg:npm/collapse-white-space@2.0.0", - "metadataType": "", - "metadata": null - }, - { - "id": "3a9cd5afdee12f9e", - "name": "end-of-stream", - "version": "1.4.4", - "type": "npm", - "foundBy": "javascript-lock-cataloger", - "locations": [ - { - "path": "testdata/package-lock.json" - } - ], - "licenses": [], - "language": "javascript", - "cpes": [ - "cpe:2.3:a:end-of-stream:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end-of-stream:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end_of_stream:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end_of_stream:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end-of:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end-of:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end_of:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end_of:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:end:end_of_stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:*:end-of-stream:1.4.4:*:*:*:*:*:*:*", - "cpe:2.3:a:*:end_of_stream:1.4.4:*:*:*:*:*:*:*" - ], - "purl": "pkg:npm/end-of-stream@1.4.4", - "metadataType": "", - "metadata": null - }, - { - "id": "1a6a787a32934992", - "name": "insert-css", - "version": "2.0.0", - "type": "npm", - "foundBy": "javascript-lock-cataloger", - "locations": [ - { - "path": "testdata/package-lock.json" - } - ], - "licenses": [], - "language": "javascript", - "cpes": [ - "cpe:2.3:a:insert-css:insert-css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:insert-css:insert_css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:insert_css:insert-css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:insert_css:insert_css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:insert:insert-css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:insert:insert_css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:*:insert-css:2.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:*:insert_css:2.0.0:*:*:*:*:*:*:*" - ], - "purl": "pkg:npm/insert-css@2.0.0", - "metadataType": "", - "metadata": null - }, - { - "id": "4705ae55f7cf3d30", - "name": "once", - "version": "1.4.0", - "type": "npm", - "foundBy": "javascript-lock-cataloger", - "locations": [ - { - "path": "testdata/package-lock.json" - } - ], - "licenses": [], - "language": "javascript", - "cpes": [ - "cpe:2.3:a:once:once:1.4.0:*:*:*:*:*:*:*", - "cpe:2.3:a:*:once:1.4.0:*:*:*:*:*:*:*" - ], - "purl": "pkg:npm/once@1.4.0", - "metadataType": "", - "metadata": null - }, - { - "id": "cbd84c0e95ea71a3", - "name": "pump", - "version": "3.0.0", - "type": "npm", - "foundBy": "javascript-lock-cataloger", - "locations": [ - { - "path": "testdata/package-lock.json" - } - ], - "licenses": [], - "language": "javascript", - "cpes": [ - "cpe:2.3:a:pump:pump:3.0.0:*:*:*:*:*:*:*", - "cpe:2.3:a:*:pump:3.0.0:*:*:*:*:*:*:*" - ], - "purl": "pkg:npm/pump@3.0.0", - "metadataType": "", - "metadata": null - }, - { - "id": "7f69702d44cabe6", - "name": "wrappy", - "version": "1.0.2", - "type": "npm", - "foundBy": "javascript-lock-cataloger", - "locations": [ - { - "path": "testdata/package-lock.json" - } - ], - "licenses": [], - "language": "javascript", - "cpes": [ - "cpe:2.3:a:wrappy:wrappy:1.0.2:*:*:*:*:*:*:*", - "cpe:2.3:a:*:wrappy:1.0.2:*:*:*:*:*:*:*" - ], - "purl": "pkg:npm/wrappy@1.0.2", - "metadataType": "", - "metadata": null - } - ], - "artifactRelationships": [], - "source": { - "type": "directory", - "target": "testdata/" - }, - "distro": { - "name": "", - "version": "", - "idLike": "" - }, - "descriptor": { - "name": "", - "version": "" - }, - "schema": { - "version": "2.0.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json" - } - }`)) + var syftOutput syftOutput + + err = json.Unmarshal(buffer.Bytes(), &syftOutput) + Expect(err).NotTo(HaveOccurred(), buffer.String()) + + Expect(syftOutput.Schema.Version).To(MatchRegexp(`7\.\d+\.\d+`), buffer.String()) + + Expect(syftOutput.Source.Type).To(Equal("directory"), buffer.String()) + Expect(syftOutput.Source.Target).To(Equal("testdata/"), buffer.String()) + Expect(syftOutput.Artifacts[0].Name).To(Equal("collapse-white-space"), buffer.String()) + Expect(syftOutput.Artifacts[1].Name).To(Equal("end-of-stream"), buffer.String()) + Expect(syftOutput.Artifacts[2].Name).To(Equal("insert-css"), buffer.String()) + Expect(syftOutput.Artifacts[3].Name).To(Equal("once"), buffer.String()) + Expect(syftOutput.Artifacts[4].Name).To(Equal("pump"), buffer.String()) + Expect(syftOutput.Artifacts[5].Name).To(Equal("wrappy"), buffer.String()) + + rerunBuffer := bytes.NewBuffer(nil) + _, err = io.Copy(rerunBuffer, sbom.NewFormattedReader(bom, sbom.Format(syft.JSONFormatID))) + Expect(err).NotTo(HaveOccurred()) + Expect(rerunBuffer.String()).To(Equal(buffer.String())) }) context("Read", func() { @@ -613,7 +299,7 @@ func testFormattedReader(t *testing.T, context spec.G, it spec.S) { it("returns an error", func() { formatter := sbom.NewFormattedReader(sbom.SBOM{}, sbom.Format("unknown-format")) _, err := formatter.Read(make([]byte, 10)) - Expect(err).To(MatchError("failed to format sbom: unsupported format: UnknownFormatOption")) + Expect(err).To(MatchError("failed to format sbom: 'unknown-format' is not a valid SBOM format identifier")) }) }) }) diff --git a/sbom/formatter.go b/sbom/formatter.go index 9756fd4b..3e28affa 100644 --- a/sbom/formatter.go +++ b/sbom/formatter.go @@ -1,20 +1,26 @@ package sbom -import "github.com/paketo-buildpacks/packit" +import ( + "github.com/anchore/syft/syft/sbom" + "github.com/paketo-buildpacks/packit/v2" +) // Formatter implements the packit.SBOMFormatter interface. type Formatter struct { - sbom SBOM - formats []Format + sbom SBOM + formatIDs []sbom.FormatID } // Formats returns a list of packit.SBOMFormat instances. func (f Formatter) Formats() []packit.SBOMFormat { var formats []packit.SBOMFormat - for _, format := range f.formats { + for _, id := range f.formatIDs { + // ignore error here; FormattedReader validates SBOM format before Read() + format, _ := sbomFormatByID(id) formats = append(formats, packit.SBOMFormat{ Extension: format.Extension(), - Content: NewFormattedReader(f.sbom, format), + // type conversion here to maintain backward compatibility of NewFormattedReader + Content: NewFormattedReader(f.sbom, Format(id)), }) } diff --git a/sbom/formatter_test.go b/sbom/formatter_test.go index 5355ed10..10bbccc6 100644 --- a/sbom/formatter_test.go +++ b/sbom/formatter_test.go @@ -4,7 +4,7 @@ import ( "io" "testing" - "github.com/paketo-buildpacks/packit/sbom" + "github.com/paketo-buildpacks/packit/v2/sbom" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/sbom/init_test.go b/sbom/init_test.go index 0a2404ce..4c5f64cc 100644 --- a/sbom/init_test.go +++ b/sbom/init_test.go @@ -8,7 +8,7 @@ import ( "github.com/sclevine/spec/report" ) -func TestSBOM(t *testing.T) { +func TestUnitSBOM(t *testing.T) { format.MaxLength = 0 suite := spec.New("sbom", spec.Report(report.Terminal{})) diff --git a/sbom/internal/constants.go b/sbom/internal/constants.go new file mode 100644 index 00000000..2beeca9f --- /dev/null +++ b/sbom/internal/constants.go @@ -0,0 +1,12 @@ +package internal + +// Copied from syft v0.42.3 +// https://github.com/anchore/syft/blob/cc2c0e57a0d02a1719b4e34d0793f09e9699c8b0/internal/constants.go +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 = "3.1.1" +) diff --git a/sbom/internal/formats/common/property_encoder.go b/sbom/internal/formats/common/property_encoder.go new file mode 100644 index 00000000..ac64c8cd --- /dev/null +++ b/sbom/internal/formats/common/property_encoder.go @@ -0,0 +1,320 @@ +package common + +import ( + "fmt" + "reflect" + "sort" + "strconv" + "strings" +) + +// FieldName return a flag to indicate this is a valid field and a name to use +type FieldName func(field reflect.StructField) (string, bool) + +// OptionalTag given a tag name, will return the defined tag or fall back to lower camel case field name +func OptionalTag(tag string) FieldName { + return func(f reflect.StructField) (string, bool) { + if n, ok := f.Tag.Lookup(tag); ok { + return n, true + } + return lowerFirst(f.Name), true + } +} + +// TrimOmitempty trims `,omitempty` from the name +func TrimOmitempty(fn FieldName) FieldName { + return func(f reflect.StructField) (string, bool) { + if v, ok := fn(f); ok { + return strings.TrimSuffix(v, ",omitempty"), true + } + return "", false + } +} + +// RequiredTag based on the given tag, only use a field if present +func RequiredTag(tag string) FieldName { + return func(f reflect.StructField) (string, bool) { + if n, ok := f.Tag.Lookup(tag); ok { + return n, true + } + return "", false + } +} + +var ( + // OptionalJSONTag uses field names defined in json tags, if available + OptionalJSONTag = TrimOmitempty(OptionalTag("json")) +) + +// lowerFirst converts the first character of the string to lower case +func lowerFirst(s string) string { + return strings.ToLower(s[0:1]) + s[1:] +} + +// Encode recursively encodes the object's properties as an ordered set of NameValue pairs +func Encode(obj interface{}, prefix string, fn FieldName) map[string]string { + if obj == nil { + return nil + } + props := map[string]string{} + encode(props, reflect.ValueOf(obj), prefix, fn) + return props +} + +// NameValue a simple type to store stringified name/value pairs +type NameValue struct { + Name string + Value string +} + +// Sorted returns a sorted set of NameValue pairs +func Sorted(values map[string]string) (out []NameValue) { + var keys []string + for k := range values { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + out = append(out, NameValue{ + Name: k, + Value: values[k], + }) + } + return +} + +func encode(out map[string]string, value reflect.Value, prefix string, fn FieldName) { + if !value.IsValid() || value.Type() == nil { + return + } + + typ := value.Type() + + switch typ.Kind() { + case reflect.Ptr: + if value.IsNil() { + return + } + value = value.Elem() + encode(out, value, prefix, fn) + case reflect.String: + v := value.String() + if v != "" { + out[prefix] = v + } + case reflect.Bool: + v := value.Bool() + out[prefix] = strconv.FormatBool(v) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v := value.Int() + out[prefix] = strconv.FormatInt(v, 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v := value.Uint() + out[prefix] = strconv.FormatUint(v, 10) + case reflect.Float32, reflect.Float64: + v := value.Float() + out[prefix] = fmt.Sprintf("%f", v) + case reflect.Array, reflect.Slice: + for idx := 0; idx < value.Len(); idx++ { + encode(out, value.Index(idx), fmt.Sprintf("%s:%d", prefix, idx), fn) + } + case reflect.Struct: + for i := 0; i < typ.NumField(); i++ { + pv := value.Field(i) + f := typ.Field(i) + name, ok := fieldName(f, prefix, fn) + if !ok { + continue + } + encode(out, pv, name, fn) + } + default: + // log.Warnf("skipping encoding of unsupported property: %s", prefix) + } +} + +// fieldName gets the name of the field using the provided FieldName function +func fieldName(f reflect.StructField, prefix string, fn FieldName) (string, bool) { + name, ok := fn(f) + if !ok { + return "", false + } + if name == "" { + return prefix, true + } + if prefix != "" { + name = fmt.Sprintf("%s:%s", prefix, name) + } + return name, true +} + +// Decode based on the given type, applies all values to hydrate a new instance +func Decode(typ reflect.Type, values map[string]string, prefix string, fn FieldName) interface{} { + isPtr := false + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + isPtr = true + } + + isSlice := false + if typ.Kind() == reflect.Slice { + typ = reflect.PtrTo(typ) + isSlice = true + } + + v := reflect.New(typ) + + decode(values, v, prefix, fn) + + switch { + case isSlice && isPtr: + return v.Elem().Interface() + case isSlice: + return PtrToStruct(v.Elem().Interface()) + case isPtr: + return v.Interface() + } + return v.Elem().Interface() +} + +// DecodeInto decodes all values to hydrate the given object instance +func DecodeInto(obj interface{}, values map[string]string, prefix string, fn FieldName) { + value := reflect.ValueOf(obj) + + for value.Type().Kind() == reflect.Ptr { + value = value.Elem() + } + + decode(values, value, prefix, fn) +} + +// nolint: funlen, gocognit, gocyclo +func decode(vals map[string]string, value reflect.Value, prefix string, fn FieldName) bool { + if !value.IsValid() || value.Type() == nil { + return false + } + + typ := value.Type() + + incoming, valid := vals[prefix] + switch typ.Kind() { + case reflect.Ptr: + t := typ.Elem() + v := value + if v.IsNil() { + v = reflect.New(t) + } + if decode(vals, v.Elem(), prefix, fn) && value.CanSet() { + // o := v.Interface() + // log.Infof("%v", o) + value.Set(v) + } else { + return false + } + case reflect.String: + if valid { + value.SetString(incoming) + } else { + return false + } + case reflect.Bool: + if !valid { + return false + } + if b, err := strconv.ParseBool(incoming); err == nil { + value.SetBool(b) + } else { + return false + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if !valid { + return false + } + if i, err := strconv.ParseInt(incoming, 10, 64); err == nil { + value.SetInt(i) + } else { + return false + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if !valid { + return false + } + if i, err := strconv.ParseUint(incoming, 10, 64); err == nil { + value.SetUint(i) + } else { + return false + } + case reflect.Float32, reflect.Float64: + if !valid { + return false + } + if i, err := strconv.ParseFloat(incoming, 64); err == nil { + value.SetFloat(i) + } else { + return false + } + case reflect.Array, reflect.Slice: + values := false + t := typ.Elem() + slice := reflect.MakeSlice(typ, 0, 0) + for idx := 0; ; idx++ { + // test for index + str := fmt.Sprintf("%s:%d", prefix, idx) + // create new placeholder and decode values + newType := t + if t.Kind() == reflect.Ptr { + newType = t.Elem() + } + v := reflect.New(newType) + if decode(vals, v.Elem(), str, fn) { + // append to slice + if t.Kind() != reflect.Ptr { + v = v.Elem() + } + slice = reflect.Append(slice, v) + values = true + } else { + break + } + } + if values { + value.Set(slice) + } else { + return false + } + case reflect.Struct: + values := false + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + v := value.Field(i) + + name, ok := fieldName(f, prefix, fn) + if !ok { + continue + } + + if decode(vals, v, name, fn) { + values = true + } + } + return values + default: + // log.Warnf("unable to set field: %s", prefix) + return false + } + return true +} + +func PtrToStruct(ptr interface{}) interface{} { + v := reflect.ValueOf(ptr) + if v.IsZero() { + return nil + } + switch v.Type().Kind() { + case reflect.Ptr: + return PtrToStruct(v.Elem().Interface()) + case reflect.Interface: + return PtrToStruct(v.Elem().Interface()) + } + return v.Interface() +} diff --git a/sbom/internal/formats/common/property_encoder_test.go b/sbom/internal/formats/common/property_encoder_test.go new file mode 100644 index 00000000..2ff63a1f --- /dev/null +++ b/sbom/internal/formats/common/property_encoder_test.go @@ -0,0 +1,143 @@ +package common + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +type T1 struct { + Name string + Val int + ValU uint + Flag bool `json:"bool_flag"` + Float float64 + T2 T2 + T2Ptr *T2 + T2Arr []T2 + T2PtrArr []*T2 + StrArr []string + IntArr []int + FloatArr []float64 + BoolArr []bool + T3Arr []T3 +} + +type T2 struct { + Name string +} + +type T3 struct { + T4Arr []T4 +} + +type T4 struct { + Typ string + IntPtr *int +} + +func Test_EncodeDecodeCycle(t *testing.T) { + val := 99 + + tests := []struct { + name string + value interface{} + }{ + { + name: "all values", + value: T1{ + Name: "name", + Val: 10, + ValU: 16, + Flag: true, + Float: 1.2, + T2: T2{ + Name: "embedded t2", + }, + T2Ptr: &T2{ + "t2 ptr", + }, + T2Arr: []T2{ + {"t2 elem 0"}, + {"t2 elem 1"}, + }, + T2PtrArr: []*T2{ + {"t2 ptr v1"}, + {"t2 ptr v2"}, + }, + StrArr: []string{"s 1", "s 2", "s 3"}, + IntArr: []int{9, 12, -1}, + FloatArr: []float64{-23.99, 15.234321, 39912342314}, + BoolArr: []bool{false, true, true, true, false}, + T3Arr: []T3{ + { + T4Arr: []T4{ + { + Typ: "t4 nested typ 1", + }, + { + Typ: "t4 nested typ 2", + IntPtr: &val, + }, + }, + }, + }, + }, + }, + { + name: "nil values", + value: T1{ + Name: "t1 test", + Val: 0, + ValU: 0, + Flag: false, + Float: 0, + T2: T2{}, + T2Ptr: nil, + T2Arr: nil, + T2PtrArr: nil, + StrArr: nil, + IntArr: nil, + FloatArr: nil, + BoolArr: nil, + T3Arr: nil, + }, + }, + { + name: "array values", + value: []T2{ + {"t2 elem 0"}, + {"t2 elem 1"}, + }, + }, + { + name: "array ptr", + value: &[]T2{ + {"t2 elem 0"}, + {"t2 elem 1"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + vals := Encode(test.value, "props", OptionalJSONTag) + + typ := reflect.TypeOf(test.value) + + if typ.Kind() != reflect.Slice && typ.Kind() != reflect.Ptr { + assert.NotEmpty(t, vals["props:bool_flag"]) + + t2 := T1{} + DecodeInto(&t2, vals, "props", OptionalJSONTag) + + assert.EqualValues(t, test.value, t2) + } + + t3 := Decode(typ, vals, "props", OptionalJSONTag) + + assert.EqualValues(t, test.value, t3) + }) + } +} diff --git a/sbom/internal/formats/common/testutils/utils.go b/sbom/internal/formats/common/testutils/utils.go new file mode 100644 index 00000000..b72ccfa6 --- /dev/null +++ b/sbom/internal/formats/common/testutils/utils.go @@ -0,0 +1,389 @@ +package testutils + +import ( + "bytes" + "math/rand" + "strings" + "testing" + "time" + + "github.com/sergi/go-diff/diffmatchpatch" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/go-testutils" + "github.com/anchore/stereoscope/pkg/filetree" + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/stereoscope/pkg/imagetest" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +type redactor func(s []byte) []byte + +type imageCfg struct { + fromSnapshot bool +} + +type ImageOption func(cfg *imageCfg) + +func FromSnapshot() ImageOption { + return func(cfg *imageCfg) { + cfg.fromSnapshot = true + } +} + +func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, json bool, redactors ...redactor) { + var buffer bytes.Buffer + + // grab the latest image contents and persist + if updateSnapshot { + imagetest.UpdateGoldenFixtureImage(t, testImage) + } + + err := format.Encode(&buffer, sbom) + assert.NoError(t, err) + actual := buffer.Bytes() + + // replace the expected snapshot contents with the current encoder contents + if updateSnapshot { + testutils.UpdateGoldenFileContents(t, actual) + } + + actual = redact(actual, redactors...) + expected := redact(testutils.GetGoldenFileContents(t), redactors...) + + if json { + require.JSONEq(t, string(expected), string(actual)) + } else if !bytes.Equal(expected, actual) { + // assert that the golden file snapshot matches the actual contents + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(string(expected), string(actual), true) + t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) + } +} + +func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, json bool, redactors ...redactor) { + var buffer bytes.Buffer + + err := format.Encode(&buffer, sbom) + assert.NoError(t, err) + actual := buffer.Bytes() + + // replace the expected snapshot contents with the current encoder contents + if updateSnapshot { + testutils.UpdateGoldenFileContents(t, actual) + } + + actual = redact(actual, redactors...) + expected := redact(testutils.GetGoldenFileContents(t), redactors...) + + if json { + 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)) + } +} + +func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM { + t.Helper() + catalog := pkg.NewCollection() + var cfg imageCfg + var img *image.Image + for _, opt := range options { + opt(&cfg) + } + + switch cfg.fromSnapshot { + case true: + img = imagetest.GetGoldenFixtureImage(t, testImage) + default: + img = imagetest.GetFixtureImage(t, "docker-archive", testImage) + } + + populateImageCatalog(catalog, img) + + // this is a hard coded value that is not given by the fixture helper and must be provided manually + img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" + + src, err := source.NewFromImage(img, "user-image-input") + assert.NoError(t, err) + + return sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: catalog, + LinuxDistribution: &linux.Release{ + PrettyName: "debian", + Name: "debian", + ID: "debian", + IDLike: []string{"like!"}, + Version: "1.2.3", + VersionID: "1.2.3", + }, + }, + Source: src.Metadata, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } +} + +func carriageRedactor(s []byte) []byte { + msg := strings.ReplaceAll(string(s), "\r\n", "\n") + return []byte(msg) +} + +func populateImageCatalog(catalog *pkg.Collection, img *image.Image) { + _, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks) + _, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks) + + // populate catalog with test data + catalog.Add(pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Locations: source.NewLocationSet( + source.NewLocationFromImage(string(ref1.RealPath), *ref1.Reference, img), + ), + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + }, + PURL: "a-purl-1", // intentionally a bad pURL for test fixtures + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + }, + }) + catalog.Add(pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Locations: source.NewLocationSet( + source.NewLocationFromImage(string(ref2.RealPath), *ref2.Reference, img), + ), + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + }, + PURL: "pkg:deb/debian/package-2@2.0.1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) +} + +func DirectoryInput(t testing.TB) sbom.SBOM { + catalog := newDirectoryCatalog() + + src, err := source.NewFromDirectory("/some/path") + assert.NoError(t, err) + + return sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: catalog, + LinuxDistribution: &linux.Release{ + PrettyName: "debian", + Name: "debian", + ID: "debian", + IDLike: []string{"like!"}, + Version: "1.2.3", + VersionID: "1.2.3", + }, + }, + Source: src.Metadata, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } +} + +func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM { + catalog := newDirectoryCatalogWithAuthorField() + + src, err := source.NewFromDirectory("/some/path") + assert.NoError(t, err) + + return sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: catalog, + LinuxDistribution: &linux.Release{ + PrettyName: "debian", + Name: "debian", + ID: "debian", + IDLike: []string{"like!"}, + Version: "1.2.3", + VersionID: "1.2.3", + }, + }, + Source: src.Metadata, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } +} + +func newDirectoryCatalog() *pkg.Collection { + catalog := pkg.NewCollection() + + // populate catalog with test data + catalog.Add(pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Locations: source.NewLocationSet( + source.NewLocation("/some/path/pkg1"), + ), + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + Files: []pkg.PythonFileRecord{ + { + Path: "/some/path/pkg1/dependencies/foo", + }, + }, + }, + PURL: "a-purl-2", // intentionally a bad pURL for test fixtures + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) + catalog.Add(pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + Locations: source.NewLocationSet( + source.NewLocation("/some/path/pkg1"), + ), + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + }, + PURL: "pkg:deb/debian/package-2@2.0.1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) + + return catalog +} + +func newDirectoryCatalogWithAuthorField() *pkg.Collection { + catalog := pkg.NewCollection() + + // populate catalog with test data + catalog.Add(pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Locations: source.NewLocationSet( + source.NewLocation("/some/path/pkg1"), + ), + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + Author: "test-author", + Files: []pkg.PythonFileRecord{ + { + Path: "/some/path/pkg1/dependencies/foo", + }, + }, + }, + PURL: "a-purl-2", // intentionally a bad pURL for test fixtures + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) + catalog.Add(pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + Locations: source.NewLocationSet( + source.NewLocation("/some/path/pkg1"), + ), + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + }, + PURL: "pkg:deb/debian/package-2@2.0.1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + }) + + return catalog +} + +//nolint:gosec +func AddSampleFileRelationships(s *sbom.SBOM) { + catalog := s.Artifacts.Packages.Sorted() + s.Artifacts.FileMetadata = map[source.Coordinates]source.FileMetadata{} + + files := []string{"/f1", "/f2", "/d1/f3", "/d2/f4", "/z1/f5", "/a1/f6"} + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) + + for _, f := range files { + meta := source.FileMetadata{} + coords := source.Coordinates{RealPath: f} + s.Artifacts.FileMetadata[coords] = meta + + s.Relationships = append(s.Relationships, artifact.Relationship{ + From: catalog[0], + To: coords, + Type: artifact.ContainsRelationship, + }) + } +} + +// remove dynamic values, which should be tested independently +func redact(b []byte, redactors ...redactor) []byte { + redactors = append(redactors, carriageRedactor) + for _, r := range redactors { + b = r(b) + } + return b +} diff --git a/sbom/internal/formats/cyclonedx13/README.md b/sbom/internal/formats/cyclonedx13/README.md new file mode 100644 index 00000000..3f85b515 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/README.md @@ -0,0 +1,9 @@ +# Source +The contents of this directory is largely based on anchore/syft's +internal `cyclonedx13json` package. The version copied is from an [old +commit](https://github.com/anchore/syft/blob/a86dd3704efdb19aea22774eb7e099d4e85d41e4/internal/formats/cyclonedx13json) +of Syft that supports CycloneDX JSON Schema 1.3. + +The implementations of `decoder` and `validator` have been omitted for +simplicity, since they are not required for buildpacks' SBOM generation. + diff --git a/sbom/internal/formats/cyclonedx13/cyclonedx/README.md b/sbom/internal/formats/cyclonedx13/cyclonedx/README.md new file mode 100644 index 00000000..4247a31c --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedx/README.md @@ -0,0 +1,12 @@ +# Source +The contents of this directory is largely based on CycloneDX/cyclonedx-go's +internal `cyclonedx` package. The version copied is from tag version +[0.4.0](https://github.com/CycloneDX/cyclonedx-go/tree/dc02c3afeacc6975b83f6c579b2adfa19f85873f), +which [supports CycloneDX Spec version +1.3](https://github.com/CycloneDX/cyclonedx-go/blob/404f9a308e0f2cdf22e16fe9fda4d7cb6a5b19a2/README.md#compatibility). + +Only the portions of the package that are strictly necessary for SBOM encoding +have been included here. + + +This is required because the upstream code base does not support versioned specs. diff --git a/sbom/internal/formats/cyclonedx13/cyclonedx/cyclonedx.go b/sbom/internal/formats/cyclonedx13/cyclonedx/cyclonedx.go new file mode 100644 index 00000000..c67f4247 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedx/cyclonedx.go @@ -0,0 +1,504 @@ +// This file is part of CycloneDX Go +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Niklas Düster. All Rights Reserved. + +package cyclonedx + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" +) + +const ( + BOMFormat = "CycloneDX" + defaultVersion = 1 + SpecVersion = "1.3" + XMLNamespace = "http://cyclonedx.org/schema/bom/1.3" +) + +type AttachedText struct { + Content string `json:"content" xml:",innerxml"` + ContentType string `json:"contentType,omitempty" xml:"content-type,attr,omitempty"` + Encoding string `json:"encoding,omitempty" xml:"encoding,attr,omitempty"` +} + +type BOM struct { + // XML specific fields + XMLName xml.Name `json:"-" xml:"bom"` + XMLNS string `json:"-" xml:"xmlns,attr"` + + // JSON specific fields + BOMFormat string `json:"bomFormat" xml:"-"` + SpecVersion string `json:"specVersion" xml:"-"` + + SerialNumber string `json:"serialNumber,omitempty" xml:"serialNumber,attr,omitempty"` + Version int `json:"version" xml:"version,attr"` + Metadata *Metadata `json:"metadata,omitempty" xml:"metadata,omitempty"` + Components *[]Component `json:"components,omitempty" xml:"components>component,omitempty"` + Services *[]Service `json:"services,omitempty" xml:"services>service,omitempty"` + ExternalReferences *[]ExternalReference `json:"externalReferences,omitempty" xml:"externalReferences>reference,omitempty"` + Dependencies *[]Dependency `json:"dependencies,omitempty" xml:"dependencies>dependency,omitempty"` + Compositions *[]Composition `json:"compositions,omitempty" xml:"compositions>composition,omitempty"` + Properties *[]Property `json:"properties,omitempty" xml:"properties>property,omitempty"` +} + +func NewBOM() *BOM { + return &BOM{ + XMLNS: XMLNamespace, + BOMFormat: BOMFormat, + SpecVersion: SpecVersion, + Version: defaultVersion, + } +} + +type BOMFileFormat int + +const ( + BOMFileFormatXML BOMFileFormat = iota + BOMFileFormatJSON +) + +// Bool is a convenience function to transform a value of the primitive type bool to a pointer of bool +func Bool(value bool) *bool { + return &value +} + +type BOMReference string + +// bomReferenceXML is temporarily used for marshalling and unmarshalling BOMReference instances to and from XML +type bomReferenceXML struct { + Ref string `json:"-" xml:"ref,attr"` +} + +func (b BOMReference) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(bomReferenceXML{Ref: string(b)}, start) +} + +func (b *BOMReference) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + bXML := bomReferenceXML{} + if err := d.DecodeElement(&bXML, &start); err != nil { + return err + } + *b = BOMReference(bXML.Ref) + return nil +} + +type ComponentType string + +const ( + ComponentTypeApplication ComponentType = "application" + ComponentTypeContainer ComponentType = "container" + ComponentTypeDevice ComponentType = "device" + ComponentTypeFile ComponentType = "file" + ComponentTypeFirmware ComponentType = "firmware" + ComponentTypeFramework ComponentType = "framework" + ComponentTypeLibrary ComponentType = "library" + ComponentTypeOS ComponentType = "operating-system" +) + +type Commit struct { + UID string `json:"uid,omitempty" xml:"uid,omitempty"` + URL string `json:"url,omitempty" xml:"url,omitempty"` + Author *IdentifiableAction `json:"author,omitempty" xml:"author,omitempty"` + Committer *IdentifiableAction `json:"committer,omitempty" xml:"committer,omitempty"` + Message string `json:"message,omitempty" xml:"message,omitempty"` +} + +type Component struct { + BOMRef string `json:"bom-ref,omitempty" xml:"bom-ref,attr,omitempty"` + MIMEType string `json:"mime-type,omitempty" xml:"mime-type,attr,omitempty"` + Type ComponentType `json:"type" xml:"type,attr"` + Supplier *OrganizationalEntity `json:"supplier,omitempty" xml:"supplier,omitempty"` + Author string `json:"author,omitempty" xml:"author,omitempty"` + Publisher string `json:"publisher,omitempty" xml:"publisher,omitempty"` + Group string `json:"group,omitempty" xml:"group,omitempty"` + Name string `json:"name" xml:"name"` + Version string `json:"version" xml:"version"` + Description string `json:"description,omitempty" xml:"description,omitempty"` + Scope Scope `json:"scope,omitempty" xml:"scope,omitempty"` + Hashes *[]Hash `json:"hashes,omitempty" xml:"hashes>hash,omitempty"` + Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"` + Copyright string `json:"copyright,omitempty" xml:"copyright,omitempty"` + CPE string `json:"cpe,omitempty" xml:"cpe,omitempty"` + PackageURL string `json:"purl,omitempty" xml:"purl,omitempty"` + SWID *SWID `json:"swid,omitempty" xml:"swid,omitempty"` + Modified *bool `json:"modified,omitempty" xml:"modified,omitempty"` + Pedigree *Pedigree `json:"pedigree,omitempty" xml:"pedigree,omitempty"` + ExternalReferences *[]ExternalReference `json:"externalReferences,omitempty" xml:"externalReferences>reference,omitempty"` + Properties *[]Property `json:"properties,omitempty" xml:"properties>property,omitempty"` + Components *[]Component `json:"components,omitempty" xml:"components>component,omitempty"` + Evidence *Evidence `json:"evidence,omitempty" xml:"evidence,omitempty"` +} + +type Composition struct { + Aggregate CompositionAggregate `json:"aggregate" xml:"aggregate"` + Assemblies *[]BOMReference `json:"assemblies,omitempty" xml:"assemblies>assembly,omitempty"` + Dependencies *[]BOMReference `json:"dependencies,omitempty" xml:"dependencies>dependency,omitempty"` +} + +type CompositionAggregate string + +const ( + CompositionAggregateComplete CompositionAggregate = "complete" + CompositionAggregateIncomplete CompositionAggregate = "incomplete" + CompositionAggregateIncompleteFirstPartyOnly CompositionAggregate = "incomplete_first_party_only" + CompositionAggregateIncompleteThirdPartyOnly CompositionAggregate = "incomplete_third_party_only" + CompositionAggregateUnknown CompositionAggregate = "unknown" + CompositionAggregateNotSpecified CompositionAggregate = "not_specified" +) + +type Copyright struct { + Text string `json:"text" xml:"-"` +} + +func (c Copyright) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(c.Text, start) +} + +func (c *Copyright) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var text string + if err := d.DecodeElement(&text, &start); err != nil { + return err + } + (*c).Text = text + return nil +} + +type DataClassification struct { + Flow DataFlow `json:"flow" xml:"flow,attr"` + Classification string `json:"classification" xml:",innerxml"` +} + +type DataFlow string + +const ( + DataFlowBidirectional DataFlow = "bi-directional" + DataFlowInbound DataFlow = "inbound" + DataFlowOutbound DataFlow = "outbound" + DataFlowUnknown DataFlow = "unknown" +) + +type Dependency struct { + Ref string `xml:"ref,attr"` + Dependencies *[]Dependency `xml:"dependency,omitempty"` +} + +// dependencyJSON is temporarily used for marshalling and unmarshalling Dependency instances to and from JSON +type dependencyJSON struct { + Ref string `json:"ref"` + DependsOn []string `json:"dependsOn,omitempty"` +} + +func (d Dependency) MarshalJSON() ([]byte, error) { + if d.Dependencies == nil || len(*d.Dependencies) == 0 { + return json.Marshal(&dependencyJSON{ + Ref: d.Ref, + }) + } + + dependencyRefs := make([]string, len(*d.Dependencies)) + for i, dependency := range *d.Dependencies { + dependencyRefs[i] = dependency.Ref + } + + return json.Marshal(&dependencyJSON{ + Ref: d.Ref, + DependsOn: dependencyRefs, + }) +} + +func (d *Dependency) UnmarshalJSON(bytes []byte) error { + dependency := new(dependencyJSON) + if err := json.Unmarshal(bytes, dependency); err != nil { + return err + } + d.Ref = dependency.Ref + + if len(dependency.DependsOn) == 0 { + return nil + } + + dependencies := make([]Dependency, len(dependency.DependsOn)) + for i, dep := range dependency.DependsOn { + dependencies[i] = Dependency{ + Ref: dep, + } + } + d.Dependencies = &dependencies + + return nil +} + +type Diff struct { + Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"` + URL string `json:"url,omitempty" xml:"url,omitempty"` +} + +type Evidence struct { + Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"` + Copyright *[]Copyright `json:"copyright,omitempty" xml:"copyright>text,omitempty"` +} + +type ExternalReference struct { + URL string `json:"url" xml:"url"` + Comment string `json:"comment,omitempty" xml:"comment,omitempty"` + Hashes *[]Hash `json:"hashes,omitempty" xml:"hashes>hash,omitempty"` + Type ExternalReferenceType `json:"type" xml:"type,attr"` +} + +type ExternalReferenceType string + +const ( + ERTypeAdvisories ExternalReferenceType = "advisories" + ERTypeBOM ExternalReferenceType = "bom" + ERTypeBuildMeta ExternalReferenceType = "build-meta" + ERTypeBuildSystem ExternalReferenceType = "build-system" + ERTypeChat ExternalReferenceType = "chat" + ERTypeDistribution ExternalReferenceType = "distribution" + ERTypeDocumentation ExternalReferenceType = "documentation" + ERTypeLicense ExternalReferenceType = "license" + ERTypeMailingList ExternalReferenceType = "mailing-list" + ERTypeOther ExternalReferenceType = "other" + ERTypeIssueTracker ExternalReferenceType = "issue-tracker" + ERTypeSocial ExternalReferenceType = "social" + ERTypeSupport ExternalReferenceType = "support" + ERTypeVCS ExternalReferenceType = "vcs" + ERTypeWebsite ExternalReferenceType = "website" +) + +type Hash struct { + Algorithm HashAlgorithm `json:"alg" xml:"alg,attr"` + Value string `json:"content" xml:",innerxml"` +} + +type HashAlgorithm string + +const ( + HashAlgoMD5 HashAlgorithm = "MD5" + HashAlgoSHA1 HashAlgorithm = "SHA-1" + HashAlgoSHA256 HashAlgorithm = "SHA-256" + HashAlgoSHA384 HashAlgorithm = "SHA-384" + HashAlgoSHA512 HashAlgorithm = "SHA-512" + HashAlgoSHA3_256 HashAlgorithm = "SHA3-256" + HashAlgoSHA3_512 HashAlgorithm = "SHA3-512" + HashAlgoBlake2b_256 HashAlgorithm = "BLAKE2b-256" + HashAlgoBlake2b_384 HashAlgorithm = "BLAKE2b-384" + HashAlgoBlake2b_512 HashAlgorithm = "BLAKE2b-512" + HashAlgoBlake3 HashAlgorithm = "BLAKE3" +) + +type IdentifiableAction struct { + Timestamp string `json:"timestamp,omitempty" xml:"timestamp,omitempty"` + Name string `json:"name,omitempty" xml:"name,omitempty"` + EMail string `json:"email,omitempty" xml:"email,omitempty"` +} + +type Issue struct { + ID string `json:"id" xml:"id"` + Name string `json:"name" xml:"name"` + Description string `json:"description" xml:"description"` + Source *Source `json:"source,omitempty" xml:"source,omitempty"` + References *[]string `json:"references,omitempty" xml:"references>url,omitempty"` + Type IssueType `json:"type" xml:"type,attr"` +} + +type IssueType string + +const ( + IssueTypeDefect IssueType = "defect" + IssueTypeEnhancement IssueType = "enhancement" + IssueTypeSecurity IssueType = "security" +) + +type License struct { + ID string `json:"id,omitempty" xml:"id,omitempty"` + Name string `json:"name,omitempty" xml:"name,omitempty"` + Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"` + URL string `json:"url,omitempty" xml:"url,omitempty"` +} + +type Licenses []LicenseChoice + +func (l Licenses) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if len(l) == 0 { + return nil + } + + if err := e.EncodeToken(start); err != nil { + return err + } + + for _, choice := range l { + if choice.License != nil && choice.Expression != "" { + return fmt.Errorf("either license or expression must be set, but not both") + } + + if choice.License != nil { + if err := e.EncodeElement(choice.License, xml.StartElement{Name: xml.Name{Local: "license"}}); err != nil { + return err + } + } else if choice.Expression != "" { + if err := e.EncodeElement(choice.Expression, xml.StartElement{Name: xml.Name{Local: "expression"}}); err != nil { + return err + } + } + } + + return e.EncodeToken(start.End()) +} + +func (l *Licenses) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { + licenses := make([]LicenseChoice, 0) + + for { + token, err := d.Token() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + + switch tokenType := token.(type) { + case xml.StartElement: + if tokenType.Name.Local == "expression" { + var expression string + if err = d.DecodeElement(&expression, &tokenType); err != nil { + return err + } + licenses = append(licenses, LicenseChoice{Expression: expression}) + } else if tokenType.Name.Local == "license" { + var license License + if err = d.DecodeElement(&license, &tokenType); err != nil { + return err + } + licenses = append(licenses, LicenseChoice{License: &license}) + } else { + return fmt.Errorf("unknown element: %s", tokenType.Name.Local) + } + } + } + + *l = licenses + return nil +} + +type LicenseChoice struct { + License *License `json:"license,omitempty" xml:"-"` + Expression string `json:"expression,omitempty" xml:"-"` +} + +type Metadata struct { + Timestamp string `json:"timestamp,omitempty" xml:"timestamp,omitempty"` + Tools *[]Tool `json:"tools,omitempty" xml:"tools>tool,omitempty"` + Authors *[]OrganizationalContact `json:"authors,omitempty" xml:"authors>author,omitempty"` + Component *Component `json:"component,omitempty" xml:"component,omitempty"` + Manufacture *OrganizationalEntity `json:"manufacture,omitempty" xml:"manufacture,omitempty"` + Supplier *OrganizationalEntity `json:"supplier,omitempty" xml:"supplier,omitempty"` + Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"` + Properties *[]Property `json:"properties,omitempty" xml:"properties>property,omitempty"` +} + +type OrganizationalContact struct { + Name string `json:"name,omitempty" xml:"name,omitempty"` + EMail string `json:"email,omitempty" xml:"email,omitempty"` + Phone string `json:"phone,omitempty" xml:"phone,omitempty"` +} + +type OrganizationalEntity struct { + Name string `json:"name" xml:"name"` + URL *[]string `json:"url,omitempty" xml:"url,omitempty"` + Contact *[]OrganizationalContact `json:"contact,omitempty" xml:"contact,omitempty"` +} + +type Patch struct { + Diff *Diff `json:"diff,omitempty" xml:"diff,omitempty"` + Resolves *[]Issue `json:"resolves,omitempty" xml:"resolves>issue,omitempty"` + Type PatchType `json:"type" xml:"type,attr"` +} + +type PatchType string + +const ( + PatchTypeBackport PatchType = "backport" + PatchTypeCherryPick PatchType = "cherry-pick" + PatchTypeMonkey PatchType = "monkey" + PatchTypeUnofficial PatchType = "unofficial" +) + +type Pedigree struct { + Ancestors *[]Component `json:"ancestors,omitempty" xml:"ancestors>component,omitempty"` + Descendants *[]Component `json:"descendants,omitempty" xml:"descendants>component,omitempty"` + Variants *[]Component `json:"variants,omitempty" xml:"variants>component,omitempty"` + Commits *[]Commit `json:"commits,omitempty" xml:"commits>commit,omitempty"` + Patches *[]Patch `json:"patches,omitempty" xml:"patches>patch,omitempty"` + Notes string `json:"notes,omitempty" xml:"notes,omitempty"` +} + +type Property struct { + Name string `json:"name" xml:"name,attr"` + Value string `json:"value" xml:",innerxml"` +} + +type Scope string + +const ( + ScopeExcluded Scope = "excluded" + ScopeOptional Scope = "optional" + ScopeRequired Scope = "required" +) + +type Service struct { + BOMRef string `json:"bom-ref,omitempty" xml:"bom-ref,attr,omitempty"` + Provider *OrganizationalEntity `json:"provider,omitempty" xml:"provider,omitempty"` + Group string `json:"group,omitempty" xml:"group,omitempty"` + Name string `json:"name" xml:"name"` + Version string `json:"version,omitempty" xml:"version,omitempty"` + Description string `json:"description,omitempty" xml:"description,omitempty"` + Endpoints *[]string `json:"endpoints,omitempty" xml:"endpoints>endpoint,omitempty"` + Authenticated *bool `json:"authenticated,omitempty" xml:"authenticated,omitempty"` + CrossesTrustBoundary *bool `json:"x-trust-boundary,omitempty" xml:"x-trust-boundary,omitempty"` + Data *[]DataClassification `json:"data,omitempty" xml:"data>classification,omitempty"` + Licenses *Licenses `json:"licenses,omitempty" xml:"licenses,omitempty"` + ExternalReferences *[]ExternalReference `json:"externalReferences,omitempty" xml:"externalReferences>reference,omitempty"` + Properties *[]Property `json:"properties,omitempty" xml:"properties>property,omitempty"` + Services *[]Service `json:"services,omitempty" xml:"services>service,omitempty"` +} + +type Source struct { + Name string `json:"name,omitempty" xml:"name,omitempty"` + URL string `json:"url,omitempty" xml:"url,omitempty"` +} + +type SWID struct { + Text *AttachedText `json:"text,omitempty" xml:"text,omitempty"` + URL string `json:"url,omitempty" xml:"url,attr,omitempty"` + TagID string `json:"tagId" xml:"tagId,attr"` + Name string `json:"name" xml:"name,attr"` + Version string `json:"version,omitempty" xml:"version,attr,omitempty"` + TagVersion *int `json:"tagVersion,omitempty" xml:"tagVersion,attr,omitempty"` + Patch *bool `json:"patch,omitempty" xml:"patch,attr,omitempty"` +} + +type Tool struct { + Vendor string `json:"vendor,omitempty" xml:"vendor,omitempty"` + Name string `json:"name" xml:"name"` + Version string `json:"version,omitempty" xml:"version,omitempty"` + Hashes *[]Hash `json:"hashes,omitempty" xml:"hashes>hash,omitempty"` +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedx/cyclonedx_test.go b/sbom/internal/formats/cyclonedx13/cyclonedx/cyclonedx_test.go new file mode 100644 index 00000000..8b7330b7 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedx/cyclonedx_test.go @@ -0,0 +1,219 @@ +// This file is part of CycloneDX Go +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Niklas Düster. All Rights Reserved. + +package cyclonedx + +import ( + "encoding/json" + "encoding/xml" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBool(t *testing.T) { + assert.Equal(t, true, *Bool(true)) + assert.Equal(t, false, *Bool(false)) +} + +func TestBOMReference_MarshalXML(t *testing.T) { + // Marshal empty bomRef + bomRef := BOMReference("") + xmlBytes, err := xml.Marshal(bomRef) + assert.NoError(t, err) + assert.Equal(t, "", string(xmlBytes)) + + // Marshal bomRef + bomRef = "bomRef" + xmlBytes, err = xml.Marshal(bomRef) + assert.NoError(t, err) + assert.Equal(t, "", string(xmlBytes)) +} + +func TestBOMReference_UnmarshalXML(t *testing.T) { + // Unmarshal empty bomRef + bomRef := new(BOMReference) + err := xml.Unmarshal([]byte(""), bomRef) + require.NoError(t, err) + require.Equal(t, "", string(*bomRef)) + + // Unmarshal bomRef + err = xml.Unmarshal([]byte(""), bomRef) + require.NoError(t, err) + require.Equal(t, "bomRef", string(*bomRef)) +} + +func TestCopyright_MarshalXML(t *testing.T) { + // Marshal empty copyright + copyright := Copyright{} + xmlBytes, err := xml.Marshal(copyright) + require.NoError(t, err) + require.Equal(t, "", string(xmlBytes)) + + // Marshal copyright + copyright.Text = "copyright" + xmlBytes, err = xml.Marshal(copyright) + require.NoError(t, err) + require.Equal(t, "copyright", string(xmlBytes)) +} + +func TestCopyright_UnmarshalXML(t *testing.T) { + // Unmarshal empty copyright + copyright := new(Copyright) + err := xml.Unmarshal([]byte(""), copyright) + require.NoError(t, err) + require.Equal(t, "", copyright.Text) + + // Unmarshal copyright + err = xml.Unmarshal([]byte("copyright"), copyright) + require.NoError(t, err) + require.Equal(t, "copyright", copyright.Text) +} + +func TestDependency_MarshalJSON(t *testing.T) { + // Marshal empty dependency + dependency := Dependency{} + jsonBytes, err := json.Marshal(dependency) + assert.NoError(t, err) + assert.Equal(t, "{\"ref\":\"\"}", string(jsonBytes)) + + // Marshal dependency with empty dependencies + dependency = Dependency{ + Ref: "dependencyRef", + Dependencies: &[]Dependency{}, + } + jsonBytes, err = json.Marshal(dependency) + assert.NoError(t, err) + assert.Equal(t, "{\"ref\":\"dependencyRef\"}", string(jsonBytes)) + + // Marshal dependency with dependencies + dependency = Dependency{ + Ref: "dependencyRef", + Dependencies: &[]Dependency{ + {Ref: "transitiveDependencyRef"}, + }, + } + jsonBytes, err = json.Marshal(dependency) + assert.NoError(t, err) + assert.Equal(t, "{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}", string(jsonBytes)) +} + +func TestDependency_UnmarshalJSON(t *testing.T) { + // Unmarshal empty dependency + dependency := new(Dependency) + err := json.Unmarshal([]byte("{}"), dependency) + assert.NoError(t, err) + assert.Equal(t, "", dependency.Ref) + assert.Nil(t, dependency.Dependencies) + + // Unmarshal dependency with empty dependencies + dependency = new(Dependency) + err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[]}"), dependency) + assert.NoError(t, err) + assert.Equal(t, "dependencyRef", dependency.Ref) + assert.Nil(t, dependency.Dependencies) + + // Unmarshal dependency with dependencies + dependency = new(Dependency) + err = json.Unmarshal([]byte("{\"ref\":\"dependencyRef\",\"dependsOn\":[\"transitiveDependencyRef\"]}"), dependency) + assert.NoError(t, err) + assert.Equal(t, "dependencyRef", dependency.Ref) + assert.Equal(t, 1, len(*dependency.Dependencies)) + assert.Equal(t, "transitiveDependencyRef", (*dependency.Dependencies)[0].Ref) +} + +func TestLicenses_MarshalXML(t *testing.T) { + // Marshal license and expressions + licenses := Licenses{ + LicenseChoice{ + Expression: "expressionValue1", + }, + LicenseChoice{ + License: &License{ + ID: "licenseID", + URL: "licenseURL", + }, + }, + LicenseChoice{ + Expression: "expressionValue2", + }, + } + xmlBytes, err := xml.MarshalIndent(licenses, "", " ") + assert.NoError(t, err) + assert.Equal(t, ` + expressionValue1 + + licenseID + licenseURL + + expressionValue2 +`, string(xmlBytes)) + + // Should return error when both license and expression are set on an element + licenses = Licenses{ + LicenseChoice{ + License: &License{ + ID: "licenseID", + }, + Expression: "expressionValue", + }, + } + _, err = xml.Marshal(licenses) + assert.Error(t, err) + + // Should encode nothing when empty + licenses = Licenses{} + xmlBytes, err = xml.Marshal(licenses) + assert.NoError(t, err) + assert.Nil(t, xmlBytes) +} + +func TestLicenses_UnmarshalXML(t *testing.T) { + // Unmarshal license and expressions + licenses := new(Licenses) + err := xml.Unmarshal([]byte(` + + expressionValue1 + + licenseID + licenseURL + + expressionValue2 +`), licenses) + assert.NoError(t, err) + assert.Len(t, *licenses, 3) + assert.Nil(t, (*licenses)[0].License) + assert.Equal(t, "expressionValue1", (*licenses)[0].Expression) + assert.NotNil(t, (*licenses)[1].License) + assert.Equal(t, "licenseID", (*licenses)[1].License.ID) + assert.Equal(t, "licenseURL", (*licenses)[1].License.URL) + assert.Empty(t, (*licenses)[1].Expression) + assert.Nil(t, (*licenses)[2].License) + assert.Equal(t, "expressionValue2", (*licenses)[2].Expression) + + // Unmarshal empty licenses + licenses = new(Licenses) + err = xml.Unmarshal([]byte(""), licenses) + assert.NoError(t, err) + assert.Empty(t, *licenses) + + // Should return error when an element is neither license nor expression + licenses = new(Licenses) + err = xml.Unmarshal([]byte("expressionValue"), licenses) + assert.Error(t, err) +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedx/encode.go b/sbom/internal/formats/cyclonedx13/cyclonedx/encode.go new file mode 100644 index 00000000..c1be7c2b --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedx/encode.go @@ -0,0 +1,75 @@ +// This file is part of CycloneDX Go +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Niklas Düster. All Rights Reserved. + +package cyclonedx + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io" +) + +type BOMEncoder interface { + Encode(*BOM) error + SetPretty(bool) +} + +func NewBOMEncoder(writer io.Writer, format BOMFileFormat) BOMEncoder { + if format == BOMFileFormatJSON { + return &jsonBOMEncoder{writer: writer} + } + return &xmlBOMEncoder{writer: writer} +} + +type jsonBOMEncoder struct { + writer io.Writer + pretty bool +} + +func (j jsonBOMEncoder) Encode(bom *BOM) error { + encoder := json.NewEncoder(j.writer) + if j.pretty { + encoder.SetIndent("", " ") + } + return encoder.Encode(bom) +} + +func (j *jsonBOMEncoder) SetPretty(pretty bool) { + j.pretty = pretty +} + +type xmlBOMEncoder struct { + writer io.Writer + pretty bool +} + +func (x xmlBOMEncoder) Encode(bom *BOM) error { + if _, err := fmt.Fprintf(x.writer, xml.Header); err != nil { + return err + } + + encoder := xml.NewEncoder(x.writer) + if x.pretty { + encoder.Indent("", " ") + } + return encoder.Encode(bom) +} + +func (x *xmlBOMEncoder) SetPretty(pretty bool) { + x.pretty = pretty +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedx/encode_test.go b/sbom/internal/formats/cyclonedx13/cyclonedx/encode_test.go new file mode 100644 index 00000000..f4b08a5d --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedx/encode_test.go @@ -0,0 +1,90 @@ +// This file is part of CycloneDX Go +// +// Licensed under the Apache License, Version 2.0 (the “License”); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an “AS IS” BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Niklas Düster. All Rights Reserved. + +package cyclonedx + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBOMEncoder(t *testing.T) { + assert.IsType(t, &jsonBOMEncoder{}, NewBOMEncoder(nil, BOMFileFormatJSON)) + assert.IsType(t, &xmlBOMEncoder{}, NewBOMEncoder(nil, BOMFileFormatXML)) +} + +func TestJsonBOMEncoder_SetPretty(t *testing.T) { + buf := new(bytes.Buffer) + encoder := NewBOMEncoder(buf, BOMFileFormatJSON) + encoder.SetPretty(true) + + bom := NewBOM() + bom.Metadata = &Metadata{ + Authors: &[]OrganizationalContact{ + { + Name: "authorName", + }, + }, + } + + require.NoError(t, encoder.Encode(bom)) + + assert.Equal(t, `{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "version": 1, + "metadata": { + "authors": [ + { + "name": "authorName" + } + ] + } +} +`, buf.String()) +} + +func TestXmlBOMEncoder_SetPretty(t *testing.T) { + buf := new(bytes.Buffer) + encoder := NewBOMEncoder(buf, BOMFileFormatXML) + encoder.SetPretty(true) + + bom := NewBOM() + bom.Metadata = &Metadata{ + Authors: &[]OrganizationalContact{ + { + Name: "authorName", + }, + }, + } + + require.NoError(t, encoder.Encode(bom)) + + assert.Equal(t, ` + + + + + authorName + + + +`, buf.String()) +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/README.md b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/README.md new file mode 100644 index 00000000..6f7cb62b --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/README.md @@ -0,0 +1,12 @@ +# Source +The contents of this directory is largely based on anchore/syft's +internal `cyclonedxhelpers` package. The version copied is from an [old +commit](https://github.com/anchore/syft/blob/a86dd3704efdb19aea22774eb7e099d4e85d41e4/internal/formats/common/cyclonedxhelpers) +of Syft that supports CycloneDX JSON Schema 1.3. + +Any helpers here remain because they contain 1.3-specific logic, so we cannot +use upstream code. + +The implementation of `decoder` has been omitted for +simplicity, since it is not required for buildpacks' SBOM generation. + diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/author.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/author.go new file mode 100644 index 00000000..97849bf7 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/author.go @@ -0,0 +1,34 @@ +package cyclonedxhelpers + +import ( + "fmt" + "strings" + + "github.com/anchore/syft/syft/pkg" +) + +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodeAuthor(p pkg.Package) string { + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.NpmPackageJSONMetadata: + return metadata.Author + case pkg.PythonPackageMetadata: + author := metadata.Author + if metadata.AuthorEmail != "" { + if author == "" { + return metadata.AuthorEmail + } + author += fmt.Sprintf(" <%s>", metadata.AuthorEmail) + } + return author + case pkg.GemMetadata: + if len(metadata.Authors) > 0 { + return strings.Join(metadata.Authors, ",") + } + return "" + } + } + return "" +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/author_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/author_test.go new file mode 100644 index 00000000..11cd95d6 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/author_test.go @@ -0,0 +1,87 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_encodeAuthor(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "from gem", + input: pkg.Package{ + Metadata: pkg.GemMetadata{ + Authors: []string{ + "auth1", + "auth2", + }, + }, + }, + expected: "auth1,auth2", + }, + { + name: "from npm", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "auth", + }, + }, + expected: "auth", + }, + { + name: "from python - just name", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + Author: "auth", + }, + }, + expected: "auth", + }, + { + name: "from python - just email", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + AuthorEmail: "auth@auth.gov", + }, + }, + expected: "auth@auth.gov", + }, + { + name: "from python - both name and email", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + Author: "auth", + AuthorEmail: "auth@auth.gov", + }, + }, + expected: "auth ", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "", + }, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeAuthor(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/component.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/component.go new file mode 100644 index 00000000..3cb5be29 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/component.go @@ -0,0 +1,45 @@ +package cyclonedxhelpers + +import ( + "github.com/anchore/syft/syft/pkg" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" +) + +// Relies on cycloneDX published structs +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodeComponent(p pkg.Package) cyclonedx.Component { + props := encodeProperties(p, "syft:package") + props = append(props, encodeCPEs(p)...) + locations := p.Locations.ToSlice() + if len(locations) > 0 { + props = append(props, encodeProperties(locations, "syft:location")...) + } + if hasMetadata(p) { + props = append(props, encodeProperties(p.Metadata, "syft:metadata")...) + } + + var properties *[]cyclonedx.Property + if len(props) > 0 { + properties = &props + } + + return cyclonedx.Component{ + Type: cyclonedx.ComponentTypeLibrary, + Name: p.Name, + Group: encodeGroup(p), + Version: p.Version, + PackageURL: p.PURL, + Licenses: encodeLicenses(p), + CPE: encodeSingleCPE(p), + Author: encodeAuthor(p), + Publisher: encodePublisher(p), + Description: encodeDescription(p), + ExternalReferences: encodeExternalReferences(p), + Properties: properties, + } +} + +func hasMetadata(p pkg.Package) bool { + return p.Metadata != nil +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/component_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/component_test.go new file mode 100644 index 00000000..3c91008c --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/component_test.go @@ -0,0 +1,144 @@ +package cyclonedxhelpers + +import ( + "testing" + + // "github.com/CycloneDX/cyclonedx-go" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func Test_encodeComponentProperties(t *testing.T) { + epoch := 2 + tests := []struct { + name string + input pkg.Package + expected *[]cyclonedx.Property + }{ + { + name: "no metadata", + input: pkg.Package{}, + expected: nil, + }, + { + name: "from apk", + input: pkg.Package{ + FoundBy: "cataloger", + Locations: source.NewLocationSet( + source.Location{LocationData: source.LocationData{Coordinates: source.Coordinates{RealPath: "test"}}}, + ), + Metadata: pkg.ApkMetadata{ + Package: "libc-utils", + OriginPackage: "libc-dev", + Maintainer: "Natanael Copa ", + Version: "0.7.2-r0", + License: "BSD", + Architecture: "x86_64", + URL: "http://alpinelinux.org", + Description: "Meta package to pull in correct libc", + Size: 0, + InstalledSize: 4096, + Dependencies: []string{"musl-utils"}, + Provides: []string{"so:libc.so.1"}, + Checksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=", + GitCommit: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479", + Files: []pkg.ApkFileRecord{}, + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "syft:package:foundBy", Value: "cataloger"}, + {Name: "syft:location:0:path", Value: "test"}, + {Name: "syft:metadata:gitCommitOfApkPort", Value: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479"}, + {Name: "syft:metadata:installedSize", Value: "4096"}, + {Name: "syft:metadata:originPackage", Value: "libc-dev"}, + {Name: "syft:metadata:provides:0", Value: "so:libc.so.1"}, + {Name: "syft:metadata:pullChecksum", Value: "Q1p78yvTLG094tHE1+dToJGbmYzQE="}, + {Name: "syft:metadata:pullDependencies:0", Value: "musl-utils"}, + {Name: "syft:metadata:size", Value: "0"}, + }, + }, + { + name: "from dpkg", + input: pkg.Package{ + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "tzdata", + Version: "2020a-0+deb10u1", + Source: "tzdata-dev", + SourceVersion: "1.0", + Architecture: "all", + InstalledSize: 3036, + Maintainer: "GNU Libc Maintainers ", + Files: []pkg.DpkgFileRecord{}, + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "syft:package:metadataType", Value: "DpkgMetadata"}, + {Name: "syft:metadata:installedSize", Value: "3036"}, + {Name: "syft:metadata:source", Value: "tzdata-dev"}, + {Name: "syft:metadata:sourceVersion", Value: "1.0"}, + }, + }, + { + name: "from go bin", + input: pkg.Package{ + Name: "golang.org/x/net", + Version: "v0.0.0-20211006190231-62292e806868", + Language: pkg.Go, + Type: pkg.GoModulePkg, + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + GoCompiledVersion: "1.17", + Architecture: "amd64", + H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=", + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "syft:package:language", Value: pkg.Go.String()}, + {Name: "syft:package:metadataType", Value: "GolangBinMetadata"}, + {Name: "syft:package:type", Value: "go-module"}, + {Name: "syft:metadata:architecture", Value: "amd64"}, + {Name: "syft:metadata:goCompiledVersion", Value: "1.17"}, + {Name: "syft:metadata:h1Digest", Value: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k="}, + }, + }, + { + name: "from rpm", + input: pkg.Package{ + Name: "dive", + Version: "0.9.2-1", + Type: pkg.RpmPkg, + MetadataType: pkg.RpmMetadataType, + Metadata: pkg.RpmMetadata{ + Name: "dive", + Epoch: &epoch, + Arch: "x86_64", + Release: "1", + Version: "0.9.2", + SourceRpm: "dive-0.9.2-1.src.rpm", + Size: 12406784, + License: "MIT", + Vendor: "", + Files: []pkg.RpmdbFileRecord{}, + }, + }, + expected: &[]cyclonedx.Property{ + {Name: "syft:package:metadataType", Value: "RpmMetadata"}, + {Name: "syft:package:type", Value: "rpm"}, + {Name: "syft:metadata:epoch", Value: "2"}, + {Name: "syft:metadata:release", Value: "1"}, + {Name: "syft:metadata:size", Value: "12406784"}, + {Name: "syft:metadata:sourceRpm", Value: "dive-0.9.2-1.src.rpm"}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c := encodeComponent(test.input) + assert.Equal(t, test.expected, c.Properties) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/cpe.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/cpe.go new file mode 100644 index 00000000..5c5882ae --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/cpe.go @@ -0,0 +1,33 @@ +package cyclonedxhelpers + +import ( + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/pkg" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" +) + +// Relies on cycloneDX published structs +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodeSingleCPE(p pkg.Package) string { + // Since the CPEs in a package are sorted by specificity + // we can extract the first CPE as the one to output in cyclonedx + if len(p.CPEs) > 0 { + return cpe.String(p.CPEs[0]) + } + return "" +} + +func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) { + for i, c := range p.CPEs { + // first CPE is "most specific" and already encoded as the component CPE + if i == 0 { + continue + } + out = append(out, cyclonedx.Property{ + Name: "syft:cpe23", + Value: cpe.String(c), + }) + } + return +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/cpe_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/cpe_test.go new file mode 100644 index 00000000..7f5f5374 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/cpe_test.go @@ -0,0 +1,59 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/pkg" +) + +func Test_encodeCPE(t *testing.T) { + testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") + testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*") + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{ + CPEs: []cpe.CPE{}, + }, + expected: "", + }, + { + name: "single CPE", + input: pkg.Package{ + CPEs: []cpe.CPE{ + testCPE, + }, + }, + expected: "cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*", + }, + { + name: "multiple CPEs", + input: pkg.Package{ + CPEs: []cpe.CPE{ + testCPE2, + testCPE, + }, + }, + expected: "cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{}, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeSingleCPE(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/description.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/description.go new file mode 100644 index 00000000..39e58efb --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/description.go @@ -0,0 +1,17 @@ +package cyclonedxhelpers + +import "github.com/anchore/syft/syft/pkg" + +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodeDescription(p pkg.Package) string { + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + return metadata.Description + case pkg.NpmPackageJSONMetadata: + return metadata.Description + } + } + return "" +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/description_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/description_test.go new file mode 100644 index 00000000..21f72270 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/description_test.go @@ -0,0 +1,56 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_encodeDescription(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "from apk", + input: pkg.Package{ + Metadata: pkg.ApkMetadata{ + Description: "a description!", + }, + }, + expected: "a description!", + }, + { + name: "from npm", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Description: "a description!", + }, + }, + expected: "a description!", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Homepage: "", + }, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeDescription(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/external_references.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/external_references.go new file mode 100644 index 00000000..c7612095 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/external_references.go @@ -0,0 +1,69 @@ +package cyclonedxhelpers + +import ( + "fmt" + + "github.com/anchore/syft/syft/pkg" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" +) + +// Relies on cycloneDX published structs +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers + +func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { + refs := []cyclonedx.ExternalReference{} + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + if metadata.URL != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.URL, + Type: cyclonedx.ERTypeDistribution, + }) + } + case pkg.CargoPackageMetadata: + if metadata.Source != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.Source, + Type: cyclonedx.ERTypeDistribution, + }) + } + case pkg.NpmPackageJSONMetadata: + if metadata.URL != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.URL, + Type: cyclonedx.ERTypeDistribution, + }) + } + if metadata.Homepage != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.Homepage, + Type: cyclonedx.ERTypeWebsite, + }) + } + case pkg.GemMetadata: + if metadata.Homepage != "" { + refs = append(refs, cyclonedx.ExternalReference{ + URL: metadata.Homepage, + Type: cyclonedx.ERTypeWebsite, + }) + } + case pkg.PythonPackageMetadata: + if metadata.DirectURLOrigin != nil && metadata.DirectURLOrigin.URL != "" { + ref := cyclonedx.ExternalReference{ + URL: metadata.DirectURLOrigin.URL, + Type: cyclonedx.ERTypeVCS, + } + if metadata.DirectURLOrigin.CommitID != "" { + ref.Comment = fmt.Sprintf("commit: %s", metadata.DirectURLOrigin.CommitID) + } + refs = append(refs, ref) + } + } + } + if len(refs) > 0 { + return &refs + } + return nil +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/external_references_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/external_references_test.go new file mode 100644 index 00000000..9c662331 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/external_references_test.go @@ -0,0 +1,134 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + // "github.com/CycloneDX/cyclonedx-go" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" + "github.com/stretchr/testify/assert" +) + +func Test_encodeExternalReferences(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected *[]cyclonedx.ExternalReference + }{ + { + name: "no metadata", + input: pkg.Package{}, + expected: nil, + }, + { + name: "from apk", + input: pkg.Package{ + Metadata: pkg.ApkMetadata{ + URL: "http://a-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, + }, + }, + { + name: "from npm", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "http://a-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, + }, + }, + { + name: "from cargo lock", + input: pkg.Package{ + Name: "ansi_term", + Version: "0.12.1", + Language: pkg.Rust, + Type: pkg.RustPkg, + MetadataType: pkg.RustCargoPackageMetadataType, + Licenses: nil, + Metadata: pkg.CargoPackageMetadata{ + Name: "ansi_term", + Version: "0.12.1", + Source: "registry+https://github.com/rust-lang/crates.io-index", + Checksum: "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2", + Dependencies: []string{ + "winapi", + }, + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "registry+https://github.com/rust-lang/crates.io-index", Type: cyclonedx.ERTypeDistribution}, + }, + }, + { + name: "from npm with homepage", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "http://a-place.gov", + Homepage: "http://homepage", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeDistribution}, + {URL: "http://homepage", Type: cyclonedx.ERTypeWebsite}, + }, + }, + { + name: "from gem", + input: pkg.Package{ + Metadata: pkg.GemMetadata{ + Homepage: "http://a-place.gov", + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeWebsite}, + }, + }, + { + name: "from python direct url", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{ + URL: "http://a-place.gov", + }, + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeVCS}, + }, + }, + { + name: "from python direct url with commit", + input: pkg.Package{ + Metadata: pkg.PythonPackageMetadata{ + DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{ + URL: "http://a-place.gov", + CommitID: "test", + }, + }, + }, + expected: &[]cyclonedx.ExternalReference{ + {URL: "http://a-place.gov", Type: cyclonedx.ERTypeVCS, Comment: "commit: test"}, + }, + }, + { + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + URL: "", + }, + }, + expected: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeExternalReferences(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/format.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/format.go new file mode 100644 index 00000000..35996727 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/format.go @@ -0,0 +1,183 @@ +package cyclonedxhelpers + +import ( + "time" + + "github.com/google/uuid" + // "github.com/CycloneDX/cyclonedx-go" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + + // "github.com/anchore/syft/internal" + "github.com/paketo-buildpacks/packit/v2/sbom/internal" + + // "github.com/anchore/syft/internal/version" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/version" +) + +// We must keep a copy of this helper instead of using the upstream because it +// references CycloneDX 1.3 which we also have to keep a copy of internally +func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { + cdxBOM := cyclonedx.NewBOM() + versionInfo := version.FromBuild() + + // NOTE(jonasagx): cycloneDX requires URN uuids (URN returns the RFC 2141 URN form of uuid): + // 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, versionInfo.Version, s.Source) + + packages := s.Artifacts.Packages.Sorted() + components := make([]cyclonedx.Component, len(packages)) + for i, p := range packages { + components[i] = encodeComponent(p) + } + components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) + cdxBOM.Components = &components + + dependencies := toDependencies(s.Relationships) + if len(dependencies) > 0 { + cdxBOM.Dependencies = &dependencies + } + + return cdxBOM +} + +func toOSComponent(distro *linux.Release) []cyclonedx.Component { + if distro == nil { + return []cyclonedx.Component{} + } + eRefs := &[]cyclonedx.ExternalReference{} + if distro.BugReportURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.BugReportURL, + Type: cyclonedx.ERTypeIssueTracker, + }) + } + if distro.HomeURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.HomeURL, + Type: cyclonedx.ERTypeWebsite, + }) + } + if distro.SupportURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.SupportURL, + Type: cyclonedx.ERTypeOther, + Comment: "support", + }) + } + if distro.PrivacyPolicyURL != "" { + *eRefs = append(*eRefs, cyclonedx.ExternalReference{ + URL: distro.PrivacyPolicyURL, + Type: cyclonedx.ERTypeOther, + Comment: "privacyPolicy", + }) + } + if len(*eRefs) == 0 { + eRefs = nil + } + props := encodeProperties(distro, "syft:distro") + var properties *[]cyclonedx.Property + if len(props) > 0 { + properties = &props + } + return []cyclonedx.Component{ + { + Type: cyclonedx.ComponentTypeOS, + // FIXME is it idiomatic to be using SWID here for specific name and version information? + SWID: &cyclonedx.SWID{ + TagID: distro.ID, + Name: distro.ID, + Version: distro.VersionID, + }, + Description: distro.PrettyName, + Name: distro.ID, + Version: distro.VersionID, + // TODO should we add a PURL? + CPE: distro.CPEName, + ExternalReferences: eRefs, + Properties: properties, + }, + } +} + +// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details. +func toBomDescriptor(name, version string, srcMetadata source.Metadata) *cyclonedx.Metadata { + return &cyclonedx.Metadata{ + Timestamp: time.Now().Format(time.RFC3339), + Tools: &[]cyclonedx.Tool{ + { + Vendor: "anchore", + Name: name, + Version: version, + }, + }, + Component: toBomDescriptorComponent(srcMetadata), + } +} + +// used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. +// NOTE: CycloneDX provides the ability to describe components and their dependency on other components. +// The dependency graph is capable of representing both direct and transitive relationships. +// If a relationship is either direct or transitive it can be included in this function. +// An example of a relationship to not include would be: OwnershipByFileOverlapRelationship. +func isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { + switch ty { + case artifact.DependencyOfRelationship: + return true + default: + return false + } +} + +func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependency { + result := make([]cyclonedx.Dependency, 0) + for _, r := range relationships { + exists := isExpressiblePackageRelationship(r.Type) + if !exists { + // log.Warnf("unable to convert relationship from CycloneDX 1.3 JSON, dropping: %+v", r) + continue + } + + innerDeps := []cyclonedx.Dependency{} + innerDeps = append(innerDeps, cyclonedx.Dependency{Ref: string(r.From.ID())}) + result = append(result, cyclonedx.Dependency{ + Ref: string(r.To.ID()), + Dependencies: &innerDeps, + }) + } + return result +} + +func toBomDescriptorComponent(srcMetadata source.Metadata) *cyclonedx.Component { + switch srcMetadata.Scheme { + case source.ImageScheme: + bomRef, err := artifact.IDByHash(srcMetadata.ImageMetadata.ID) + if err != nil { //nolint:staticcheck + // log.Warnf("unable to get fingerprint of image metadata=%s: %+v", srcMetadata.ImageMetadata.ID, err) + } + return &cyclonedx.Component{ + BOMRef: string(bomRef), + Type: cyclonedx.ComponentTypeContainer, + Name: srcMetadata.ImageMetadata.UserInput, + Version: srcMetadata.ImageMetadata.ManifestDigest, + } + case source.DirectoryScheme, source.FileScheme: + bomRef, err := artifact.IDByHash(srcMetadata.Path) + if err != nil { //nolint:staticcheck + // log.Warnf("unable to get fingerprint of source metadata path=%s: %+v", srcMetadata.Path, err) + } + return &cyclonedx.Component{ + BOMRef: string(bomRef), + Type: cyclonedx.ComponentTypeFile, + Name: srcMetadata.Path, + } + } + + return nil +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/group.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/group.go new file mode 100644 index 00000000..29fb5cfc --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/group.go @@ -0,0 +1,14 @@ +package cyclonedxhelpers + +import "github.com/anchore/syft/syft/pkg" + +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodeGroup(p pkg.Package) string { + if hasMetadata(p) { + if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil { + return metadata.PomProperties.GroupID + } + } + return "" +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/group_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/group_test.go new file mode 100644 index 00000000..38352131 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/group_test.go @@ -0,0 +1,52 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_encodeGroup(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "metadata is not Java", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{}, + }, + expected: "", + }, + { + name: "metadata is Java but pom properties is empty", + input: pkg.Package{ + Metadata: pkg.JavaMetadata{}, + }, + expected: "", + }, + { + name: "metadata is Java and contains pomProperties", + input: pkg.Package{ + Metadata: pkg.JavaMetadata{ + PomProperties: &pkg.PomProperties{ + GroupID: "test", + }, + }, + }, + expected: "test", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeGroup(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/licenses.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/licenses.go new file mode 100644 index 00000000..00bb67de --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/licenses.go @@ -0,0 +1,27 @@ +package cyclonedxhelpers + +import ( + "github.com/anchore/syft/syft/pkg" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/spdxlicense" +) + +// Relies on cycloneDX published structs +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { + lc := cyclonedx.Licenses{} + for _, licenseName := range p.Licenses { + if value, exists := spdxlicense.ID(licenseName); exists { + lc = append(lc, cyclonedx.LicenseChoice{ + License: &cyclonedx.License{ + ID: value, + }, + }) + } + } + if len(lc) > 0 { + return &lc + } + return nil +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/licenses_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/licenses_test.go new file mode 100644 index 00000000..e7a9b199 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/licenses_test.go @@ -0,0 +1,84 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + // "github.com/CycloneDX/cyclonedx-go" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" + "github.com/stretchr/testify/assert" +) + +func Test_encodeLicense(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected *cyclonedx.Licenses + }{ + { + name: "no licenses", + input: pkg.Package{}, + expected: nil, + }, + { + name: "no SPDX licenses", + input: pkg.Package{ + Licenses: []string{ + "made-up", + }, + }, + expected: nil, + }, + { + name: "with SPDX license", + input: pkg.Package{ + Licenses: []string{ + "MIT", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "MIT"}}, + }, + }, + { + name: "with SPDX license expression", + input: pkg.Package{ + Licenses: []string{ + "MIT", + "GPL-3.0", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "MIT"}}, + {License: &cyclonedx.License{ID: "GPL-3.0"}}, + }, + }, + { + name: "cap insensitive", + input: pkg.Package{ + Licenses: []string{ + "gpl-3.0", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "GPL-3.0"}}, + }, + }, + { + name: "debian to spdx conversion", + input: pkg.Package{ + Licenses: []string{ + "GPL-2", + }, + }, + expected: &cyclonedx.Licenses{ + {License: &cyclonedx.License{ID: "GPL-2.0"}}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodeLicenses(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/properties.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/properties.go new file mode 100644 index 00000000..d0c87300 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/properties.go @@ -0,0 +1,23 @@ +package cyclonedxhelpers + +import ( + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" +) + +// Relies on cycloneDX published structs +var ( + CycloneDXFields = common.RequiredTag("cyclonedx") +) + +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers and it relies on our internal copy of cyclonedx 1.3 +func encodeProperties(obj interface{}, prefix string) (out []cyclonedx.Property) { + for _, p := range common.Sorted(common.Encode(obj, prefix, CycloneDXFields)) { + out = append(out, cyclonedx.Property{ + Name: p.Name, + Value: p.Value, + }) + } + return +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/publisher.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/publisher.go new file mode 100644 index 00000000..be4b9ba0 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/publisher.go @@ -0,0 +1,21 @@ +package cyclonedxhelpers + +import ( + "github.com/anchore/syft/syft/pkg" +) + +// We must copy this helper in because it's not exported from +// syft/formats/common/cyclonedxhelpers +func encodePublisher(p pkg.Package) string { + if hasMetadata(p) { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + return metadata.Maintainer + case pkg.RpmMetadata: + return metadata.Vendor + case pkg.DpkgMetadata: + return metadata.Maintainer + } + } + return "" +} diff --git a/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/publisher_test.go b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/publisher_test.go new file mode 100644 index 00000000..06730598 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/cyclonedxhelpers/publisher_test.go @@ -0,0 +1,65 @@ +package cyclonedxhelpers + +import ( + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/stretchr/testify/assert" +) + +func Test_encodePublisher(t *testing.T) { + tests := []struct { + name string + input pkg.Package + expected string + }{ + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "no metadata", + input: pkg.Package{}, + expected: "", + }, + { + name: "from apk", + input: pkg.Package{ + Metadata: pkg.ApkMetadata{ + Maintainer: "auth", + }, + }, + expected: "auth", + }, + { + name: "from rpm", + input: pkg.Package{ + Metadata: pkg.RpmMetadata{ + Vendor: "auth", + }, + }, + expected: "auth", + }, + { + name: "from dpkg", + input: pkg.Package{ + Metadata: pkg.DpkgMetadata{ + Maintainer: "auth", + }, + }, + expected: "auth", + }, + { + // note: since this is an optional field, no value is preferred over NONE or NOASSERTION + name: "empty", + input: pkg.Package{ + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "", + }, + }, + expected: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, encodePublisher(test.input)) + }) + } +} diff --git a/sbom/internal/formats/cyclonedx13/encoder.go b/sbom/internal/formats/cyclonedx13/encoder.go new file mode 100644 index 00000000..f7a539c8 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/encoder.go @@ -0,0 +1,19 @@ +package cyclonedx13 + +import ( + "io" + + // "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/sbom" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedx" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13/cyclonedxhelpers" +) + +func encoder(output io.Writer, s sbom.SBOM) error { + bom := cyclonedxhelpers.ToFormatModel(s) + enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON) + enc.SetPretty(true) + + err := enc.Encode(bom) + return err +} diff --git a/sbom/internal/formats/cyclonedx13/encoder_test.go b/sbom/internal/formats/cyclonedx13/encoder_test.go new file mode 100644 index 00000000..507e4fff --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/encoder_test.go @@ -0,0 +1,50 @@ +package cyclonedx13 + +import ( + "flag" + "regexp" + "testing" + + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" +) + +var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx encoders") + +func TestCycloneDxDirectoryEncoder(t *testing.T) { + testutils.AssertEncoderAgainstGoldenSnapshot(t, + Format(), + testutils.DirectoryInput(t), + *updateCycloneDx, + true, + cycloneDxRedactor, + ) +} + +func TestCycloneDxImageEncoder(t *testing.T) { + testImage := "image-simple" + testutils.AssertEncoderAgainstGoldenImageSnapshot(t, + Format(), + testutils.ImageInput(t, testImage), + testImage, + *updateCycloneDx, + true, + cycloneDxRedactor, + ) +} + +func cycloneDxRedactor(s []byte) []byte { + replacements := map[string]string{ + // UUIDs + `urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`: `urn:uuid:redacted`, + // timestamps + `([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `timestamp:redacted`, + // image hashes + `sha256:[A-Fa-f0-9]{64}`: `sha256:redacted`, + // bom-refs + `"bom-ref":\s*"[^"]+"`: `"bom-ref": "redacted"`, + } + for pattern, replacement := range replacements { + s = regexp.MustCompile(pattern).ReplaceAll(s, []byte(replacement)) + } + return s +} diff --git a/sbom/internal/formats/cyclonedx13/format.go b/sbom/internal/formats/cyclonedx13/format.go new file mode 100644 index 00000000..b60c3b31 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/format.go @@ -0,0 +1,20 @@ +package cyclonedx13 + +import ( + "io" + + "github.com/anchore/syft/syft/sbom" +) + +// TODO: Decide version granularity of IDs +const ID sbom.FormatID = "cyclonedx-1.3-json" + +func Format() sbom.Format { + return sbom.NewFormat( + sbom.AnyVersion, + encoder, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, + ) +} diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/cache/stereoscope-fixture-image-simple-85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b.tar b/sbom/internal/formats/cyclonedx13/test-fixtures/cache/stereoscope-fixture-image-simple-85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b.tar new file mode 100644 index 00000000..8d750fd5 Binary files /dev/null and b/sbom/internal/formats/cyclonedx13/test-fixtures/cache/stereoscope-fixture-image-simple-85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b.tar differ diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/Dockerfile b/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/Dockerfile new file mode 100644 index 00000000..79cfa759 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-2.txt diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/file-1.txt b/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/file-1.txt new file mode 100644 index 00000000..985d3408 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/file-2.txt b/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/file-2.txt new file mode 100644 index 00000000..396d08bb --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden new file mode 100644 index 00000000..640061ce --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -0,0 +1,118 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:195a66a2-6d39-472e-b62b-0cafb9bfedd4", + "version": 1, + "metadata": { + "timestamp": "2022-02-25T12:54:25-05:00", + "tools": [ + { + "vendor": "anchore", + "name": "syft", + "version": "[not provided]" + } + ], + "component": { + "bom-ref": "163686ac6e30c752", + "type": "file", + "name": "/some/path", + "version": "" + } + }, + "components": [ + { + "type": "library", + "name": "package-1", + "version": "1.0.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "purl": "a-purl-2", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "the-cataloger-1" + }, + { + "name": "syft:package:language", + "value": "python" + }, + { + "name": "syft:package:metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "syft:package:type", + "value": "python" + }, + { + "name": "syft:location:0:path", + "value": "/some/path/pkg1" + } + ] + }, + { + "type": "library", + "name": "package-2", + "version": "2.0.1", + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "purl": "pkg:deb/debian/package-2@2.0.1", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "the-cataloger-2" + }, + { + "name": "syft:package:metadataType", + "value": "DpkgMetadata" + }, + { + "name": "syft:package:type", + "value": "deb" + }, + { + "name": "syft:location:0:path", + "value": "/some/path/pkg1" + }, + { + "name": "syft:metadata:installedSize", + "value": "0" + } + ] + }, + { + "type": "operating-system", + "name": "debian", + "version": "1.2.3", + "description": "debian", + "swid": { + "tagId": "debian", + "name": "debian", + "version": "1.2.3" + }, + "properties": [ + { + "name": "syft:distro:id", + "value": "debian" + }, + { + "name": "syft:distro:idLike:0", + "value": "like!" + }, + { + "name": "syft:distro:prettyName", + "value": "debian" + }, + { + "name": "syft:distro:versionID", + "value": "1.2.3" + } + ] + } + ] +} diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden new file mode 100644 index 00000000..6aa74196 --- /dev/null +++ b/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -0,0 +1,126 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:78116a1b-b709-4734-8411-d0e339308edd", + "version": 1, + "metadata": { + "timestamp": "2022-02-25T12:54:25-05:00", + "tools": [ + { + "vendor": "anchore", + "name": "syft", + "version": "[not provided]" + } + ], + "component": { + "bom-ref": "4f9453fd20e0cf80", + "type": "container", + "name": "user-image-input", + "version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" + } + }, + "components": [ + { + "type": "library", + "name": "package-1", + "version": "1.0.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "cpe": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + "purl": "a-purl-1", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "the-cataloger-1" + }, + { + "name": "syft:package:language", + "value": "python" + }, + { + "name": "syft:package:metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "syft:package:type", + "value": "python" + }, + { + "name": "syft:location:0:layerID", + "value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe" + }, + { + "name": "syft:location:0:path", + "value": "/somefile-1.txt" + } + ] + }, + { + "type": "library", + "name": "package-2", + "version": "2.0.1", + "cpe": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "purl": "pkg:deb/debian/package-2@2.0.1", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "the-cataloger-2" + }, + { + "name": "syft:package:metadataType", + "value": "DpkgMetadata" + }, + { + "name": "syft:package:type", + "value": "deb" + }, + { + "name": "syft:location:0:layerID", + "value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa" + }, + { + "name": "syft:location:0:path", + "value": "/somefile-2.txt" + }, + { + "name": "syft:metadata:installedSize", + "value": "0" + } + ] + }, + { + "type": "operating-system", + "name": "debian", + "version": "1.2.3", + "description": "debian", + "swid": { + "tagId": "debian", + "name": "debian", + "version": "1.2.3" + }, + "properties": [ + { + "name": "syft:distro:id", + "value": "debian" + }, + { + "name": "syft:distro:idLike:0", + "value": "like!" + }, + { + "name": "syft:distro:prettyName", + "value": "debian" + }, + { + "name": "syft:distro:versionID", + "value": "1.2.3" + } + ] + } + ] +} diff --git a/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden new file mode 100644 index 00000000..afbd18d5 Binary files /dev/null and b/sbom/internal/formats/cyclonedx13/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/sbom/internal/formats/spdx22/README.md b/sbom/internal/formats/spdx22/README.md new file mode 100644 index 00000000..fc71c338 --- /dev/null +++ b/sbom/internal/formats/spdx22/README.md @@ -0,0 +1,16 @@ +# Source +The contents of this directory is largely based on anchore/syft's `spdxjson` +package. The version copied is from the (current) latest release of Syft, +[v0.65.0](https://github.com/anchore/syft/tree/v0.65.0/syft/formats/spdxjson), +which currently supports SPDX 2.3. This choice was made so that we can leverage +as much as the common Syft helper functionality (such as +[spdxhelpers](https://github.com/anchore/syft/tree/v0.65.0/syft/formats/common/spdxhelpers)) +as possible. + +Modifications to the code have been made to the code, and to the code in the +`model` directory to include subtle differences that apply to SPDX 2.2. These +changes were largely based on +[v0.60.3](https://github.com/anchore/syft/tree/v0.60.3/syft/formats/spdx22). + +The implementations of `decoder` and `validator` have been omitted for +simplicity, since they are not required for buildpacks' SBOM generation. diff --git a/sbom/internal/formats/spdx22/encoder.go b/sbom/internal/formats/spdx22/encoder.go new file mode 100644 index 00000000..eb608e59 --- /dev/null +++ b/sbom/internal/formats/spdx22/encoder.go @@ -0,0 +1,19 @@ +package spdx22 + +import ( + "encoding/json" + "io" + + "github.com/anchore/syft/syft/sbom" +) + +func encoder(output io.Writer, s sbom.SBOM) error { + doc := ToFormatModel(s) + + enc := json.NewEncoder(output) + // prevent > and < from being escaped in the payload + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + + return enc.Encode(doc) +} diff --git a/sbom/internal/formats/spdx22/encoder_test.go b/sbom/internal/formats/spdx22/encoder_test.go new file mode 100644 index 00000000..56177343 --- /dev/null +++ b/sbom/internal/formats/spdx22/encoder_test.go @@ -0,0 +1,57 @@ +package spdx22 + +import ( + "flag" + "regexp" + "testing" + + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" +) + +var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders") + +func TestSPDXJSONDirectoryEncoder(t *testing.T) { + testutils.AssertEncoderAgainstGoldenSnapshot(t, + Format(), + testutils.DirectoryInput(t), + *updateSpdxJson, + true, + spdxJsonRedactor, + ) +} + +func TestSPDXJSONImageEncoder(t *testing.T) { + testImage := "image-simple" + testutils.AssertEncoderAgainstGoldenImageSnapshot(t, + Format(), + testutils.ImageInput(t, testImage, testutils.FromSnapshot()), + testImage, + *updateSpdxJson, + true, + spdxJsonRedactor, + ) +} + +func TestSPDXRelationshipOrder(t *testing.T) { + testImage := "image-simple" + s := testutils.ImageInput(t, testImage, testutils.FromSnapshot()) + testutils.AddSampleFileRelationships(&s) + testutils.AssertEncoderAgainstGoldenImageSnapshot(t, + Format(), + s, + testImage, + *updateSpdxJson, + true, + spdxJsonRedactor, + ) +} +func spdxJsonRedactor(s []byte) []byte { + // each SBOM reports the time it was generated, which is not useful during snapshot testing + s = regexp.MustCompile(`"created":\s+"[^"]*"`).ReplaceAll(s, []byte(`"created":""`)) + + // each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing + s = regexp.MustCompile(`"documentNamespace":\s+"[^"]*"`).ReplaceAll(s, []byte(`"documentNamespace":""`)) + + // the license list will be updated periodically, the value here should not be directly tested in snapshot tests + return regexp.MustCompile(`"licenseListVersion":\s+"[^"]*"`).ReplaceAll(s, []byte(`"licenseListVersion":""`)) +} diff --git a/sbom/internal/formats/spdx22/format.go b/sbom/internal/formats/spdx22/format.go new file mode 100644 index 00000000..95bee545 --- /dev/null +++ b/sbom/internal/formats/spdx22/format.go @@ -0,0 +1,19 @@ +package spdx22 + +import ( + "io" + + "github.com/anchore/syft/syft/sbom" +) + +const ID sbom.FormatID = "spdx-2-json" + +func Format() sbom.Format { + return sbom.NewFormat( + "2.2", + encoder, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, + ) +} diff --git a/sbom/internal/formats/spdx22/model/license.go b/sbom/internal/formats/spdx22/model/license.go new file mode 100644 index 00000000..6acd3c6a --- /dev/null +++ b/sbom/internal/formats/spdx22/model/license.go @@ -0,0 +1,38 @@ +package model + +import ( + "strings" + + "github.com/anchore/syft/syft/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/pkg" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/spdxlicense" +) + +func License(p pkg.Package) string { + // source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license + // The options to populate this field are limited to: + // A valid SPDX License Expression as defined in Appendix IV; + // NONE, if the SPDX file creator concludes there is no license available for this package; or + // NOASSERTION if: + // (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination; + // (ii) the SPDX file creator has made no attempt to determine this field; or + // (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so). + + if len(p.Licenses) == 0 { + return spdxhelpers.NONE + } + + // take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/ + var parsedLicenses []string + for _, l := range p.Licenses { + if value, exists := spdxlicense.ID(l); exists { + parsedLicenses = append(parsedLicenses, value) + } + } + + if len(parsedLicenses) == 0 { + return spdxhelpers.NOASSERTION + } + + return strings.Join(parsedLicenses, " AND ") +} diff --git a/sbom/internal/formats/spdx22/model/mimetype_helper.go b/sbom/internal/formats/spdx22/model/mimetype_helper.go new file mode 100644 index 00000000..d467873e --- /dev/null +++ b/sbom/internal/formats/spdx22/model/mimetype_helper.go @@ -0,0 +1,73 @@ +package model + +import "github.com/scylladb/go-set/strset" + +var ( + ArchiveMIMETypeSet = strset.New( + // derived from https://en.wikipedia.org/wiki/List_of_archive_formats + []string{ + // archive only + "application/x-archive", + "application/x-cpio", + "application/x-shar", + "application/x-iso9660-image", + "application/x-sbx", + "application/x-tar", + // compression only + "application/x-bzip2", + "application/gzip", + "application/x-lzip", + "application/x-lzma", + "application/x-lzop", + "application/x-snappy-framed", + "application/x-xz", + "application/x-compress", + "application/zstd", + // archiving and compression + "application/x-7z-compressed", + "application/x-ace-compressed", + "application/x-astrotite-afa", + "application/x-alz-compressed", + "application/vnd.android.package-archive", + "application/x-freearc", + "application/x-arj", + "application/x-b1", + "application/vnd.ms-cab-compressed", + "application/x-cfs-compressed", + "application/x-dar", + "application/x-dgc-compressed", + "application/x-apple-diskimage", + "application/x-gca-compressed", + "application/java-archive", + "application/x-lzh", + "application/x-lzx", + "application/x-rar-compressed", + "application/x-stuffit", + "application/x-stuffitx", + "application/x-gtar", + "application/x-ms-wim", + "application/x-xar", + "application/zip", + "application/x-zoo", + }..., + ) + + ExecutableMIMETypeSet = strset.New( + []string{ + "application/x-executable", + "application/x-mach-binary", + "application/x-elf", + "application/x-sharedlib", + "application/vnd.microsoft.portable-executable", + "application/x-executable", + }..., + ) +) + +func IsArchive(mimeType string) bool { + return ArchiveMIMETypeSet.Has(mimeType) +} + +func IsExecutable(mimeType string) bool { + return ExecutableMIMETypeSet.Has(mimeType) +} diff --git a/sbom/internal/formats/spdx22/model/to_syft_model.go b/sbom/internal/formats/spdx22/model/to_syft_model.go new file mode 100644 index 00000000..e438abc5 --- /dev/null +++ b/sbom/internal/formats/spdx22/model/to_syft_model.go @@ -0,0 +1,412 @@ +package model + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + spdx "github.com/spdx/tools-golang/spdx/v2/v2_2" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/formats/common/util" + "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 ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) { + if doc == nil { + return nil, errors.New("cannot convert SPDX document to Syft model because document is nil") + } + + spdxIDMap := make(map[string]interface{}) + + src := source.Metadata{Scheme: source.UnknownScheme} + src.Scheme = extractSchemeFromNamespace(doc.DocumentNamespace) + + s := &sbom.SBOM{ + Source: src, + Artifacts: sbom.Artifacts{ + Packages: pkg.NewCollection(), + FileMetadata: map[source.Coordinates]source.FileMetadata{}, + FileDigests: map[source.Coordinates][]file.Digest{}, + LinuxDistribution: findLinuxReleaseByPURL(doc), + }, + } + + collectSyftPackages(s, spdxIDMap, doc) + + collectSyftFiles(s, spdxIDMap, doc) + + s.Relationships = toSyftRelationships(spdxIDMap, doc) + + return s, nil +} + +// NOTE(jonas): SPDX doesn't inform what an SBOM is about, +// image, directory, for example. This is our best effort to determine +// the scheme. Syft-generated SBOMs have in the namespace +// field a type encoded, which we try to identify here. +func extractSchemeFromNamespace(ns string) source.Scheme { + u, err := url.Parse(ns) + if err != nil { + return source.UnknownScheme + } + + parts := strings.Split(u.Path, "/") + for _, p := range parts { + switch p { + case "file": + return source.FileScheme + case "image": + return source.ImageScheme + case "dir": + return source.DirectoryScheme + } + } + return source.UnknownScheme +} + +func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release { + for _, p := range doc.Packages { + purlValue := findPURLValue(p) + if purlValue == "" { + continue + } + purl, err := packageurl.FromString(purlValue) + if err != nil { + // log.Warnf("unable to parse purl: %s", purlValue) + fmt.Printf("unable to parse purl: %s", purlValue) + continue + } + distro := findQualifierValue(purl, pkg.PURLQualifierDistro) + if distro != "" { + parts := strings.Split(distro, "-") + name := parts[0] + version := "" + if len(parts) > 1 { + version = parts[1] + } + return &linux.Release{ + PrettyName: name, + Name: name, + ID: name, + IDLike: []string{name}, + Version: version, + VersionID: version, + } + } + } + + return nil +} + +func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) { + for _, p := range doc.Packages { + syftPkg := toSyftPackage(p) + spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg + s.Artifacts.Packages.Add(*syftPkg) + } +} + +func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) { + for _, f := range doc.Files { + l := toSyftLocation(f) + spdxIDMap[string(f.FileSPDXIdentifier)] = l + + s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f) + s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f) + } +} + +func toFileDigests(f *spdx.File) (digests []file.Digest) { + for _, digest := range f.Checksums { + digests = append(digests, file.Digest{ + Algorithm: string(digest.Algorithm), + Value: digest.Value, + }) + } + return digests +} + +func toFileMetadata(f *spdx.File) (meta source.FileMetadata) { + // FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes + for _, typ := range f.FileTypes { + switch spdxhelpers.FileType(typ) { + case spdxhelpers.ImageFileType: + meta.MIMEType = "image/" + case spdxhelpers.VideoFileType: + meta.MIMEType = "video/" + case spdxhelpers.ApplicationFileType: + meta.MIMEType = "application/" + case spdxhelpers.TextFileType: + meta.MIMEType = "text/" + case spdxhelpers.AudioFileType: + meta.MIMEType = "audio/" + case spdxhelpers.BinaryFileType: + case spdxhelpers.ArchiveFileType: + case spdxhelpers.OtherFileType: + } + } + return meta +} + +func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document) []artifact.Relationship { + var 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) { + // log.Debugf("ignoring relationship to external document: %+v", r) + fmt.Printf("ignoring relationship to external document: %+v", r) + continue + } + a := spdxIDMap[string(r.RefA.ElementRefID)] + b := spdxIDMap[string(r.RefB.ElementRefID)] + from, fromOk := a.(*pkg.Package) + toPackage, toPackageOk := b.(*pkg.Package) + toLocation, toLocationOk := b.(*source.Location) + if !fromOk || !(toPackageOk || toLocationOk) { + // log.Debugf("unable to find valid relationship mapping from SPDX 2.2 JSON, ignoring: (from: %+v) (to: %+v)", a, b) + fmt.Printf("unable to find valid relationship mapping from SPDX 2.2 JSON, ignoring: (from: %+v) (to: %+v)", a, b) + continue + } + var to artifact.Identifiable + var typ artifact.RelationshipType + if toLocationOk { + if r.Relationship == string(spdxhelpers.ContainsRelationship) { + typ = artifact.ContainsRelationship + to = toLocation + } + } else { + switch spdxhelpers.RelationshipType(r.Relationship) { + case spdxhelpers.ContainsRelationship: + typ = artifact.ContainsRelationship + to = toPackage + case spdxhelpers.OtherRelationship: + // Encoding uses a specifically formatted comment... + if strings.Index(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) == 0 { + typ = artifact.DependencyOfRelationship + to = toPackage + } + } + } + if typ != "" && to != nil { + out = append(out, artifact.Relationship{ + From: from, + To: to, + Type: typ, + }) + } + } + return out +} + +func toSyftCoordinates(f *spdx.File) source.Coordinates { + const layerIDPrefix = "layerID: " + var fileSystemID string + if strings.Index(f.FileComment, layerIDPrefix) == 0 { + fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix) + } + if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 { + fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix) + } + return source.Coordinates{ + RealPath: f.FileName, + FileSystemID: fileSystemID, + } +} + +func toSyftLocation(f *spdx.File) *source.Location { + return &source.Location{ + LocationData: source.LocationData{ + Coordinates: toSyftCoordinates(f), + VirtualPath: f.FileName, + }, + } +} + +func requireAndTrimPrefix(val interface{}, prefix string) string { + if v, ok := val.(string); ok { + if i := strings.Index(v, prefix); i == 0 { + return strings.Replace(v, prefix, "", 1) + } + } + return "" +} + +type pkgInfo struct { + purl packageurl.PackageURL + typ pkg.Type + lang pkg.Language +} + +func (p *pkgInfo) qualifierValue(name string) string { + return findQualifierValue(p.purl, name) +} + +func findQualifierValue(purl packageurl.PackageURL, qualifier string) string { + for _, q := range purl.Qualifiers { + if q.Key == qualifier { + return q.Value + } + } + return "" +} + +func extractPkgInfo(p *spdx.Package) pkgInfo { + pu := findPURLValue(p) + purl, err := packageurl.FromString(pu) + if err != nil { + return pkgInfo{} + } + return pkgInfo{ + purl, + pkg.TypeByName(purl.Type), + pkg.LanguageByName(purl.Type), + } +} + +func toSyftPackage(p *spdx.Package) *pkg.Package { + info := extractPkgInfo(p) + metadataType, metadata := extractMetadata(p, info) + sP := pkg.Package{ + Type: info.typ, + Name: p.PackageName, + Version: p.PackageVersion, + Licenses: parseLicense(p.PackageLicenseDeclared), + CPEs: extractCPEs(p), + PURL: info.purl.String(), + Language: info.lang, + MetadataType: metadataType, + Metadata: metadata, + } + + sP.SetID() + + return &sP +} + +//nolint:funlen +func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface{}) { + arch := info.qualifierValue(pkg.PURLQualifierArch) + upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream) + upstream := strings.SplitN(upstreamValue, "@", 2) + upstreamName := upstream[0] + upstreamVersion := "" + if len(upstream) > 1 { + upstreamVersion = upstream[1] + } + supplier := "" + if p.PackageSupplier != nil { + supplier = p.PackageSupplier.Supplier + } + originator := "" + if p.PackageOriginator != nil { + originator = p.PackageOriginator.Originator + } + switch info.typ { + case pkg.ApkPkg: + return pkg.ApkMetadataType, pkg.ApkMetadata{ + Package: p.PackageName, + OriginPackage: upstreamName, + Maintainer: supplier, + Version: p.PackageVersion, + License: p.PackageLicenseDeclared, + Architecture: arch, + URL: p.PackageHomePage, + Description: p.PackageDescription, + } + case pkg.RpmPkg: + converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch)) + var epoch *int + if err != nil { + epoch = nil + } else { + epoch = &converted + } + license := p.PackageLicenseDeclared + if license == "" { + license = p.PackageLicenseConcluded + } + return pkg.RpmMetadataType, pkg.RpmMetadata{ + Name: p.PackageName, + Version: p.PackageVersion, + Epoch: epoch, + Arch: arch, + SourceRpm: upstreamValue, + License: license, + Vendor: originator, + } + case pkg.DebPkg: + return pkg.DpkgMetadataType, pkg.DpkgMetadata{ + Package: p.PackageName, + Source: upstreamName, + Version: p.PackageVersion, + SourceVersion: upstreamVersion, + Architecture: arch, + Maintainer: originator, + } + case pkg.JavaPkg: + var digests []file.Digest + for _, value := range p.PackageChecksums { + digests = append(digests, file.Digest{Algorithm: string(value.Algorithm), Value: value.Value}) + } + return pkg.JavaMetadataType, pkg.JavaMetadata{ + ArchiveDigests: digests, + } + case pkg.GoModulePkg: + var h1Digest string + for _, value := range p.PackageChecksums { + digest, err := util.HDigestFromSHA(string(value.Algorithm), value.Value) + if err != nil { + // log.Debugf("invalid h1digest: %v %v", value, err) + fmt.Printf("invalid h1digest: %v %v", value, err) + continue + } + h1Digest = digest + break + } + return pkg.GolangBinMetadataType, pkg.GolangBinMetadata{ + H1Digest: h1Digest, + } + } + return pkg.UnknownMetadataType, nil +} + +func findPURLValue(p *spdx.Package) string { + for _, r := range p.PackageExternalReferences { + if r.RefType == string(spdxhelpers.PurlExternalRefType) { + return r.Locator + } + } + return "" +} + +func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) { + for _, r := range p.PackageExternalReferences { + if r.RefType == string(spdxhelpers.Cpe23ExternalRefType) { + c, err := cpe.New(r.Locator) + if err != nil { + // log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err) + fmt.Printf("unable to extract SPDX CPE=%q: %+v", r.Locator, err) + continue + } + cpes = append(cpes, c) + } + } + return cpes +} + +func parseLicense(l string) []string { + if l == spdxhelpers.NOASSERTION || l == spdxhelpers.NONE { + return nil + } + return strings.Split(l, " AND ") +} diff --git a/sbom/internal/formats/spdx22/model/to_syft_model_test.go b/sbom/internal/formats/spdx22/model/to_syft_model_test.go new file mode 100644 index 00000000..dc4bf002 --- /dev/null +++ b/sbom/internal/formats/spdx22/model/to_syft_model_test.go @@ -0,0 +1,310 @@ +package model + +import ( + "testing" + + "github.com/spdx/tools-golang/spdx/v2/common" + spdx "github.com/spdx/tools-golang/spdx/v2/v2_2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func TestToSyftModel(t *testing.T) { + sbom, err := ToSyftModel(&spdx.Document{ + SPDXVersion: "1", + DataLicense: "GPL", + SPDXIdentifier: "id-doc-1", + DocumentName: "docName", + DocumentNamespace: "docNamespace", + ExternalDocumentReferences: nil, + DocumentComment: "", + CreationInfo: &spdx.CreationInfo{ + LicenseListVersion: "", + Created: "", + CreatorComment: "", + }, + Packages: []*spdx.Package{ + { + PackageName: "pkg-1", + PackageSPDXIdentifier: "id-pkg-1", + PackageVersion: "5.4.3", + PackageLicenseDeclared: "", + PackageDescription: "", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg-1:pkg-1:5.4.3:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg_1:pkg_1:5.4.3:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9", + RefType: "purl", + }, + }, + Files: nil, + }, + { + PackageName: "pkg-2", + PackageSPDXIdentifier: "id-pkg-2", + PackageVersion: "7.3.1", + PackageLicenseDeclared: "", + PackageDescription: "", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg-2:pkg-2:7.3.1:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg_2:pkg_2:7.3.1:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "SECURITY", + Locator: "cpe:2.3:a:pkg-2:pkg_2:7.3.1:*:*:*:*:*:*:*", + RefType: "cpe23Type", + }, + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=p2-origin@9.1.3&distro=debian-3.10.9", + RefType: "purl", + }, + }, + Files: nil, + }, + }, + Relationships: []*spdx.Relationship{}, + }) + + assert.NoError(t, err) + + assert.NotNil(t, sbom) + + pkgs := sbom.Artifacts.Packages.Sorted() + + assert.Len(t, pkgs, 2) + + p1 := pkgs[0] + assert.Equal(t, p1.Name, "pkg-1") + assert.Equal(t, p1.MetadataType, pkg.ApkMetadataType) + p1meta := p1.Metadata.(pkg.ApkMetadata) + assert.Equal(t, p1meta.OriginPackage, "p1-origin") + assert.Len(t, p1.CPEs, 2) + + p2 := pkgs[1] + assert.Equal(t, p2.Name, "pkg-2") + assert.Equal(t, p2.MetadataType, pkg.DpkgMetadataType) + p2meta := p2.Metadata.(pkg.DpkgMetadata) + assert.Equal(t, p2meta.Source, "p2-origin") + assert.Equal(t, p2meta.SourceVersion, "9.1.3") + assert.Len(t, p2.CPEs, 3) +} + +func Test_extractMetadata(t *testing.T) { + oneTwoThreeFour := 1234 + tests := []struct { + pkg spdx.Package + metaType pkg.MetadataType + meta interface{} + }{ + { + pkg: spdx.Package{ + PackageName: "SomeDebPkg", + PackageVersion: "43.1.235", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=somedebpkg-origin@9.1.3&distro=debian-3.10.9", + RefType: "purl", + }, + }, + }, + metaType: pkg.DpkgMetadataType, + meta: pkg.DpkgMetadata{ + Package: "SomeDebPkg", + Source: "somedebpkg-origin", + Version: "43.1.235", + SourceVersion: "9.1.3", + Architecture: "x86_64", + }, + }, + { + pkg: spdx.Package{ + PackageName: "SomeApkPkg", + PackageVersion: "3.2.9", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9", + RefType: "purl", + }, + }, + }, + metaType: pkg.ApkMetadataType, + meta: pkg.ApkMetadata{ + Package: "SomeApkPkg", + OriginPackage: "apk-origin", + Version: "3.2.9", + Architecture: "x86_64", + }, + }, + { + pkg: spdx.Package{ + PackageName: "SomeRpmPkg", + PackageVersion: "13.2.79", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:rpm/pkg-2@7.3.1?arch=x86_64&epoch=1234&upstream=some-rpm-origin-1.16.3&distro=alpine-3.10.9", + RefType: "purl", + }, + }, + }, + metaType: pkg.RpmMetadataType, + meta: pkg.RpmMetadata{ + Name: "SomeRpmPkg", + Version: "13.2.79", + Epoch: &oneTwoThreeFour, + Arch: "x86_64", + Release: "", + SourceRpm: "some-rpm-origin-1.16.3", + }, + }, + } + + for _, test := range tests { + t.Run(test.pkg.PackageName, func(t *testing.T) { + info := extractPkgInfo(&test.pkg) + metaType, meta := extractMetadata(&test.pkg, info) + assert.Equal(t, test.metaType, metaType) + assert.EqualValues(t, test.meta, meta) + }) + } +} + +func TestExtractSourceFromNamespaces(t *testing.T) { + tests := []struct { + namespace string + expected source.Scheme + }{ + { + namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3", + expected: source.FileScheme, + }, + { + namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3", + expected: source.ImageScheme, + }, + { + namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3", + expected: source.DirectoryScheme, + }, + { + namespace: "https://another-host/blob/123", + expected: source.UnknownScheme, + }, + { + namespace: "bla bla", + expected: source.UnknownScheme, + }, + { + namespace: "", + expected: source.UnknownScheme, + }, + } + + for _, tt := range tests { + require.Equal(t, tt.expected, extractSchemeFromNamespace(tt.namespace)) + } +} + +func TestH1Digest(t *testing.T) { + tests := []struct { + name string + pkg spdx.Package + expectedDigest string + }{ + { + name: "valid h1digest", + pkg: spdx.Package{ + PackageName: "github.com/googleapis/gnostic", + PackageVersion: "v0.5.5", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", + RefType: "purl", + }, + }, + PackageChecksums: []common.Checksum{ + { + Algorithm: common.SHA256, + Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + }, + }, + expectedDigest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + { + name: "invalid h1digest algorithm", + pkg: spdx.Package{ + PackageName: "github.com/googleapis/gnostic", + PackageVersion: "v0.5.5", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", + RefType: "purl", + }, + }, + PackageChecksums: []common.Checksum{ + { + Algorithm: common.SHA1, + Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + }, + }, + expectedDigest: "", + }, + { + name: "invalid h1digest digest", + pkg: spdx.Package{ + PackageName: "github.com/googleapis/gnostic", + PackageVersion: "v0.5.5", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: "PACKAGE-MANAGER", + Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5", + RefType: "purl", + }, + }, + PackageChecksums: []common.Checksum{ + { + Algorithm: common.SHA256, + Value: "", + }, + }, + }, + expectedDigest: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := toSyftPackage(&test.pkg) + require.Equal(t, pkg.GolangBinMetadataType, p.MetadataType) + meta := p.Metadata.(pkg.GolangBinMetadata) + require.Equal(t, test.expectedDigest, meta.H1Digest) + }) + } +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/image-simple/Dockerfile b/sbom/internal/formats/spdx22/test-fixtures/image-simple/Dockerfile new file mode 100644 index 00000000..79cfa759 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-2.txt diff --git a/sbom/internal/formats/spdx22/test-fixtures/image-simple/file-1.txt b/sbom/internal/formats/spdx22/test-fixtures/image-simple/file-1.txt new file mode 100644 index 00000000..985d3408 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/sbom/internal/formats/spdx22/test-fixtures/image-simple/file-2.txt b/sbom/internal/formats/spdx22/test-fixtures/image-simple/file-2.txt new file mode 100644 index 00000000..396d08bb --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden b/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden new file mode 100644 index 00000000..a6cd0453 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXJSONDirectoryEncoder.golden @@ -0,0 +1,76 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "/some/path", + "documentNamespace": "https://anchore.com/syft/dir/some/path-0567e7df-bcf5-4ee0-8565-ca4f9ecc7f0d", + "creationInfo": { + "licenseListVersion": "3.16", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-v0.42.0-bogus" + ], + "created": "2023-01-26T15:28:14Z" + }, + "packages": [ + { + "name": "package-1", + "SPDXID": "SPDXRef-Package-python-package-1-1b1d0be59ac59d2c", + "versionInfo": "1.0.1", + "downloadLocation": "NOASSERTION", + "packageVerificationCode": { + "packageVerificationCodeValue": "" + }, + "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", + "licenseConcluded": "MIT", + "licenseInfoFromFiles": null, + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "a-purl-2" + } + ] + }, + { + "name": "package-2", + "SPDXID": "SPDXRef-Package-deb-package-2-db4abfe497c180d3", + "versionInfo": "2.0.1", + "downloadLocation": "NOASSERTION", + "packageVerificationCode": { + "packageVerificationCodeValue": "" + }, + "sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1", + "licenseConcluded": "NONE", + "licenseInfoFromFiles": null, + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:deb/debian/package-2@2.0.1" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden b/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden new file mode 100644 index 00000000..5fc1e0e8 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXJSONImageEncoder.golden @@ -0,0 +1,76 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "user-image-input", + "documentNamespace": "https://anchore.com/syft/image/user-image-input-1aca09fa-755d-453d-9bdf-481b438f386b", + "creationInfo": { + "licenseListVersion": "3.16", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-v0.42.0-bogus" + ], + "created": "2023-01-26T15:31:27Z" + }, + "packages": [ + { + "name": "package-1", + "SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "versionInfo": "1.0.1", + "downloadLocation": "NOASSERTION", + "packageVerificationCode": { + "packageVerificationCodeValue": "" + }, + "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", + "licenseConcluded": "MIT", + "licenseInfoFromFiles": null, + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "a-purl-1" + } + ] + }, + { + "name": "package-2", + "SPDXID": "SPDXRef-Package-deb-package-2-958443e2d9304af4", + "versionInfo": "2.0.1", + "downloadLocation": "NOASSERTION", + "packageVerificationCode": { + "packageVerificationCodeValue": "" + }, + "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", + "licenseConcluded": "NONE", + "licenseInfoFromFiles": null, + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:deb/debian/package-2@2.0.1" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden b/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden new file mode 100644 index 00000000..ebbd3f02 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/snapshot/TestSPDXRelationshipOrder.golden @@ -0,0 +1,204 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "user-image-input", + "documentNamespace": "https://anchore.com/syft/image/user-image-input-6c400694-c3e4-46f9-a7e7-9e826c9ced8b", + "creationInfo": { + "licenseListVersion": "3.16", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-v0.42.0-bogus" + ], + "created": "2023-01-26T15:33:01Z" + }, + "packages": [ + { + "name": "package-1", + "SPDXID": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "versionInfo": "1.0.1", + "downloadLocation": "NOASSERTION", + "packageVerificationCode": { + "packageVerificationCodeValue": "" + }, + "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", + "licenseConcluded": "MIT", + "licenseInfoFromFiles": null, + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "a-purl-1" + } + ] + }, + { + "name": "package-2", + "SPDXID": "SPDXRef-Package-deb-package-2-958443e2d9304af4", + "versionInfo": "2.0.1", + "downloadLocation": "NOASSERTION", + "packageVerificationCode": { + "packageVerificationCodeValue": "" + }, + "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", + "licenseConcluded": "NONE", + "licenseInfoFromFiles": null, + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:deb/debian/package-2@2.0.1" + } + ] + } + ], + "files": [ + { + "fileName": "/a1/f6", + "SPDXID": "SPDXRef-9c2f7510199b17f6", + "fileTypes": [ + "OTHER" + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": null, + "copyrightText": "" + }, + { + "fileName": "/d1/f3", + "SPDXID": "SPDXRef-c6f5b29dca12661f", + "fileTypes": [ + "OTHER" + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": null, + "copyrightText": "" + }, + { + "fileName": "/d2/f4", + "SPDXID": "SPDXRef-c641caa71518099f", + "fileTypes": [ + "OTHER" + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": null, + "copyrightText": "" + }, + { + "fileName": "/f1", + "SPDXID": "SPDXRef-5265a4dde3edbf7c", + "fileTypes": [ + "OTHER" + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": null, + "copyrightText": "" + }, + { + "fileName": "/f2", + "SPDXID": "SPDXRef-f9e49132a4b96ccd", + "fileTypes": [ + "OTHER" + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": null, + "copyrightText": "" + }, + { + "fileName": "/z1/f5", + "SPDXID": "SPDXRef-839d99ee67d9d174", + "fileTypes": [ + "OTHER" + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": null, + "copyrightText": "" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "relatedSpdxElement": "SPDXRef-5265a4dde3edbf7c", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "relatedSpdxElement": "SPDXRef-839d99ee67d9d174", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "relatedSpdxElement": "SPDXRef-9c2f7510199b17f6", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "relatedSpdxElement": "SPDXRef-c641caa71518099f", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "relatedSpdxElement": "SPDXRef-c6f5b29dca12661f", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6", + "relatedSpdxElement": "SPDXRef-f9e49132a4b96ccd", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/sbom/internal/formats/spdx22/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden new file mode 100644 index 00000000..e58b9849 Binary files /dev/null and b/sbom/internal/formats/spdx22/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/alpine-3.10.syft.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/alpine-3.10.syft.spdx.json new file mode 100644 index 00000000..652aaeca --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/alpine-3.10.syft.spdx.json @@ -0,0 +1,170 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "name": "alpine-3.10", + "spdxVersion": "SPDX-2.2", + "creationInfo": { + "created": "2022-01-20T21:40:24.439211Z", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-[not provided]" + ], + "licenseListVersion": "3.15" + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "https://anchore.com/syft/image/alpine-3.10-204b304b-beb3-4413-9b38-d8a2e58e3dfb", + "packages": [ + { + "SPDXID": "SPDXRef-a61243292e73923", + "name": "busybox", + "licenseConcluded": "GPL-2.0", + "description": "Size optimized toolbox of many common UNIX utilities", + "downloadLocation": "https://busybox.net/", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:busybox:busybox:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "pkg:alpine/busybox@1.30.1-r5?arch=x86_64&distro=alpine-3.10.9", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "GPL-2.0", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "1.30.1-r5" + }, + { + "SPDXID": "SPDXRef-d2f55e316dbe92e4", + "name": "libssl1.1", + "licenseConcluded": "OpenSSL", + "description": "SSL shared libraries", + "downloadLocation": "https://www.openssl.org", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:libssl1.1:libssl1.1:1.1.1k-r0:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "pkg:alpine/libssl1.1@1.1.1k-r0?arch=x86_64&distro=alpine-3.10.9", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "OpenSSL", + "originator": "Person: Timo Teras ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "1.1.1k-r0" + }, + { + "SPDXID": "SPDXRef-2b24657ad7aaafea", + "name": "ssl_client", + "licenseConcluded": "GPL-2.0", + "description": "EXternal ssl_client for busybox wget", + "downloadLocation": "https://busybox.net/", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl-client:ssl-client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl-client:ssl_client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl_client:ssl-client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl_client:ssl_client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl:ssl-client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:a:ssl:ssl_client:1.30.1-r5:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "pkg:alpine/ssl_client@1.30.1-r5?arch=x86_64&upstream=busybox&distro=alpine-3.10.9", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "GPL-2.0", + "originator": "Person: Natanael Copa ", + "sourceInfo": "acquired package info from APK DB: /lib/apk/db/installed", + "versionInfo": "1.30.1-r5" + } + ], + "files": [ + { + "SPDXID": "SPDXRef-a07392483a2d0750", + "comment": "layerID: sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", + "licenseConcluded": "NOASSERTION", + "fileName": "/bin/busybox" + }, + { + "SPDXID": "SPDXRef-aa3cfed221706d80", + "comment": "layerID: sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", + "licenseConcluded": "NOASSERTION", + "fileName": "/lib/libssl.so.1.1" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-a07392483a2d0750" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-a07392483a2d0750" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-499bb68237b0f2b8" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-df78c68c8206be69" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-7c980486fc17af43" + }, + { + "spdxElementId": "SPDXRef-a61243292e73923", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-8762661e65166719" + }, + { + "spdxElementId": "SPDXRef-d2f55e316dbe92e4", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-aa3cfed221706d80" + }, + { + "spdxElementId": "SPDXRef-d2f55e316dbe92e4", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-aa3cfed221706d80" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/alpine-3.10.vendor.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/alpine-3.10.vendor.spdx.json new file mode 100644 index 00000000..4cf87637 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/alpine-3.10.vendor.spdx.json @@ -0,0 +1,78 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.2", + "creationInfo": { + "created": "2022-01-18T22:16:16Z", + "creators": [ + "Tool: vendor" + ], + "licenseListVersion": "3.8" + }, + "name": "alpine:3.10", + "dataLicense": "CC0-1.0", + "documentNamespace": "https://spdx.org/spdxdocs/alpine-154c794c-4264-4e9f-a2ff-5c01bfbfc02c", + "documentDescribes": [ + "SPDXRef-alpine-3.10" + ], + "packages": [ + { + "name": "alpine", + "SPDXID": "SPDXRef-alpine-3.10", + "versionInfo": "3.10", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "name": "busybox", + "SPDXID": "SPDXRef-busybox-1.30.1-r5", + "versionInfo": "1.30.1-r5", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "LicenseRef-7d19a72", + "copyrightText": "NONE", + "comment": "busybox:\n\twarning: No metadata for key: copyright\n\twarning: No metadata for key: download_url\n\twarning: No metadata for key: checksum\n\twarning: No metadata for key: pkg_licenses\n\twarning: No metadata for key: pkg_format\n\twarning: No metadata for key: src_name\n\twarning: No metadata for key: src_version\n" + }, + { + "name": "ssl_client", + "SPDXID": "SPDXRef-ssl_client-1.30.1-r5", + "versionInfo": "1.30.1-r5", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "LicenseRef-de5acdd", + "copyrightText": "NONE", + "comment": "ssl_client:\n\twarning: No metadata for key: copyright\n\twarning: No metadata for key: download_url\n\twarning: No metadata for key: checksum\n\twarning: No metadata for key: pkg_licenses\n\twarning: No metadata for key: pkg_format\n\twarning: No metadata for key: src_name\n\twarning: No metadata for key: src_version\n" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-alpine-3.10", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-9fb3aa2f8b", + "relatedSpdxElement": "SPDXRef-busybox-1.30.1-r5", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-9fb3aa2f8b", + "relatedSpdxElement": "SPDXRef-ssl_client-1.30.1-r5", + "relationshipType": "CONTAINS" + } + ], + "hasExtractedLicensingInfos": [ + { + "extractedText": "OpenSSL", + "licenseId": "LicenseRef-de5acdd" + }, + { + "extractedText": "GPL-2.0", + "licenseId": "LicenseRef-7d19a72" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-bin.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-bin.spdx.json new file mode 100644 index 00000000..0d4ad527 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-bin.spdx.json @@ -0,0 +1,72 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "hello-go-binary.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-binary", + "externalDocumentRefs": [ + { + "externalDocumentId": "DocumentRef-hello-go-module", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "d661f8f831a99c288a64e5843b4794ad5181224a" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-go-module-cfa0c58d-79db-4860-99b6-258477e4838b" + }, + { + "externalDocumentId": "DocumentRef-golang-dist", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "b6cf54a46329e7cc7610aa5d244018b80103d111" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/golang-dist-492dfde4-318b-49f7-b48c-934bfafbde48" + }, + { + "externalDocumentId": "DocumentRef-hello-imports", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "14ff98203c3ddd2bd4803c00b5225d2551ca603c" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-imports-c2d068df-67aa-4c68-98c8-100b450fc408" + } + ], + "documentDescribes": [ + "SPDXRef-go-bin-hello" + ], + "packages": [ + { + "packageName": "hello", + "SPDXID": "SPDXRef-go-bin-hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/build/hello", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ], + "relationships": [ + { + "spdxElementId": "DocumentRef-golang-dist", + "relatedSpdxElement": "DocumentRef-hello-go-module", + "relationshipType": "BUILD_TOOL_OF" + }, + { + "spdxElementId": "DocumentRef-golang-dist:SPDXRef-go-compiler", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "GENERATES" + }, + { + "spdxElementId": "DocumentRef-hello-imports", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "STATIC_LINK" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-go-module.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-go-module.spdx.json new file mode 100644 index 00000000..9d0e2e8b --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-go-module.spdx.json @@ -0,0 +1,29 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "hello-go-module.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-module", + "documentDescribes": [ + "SPDXRef-go-module-example.com/hello" + ], + "packages": [ + { + "packageName": "example.com/hello", + "SPDXID": "SPDXRef-go-module-example.com/hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/src/hello", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-golang.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-golang.spdx.json new file mode 100644 index 00000000..9c1dc5c3 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-golang.spdx.json @@ -0,0 +1,46 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "golang-dist", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/golang-dist", + "documentDescribes": [ + "SPDXRef-golang-dist" + ], + "packages": [ + { + "packageName": "go1.16.4.linux-amd64", + "SPDXID": "SPDXRef-golang-dist", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "packageVersion": "1.16.4", + "filesAnalyzed": "false", + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": "7154e88f5a8047aad4b80ebace58a059e36e7e2e4eb3b383127a28c711b4ff59" + } + ], + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "LicenseRef-Golang-BSD-plus-Patents", + "packageCopyrightText": "Copyright (c) 2009 The Go Authors. All rights reserved." + }, + { + "packageName": "go", + "SPDXID": "SPDXRef-go-compiler", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "packageVersion": "1.16.4", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json new file mode 100644 index 00000000..626ad04a --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/bad/example7-third-party-modules.spdx.json @@ -0,0 +1,49 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ + { + "Person": "Nisha K (nishak@vmware.com)" + } + ] + }, + "name": "hello-imports.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-imports", + "documentDescribes": [ + "SPDXRef-go-module-golang.org/x/text", + "SPDXRef-go-module-rsc.io/quote", + "SPDXRef-go-module-rsc.io/sampler" + ], + "packages": [ + { + "packageName": "golang.org/x/text", + "SPDXID": "SPDXRef-go-module-golang.org/x/text", + "downloadLocation": "go://golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + }, + { + "packageName": "rsc.io/quote", + "SPDXID": "SPDXRef-go-module-rsc.io/quote", + "downloadLocation": "go://rsc.io/quote@v1.5.2", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + }, + { + "packageName": "rsc.io/sampler", + "SPDXID": "SPDXRef-go-module-rsc.io/sampler", + "downloadLocation": "go://rsc.io/sampler@v1.3.0", + "filesAnalyzed": "false", + "packageLicenseConcluded": "NOASSERTION", + "packageLicenseDeclared": "NOASSERTION", + "packageCopyrightText": "NOASSERTION" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-bin.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-bin.spdx.json new file mode 100644 index 00000000..563b40a7 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-bin.spdx.json @@ -0,0 +1,68 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "hello-go-binary.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-binary", + "externalDocumentRefs": [ + { + "externalDocumentId": "DocumentRef-hello-go-module", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "d661f8f831a99c288a64e5843b4794ad5181224a" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-go-module-cfa0c58d-79db-4860-99b6-258477e4838b" + }, + { + "externalDocumentId": "DocumentRef-golang-dist", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "b6cf54a46329e7cc7610aa5d244018b80103d111" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/golang-dist-492dfde4-318b-49f7-b48c-934bfafbde48" + }, + { + "externalDocumentId": "DocumentRef-hello-imports", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "14ff98203c3ddd2bd4803c00b5225d2551ca603c" + }, + "spdxDocument": "https://swinslow.net/spdx-examples/example7/hello-imports-c2d068df-67aa-4c68-98c8-100b450fc408" + } + ], + "documentDescribes": [ + "SPDXRef-go-bin-hello" + ], + "packages": [ + { + "name": "hello", + "SPDXID": "SPDXRef-go-bin-hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/build/hello", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ], + "relationships": [ + { + "spdxElementId": "DocumentRef-golang-dist:SPDXRef-golang-dist", + "relatedSpdxElement": "DocumentRef-hello-go-module:SPDXRef-hello-go-module", + "relationshipType": "BUILD_TOOL_OF" + }, + { + "spdxElementId": "DocumentRef-golang-dist:SPDXRef-go-compiler", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "GENERATES" + }, + { + "spdxElementId": "DocumentRef-hello-imports:SPDXRef-hello-imports", + "relatedSpdxElement": "SPDXRef-go-bin-hello", + "relationshipType": "STATIC_LINK" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-go-module.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-go-module.spdx.json new file mode 100644 index 00000000..682b0642 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-go-module.spdx.json @@ -0,0 +1,25 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "hello-go-module.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-go-module", + "documentDescribes": [ + "SPDXRef-go-module-example.com/hello" + ], + "packages": [ + { + "name": "example.com/hello", + "SPDXID": "SPDXRef-go-module-example.com/hello", + "downloadLocation": "git@github.com:swinslow/spdx-examples.git#example7/content/src/hello", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-golang.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-golang.spdx.json new file mode 100644 index 00000000..e9800309 --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-golang.spdx.json @@ -0,0 +1,42 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "golang-dist", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/golang-dist", + "documentDescribes": [ + "SPDXRef-golang-dist" + ], + "packages": [ + { + "name": "go1.16.4.linux-amd64", + "SPDXID": "SPDXRef-golang-dist", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "versionInfo": "1.16.4", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": "7154e88f5a8047aad4b80ebace58a059e36e7e2e4eb3b383127a28c711b4ff59" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "LicenseRef-Golang-BSD-plus-Patents", + "copyrightText": "Copyright (c) 2009 The Go Authors. All rights reserved." + }, + { + "name": "go", + "SPDXID": "SPDXRef-go-compiler", + "downloadLocation": "https://golang.org/dl/go1.16.4.linux-amd64.tar.gz", + "versionInfo": "1.16.4", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-third-party-modules.spdx.json b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-third-party-modules.spdx.json new file mode 100644 index 00000000..fa7ce80a --- /dev/null +++ b/sbom/internal/formats/spdx22/test-fixtures/spdx/example7-third-party-modules.spdx.json @@ -0,0 +1,45 @@ +{ + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2020-11-24T01:12:27Z", + "creators": [ "Person: Nisha K (nishak@vmware.com)" ] + }, + "name": "hello-imports.spdx.json", + "documentNamespace": "https://swinslow.net/spdx-examples/example7/hello-imports", + "documentDescribes": [ + "SPDXRef-go-module-golang.org/x/text", + "SPDXRef-go-module-rsc.io/quote", + "SPDXRef-go-module-rsc.io/sampler" + ], + "packages": [ + { + "name": "golang.org/x/text", + "SPDXID": "SPDXRef-go-module-golang.org/x/text", + "downloadLocation": "go://golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "name": "rsc.io/quote", + "SPDXID": "SPDXRef-go-module-rsc.io/quote", + "downloadLocation": "go://rsc.io/quote@v1.5.2", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + }, + { + "name": "rsc.io/sampler", + "SPDXID": "SPDXRef-go-module-rsc.io/sampler", + "downloadLocation": "go://rsc.io/sampler@v1.3.0", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/sbom/internal/formats/spdx22/to_format_model.go b/sbom/internal/formats/spdx22/to_format_model.go new file mode 100644 index 00000000..f201b134 --- /dev/null +++ b/sbom/internal/formats/spdx22/to_format_model.go @@ -0,0 +1,564 @@ +package spdx22 + +import ( + "crypto/sha1" + "fmt" + "sort" + "strings" + "time" + + "github.com/apex/log" + "github.com/spdx/tools-golang/spdx/v2/common" + spdx "github.com/spdx/tools-golang/spdx/v2/v2_2" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/formats/common/util" + + "github.com/anchore/syft/syft/artifact" + + "github.com/anchore/syft/syft/sbom" + + "github.com/anchore/syft/syft/formats/common/spdxhelpers" + "github.com/paketo-buildpacks/packit/v2/sbom/internal" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/spdx22/model" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/spdxlicense" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +const ( + spdxVersion = "SPDX-2.2" + noAssertion = "NOASSERTION" +) + +// ToFormatModel transforms the sbom import a format-specific model. +// note: this is needed for anchore import functionality +// TODO: unexport this when/if anchore import functionality is removed +func ToFormatModel(s sbom.SBOM) *spdx.Document { + name, namespace := spdxhelpers.DocumentNameAndNamespace(s.Source) + relationships := toRelationships(s.RelationshipsSorted()) + + // for valid SPDX we need a document describes relationship + // TODO: remove this placeholder after deciding on correct behavior + // for the primary package purpose field: + // https://spdx.github.io/spdx-spec/v2.2/package-information/#724-primary-package-purpose-field + documentDescribesRelationship := &spdx.Relationship{ + RefA: common.DocElementID{ + ElementRefID: "DOCUMENT", + }, + Relationship: string(spdxhelpers.DescribesRelationship), + RefB: common.DocElementID{ + ElementRefID: "DOCUMENT", + }, + RelationshipComment: "", + } + + relationships = append(relationships, documentDescribesRelationship) + + return &spdx.Document{ + // 6.1: SPDX Version; should be in the format "SPDX-x.x" + // Cardinality: mandatory, one + SPDXVersion: spdxVersion, + + // 6.2: Data License; should be "CC0-1.0" + // Cardinality: mandatory, one + DataLicense: "CC0-1.0", + + // 6.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT + // Cardinality: mandatory, one + SPDXIdentifier: "DOCUMENT", + + // 6.4: Document Name + // Cardinality: mandatory, one + DocumentName: name, + + // 6.5: Document Namespace + // Cardinality: mandatory, one + // Purpose: Provide an SPDX document specific namespace as a unique absolute Uniform Resource + // Identifier (URI) as specified in RFC-3986, with the exception of the ‘#’ delimiter. The SPDX + // Document URI cannot contain a URI "part" (e.g. the "#" character), since the ‘#’ is used in SPDX + // element URIs (packages, files, snippets, etc) to separate the document namespace from the + // element’s SPDX identifier. Additionally, a scheme (e.g. “https:”) is required. + + // The URI must be unique for the SPDX document including the specific version of the SPDX document. + // If the SPDX document is updated, thereby creating a new version, a new URI for the updated + // document must be used. There can only be one URI for an SPDX document and only one SPDX document + // for a given URI. + + // Note that the URI does not have to be accessible. It is only intended to provide a unique ID. + // In many cases, the URI will point to a web accessible document, but this should not be assumed + // to be the case. + + DocumentNamespace: namespace, + + // 6.6: External Document References + // Cardinality: optional, one or many + ExternalDocumentReferences: nil, + + // 6.11: Document Comment + // Cardinality: optional, one + DocumentComment: "", + + CreationInfo: &spdx.CreationInfo{ + // 6.7: License List Version + // Cardinality: optional, one + LicenseListVersion: spdxlicense.Version, + + // 6.8: Creators: may have multiple keys for Person, Organization + // and/or Tool + // Cardinality: mandatory, one or many + Creators: []common.Creator{ + { + Creator: "Anchore, Inc", + CreatorType: "Organization", + }, + { + Creator: internal.ApplicationName + "-" + s.Descriptor.Version, + CreatorType: "Tool", + }, + }, + + // 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ + // Cardinality: mandatory, one + Created: time.Now().UTC().Format(time.RFC3339), + }, + Packages: toPackages(s.Artifacts.Packages, s), + Files: toFiles(s), + Relationships: relationships, + } +} + +func toPackages(catalog *pkg.Collection, sbom sbom.SBOM) (results []*spdx.Package) { + for _, p := range catalog.Sorted() { + // name should be guaranteed to be unique, but semantically useful and stable + id := toSPDXID(p) + + // If the Concluded License is not the same as the Declared License, a written explanation should be provided + // in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in + // the Comments on License field (section 7.16) is preferred. + license := model.License(p) + + // two ways to get filesAnalyzed == true: + // 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger + // 2. syft has generated a sha1 digest for the package's contents + packageChecksums, filesAnalyzed := toPackageChecksums(p) + + packageVerificationCode := newPackageVerificationCode(p, sbom) + if packageVerificationCode != nil { + filesAnalyzed = true + } + + if packageVerificationCode == nil { + // added to avoid null pointer exception errors + packageVerificationCode = &common.PackageVerificationCode{} + // invalid SPDX document state + if filesAnalyzed { + // this is an invalid document state + // we reset the filesAnalyzed flag to false to avoid + // cases where a package digest was generated but there was + // not enough metadata to generate a verification code regarding the files + filesAnalyzed = false + } + } + + results = append(results, &spdx.Package{ + // NOT PART OF SPEC + // flag: does this "package" contain files that were in fact "unpackaged", + // e.g. included directly in the Document without being in a Package? + IsUnpackaged: false, + + // 7.1: Package Name + // Cardinality: mandatory, one + PackageName: p.Name, + + // 7.2: Package SPDX Identifier: "SPDXRef-[idstring]" + // Cardinality: mandatory, one + PackageSPDXIdentifier: id, + + // 7.3: Package Version + // Cardinality: optional, one + PackageVersion: p.Version, + + // 7.4: Package File Name + // Cardinality: optional, one + PackageFileName: "", + + // 7.5: Package Supplier: may have single result for either Person or Organization, + // or NOASSERTION + // Cardinality: optional, one + + // 7.6: Package Originator: may have single result for either Person or Organization, + // or NOASSERTION + // Cardinality: optional, one + PackageSupplier: nil, + + PackageOriginator: toPackageOriginator(p), + + // 7.7: Package Download Location + // Cardinality: mandatory, one + // NONE if there is no download location whatsoever. + // NOASSERTION if: + // (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination; + // (ii) the SPDX file creator has made no attempt to determine this field; or + // (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so). + PackageDownloadLocation: spdxhelpers.DownloadLocation(p), + + // 7.8: FilesAnalyzed + // Cardinality: optional, one; default value is "true" if omitted + + // Purpose: Indicates whether the file content of this package has been available for or subjected to + // analysis when creating the SPDX document. If false, indicates packages that represent metadata or + // URI references to a project, product, artifact, distribution or a component. If false, the package + // must not contain any files. + + // Intent: A package can refer to a project, product, artifact, distribution or a component that is + // external to the SPDX document. + FilesAnalyzed: filesAnalyzed, + // NOT PART OF SPEC: did FilesAnalyzed tag appear? + IsFilesAnalyzedTagPresent: true, + + // 7.9: Package Verification Code + PackageVerificationCode: *packageVerificationCode, + + // 7.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5 + // Cardinality: optional, one or many + + // 7.10.1 Purpose: Provide an independently reproducible mechanism that permits unique identification of + // a specific package that correlates to the data in this SPDX file. This identifier enables a recipient + // to determine if any file in the original package has been changed. If the SPDX file is to be included + // in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the + // checksum by default. + PackageChecksums: packageChecksums, + + // 7.11: Package Home Page + // Cardinality: optional, one + PackageHomePage: spdxhelpers.Homepage(p), + + // 7.12: Source Information + // Cardinality: optional, one + PackageSourceInfo: spdxhelpers.SourceInfo(p), + + // 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one + // Purpose: Contain the license the SPDX file creator has concluded as governing the + // package or alternative values, if the governing license cannot be determined. + PackageLicenseConcluded: license, + + // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one or many if filesAnalyzed is true / omitted; + // zero (must be omitted) if filesAnalyzed is false + PackageLicenseInfoFromFiles: nil, + + // 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one + // Purpose: List the licenses that have been declared by the authors of the package. + // Any license information that does not originate from the package authors, e.g. license + // information from a third party repository, should not be included in this field. + PackageLicenseDeclared: license, + + // 7.16: Comments on License + // Cardinality: optional, one + PackageLicenseComments: "", + + // 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one + // Purpose: IdentifyFormat the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to: + // + // Any text related to a copyright notice, even if not complete; + // NONE if the package contains no copyright information whatsoever; or + // NOASSERTION, if + // (i) the SPDX document creator has made no attempt to determine this field; or + // (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so). + PackageCopyrightText: noAssertion, + + // 7.18: Package Summary Description + // Cardinality: optional, one + PackageSummary: "", + + // 7.19: Package Detailed Description + // Cardinality: optional, one + PackageDescription: spdxhelpers.Description(p), + + // 7.20: Package Comment + // Cardinality: optional, one + PackageComment: "", + + // 7.21: Package External Reference + // Cardinality: optional, one or many + PackageExternalReferences: formatSPDXExternalRefs(p), + + // 7.22: Package External Reference Comment + // Cardinality: conditional (optional, one) for each External Reference + // contained within PackageExternalReference2_1 struct, if present + + // 7.23: Package Attribution Text + // Cardinality: optional, one or many + PackageAttributionTexts: nil, + }) + } + return results +} + +func toSPDXID(identifiable artifact.Identifiable) common.ElementID { + id := "" + if p, ok := identifiable.(pkg.Package); ok { + id = spdxhelpers.SanitizeElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.ID())) + } else { + id = string(identifiable.ID()) + } + // NOTE: the spdx libraries prepend SPDXRef-, so we don't do it here + return common.ElementID(id) +} + +func lookupRelationship(ty artifact.RelationshipType) (bool, spdxhelpers.RelationshipType, string) { + switch ty { + case artifact.ContainsRelationship: + return true, spdxhelpers.ContainsRelationship, "" + case artifact.DependencyOfRelationship: + return true, spdxhelpers.DependencyOfRelationship, "" + case artifact.OwnershipByFileOverlapRelationship: + return true, spdxhelpers.OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty) + } + return false, "", "" +} + +func toFiles(s sbom.SBOM) (results []*spdx.File) { + artifacts := s.Artifacts + + for _, coordinates := range s.AllCoordinates() { + var metadata *source.FileMetadata + if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists { + metadata = &metadataForLocation + } + + var digests []file.Digest + if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists { + digests = digestsForLocation + } + + // if we don't have any metadata or digests for this location + // then the file is most likely a symlink or non-regular file + // for now we include a 0 sha1 digest as requested by the spdx spec + // TODO: update location code in core SBOM so that we can map complex links + // back to their real file digest location. + if len(digests) == 0 { + digests = append(digests, file.Digest{Algorithm: "sha1", Value: "0000000000000000000000000000000000000000"}) + } + + // TODO: add file classifications (?) and content as a snippet + + var comment string + if coordinates.FileSystemID != "" { + comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID) + } + + results = append(results, &spdx.File{ + FileSPDXIdentifier: toSPDXID(coordinates), + FileComment: comment, + // required, no attempt made to determine license information + LicenseConcluded: noAssertion, + Checksums: toFileChecksums(digests), + FileName: coordinates.RealPath, + FileTypes: toFileTypes(metadata), + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + if results[i].FileName == results[j].FileName { + return results[i].FileSPDXIdentifier < results[j].FileSPDXIdentifier + } + return results[i].FileName < results[j].FileName + }) + return results +} + +func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference) { + for _, ref := range spdxhelpers.ExternalRefs(p) { + refs = append(refs, &spdx.PackageExternalReference{ + Category: string(ref.ReferenceCategory), + RefType: string(ref.ReferenceType), + Locator: ref.ReferenceLocator, + ExternalRefComment: ref.Comment, + }) + } + return refs +} + +func toRelationships(relationships []artifact.Relationship) (result []*spdx.Relationship) { + for _, r := range relationships { + exists, relationshipType, comment := lookupRelationship(r.Type) + + if !exists { + log.Debugf("unable to convert relationship to SPDX, dropping: %+v", r) + continue + } + + // FIXME: we are only currently including Package -> * relationships + if _, ok := r.From.(pkg.Package); !ok { + log.Debugf("skipping non-package relationship: %+v", r) + continue + } + + result = append(result, &spdx.Relationship{ + RefA: common.DocElementID{ + ElementRefID: toSPDXID(r.From), + }, + Relationship: string(relationshipType), + RefB: common.DocElementID{ + ElementRefID: toSPDXID(r.To), + }, + RelationshipComment: comment, + }) + } + return result +} + +func toPackageChecksums(p pkg.Package) ([]common.Checksum, bool) { + filesAnalyzed := false + var checksums []common.Checksum + switch meta := p.Metadata.(type) { + // we generate digest for some Java packages + // spdx.github.io/spdx-spec/package-information/#710-package-checksum-field + case pkg.JavaMetadata: + // if syft has generated the digest here then filesAnalyzed is true + if len(meta.ArchiveDigests) > 0 { + filesAnalyzed = true + for _, digest := range meta.ArchiveDigests { + algo := strings.ToUpper(digest.Algorithm) + checksums = append(checksums, common.Checksum{ + Algorithm: common.ChecksumAlgorithm(algo), + Value: digest.Value, + }) + } + } + case pkg.GolangBinMetadata: + // because the H1 digest is found in the Golang metadata we cannot claim that the files were analyzed + algo, hexStr, err := util.HDigestToSHA(meta.H1Digest) + if err != nil { + log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err) + break + } + algo = strings.ToUpper(algo) + checksums = append(checksums, common.Checksum{ + Algorithm: common.ChecksumAlgorithm(algo), + Value: hexStr, + }) + } + return checksums, filesAnalyzed +} + +func toFileChecksums(digests []file.Digest) (checksums []common.Checksum) { + checksums = make([]common.Checksum, 0, len(digests)) + for _, digest := range digests { + checksums = append(checksums, common.Checksum{ + Algorithm: toChecksumAlgorithm(digest.Algorithm), + Value: digest.Value, + }) + } + return checksums +} + +func toChecksumAlgorithm(algorithm string) common.ChecksumAlgorithm { + // this needs to be an uppercase version of our algorithm + return common.ChecksumAlgorithm(strings.ToUpper(algorithm)) +} + +func toFileTypes(metadata *source.FileMetadata) (ty []string) { + if metadata == nil { + return nil + } + + mimeTypePrefix := strings.Split(metadata.MIMEType, "/")[0] + switch mimeTypePrefix { + case "image": + ty = append(ty, string(spdxhelpers.ImageFileType)) + case "video": + ty = append(ty, string(spdxhelpers.VideoFileType)) + case "application": + ty = append(ty, string(spdxhelpers.ApplicationFileType)) + case "text": + ty = append(ty, string(spdxhelpers.TextFileType)) + case "audio": + ty = append(ty, string(spdxhelpers.AudioFileType)) + } + + if model.IsExecutable(metadata.MIMEType) { + ty = append(ty, string(spdxhelpers.BinaryFileType)) + } + + if model.IsArchive(metadata.MIMEType) { + ty = append(ty, string(spdxhelpers.ArchiveFileType)) + } + + // TODO: add support for source, spdx, and documentation file types + if len(ty) == 0 { + ty = append(ty, string(spdxhelpers.OtherFileType)) + } + + return ty +} + +func toPackageOriginator(p pkg.Package) *common.Originator { + kind, originator := spdxhelpers.Originator(p) + if kind == "" || originator == "" { + return nil + } + return &common.Originator{ + Originator: originator, + OriginatorType: kind, + } +} + +// TODO: handle SPDX excludes file case +// f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */ +// see: https://spdx.github.io/spdx-spec/v2.2/package-information/#79-package-verification-code-field +// the above link contains the SPDX algorithm for a package verification code +func newPackageVerificationCode(p pkg.Package, sbom sbom.SBOM) *common.PackageVerificationCode { + // key off of the contains relationship; + // spdx validator will fail if a package claims to contain a file but no sha1 provided + // if a sha1 for a file is provided then the validator will fail if the package does not have + // a package verification code + coordinates := sbom.CoordinatesForPackage(p, artifact.ContainsRelationship) + var digests []file.Digest + for _, c := range coordinates { + digest := sbom.Artifacts.FileDigests[c] + if len(digest) == 0 { + continue + } + + var d file.Digest + for _, digest := range digest { + if digest.Algorithm == "sha1" { + d = digest + break + } + } + digests = append(digests, d) + } + + if len(digests) == 0 { + return nil + } + + // sort templist in ascending order by SHA1 value + sort.SliceStable(digests, func(i, j int) bool { + return digests[i].Value < digests[j].Value + }) + + // filelist = templist with "/n"s removed. /* ordered sequence of SHA1 values with no separators + var b strings.Builder + for _, digest := range digests { + b.WriteString(digest.Value) + } + + //nolint:gosec + hasher := sha1.New() + _, _ = hasher.Write([]byte(b.String())) + return &common.PackageVerificationCode{ + // 7.9.1: Package Verification Code Value + // Cardinality: mandatory, one + Value: fmt.Sprintf("%+x", hasher.Sum(nil)), + } +} diff --git a/sbom/internal/formats/spdx22/to_format_model_test.go b/sbom/internal/formats/spdx22/to_format_model_test.go new file mode 100644 index 00000000..fc6cb357 --- /dev/null +++ b/sbom/internal/formats/spdx22/to_format_model_test.go @@ -0,0 +1,436 @@ +package spdx22 + +import ( + "fmt" + "testing" + + "github.com/spdx/tools-golang/spdx/v2/common" + spdx "github.com/spdx/tools-golang/spdx/v2/v2_2" + "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/formats/common/spdxhelpers" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +// TODO: Add ToFormatModel tests + +func Test_toPackageChecksums(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expected []common.Checksum + filesAnalyzed bool + }{ + { + name: "Java Package", + pkg: pkg.Package{ + Name: "test", + Version: "1.0.0", + Language: pkg.Java, + Metadata: pkg.JavaMetadata{ + ArchiveDigests: []file.Digest{ + { + Algorithm: "sha1", // SPDX expects these to be uppercase + Value: "1234", + }, + }, + }, + }, + expected: []common.Checksum{ + { + Algorithm: "SHA1", + Value: "1234", + }, + }, + filesAnalyzed: true, + }, + { + name: "Java Package with no archive digests", + pkg: pkg.Package{ + Name: "test", + Version: "1.0.0", + Language: pkg.Java, + Metadata: pkg.JavaMetadata{ + ArchiveDigests: []file.Digest{}, + }, + }, + expected: []common.Checksum{}, + filesAnalyzed: false, + }, + { + name: "Java Package with no metadata", + pkg: pkg.Package{ + Name: "test", + Version: "1.0.0", + Language: pkg.Java, + }, + expected: []common.Checksum{}, + filesAnalyzed: false, + }, + { + name: "Go Binary Package", + pkg: pkg.Package{ + Name: "test", + Version: "1.0.0", + Language: pkg.Go, + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expected: []common.Checksum{ + { + Algorithm: "SHA256", + Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + }, + filesAnalyzed: false, + }, + { + name: "Package with no metadata type", + pkg: pkg.Package{ + Name: "test", + Version: "1.0.0", + Language: pkg.Java, + Metadata: struct{}{}, + }, + expected: []common.Checksum{}, + filesAnalyzed: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + commonSum, filesAnalyzed := toPackageChecksums(test.pkg) + assert.ElementsMatch(t, test.expected, commonSum) + assert.Equal(t, test.filesAnalyzed, filesAnalyzed) + }) + } +} + +func Test_toFileTypes(t *testing.T) { + + tests := []struct { + name string + metadata source.FileMetadata + expected []string + }{ + { + name: "application", + metadata: source.FileMetadata{ + MIMEType: "application/vnd.unknown", + }, + expected: []string{ + string(spdxhelpers.ApplicationFileType), + }, + }, + { + name: "archive", + metadata: source.FileMetadata{ + MIMEType: "application/zip", + }, + expected: []string{ + string(spdxhelpers.ApplicationFileType), + string(spdxhelpers.ArchiveFileType), + }, + }, + { + name: "audio", + metadata: source.FileMetadata{ + MIMEType: "audio/ogg", + }, + expected: []string{ + string(spdxhelpers.AudioFileType), + }, + }, + { + name: "video", + metadata: source.FileMetadata{ + MIMEType: "video/3gpp", + }, + expected: []string{ + string(spdxhelpers.VideoFileType), + }, + }, + { + name: "text", + metadata: source.FileMetadata{ + MIMEType: "text/html", + }, + expected: []string{ + string(spdxhelpers.TextFileType), + }, + }, + { + name: "image", + metadata: source.FileMetadata{ + MIMEType: "image/png", + }, + expected: []string{ + string(spdxhelpers.ImageFileType), + }, + }, + { + name: "binary", + metadata: source.FileMetadata{ + MIMEType: "application/x-sharedlib", + }, + expected: []string{ + string(spdxhelpers.ApplicationFileType), + string(spdxhelpers.BinaryFileType), + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata)) + }) + } +} + +func Test_lookupRelationship(t *testing.T) { + + tests := []struct { + input artifact.RelationshipType + exists bool + ty spdxhelpers.RelationshipType + comment string + }{ + { + input: artifact.ContainsRelationship, + exists: true, + ty: spdxhelpers.ContainsRelationship, + }, + { + input: artifact.OwnershipByFileOverlapRelationship, + exists: true, + ty: spdxhelpers.OtherRelationship, + comment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", + }, + { + input: "made-up", + exists: false, + }, + } + for _, test := range tests { + t.Run(string(test.input), func(t *testing.T) { + exists, ty, comment := lookupRelationship(test.input) + assert.Equal(t, exists, test.exists) + assert.Equal(t, ty, test.ty) + assert.Equal(t, comment, test.comment) + }) + } +} + +func Test_toFileChecksums(t *testing.T) { + tests := []struct { + name string + digests []file.Digest + expected []common.Checksum + }{ + { + name: "empty", + }, + { + name: "has digests", + digests: []file.Digest{ + { + Algorithm: "SHA256", + Value: "deadbeefcafe", + }, + { + Algorithm: "md5", + Value: "meh", + }, + }, + expected: []common.Checksum{ + { + Algorithm: "SHA256", + Value: "deadbeefcafe", + }, + { + Algorithm: "MD5", + Value: "meh", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests)) + }) + } +} + +func Test_fileIDsForPackage(t *testing.T) { + p := pkg.Package{ + Name: "bogus", + } + + c := source.Coordinates{ + RealPath: "/path", + FileSystemID: "nowhere", + } + + docElementId := func(identifiable artifact.Identifiable) common.DocElementID { + return common.DocElementID{ + ElementRefID: toSPDXID(identifiable), + } + } + + tests := []struct { + name string + relationships []artifact.Relationship + expected []*spdx.Relationship + }{ + { + name: "package-to-file contains relationships", + relationships: []artifact.Relationship{ + { + From: p, + To: c, + Type: artifact.ContainsRelationship, + }, + }, + expected: []*spdx.Relationship{ + { + Relationship: "CONTAINS", + RefA: docElementId(p), + RefB: docElementId(c), + }, + }, + }, + { + name: "package-to-package", + relationships: []artifact.Relationship{ + { + From: p, + To: p, + Type: artifact.ContainsRelationship, + }, + }, + expected: []*spdx.Relationship{ + { + Relationship: "CONTAINS", + RefA: docElementId(p), + RefB: docElementId(p), + }, + }, + }, + { + name: "ignore file-to-file", + relationships: []artifact.Relationship{ + { + From: c, + To: c, + Type: artifact.ContainsRelationship, + }, + }, + expected: nil, + }, + { + name: "ignore file-to-package", + relationships: []artifact.Relationship{ + { + From: c, + To: p, + Type: artifact.ContainsRelationship, + }, + }, + expected: nil, + }, + { + name: "include package-to-file overlap relationships", + relationships: []artifact.Relationship{ + { + From: p, + To: c, + Type: artifact.OwnershipByFileOverlapRelationship, + }, + }, + expected: []*spdx.Relationship{ + { + Relationship: "OTHER", + RefA: docElementId(p), + RefB: docElementId(c), + RelationshipComment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + relationships := toRelationships(test.relationships) + assert.Equal(t, test.expected, relationships) + }) + } +} + +func Test_H1Digest(t *testing.T) { + sbom := sbom.SBOM{} + tests := []struct { + name string + pkg pkg.Package + expectedDigest string + }{ + { + name: "valid h1digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c", + }, + { + name: "invalid h1digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h1:9fHAtK0uzzz", + }, + }, + expectedDigest: "", + }, + { + name: "unsupported h-digest", + pkg: pkg.Package{ + Name: "github.com/googleapis/gnostic", + Version: "v0.5.5", + MetadataType: pkg.GolangBinMetadataType, + Metadata: pkg.GolangBinMetadata{ + H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=", + }, + }, + expectedDigest: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + catalog := pkg.NewCollection(test.pkg) + pkgs := toPackages(catalog, sbom) + require.Len(t, pkgs, 1) + for _, p := range pkgs { + if test.expectedDigest == "" { + require.Len(t, p.PackageChecksums, 0) + } else { + require.Len(t, p.PackageChecksums, 1) + for _, c := range p.PackageChecksums { + require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value)) + } + } + } + }) + } +} diff --git a/sbom/internal/formats/syft2/README.md b/sbom/internal/formats/syft2/README.md new file mode 100644 index 00000000..c06977d7 --- /dev/null +++ b/sbom/internal/formats/syft2/README.md @@ -0,0 +1,11 @@ +# Source +The contents of this directory is largely based on anchore/syft's +internal `syftjson` package. The version copied is from an [old +commit](https://github.com/anchore/syft/tree/dfefd2ea4e9d44187b4f861bc202970247dd34c8) +of Syft that supports Syft JSON Schema 2.0.2. + +The implementations of `decoder` and `validator` have been omitted for +simplicity, since they are not required for buildpacks' SBOM generation. + +Aspects of the model have been copied over due to slight deviations against the +latest Syft JSON model. diff --git a/sbom/internal/formats/syft2/encoder.go b/sbom/internal/formats/syft2/encoder.go new file mode 100644 index 00000000..a33237a9 --- /dev/null +++ b/sbom/internal/formats/syft2/encoder.go @@ -0,0 +1,19 @@ +package syft2 + +import ( + "encoding/json" + "io" + + "github.com/anchore/syft/syft/sbom" +) + +func encoder(output io.Writer, s sbom.SBOM) error { + doc := ToFormatModel(s) + + enc := json.NewEncoder(output) + // prevent > and < from being escaped in the payload + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + + return enc.Encode(&doc) +} diff --git a/sbom/internal/formats/syft2/encoder_test.go b/sbom/internal/formats/syft2/encoder_test.go new file mode 100644 index 00000000..c2d957db --- /dev/null +++ b/sbom/internal/formats/syft2/encoder_test.go @@ -0,0 +1,221 @@ +package syft2 + +import ( + "flag" + "regexp" + "testing" + + stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" +) + +// Source https://github.com/anchore/syft/blob/dfefd2ea4e9d44187b4f861bc202970247dd34c8/internal/formats/syftjson/encoder_test.go + +var updateJson = flag.Bool("update-json", false, "update the *.golden files for json encoders") + +func TestDirectoryEncoder(t *testing.T) { + testutils.AssertEncoderAgainstGoldenSnapshot(t, + Format(), + testutils.DirectoryInput(t), + *updateJson, + true, + schemaVersionRedactor, + ) +} + +func TestImageEncoder(t *testing.T) { + testImage := "image-simple" + testutils.AssertEncoderAgainstGoldenImageSnapshot(t, + Format(), + testutils.ImageInput(t, testImage, testutils.FromSnapshot()), + testImage, + *updateJson, + true, + schemaVersionRedactor, + ) +} +func schemaVersionRedactor(s []byte) []byte { + pattern := regexp.MustCompile(`,?\s*"schema":\s*\{[^}]*}`) + out := pattern.ReplaceAll(s, []byte("")) + return out +} + +func TestEncodeFullJSONDocument(t *testing.T) { + catalog := pkg.NewCollection() + + p1 := pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Locations: source.NewLocationSet( + source.Location{ + LocationData: source.LocationData{ + Coordinates: source.Coordinates{ + RealPath: "/a/place/a", + }, + }, + }, + ), + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + Files: []pkg.PythonFileRecord{}, + }, + PURL: "a-purl-1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + }, + } + + p2 := pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Locations: source.NewLocationSet( + source.Location{ + LocationData: source.LocationData{ + Coordinates: source.Coordinates{ + RealPath: "/b/place/b", + }, + }, + }, + ), + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + Files: []pkg.DpkgFileRecord{}, + }, + PURL: "a-purl-2", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + } + + catalog.Add(p1) + catalog.Add(p2) + + s := sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: catalog, + FileMetadata: map[source.Coordinates]source.FileMetadata{ + source.NewLocation("/a/place").Coordinates: { + Mode: 0775, + Type: stereoFile.TypeDirectory, + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/a/place/a").Coordinates: { + Mode: 0775, + Type: stereoFile.TypeRegular, + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/b").Coordinates: { + Mode: 0775, + Type: stereoFile.TypeSymLink, + LinkDestination: "/c", + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/b/place/b").Coordinates: { + Mode: 0644, + Type: stereoFile.TypeRegular, + UserID: 1, + GroupID: 2, + }, + }, + FileDigests: map[source.Coordinates][]file.Digest{ + source.NewLocation("/a/place/a").Coordinates: { + { + Algorithm: "sha256", + Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + }, + }, + source.NewLocation("/b/place/b").Coordinates: { + { + Algorithm: "sha256", + Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c", + }, + }, + }, + FileContents: map[source.Coordinates]string{ + source.NewLocation("/a/place/a").Coordinates: "the-contents", + }, + LinuxDistribution: &linux.Release{ + ID: "redhat", + Version: "7", + VersionID: "7", + IDLike: []string{ + "rhel", + }, + }, + }, + Relationships: []artifact.Relationship{ + { + From: p1, + To: p2, + Type: artifact.OwnershipByFileOverlapRelationship, + Data: map[string]string{ + "file": "path", + }, + }, + }, + Source: source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: source.ImageMetadata{ + UserInput: "user-image-input", + ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", + ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Tags: []string{ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b", + }, + Size: 38, + Layers: []source.LayerMetadata{ + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", + Size: 22, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + Size: 16, + }, + }, + RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."), + RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."), + RepoDigests: []string{}, + }, + }, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } + testutils.AssertEncoderAgainstGoldenSnapshot(t, + Format(), + s, + *updateJson, + true, + schemaVersionRedactor, + ) +} diff --git a/sbom/internal/formats/syft2/format.go b/sbom/internal/formats/syft2/format.go new file mode 100644 index 00000000..d1e33114 --- /dev/null +++ b/sbom/internal/formats/syft2/format.go @@ -0,0 +1,20 @@ +package syft2 + +import ( + "io" + + "github.com/anchore/syft/syft/sbom" +) + +const ID sbom.FormatID = "syft-2.0-json" +const JSONSchemaVersion string = "2.0.2" + +func Format() sbom.Format { + return sbom.NewFormat( + "2.0.2", + encoder, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, + ) +} diff --git a/sbom/internal/formats/syft2/model/distro.go b/sbom/internal/formats/syft2/model/distro.go new file mode 100644 index 00000000..b136f250 --- /dev/null +++ b/sbom/internal/formats/syft2/model/distro.go @@ -0,0 +1,8 @@ +package model + +// Distro provides information about a detected Linux Distro. +type Distro struct { + Name string `json:"name"` // Name of the Linux distribution + Version string `json:"version"` // Version of the Linux distribution (major or major.minor version) + IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file +} diff --git a/sbom/internal/formats/syft2/model/document.go b/sbom/internal/formats/syft2/model/document.go new file mode 100644 index 00000000..e5924722 --- /dev/null +++ b/sbom/internal/formats/syft2/model/document.go @@ -0,0 +1,27 @@ +package model + +import "github.com/anchore/syft/syft/formats/syftjson/model" + +// Document represents the syft cataloging findings as a JSON document +type Document struct { + Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog + ArtifactRelationships []model.Relationship `json:"artifactRelationships"` + Files []model.File `json:"files,omitempty"` // note: must have omitempty + Secrets []model.Secrets `json:"secrets,omitempty"` // note: must have omitempty + Source Source `json:"source"` // Source represents the original object that was cataloged + Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source + Descriptor model.Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft + Schema model.Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape +} + +// Descriptor describes what created the document as well as surrounding metadata +type Descriptor struct { + Name string `json:"name"` + Version string `json:"version"` + Configuration interface{} `json:"configuration,omitempty"` +} + +type Schema struct { + Version string `json:"version"` + URL string `json:"url"` +} diff --git a/sbom/internal/formats/syft2/model/package.go b/sbom/internal/formats/syft2/model/package.go new file mode 100644 index 00000000..4251fd47 --- /dev/null +++ b/sbom/internal/formats/syft2/model/package.go @@ -0,0 +1,123 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/anchore/syft/syft/source" + + "github.com/anchore/syft/syft/pkg" +) + +// Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling. +type Package struct { + PackageBasicData + PackageCustomData +} + +// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package. +type PackageBasicData struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Type pkg.Type `json:"type"` + FoundBy string `json:"foundBy"` + Locations []source.Coordinates `json:"locations"` + Licenses []string `json:"licenses"` + Language pkg.Language `json:"language"` + CPEs []string `json:"cpes"` + PURL string `json:"purl"` +} + +// PackageCustomData contains ambiguous values (type-wise) from pkg.Package. +type PackageCustomData struct { + MetadataType pkg.MetadataType `json:"metadataType"` + Metadata interface{} `json:"metadata"` +} + +// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling. +type packageMetadataUnpacker struct { + MetadataType pkg.MetadataType `json:"metadataType"` + Metadata json.RawMessage `json:"metadata"` +} + +func (p *packageMetadataUnpacker) String() string { + return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata)) +} + +// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types. +// nolint:funlen +func (p *Package) UnmarshalJSON(b []byte) error { + var basic PackageBasicData + if err := json.Unmarshal(b, &basic); err != nil { + return err + } + p.PackageBasicData = basic + + var unpacker packageMetadataUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + // log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err) + return err + } + + p.MetadataType = unpacker.MetadataType + + switch p.MetadataType { + case pkg.ApkMetadataType: + var payload pkg.ApkMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.RpmMetadataType: + var payload pkg.RpmMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.DpkgMetadataType: + var payload pkg.DpkgMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.JavaMetadataType: + var payload pkg.JavaMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.RustCargoPackageMetadataType: + var payload pkg.CargoPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.GemMetadataType: + var payload pkg.GemMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.KbPackageMetadataType: + var payload pkg.KbPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.PythonPackageMetadataType: + var payload pkg.PythonPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.NpmPackageJSONMetadataType: + var payload pkg.NpmPackageJSONMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + } + + return nil +} diff --git a/sbom/internal/formats/syft2/model/source.go b/sbom/internal/formats/syft2/model/source.go new file mode 100644 index 00000000..4cc2bba2 --- /dev/null +++ b/sbom/internal/formats/syft2/model/source.go @@ -0,0 +1,53 @@ +package model + +import ( + "encoding/json" + "fmt" + "strconv" + + // "github.com/anchore/syft/syft/source" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/source" +) + +// Source object represents the thing that was cataloged +type Source struct { + Type string `json:"type"` + Target interface{} `json:"target"` +} + +// sourceUnpacker is used to unmarshal Source objects +type sourceUnpacker struct { + Type string `json:"type"` + Target json.RawMessage `json:"target"` +} + +// UnmarshalJSON populates a source object from JSON bytes. +func (s *Source) UnmarshalJSON(b []byte) error { + var unpacker sourceUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + return err + } + + s.Type = unpacker.Type + + switch s.Type { + case "directory": + if target, err := strconv.Unquote(string(unpacker.Target)); err == nil { + s.Target = target + } else { + s.Target = string(unpacker.Target[:]) + } + + case "image": + var payload source.ImageMetadata + if err := json.Unmarshal(unpacker.Target, &payload); err != nil { + return err + } + s.Target = payload + + default: + return fmt.Errorf("unsupported package metadata type: %+v", s.Type) + } + + return nil +} diff --git a/sbom/internal/formats/syft2/source/image_metadata.go b/sbom/internal/formats/syft2/source/image_metadata.go new file mode 100644 index 00000000..ed2b0211 --- /dev/null +++ b/sbom/internal/formats/syft2/source/image_metadata.go @@ -0,0 +1,58 @@ +package source + +import "github.com/anchore/syft/syft/source" + +func ConvertImageMetadata(metadata source.ImageMetadata) ImageMetadata { + // TODO: (packit) Is there a cleaner way to unpack the new struct into the legacy one? + var layers []LayerMetadata + for _, l := range metadata.Layers { + layers = append(layers, LayerMetadata{ + MediaType: l.MediaType, + Digest: l.Digest, + Size: l.Size, + }) + } + + // Create RepoDigests slice this way to ensure that it's encoded + // as an empty array (not null) if empty + repoDigests := make([]string, 0) + if metadata.RepoDigests != nil { + repoDigests = metadata.RepoDigests + } + + return ImageMetadata{ + UserInput: metadata.UserInput, + ID: metadata.ID, + ManifestDigest: metadata.ManifestDigest, + MediaType: metadata.MediaType, + Tags: metadata.Tags, + Size: metadata.Size, + Layers: layers, + RawManifest: metadata.RawManifest, + RawConfig: metadata.RawConfig, + RepoDigests: repoDigests, + } +} + +// Source: https://github.com/anchore/syft/blob/dfefd2ea4e9d44187b4f861bc202970247dd34c8/syft/source/image_metadata.go +// ImageMetadata represents all static metadata that defines what a container image is. This is useful to later describe +// "what" was cataloged without needing the more complicated stereoscope Image objects or FileResolver objects. +type ImageMetadata struct { + UserInput string `json:"userInput"` + ID string `json:"imageID"` + ManifestDigest string `json:"manifestDigest"` + MediaType string `json:"mediaType"` + Tags []string `json:"tags"` + Size int64 `json:"imageSize"` + Layers []LayerMetadata `json:"layers"` + RawManifest []byte `json:"manifest"` + RawConfig []byte `json:"config"` + RepoDigests []string `json:"repoDigests"` +} + +// LayerMetadata represents all static metadata that defines what a container image layer is. +type LayerMetadata struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int64 `json:"size"` +} diff --git a/sbom/internal/formats/syft2/test-fixtures/image-simple/Dockerfile b/sbom/internal/formats/syft2/test-fixtures/image-simple/Dockerfile new file mode 100644 index 00000000..79cfa759 --- /dev/null +++ b/sbom/internal/formats/syft2/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-2.txt diff --git a/sbom/internal/formats/syft2/test-fixtures/image-simple/file-1.txt b/sbom/internal/formats/syft2/test-fixtures/image-simple/file-1.txt new file mode 100644 index 00000000..985d3408 --- /dev/null +++ b/sbom/internal/formats/syft2/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/sbom/internal/formats/syft2/test-fixtures/image-simple/file-2.txt b/sbom/internal/formats/syft2/test-fixtures/image-simple/file-2.txt new file mode 100644 index 00000000..396d08bb --- /dev/null +++ b/sbom/internal/formats/syft2/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/sbom/internal/formats/syft2/test-fixtures/snapshot/TestDirectoryEncoder.golden b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestDirectoryEncoder.golden new file mode 100644 index 00000000..94c1e848 --- /dev/null +++ b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -0,0 +1,89 @@ +{ + "artifacts": [ + { + "id": "1b1d0be59ac59d2c", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/some/path/pkg1" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "files": [ + { + "path": "/some/path/pkg1/dependencies/foo" + } + ], + "sitePackagesRootPath": "" + } + }, + { + "id": "db4abfe497c180d3", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/some/path/pkg1" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "pkg:deb/debian/package-2@2.0.1", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": null + } + } + ], + "artifactRelationships": [], + "source": { + "type": "directory", + "target": "/some/path" + }, + "distro": { + "name": "debian", + "version": "1.2.3", + "idLike": "like!" + }, + "descriptor": { + "name": "syft", + "version": "v0.42.0-bogus", + "configuration": { + "config-key": "config-value" + } + }, + "schema": { + "version": "2.0.2", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json" + } +} diff --git a/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden new file mode 100644 index 00000000..bf53456e --- /dev/null +++ b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -0,0 +1,189 @@ +{ + "artifacts": [ + { + "id": "304a5a8e5958a49d", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/a/place/a" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + ], + "purl": "a-purl-1", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "sitePackagesRootPath": "" + } + }, + { + "id": "9fd0b9f41034991d", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/b/place/b" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": [] + } + } + ], + "artifactRelationships": [ + { + "parent": "", + "child": "", + "type": "ownership-by-file-overlap", + "metadata": { + "file": "path" + } + } + ], + "files": [ + { + "id": "913b4592e2c2ebdf", + "location": { + "path": "/a/place" + }, + "metadata": { + "mode": 775, + "size": 0, + "type": "Directory", + "userID": 0, + "groupID": 0, + "mimeType": "" + } + }, + { + "id": "e7c88bd18e11b0b", + "location": { + "path": "/a/place/a" + }, + "metadata": { + "mode": 775, + "size": 0, + "type": "RegularFile", + "userID": 0, + "groupID": 0, + "mimeType": "" + }, + "contents": "the-contents", + "digests": [ + { + "algorithm": "sha256", + "value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703" + } + ] + }, + { + "id": "5c3dc6885f48b5a1", + "location": { + "path": "/b" + }, + "metadata": { + "mode": 775, + "size": 0, + "type": "SymbolicLink", + "linkDestination": "/c", + "userID": 0, + "groupID": 0, + "mimeType": "" + } + }, + { + "id": "799d2f12da0bcec4", + "location": { + "path": "/b/place/b" + }, + "metadata": { + "mode": 644, + "size": 0, + "type": "RegularFile", + "userID": 1, + "groupID": 2, + "mimeType": "" + }, + "digests": [ + { + "algorithm": "sha256", + "value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c" + } + ] + } + ], + "source": { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", + "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b" + ], + "imageSize": 38, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + "size": 16 + } + ], + "manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=", + "config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==", + "repoDigests": [] + } + }, + "distro": { + "name": "redhat", + "version": "7", + "idLike": "rhel" + }, + "descriptor": { + "name": "syft", + "version": "v0.42.0-bogus", + "configuration": { + "config-key": "config-value" + } + }, + "schema": { + "version": "2.0.2", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json" + } +} diff --git a/sbom/internal/formats/syft2/test-fixtures/snapshot/TestImageEncoder.golden b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestImageEncoder.golden new file mode 100644 index 00000000..9c0231fd --- /dev/null +++ b/sbom/internal/formats/syft2/test-fixtures/snapshot/TestImageEncoder.golden @@ -0,0 +1,110 @@ +{ + "artifacts": [ + { + "id": "66ba429119b8bec6", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/somefile-1.txt", + "layerID": "sha256:5cfa01376a90b8c5f98cebf09cbe95787159204609eb9745d0737c8d0b9906d3" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + ], + "purl": "a-purl-1", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "sitePackagesRootPath": "" + } + }, + { + "id": "958443e2d9304af4", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/somefile-2.txt", + "layerID": "sha256:fea46ffc9ebb89aaf4ceae475266d57cdd9431f6a10cec18b28731c84a601bec" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "pkg:deb/debian/package-2@2.0.1", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": null + } + } + ], + "artifactRelationships": [], + "source": { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:f09538f29e84126b5935870d2226c99561a668709065b6bdc3be9453164eb848", + "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b" + ], + "imageSize": 38, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:5cfa01376a90b8c5f98cebf09cbe95787159204609eb9745d0737c8d0b9906d3", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:fea46ffc9ebb89aaf4ceae475266d57cdd9431f6a10cec18b28731c84a601bec", + "size": 16 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjpmMDk1MzhmMjllODQxMjZiNTkzNTg3MGQyMjI2Yzk5NTYxYTY2ODcwOTA2NWI2YmRjM2JlOTQ1MzE2NGViODQ4In0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo1Y2ZhMDEzNzZhOTBiOGM1Zjk4Y2ViZjA5Y2JlOTU3ODcxNTkyMDQ2MDllYjk3NDVkMDczN2M4ZDBiOTkwNmQzIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmZlYTQ2ZmZjOWViYjg5YWFmNGNlYWU0NzUyNjZkNTdjZGQ5NDMxZjZhMTBjZWMxOGIyODczMWM4NGE2MDFiZWMifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDMtMTFUMjA6NTQ6MjAuMjU4OTM3MVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMi0wMy0xMVQyMDo1NDoyMC4yMTQxMjM2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMi0wMy0xMVQyMDo1NDoyMC4yNTg5MzcxWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NWNmYTAxMzc2YTkwYjhjNWY5OGNlYmYwOWNiZTk1Nzg3MTU5MjA0NjA5ZWI5NzQ1ZDA3MzdjOGQwYjk5MDZkMyIsInNoYTI1NjpmZWE0NmZmYzllYmI4OWFhZjRjZWFlNDc1MjY2ZDU3Y2RkOTQzMWY2YTEwY2VjMThiMjg3MzFjODRhNjAxYmVjIl19fQ==", + "repoDigests": [] + } + }, + "distro": { + "name": "debian", + "version": "1.2.3", + "idLike": "like!" + }, + "descriptor": { + "name": "syft", + "version": "v0.42.0-bogus", + "configuration": { + "config-key": "config-value" + } + }, + "schema": { + "version": "2.0.2", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.2.json" + } +} diff --git a/sbom/internal/formats/syft2/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/sbom/internal/formats/syft2/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden new file mode 100644 index 00000000..fc8bec3c Binary files /dev/null and b/sbom/internal/formats/syft2/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/sbom/internal/formats/syft2/to_format_model.go b/sbom/internal/formats/syft2/to_format_model.go new file mode 100644 index 00000000..f4fdba02 --- /dev/null +++ b/sbom/internal/formats/syft2/to_format_model.go @@ -0,0 +1,258 @@ +package syft2 + +import ( + "fmt" + "sort" + "strconv" + + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/formats/syftjson/model" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + internalmodel "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/model" + syft2source "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/source" +) + +// NOTE: Adaptions have been added to functions in this file to translate from latest +// syft package representations to legacy JSON schema + +func ToFormatModel(s sbom.SBOM) internalmodel.Document { + src, err := toSourceModel(s.Source) + if err != nil { //nolint:staticcheck + // log.Warnf("unable to create syft-json source object: %+v", err) + } + + return internalmodel.Document{ + Artifacts: toPackageModels(s.Artifacts.Packages), + ArtifactRelationships: toRelationshipModel(s.Relationships), + Files: toFile(s), + Secrets: toSecrets(s.Artifacts.Secrets), + Source: src, + Distro: toDistroModel(s.Artifacts.LinuxDistribution), + Descriptor: toDescriptor(s.Descriptor), + Schema: model.Schema{ + Version: JSONSchemaVersion, + URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", JSONSchemaVersion), + }, + } +} + +func toDescriptor(d sbom.Descriptor) model.Descriptor { + return model.Descriptor{ + Name: d.Name, + Version: d.Version, + Configuration: d.Configuration, + } +} + +func toSecrets(data map[source.Coordinates][]file.SearchResult) []model.Secrets { + results := make([]model.Secrets, 0) + for coordinates, secrets := range data { + results = append(results, model.Secrets{ + Location: coordinates, + Secrets: secrets, + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + return results[i].Location.RealPath < results[j].Location.RealPath + }) + return results +} + +func toFile(s sbom.SBOM) []model.File { + results := make([]model.File, 0) + artifacts := s.Artifacts + + for _, coordinates := range s.AllCoordinates() { + var metadata *source.FileMetadata + if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists { + metadata = &metadataForLocation + } + + var digests []file.Digest + if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists { + digests = digestsForLocation + } + + var contents string + if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists { + contents = contentsForLocation + } + + results = append(results, model.File{ + ID: string(coordinates.ID()), + Location: coordinates, + Metadata: toFileMetadataEntry(coordinates, metadata), + Digests: digests, + Contents: contents, + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + return results[i].Location.RealPath < results[j].Location.RealPath + }) + return results +} + +func toFileMetadataEntry(coordinates source.Coordinates, metadata *source.FileMetadata) *model.FileMetadataEntry { + if metadata == nil { + return nil + } + + mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode)) + if err != nil { + // log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err + mode = 0 + } + + return &model.FileMetadataEntry{ + Mode: mode, + Type: toFileType(metadata.Type), + LinkDestination: metadata.LinkDestination, + UserID: metadata.UserID, + GroupID: metadata.GroupID, + MIMEType: metadata.MIMEType, + } +} + +func toFileType(ty stereoscopeFile.Type) string { + switch ty { + case stereoscopeFile.TypeSymLink: + return "SymbolicLink" + case stereoscopeFile.TypeHardLink: + return "HardLink" + case stereoscopeFile.TypeDirectory: + return "Directory" + case stereoscopeFile.TypeSocket: + return "Socket" + case stereoscopeFile.TypeBlockDevice: + return "BlockDevice" + case stereoscopeFile.TypeCharacterDevice: + return "CharacterDevice" + case stereoscopeFile.TypeFIFO: + return "FIFONode" + case stereoscopeFile.TypeRegular: + return "RegularFile" + case stereoscopeFile.TypeIrregular: + return "IrregularFile" + default: + return "Unknown" + } +} + +func toPackageModels(catalog *pkg.Collection) []internalmodel.Package { + artifacts := make([]internalmodel.Package, 0) + if catalog == nil { + return artifacts + } + for _, p := range catalog.Sorted() { + artifacts = append(artifacts, toPackageModel(p)) + } + return artifacts +} + +// toPackageModel crates a new Package from the given pkg.Package. +func toPackageModel(p pkg.Package) internalmodel.Package { + var cpes = make([]string, len(p.CPEs)) + for i, c := range p.CPEs { + cpes[i] = cpe.String(c) + } + + var licenses = make([]string, 0) + if p.Licenses != nil { + licenses = p.Licenses + } + + locations := p.Locations.ToSlice() + var coordinates = make([]source.Coordinates, len(locations)) + for i, l := range locations { + coordinates[i] = l.Coordinates + } + + return internalmodel.Package{ + PackageBasicData: internalmodel.PackageBasicData{ + ID: string(p.ID()), + Name: p.Name, + Version: p.Version, + Type: p.Type, + FoundBy: p.FoundBy, + Locations: coordinates, + Licenses: licenses, + Language: p.Language, + CPEs: cpes, + PURL: p.PURL, + }, + PackageCustomData: internalmodel.PackageCustomData{ + MetadataType: p.MetadataType, + Metadata: p.Metadata, + }, + } +} + +func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship { + result := make([]model.Relationship, len(relationships)) + for i, r := range relationships { + result[i] = model.Relationship{ + Parent: string(r.From.ID()), + Child: string(r.To.ID()), + Type: string(r.Type), + Metadata: r.Data, + } + } + return result +} + +// toSourceModel creates a new source object to be represented into JSON. +// NOTE: THIS FUNCTION is NOT identical to the one that appears in the original version of this file. +// It converts ImageMetadata into a struct that matches the old Syft schema. +func toSourceModel(src source.Metadata) (internalmodel.Source, error) { + switch src.Scheme { + case source.ImageScheme: + return internalmodel.Source{ + Type: "image", + // convert src.ImageMetadata into a struct with the old syft metadata fields + Target: syft2source.ConvertImageMetadata(src.ImageMetadata), + }, nil + case source.DirectoryScheme: + return internalmodel.Source{ + Type: "directory", + Target: src.Path, + }, nil + case source.FileScheme: + return internalmodel.Source{ + Type: "file", + Target: src.Path, + }, nil + default: + return internalmodel.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + } +} + +// // toDistroModel creates a struct with the Linux distribution to be represented in JSON. +// NOTE: THIS FUNCTION is NOT identical to the one that appears in the original version of this file. +// It now converts from a linux.Release to a model.Distro to maintain backward compatibility. +func toDistroModel(d *linux.Release) internalmodel.Distro { + if d == nil { + return internalmodel.Distro{} + } + + idLike := d.ID + if len(d.IDLike) > 0 { + // TODO: (packit) Is picking the 1st from this list the right thing to do? + idLike = d.IDLike[0] + } + + return internalmodel.Distro{ + Name: d.ID, + Version: d.Version, + IDLike: idLike, + } +} diff --git a/sbom/internal/formats/syft2/to_format_model_test.go b/sbom/internal/formats/syft2/to_format_model_test.go new file mode 100644 index 00000000..f3e0b824 --- /dev/null +++ b/sbom/internal/formats/syft2/to_format_model_test.go @@ -0,0 +1,88 @@ +package syft2 + +import ( + "testing" + + "github.com/scylladb/go-set/strset" + + "github.com/anchore/syft/syft/source" + syft2source "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/source" + + // "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_toSourceModel(t *testing.T) { + allSchemes := strset.New() + for _, s := range source.AllSchemes { + allSchemes.Add(string(s)) + } + testedSchemes := strset.New() + + tests := []struct { + name string + src source.Metadata + expected model.Source + }{ + { + name: "directory", + src: source.Metadata{ + Scheme: source.DirectoryScheme, + Path: "some/path", + }, + expected: model.Source{ + Type: "directory", + Target: "some/path", + }, + }, + { + name: "file", + src: source.Metadata{ + Scheme: source.FileScheme, + Path: "some/path", + }, + expected: model.Source{ + Type: "file", + Target: "some/path", + }, + }, + { + name: "image", + src: source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: source.ImageMetadata{ + UserInput: "user-input", + ID: "id...", + ManifestDigest: "digest...", + MediaType: "type...", + }, + }, + expected: model.Source{ + Type: "image", + Target: syft2source.ImageMetadata{ + UserInput: "user-input", + ID: "id...", + ManifestDigest: "digest...", + MediaType: "type...", + RepoDigests: make([]string, 0), + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // track each scheme tested (passed or not) + testedSchemes.Add(string(test.src.Scheme)) + + // assert the model transformation is correct + actual, err := toSourceModel(test.src) + require.NoError(t, err) + assert.Equal(t, test.expected, actual) + }) + } + + // assert all possible schemes were under test + assert.ElementsMatch(t, allSchemes.List(), testedSchemes.List(), "not all source.Schemes are under test") +} diff --git a/sbom/internal/formats/syft301/README.md b/sbom/internal/formats/syft301/README.md new file mode 100644 index 00000000..65cc6a86 --- /dev/null +++ b/sbom/internal/formats/syft301/README.md @@ -0,0 +1,11 @@ +# Source +The contents of this directory is largely based on anchore/syft's internal +`syftjson` package. The version copied is from +[v0.41.1](https://github.com/anchore/syft/blob/07d3c9af52f241613971ccadd18c8f8ab67abc4e) +of Syft that supports Syft JSON Schema 3.0.1. + +The implementations of `decoder` and `validator` have been omitted for +simplicity, since they are not required for buildpacks' SBOM generation. + +Aspects of the model have been copied over due to slight deviations against the +latest Syft JSON model. diff --git a/sbom/internal/formats/syft301/encoder.go b/sbom/internal/formats/syft301/encoder.go new file mode 100644 index 00000000..eb862bd3 --- /dev/null +++ b/sbom/internal/formats/syft301/encoder.go @@ -0,0 +1,19 @@ +package syft301 + +import ( + "encoding/json" + "io" + + "github.com/anchore/syft/syft/sbom" +) + +func encoder(output io.Writer, s sbom.SBOM) error { + doc := ToFormatModel(s) + + enc := json.NewEncoder(output) + // prevent > and < from being escaped in the payload + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + + return enc.Encode(&doc) +} diff --git a/sbom/internal/formats/syft301/encoder_test.go b/sbom/internal/formats/syft301/encoder_test.go new file mode 100644 index 00000000..f9e60de5 --- /dev/null +++ b/sbom/internal/formats/syft301/encoder_test.go @@ -0,0 +1,215 @@ +package syft301 + +import ( + "flag" + "regexp" + "testing" + + stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/common/testutils" +) + +var updateJson = flag.Bool("update-json", false, "update the *.golden files for json encoders") + +func TestDirectoryEncoder(t *testing.T) { + testutils.AssertEncoderAgainstGoldenSnapshot(t, + Format(), + testutils.DirectoryInput(t), + *updateJson, + true, + schemaVersionRedactor, + ) +} +func TestImageEncoder(t *testing.T) { + testImage := "image-simple" + testutils.AssertEncoderAgainstGoldenImageSnapshot(t, + Format(), + testutils.ImageInput(t, testImage, testutils.FromSnapshot()), + testImage, + *updateJson, + true, + schemaVersionRedactor, + ) +} +func schemaVersionRedactor(s []byte) []byte { + pattern := regexp.MustCompile(`,?\s*"schema":\s*\{[^}]*}`) + out := pattern.ReplaceAll(s, []byte("")) + return out +} + +func TestEncodeFullJSONDocument(t *testing.T) { + catalog := pkg.NewCollection() + + p1 := pkg.Package{ + Name: "package-1", + Version: "1.0.1", + Locations: source.NewLocationSet( + source.NewLocationFromCoordinates( + source.Coordinates{ + RealPath: "/a/place/a", + }, + ), + ), + Type: pkg.PythonPkg, + FoundBy: "the-cataloger-1", + Language: pkg.Python, + MetadataType: pkg.PythonPackageMetadataType, + Licenses: []string{"MIT"}, + Metadata: pkg.PythonPackageMetadata{ + Name: "package-1", + Version: "1.0.1", + Files: []pkg.PythonFileRecord{}, + }, + PURL: "a-purl-1", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"), + }, + } + + p2 := pkg.Package{ + Name: "package-2", + Version: "2.0.1", + Locations: source.NewLocationSet( + source.NewLocationFromCoordinates( + source.Coordinates{ + RealPath: "/b/place/b", + }, + ), + ), + Type: pkg.DebPkg, + FoundBy: "the-cataloger-2", + MetadataType: pkg.DpkgMetadataType, + Metadata: pkg.DpkgMetadata{ + Package: "package-2", + Version: "2.0.1", + Files: []pkg.DpkgFileRecord{}, + }, + PURL: "a-purl-2", + CPEs: []cpe.CPE{ + cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"), + }, + } + + catalog.Add(p1) + catalog.Add(p2) + + s := sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: catalog, + FileMetadata: map[source.Coordinates]source.FileMetadata{ + source.NewLocation("/a/place").LocationData.Coordinates: { + Mode: 0775, + Type: stereoFile.TypeDirectory, + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/a/place/a").LocationData.Coordinates: { + Mode: 0775, + Type: stereoFile.TypeRegular, + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/b").LocationData.Coordinates: { + Mode: 0775, + Type: stereoFile.TypeSymLink, + LinkDestination: "/c", + UserID: 0, + GroupID: 0, + }, + source.NewLocation("/b/place/b").LocationData.Coordinates: { + Mode: 0644, + Type: stereoFile.TypeRegular, + UserID: 1, + GroupID: 2, + }, + }, + FileDigests: map[source.Coordinates][]file.Digest{ + source.NewLocation("/a/place/a").LocationData.Coordinates: { + { + Algorithm: "sha256", + Value: "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + }, + }, + source.NewLocation("/b/place/b").LocationData.Coordinates: { + { + Algorithm: "sha256", + Value: "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c", + }, + }, + }, + FileContents: map[source.Coordinates]string{ + source.NewLocation("/a/place/a").LocationData.Coordinates: "the-contents", + }, + LinuxDistribution: &linux.Release{ + ID: "redhat", + Version: "7", + VersionID: "7", + IDLike: []string{ + "rhel", + }, + }, + }, + Relationships: []artifact.Relationship{ + { + From: p1, + To: p2, + Type: artifact.OwnershipByFileOverlapRelationship, + Data: map[string]string{ + "file": "path", + }, + }, + }, + Source: source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: source.ImageMetadata{ + UserInput: "user-image-input", + ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", + ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Tags: []string{ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b", + }, + Size: 38, + Layers: []source.LayerMetadata{ + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", + Size: 22, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + Size: 16, + }, + }, + RawManifest: []byte("eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJh..."), + RawConfig: []byte("eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZp..."), + RepoDigests: []string{}, + }, + }, + Descriptor: sbom.Descriptor{ + Name: "syft", + Version: "v0.42.0-bogus", + // the application configuration should be persisted here, however, we do not want to import + // the application configuration in this package (it's reserved only for ingestion by the cmd package) + Configuration: map[string]string{ + "config-key": "config-value", + }, + }, + } + + testutils.AssertEncoderAgainstGoldenSnapshot(t, + Format(), + s, + *updateJson, + true, + schemaVersionRedactor, + ) +} diff --git a/sbom/internal/formats/syft301/format.go b/sbom/internal/formats/syft301/format.go new file mode 100644 index 00000000..6b3ef02b --- /dev/null +++ b/sbom/internal/formats/syft301/format.go @@ -0,0 +1,20 @@ +package syft301 + +import ( + "io" + + "github.com/anchore/syft/syft/sbom" +) + +const ID sbom.FormatID = "syft-3.0.1-json" +const JSONSchemaVersion string = "3.0.1" + +func Format() sbom.Format { + return sbom.NewFormat( + "3.0.1", + encoder, + func(input io.Reader) (*sbom.SBOM, error) { return nil, nil }, + func(input io.Reader) error { return nil }, + ID, + ) +} diff --git a/sbom/internal/formats/syft301/model/document.go b/sbom/internal/formats/syft301/model/document.go new file mode 100644 index 00000000..0175cd38 --- /dev/null +++ b/sbom/internal/formats/syft301/model/document.go @@ -0,0 +1,27 @@ +package model + +import "github.com/anchore/syft/syft/formats/syftjson/model" + +// Document represents the syft cataloging findings as a JSON document +type Document struct { + Artifacts []model.Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog + ArtifactRelationships []model.Relationship `json:"artifactRelationships"` + Files []model.File `json:"files,omitempty"` // note: must have omitempty + Secrets []model.Secrets `json:"secrets,omitempty"` // note: must have omitempty + Source Source `json:"source"` // Source represents the original object that was cataloged + Distro model.LinuxRelease `json:"distro"` // Distro represents the Linux distribution that was detected from the source + Descriptor model.Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft + Schema model.Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape +} + +// Descriptor describes what created the document as well as surrounding metadata +type Descriptor struct { + Name string `json:"name"` + Version string `json:"version"` + Configuration interface{} `json:"configuration,omitempty"` +} + +type Schema struct { + Version string `json:"version"` + URL string `json:"url"` +} diff --git a/sbom/internal/formats/syft301/model/source.go b/sbom/internal/formats/syft301/model/source.go new file mode 100644 index 00000000..16940522 --- /dev/null +++ b/sbom/internal/formats/syft301/model/source.go @@ -0,0 +1,52 @@ +package model + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/anchore/syft/syft/source" +) + +// Source object represents the thing that was cataloged +type Source struct { + Type string `json:"type"` + Target interface{} `json:"target"` +} + +// sourceUnpacker is used to unmarshal Source objects +type sourceUnpacker struct { + Type string `json:"type"` + Target json.RawMessage `json:"target"` +} + +// UnmarshalJSON populates a source object from JSON bytes. +func (s *Source) UnmarshalJSON(b []byte) error { + var unpacker sourceUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + return err + } + + s.Type = unpacker.Type + + switch s.Type { + case "directory", "file": + if target, err := strconv.Unquote(string(unpacker.Target)); err == nil { + s.Target = target + } else { + s.Target = string(unpacker.Target[:]) + } + + case "image": + var payload source.ImageMetadata + if err := json.Unmarshal(unpacker.Target, &payload); err != nil { + return err + } + s.Target = payload + + default: + return fmt.Errorf("unsupported package metadata type: %+v", s.Type) + } + + return nil +} diff --git a/sbom/internal/formats/syft301/test-fixtures/image-simple/Dockerfile b/sbom/internal/formats/syft301/test-fixtures/image-simple/Dockerfile new file mode 100644 index 00000000..79cfa759 --- /dev/null +++ b/sbom/internal/formats/syft301/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-2.txt diff --git a/sbom/internal/formats/syft301/test-fixtures/image-simple/file-1.txt b/sbom/internal/formats/syft301/test-fixtures/image-simple/file-1.txt new file mode 100644 index 00000000..985d3408 --- /dev/null +++ b/sbom/internal/formats/syft301/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/sbom/internal/formats/syft301/test-fixtures/image-simple/file-2.txt b/sbom/internal/formats/syft301/test-fixtures/image-simple/file-2.txt new file mode 100644 index 00000000..396d08bb --- /dev/null +++ b/sbom/internal/formats/syft301/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/sbom/internal/formats/syft301/test-fixtures/snapshot/TestDirectoryEncoder.golden b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestDirectoryEncoder.golden new file mode 100644 index 00000000..7926e72c --- /dev/null +++ b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestDirectoryEncoder.golden @@ -0,0 +1,94 @@ +{ + "artifacts": [ + { + "id": "1b1d0be59ac59d2c", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/some/path/pkg1" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "files": [ + { + "path": "/some/path/pkg1/dependencies/foo" + } + ], + "sitePackagesRootPath": "" + } + }, + { + "id": "db4abfe497c180d3", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/some/path/pkg1" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "pkg:deb/debian/package-2@2.0.1", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": null + } + } + ], + "artifactRelationships": [], + "source": { + "type": "directory", + "target": "/some/path" + }, + "distro": { + "prettyName": "debian", + "name": "debian", + "id": "debian", + "idLike": [ + "like!" + ], + "version": "1.2.3", + "versionID": "1.2.3" + }, + "descriptor": { + "name": "syft", + "version": "v0.42.0-bogus", + "configuration": { + "config-key": "config-value" + } + }, + "schema": { + "version": "3.0.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.0.1.json" + } +} diff --git a/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden new file mode 100644 index 00000000..8f993d71 --- /dev/null +++ b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -0,0 +1,194 @@ +{ + "artifacts": [ + { + "id": "304a5a8e5958a49d", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/a/place/a" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + ], + "purl": "a-purl-1", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "sitePackagesRootPath": "" + } + }, + { + "id": "9fd0b9f41034991d", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/b/place/b" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "a-purl-2", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": [] + } + } + ], + "artifactRelationships": [ + { + "parent": "", + "child": "", + "type": "ownership-by-file-overlap", + "metadata": { + "file": "path" + } + } + ], + "files": [ + { + "id": "913b4592e2c2ebdf", + "location": { + "path": "/a/place" + }, + "metadata": { + "mode": 775, + "size": 0, + "type": "Directory", + "userID": 0, + "groupID": 0, + "mimeType": "" + } + }, + { + "id": "e7c88bd18e11b0b", + "location": { + "path": "/a/place/a" + }, + "metadata": { + "mode": 775, + "size": 0, + "type": "RegularFile", + "userID": 0, + "groupID": 0, + "mimeType": "" + }, + "contents": "the-contents", + "digests": [ + { + "algorithm": "sha256", + "value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703" + } + ] + }, + { + "id": "5c3dc6885f48b5a1", + "location": { + "path": "/b" + }, + "metadata": { + "mode": 775, + "size": 0, + "type": "SymbolicLink", + "linkDestination": "/c", + "userID": 0, + "groupID": 0, + "mimeType": "" + } + }, + { + "id": "799d2f12da0bcec4", + "location": { + "path": "/b/place/b" + }, + "metadata": { + "mode": 644, + "size": 0, + "type": "RegularFile", + "userID": 1, + "groupID": 2, + "mimeType": "" + }, + "digests": [ + { + "algorithm": "sha256", + "value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c" + } + ] + } + ], + "source": { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0", + "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b" + ], + "imageSize": 38, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703", + "size": 16 + } + ], + "manifest": "ZXlKelkyaGxiV0ZXWlhKemFXOXVJam95TENKdFpXUnBZVlI1Y0dVaU9pSmguLi4=", + "config": "ZXlKaGNtTm9hWFJsWTNSMWNtVWlPaUpoYldRMk5DSXNJbU52Ym1acC4uLg==", + "repoDigests": [], + "architecture": "", + "os": "" + } + }, + "distro": { + "id": "redhat", + "idLike": [ + "rhel" + ], + "version": "7", + "versionID": "7" + }, + "descriptor": { + "name": "syft", + "version": "v0.42.0-bogus", + "configuration": { + "config-key": "config-value" + } + }, + "schema": { + "version": "3.0.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.0.1.json" + } +} diff --git a/sbom/internal/formats/syft301/test-fixtures/snapshot/TestImageEncoder.golden b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestImageEncoder.golden new file mode 100644 index 00000000..dbcc9fb1 --- /dev/null +++ b/sbom/internal/formats/syft301/test-fixtures/snapshot/TestImageEncoder.golden @@ -0,0 +1,117 @@ +{ + "artifacts": [ + { + "id": "66ba429119b8bec6", + "name": "package-1", + "version": "1.0.1", + "type": "python", + "foundBy": "the-cataloger-1", + "locations": [ + { + "path": "/somefile-1.txt", + "layerID": "sha256:5cfa01376a90b8c5f98cebf09cbe95787159204609eb9745d0737c8d0b9906d3" + } + ], + "licenses": [ + "MIT" + ], + "language": "python", + "cpes": [ + "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*" + ], + "purl": "a-purl-1", + "metadataType": "PythonPackageMetadata", + "metadata": { + "name": "package-1", + "version": "1.0.1", + "license": "", + "author": "", + "authorEmail": "", + "platform": "", + "sitePackagesRootPath": "" + } + }, + { + "id": "958443e2d9304af4", + "name": "package-2", + "version": "2.0.1", + "type": "deb", + "foundBy": "the-cataloger-2", + "locations": [ + { + "path": "/somefile-2.txt", + "layerID": "sha256:fea46ffc9ebb89aaf4ceae475266d57cdd9431f6a10cec18b28731c84a601bec" + } + ], + "licenses": [], + "language": "", + "cpes": [ + "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*" + ], + "purl": "pkg:deb/debian/package-2@2.0.1", + "metadataType": "DpkgMetadata", + "metadata": { + "package": "package-2", + "source": "", + "version": "2.0.1", + "sourceVersion": "", + "architecture": "", + "maintainer": "", + "installedSize": 0, + "files": null + } + } + ], + "artifactRelationships": [], + "source": { + "type": "image", + "target": { + "userInput": "user-image-input", + "imageID": "sha256:f09538f29e84126b5935870d2226c99561a668709065b6bdc3be9453164eb848", + "manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "tags": [ + "stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b" + ], + "imageSize": 38, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:5cfa01376a90b8c5f98cebf09cbe95787159204609eb9745d0737c8d0b9906d3", + "size": 22 + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "sha256:fea46ffc9ebb89aaf4ceae475266d57cdd9431f6a10cec18b28731c84a601bec", + "size": 16 + } + ], + "manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjpmMDk1MzhmMjllODQxMjZiNTkzNTg3MGQyMjI2Yzk5NTYxYTY2ODcwOTA2NWI2YmRjM2JlOTQ1MzE2NGViODQ4In0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1Njo1Y2ZhMDEzNzZhOTBiOGM1Zjk4Y2ViZjA5Y2JlOTU3ODcxNTkyMDQ2MDllYjk3NDVkMDczN2M4ZDBiOTkwNmQzIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OmZlYTQ2ZmZjOWViYjg5YWFmNGNlYWU0NzUyNjZkNTdjZGQ5NDMxZjZhMTBjZWMxOGIyODczMWM4NGE2MDFiZWMifV19", + "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjItMDMtMTFUMjA6NTQ6MjAuMjU4OTM3MVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMi0wMy0xMVQyMDo1NDoyMC4yMTQxMjM2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMi0wMy0xMVQyMDo1NDoyMC4yNTg5MzcxWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6NWNmYTAxMzc2YTkwYjhjNWY5OGNlYmYwOWNiZTk1Nzg3MTU5MjA0NjA5ZWI5NzQ1ZDA3MzdjOGQwYjk5MDZkMyIsInNoYTI1NjpmZWE0NmZmYzllYmI4OWFhZjRjZWFlNDc1MjY2ZDU3Y2RkOTQzMWY2YTEwY2VjMThiMjg3MzFjODRhNjAxYmVjIl19fQ==", + "repoDigests": [], + "architecture": "", + "os": "" + } + }, + "distro": { + "prettyName": "debian", + "name": "debian", + "id": "debian", + "idLike": [ + "like!" + ], + "version": "1.2.3", + "versionID": "1.2.3" + }, + "descriptor": { + "name": "syft", + "version": "v0.42.0-bogus", + "configuration": { + "config-key": "config-value" + } + }, + "schema": { + "version": "3.0.1", + "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.0.1.json" + } +} diff --git a/sbom/internal/formats/syft301/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/sbom/internal/formats/syft301/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden new file mode 100644 index 00000000..fc8bec3c Binary files /dev/null and b/sbom/internal/formats/syft301/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/sbom/internal/formats/syft301/to_format_model.go b/sbom/internal/formats/syft301/to_format_model.go new file mode 100644 index 00000000..b9be1c0e --- /dev/null +++ b/sbom/internal/formats/syft301/to_format_model.go @@ -0,0 +1,257 @@ +package syft301 + +import ( + "fmt" + "sort" + "strconv" + + stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/formats/syftjson/model" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + internalmodel "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft301/model" +) + +// ToFormatModel transforms the sbom import a format-specific model. +// note: this is needed for anchore import functionality +// We must maintain our own copy in order to pass in the JSON Schema Version that we need +// TODO: remove this if Syft introduces a way to pass that version in +func ToFormatModel(s sbom.SBOM) internalmodel.Document { + src, err := toSourceModel(s.Source) + if err != nil { //nolint:staticcheck + // log.Warnf("unable to create syft-json source object: %+v", err) + } + + return internalmodel.Document{ + Artifacts: toPackageModels(s.Artifacts.Packages), + ArtifactRelationships: toRelationshipModel(s.Relationships), + Files: toFile(s), + Secrets: toSecrets(s.Artifacts.Secrets), + Source: src, + Distro: toLinuxReleaser(s.Artifacts.LinuxDistribution), + Descriptor: toDescriptor(s.Descriptor), + Schema: model.Schema{ + Version: JSONSchemaVersion, + URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", JSONSchemaVersion), + }, + } +} + +func toLinuxReleaser(d *linux.Release) model.LinuxRelease { + if d == nil { + return model.LinuxRelease{} + } + return model.LinuxRelease{ + PrettyName: d.PrettyName, + Name: d.Name, + ID: d.ID, + IDLike: d.IDLike, + Version: d.Version, + VersionID: d.VersionID, + Variant: d.Variant, + VariantID: d.VariantID, + HomeURL: d.HomeURL, + SupportURL: d.SupportURL, + BugReportURL: d.BugReportURL, + PrivacyPolicyURL: d.PrivacyPolicyURL, + CPEName: d.CPEName, + } +} + +func toDescriptor(d sbom.Descriptor) model.Descriptor { + return model.Descriptor{ + Name: d.Name, + Version: d.Version, + Configuration: d.Configuration, + } +} + +func toSecrets(data map[source.Coordinates][]file.SearchResult) []model.Secrets { + results := make([]model.Secrets, 0) + for coordinates, secrets := range data { + results = append(results, model.Secrets{ + Location: coordinates, + Secrets: secrets, + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + return results[i].Location.RealPath < results[j].Location.RealPath + }) + return results +} + +func toFile(s sbom.SBOM) []model.File { + results := make([]model.File, 0) + artifacts := s.Artifacts + + for _, coordinates := range s.AllCoordinates() { + var metadata *source.FileMetadata + if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists { + metadata = &metadataForLocation + } + + var digests []file.Digest + if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists { + digests = digestsForLocation + } + + var contents string + if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists { + contents = contentsForLocation + } + + results = append(results, model.File{ + ID: string(coordinates.ID()), + Location: coordinates, + Metadata: toFileMetadataEntry(metadata), + Digests: digests, + Contents: contents, + }) + } + + // sort by real path then virtual path to ensure the result is stable across multiple runs + sort.SliceStable(results, func(i, j int) bool { + return results[i].Location.RealPath < results[j].Location.RealPath + }) + return results +} + +func toFileMetadataEntry(metadata *source.FileMetadata) *model.FileMetadataEntry { + if metadata == nil { + return nil + } + + mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode)) + if err != nil { + // log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err) + mode = 0 + } + + return &model.FileMetadataEntry{ + Mode: mode, + Type: toFileType(metadata.Type), + LinkDestination: metadata.LinkDestination, + UserID: metadata.UserID, + GroupID: metadata.GroupID, + MIMEType: metadata.MIMEType, + } +} + +func toFileType(ty stereoscopeFile.Type) string { + switch ty { + case stereoscopeFile.TypeSymLink: + return "SymbolicLink" + case stereoscopeFile.TypeHardLink: + return "HardLink" + case stereoscopeFile.TypeDirectory: + return "Directory" + case stereoscopeFile.TypeSocket: + return "Socket" + case stereoscopeFile.TypeBlockDevice: + return "BlockDevice" + case stereoscopeFile.TypeCharacterDevice: + return "CharacterDevice" + case stereoscopeFile.TypeFIFO: + return "FIFONode" + case stereoscopeFile.TypeRegular: + return "RegularFile" + case stereoscopeFile.TypeIrregular: + return "IrregularFile" + default: + return "Unknown" + } +} + +func toPackageModels(catalog *pkg.Collection) []model.Package { + artifacts := make([]model.Package, 0) + if catalog == nil { + return artifacts + } + for _, p := range catalog.Sorted() { + artifacts = append(artifacts, toPackageModel(p)) + } + return artifacts +} + +// toPackageModel crates a new Package from the given pkg.Package. +func toPackageModel(p pkg.Package) model.Package { + var cpes = make([]string, len(p.CPEs)) + for i, c := range p.CPEs { + cpes[i] = cpe.String(c) + } + + var licenses = make([]string, 0) + if p.Licenses != nil { + licenses = p.Licenses + } + + return model.Package{ + PackageBasicData: model.PackageBasicData{ + ID: string(p.ID()), + Name: p.Name, + Version: p.Version, + Type: p.Type, + FoundBy: p.FoundBy, + Locations: p.Locations.ToSlice(), + Licenses: licenses, + Language: p.Language, + CPEs: cpes, + PURL: p.PURL, + }, + PackageCustomData: model.PackageCustomData{ + MetadataType: p.MetadataType, + Metadata: p.Metadata, + }, + } +} + +func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship { + result := make([]model.Relationship, len(relationships)) + for i, r := range relationships { + result[i] = model.Relationship{ + Parent: string(r.From.ID()), + Child: string(r.To.ID()), + Type: string(r.Type), + Metadata: r.Data, + } + } + return result +} + +// toSourceModel creates a new source object to be represented into JSON. +func toSourceModel(src source.Metadata) (internalmodel.Source, error) { + switch src.Scheme { + case source.ImageScheme: + metadata := src.ImageMetadata + // ensure that empty collections are not shown as null + if metadata.RepoDigests == nil { + metadata.RepoDigests = []string{} + } + if metadata.Tags == nil { + metadata.Tags = []string{} + } + return internalmodel.Source{ + Type: "image", + Target: metadata, + }, nil + case source.DirectoryScheme: + return internalmodel.Source{ + Type: "directory", + Target: src.Path, + }, nil + case source.FileScheme: + return internalmodel.Source{ + Type: "file", + Target: src.Path, + }, nil + default: + return internalmodel.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + } +} diff --git a/sbom/internal/formats/syft301/to_format_model_test.go b/sbom/internal/formats/syft301/to_format_model_test.go new file mode 100644 index 00000000..b6ed8632 --- /dev/null +++ b/sbom/internal/formats/syft301/to_format_model_test.go @@ -0,0 +1,86 @@ +package syft301 + +import ( + "testing" + + "github.com/scylladb/go-set/strset" + + "github.com/anchore/syft/syft/source" + internalmodel "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft301/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_toSourceModel(t *testing.T) { + allSchemes := strset.New() + for _, s := range source.AllSchemes { + allSchemes.Add(string(s)) + } + testedSchemes := strset.New() + + tests := []struct { + name string + src source.Metadata + expected internalmodel.Source + }{ + { + name: "directory", + src: source.Metadata{ + Scheme: source.DirectoryScheme, + Path: "some/path", + }, + expected: internalmodel.Source{ + Type: "directory", + Target: "some/path", + }, + }, + { + name: "file", + src: source.Metadata{ + Scheme: source.FileScheme, + Path: "some/path", + }, + expected: internalmodel.Source{ + Type: "file", + Target: "some/path", + }, + }, + { + name: "image", + src: source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: source.ImageMetadata{ + UserInput: "user-input", + ID: "id...", + ManifestDigest: "digest...", + MediaType: "type...", + }, + }, + expected: internalmodel.Source{ + Type: "image", + Target: source.ImageMetadata{ + UserInput: "user-input", + ID: "id...", + ManifestDigest: "digest...", + MediaType: "type...", + RepoDigests: []string{}, + Tags: []string{}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // track each scheme tested (passed or not) + testedSchemes.Add(string(test.src.Scheme)) + + // assert the model transformation is correct + actual, err := toSourceModel(test.src) + require.NoError(t, err) + assert.Equal(t, test.expected, actual) + }) + } + + // assert all possible schemes were under test + assert.ElementsMatch(t, allSchemes.List(), testedSchemes.List(), "not all source.Schemes are under test") +} diff --git a/sbom/internal/spdxlicense/README.md b/sbom/internal/spdxlicense/README.md new file mode 100644 index 00000000..ca7549f5 --- /dev/null +++ b/sbom/internal/spdxlicense/README.md @@ -0,0 +1,5 @@ +# Source +The contents of this directory has been copied from anchore/syft's internal +`spdxlicense` package. The version copied is current as of [syft +0.42.3](https://github.com/anchore/syft/blob/cc2c0e57a0d02a1719b4e34d0793f09e9699c8b0/internal/spdxlicense/). + diff --git a/sbom/internal/spdxlicense/generate/generate_license_list.go b/sbom/internal/spdxlicense/generate/generate_license_list.go new file mode 100644 index 00000000..c569a5fe --- /dev/null +++ b/sbom/internal/spdxlicense/generate/generate_license_list.go @@ -0,0 +1,190 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "regexp" + "sort" + "strings" + "text/template" + "time" + + "github.com/scylladb/go-set/strset" +) + +// This program generates license_list.go. +const ( + source = "license_list.go" + url = "https://spdx.org/licenses/licenses.json" +) + +var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots at {{ .Timestamp }} +// using data from {{ .URL }} +package spdxlicense + +const Version = {{ printf "%q" .Version }} + +var licenseIDs = map[string]string{ +{{- range $k, $v := .LicenseIDs }} + {{ printf "%q" $k }}: {{ printf "%q" $v }}, +{{- end }} +} +`)) + +var versionMatch = regexp.MustCompile(`-([0-9]+)\.?([0-9]+)?\.?([0-9]+)?\.?`) + +type LicenseList struct { + Version string `json:"licenseListVersion"` + Licenses []struct { + ID string `json:"licenseId"` + Name string `json:"name"` + Text string `json:"licenseText"` + Deprecated bool `json:"isDeprecatedLicenseId"` + OSIApproved bool `json:"isOsiApproved"` + SeeAlso []string `json:"seeAlso"` + } `json:"licenses"` +} + +func main() { + if err := run(); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} + +func run() error { + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("unable to get licenses list: %+v", err) + } + + var result LicenseList + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("unable to decode license list: %+v", err) + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Fatalf("unable to close body: %+v", err) + } + }() + + f, err := os.Create(source) + if err != nil { + return fmt.Errorf("unable to create %q: %+v", source, err) + } + defer func() { + if err := f.Close(); err != nil { + log.Fatalf("unable to close %q: %+v", source, err) + } + }() + + licenseIDs := processSPDXLicense(result) + + err = tmp.Execute(f, struct { + Timestamp time.Time + URL string + Version string + LicenseIDs map[string]string + }{ + Timestamp: time.Now(), + URL: url, + Version: result.Version, + LicenseIDs: licenseIDs, + }) + + if err != nil { + return fmt.Errorf("unable to generate template: %+v", err) + } + return nil +} + +// Parsing the provided SPDX license list necessitates a two pass approach. +// The first pass is only related to what SPDX considers the truth. These K:V pairs will never be overwritten. +// The second pass attempts to generate known short/long version listings for each key. +// For info on some short name conventions see this document: +// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name. +// The short long listing generation attempts to build all license permutations for a given key. +// The new keys are then also associated with their relative SPDX value. If a key has already been entered +// we know to ignore it since it came from the first pass which is considered the SPDX source of truth. +// We also sort the licenses for the second pass so that cases like `GPL-1` associate to `GPL-1.0` and not `GPL-1.1`. +func processSPDXLicense(result LicenseList) map[string]string { + // first pass build map + var licenseIDs = make(map[string]string) + for _, l := range result.Licenses { + cleanID := strings.ToLower(l.ID) + if _, exists := licenseIDs[cleanID]; exists { + log.Fatalf("duplicate license ID found: %q", cleanID) + } + licenseIDs[cleanID] = l.ID + } + + sort.Slice(result.Licenses, func(i, j int) bool { + return result.Licenses[i].ID < result.Licenses[j].ID + }) + + // second pass build exceptions + // do not overwrite if already exists + for _, l := range result.Licenses { + var multipleID []string + cleanID := strings.ToLower(l.ID) + multipleID = append(multipleID, buildLicensePermutations(cleanID)...) + for _, id := range multipleID { + if _, exists := licenseIDs[id]; !exists { + licenseIDs[id] = l.ID + } + } + } + + return licenseIDs +} + +func buildLicensePermutations(license string) (perms []string) { + lv := findLicenseVersion(license) + vp := versionPermutations(lv) + + version := strings.Join(lv, ".") + for _, p := range vp { + perms = append(perms, strings.Replace(license, version, p, 1)) + } + + return perms +} + +func findLicenseVersion(license string) (version []string) { + versionList := versionMatch.FindAllStringSubmatch(license, -1) + + if len(versionList) == 0 { + return version + } + + for i, v := range versionList[0] { + if v != "" && i != 0 { + version = append(version, v) + } + } + + return version +} + +func versionPermutations(version []string) []string { + ver := append([]string(nil), version...) + perms := strset.New() + for i := 1; i <= 3; i++ { + if len(ver) < i+1 { + ver = append(ver, "0") + } + + perm := strings.Join(ver[:i], ".") + badCount := strings.Count(perm, "0") + strings.Count(perm, ".") + + if badCount != len(perm) { + perms.Add(perm) + } + } + + return perms.List() +} diff --git a/sbom/internal/spdxlicense/license.go b/sbom/internal/spdxlicense/license.go new file mode 100644 index 00000000..c0f7f935 --- /dev/null +++ b/sbom/internal/spdxlicense/license.go @@ -0,0 +1,20 @@ +package spdxlicense + +import ( + "strings" +) + +// https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-short-name +// License generated in license_list.go uses a regular expression to help resolve cases where +// x.0.0 and x are supplied as version numbers. For SPDX compatibility, versions with trailing +// dot-zeroes are considered to be equivalent to versions without (e.g., “2.0.0” is considered equal to “2.0” and “2”). +// EX: gpl-2+ ---> GPL-2.0+ +// EX: gpl-2.0.0-only ---> GPL-2.0-only +// See the debian link for more details on the spdx license differences + +//go:generate go run generate/generate_license_list.go + +func ID(id string) (string, bool) { + value, exists := licenseIDs[strings.ToLower(id)] + return value, exists +} diff --git a/sbom/internal/spdxlicense/license_list.go b/sbom/internal/spdxlicense/license_list.go new file mode 100644 index 00000000..0b2c8f12 --- /dev/null +++ b/sbom/internal/spdxlicense/license_list.go @@ -0,0 +1,1052 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots at 2022-03-21 17:08:18.691238 -0400 EDT m=+0.354172873 +// using data from https://spdx.org/licenses/licenses.json +package spdxlicense + +const Version = "3.16" + +var licenseIDs = map[string]string{ + "0bsd": "0BSD", + "aal": "AAL", + "abstyles": "Abstyles", + "adobe-2006": "Adobe-2006", + "adobe-2006.0": "Adobe-2006", + "adobe-2006.0.0": "Adobe-2006", + "adobe-glyph": "Adobe-Glyph", + "adsl": "ADSL", + "afl-1": "AFL-1.1", + "afl-1.1": "AFL-1.1", + "afl-1.1.0": "AFL-1.1", + "afl-1.2": "AFL-1.2", + "afl-1.2.0": "AFL-1.2", + "afl-2": "AFL-2.0", + "afl-2.0": "AFL-2.0", + "afl-2.0.0": "AFL-2.0", + "afl-2.1": "AFL-2.1", + "afl-2.1.0": "AFL-2.1", + "afl-3": "AFL-3.0", + "afl-3.0": "AFL-3.0", + "afl-3.0.0": "AFL-3.0", + "afmparse": "Afmparse", + "agpl-1": "AGPL-1.0", + "agpl-1-only": "AGPL-1.0-only", + "agpl-1-or-later": "AGPL-1.0-or-later", + "agpl-1.0": "AGPL-1.0", + "agpl-1.0-only": "AGPL-1.0-only", + "agpl-1.0-or-later": "AGPL-1.0-or-later", + "agpl-1.0.0": "AGPL-1.0", + "agpl-1.0.0-only": "AGPL-1.0-only", + "agpl-1.0.0-or-later": "AGPL-1.0-or-later", + "agpl-3": "AGPL-3.0", + "agpl-3-only": "AGPL-3.0-only", + "agpl-3-or-later": "AGPL-3.0-or-later", + "agpl-3.0": "AGPL-3.0", + "agpl-3.0-only": "AGPL-3.0-only", + "agpl-3.0-or-later": "AGPL-3.0-or-later", + "agpl-3.0.0": "AGPL-3.0", + "agpl-3.0.0-only": "AGPL-3.0-only", + "agpl-3.0.0-or-later": "AGPL-3.0-or-later", + "aladdin": "Aladdin", + "amdplpa": "AMDPLPA", + "aml": "AML", + "ampas": "AMPAS", + "antlr-pd": "ANTLR-PD", + "antlr-pd-fallback": "ANTLR-PD-fallback", + "apache-1": "Apache-1.0", + "apache-1.0": "Apache-1.0", + "apache-1.0.0": "Apache-1.0", + "apache-1.1": "Apache-1.1", + "apache-1.1.0": "Apache-1.1", + "apache-2": "Apache-2.0", + "apache-2.0": "Apache-2.0", + "apache-2.0.0": "Apache-2.0", + "apafml": "APAFML", + "apl-1": "APL-1.0", + "apl-1.0": "APL-1.0", + "apl-1.0.0": "APL-1.0", + "app-s2p": "App-s2p", + "apsl-1": "APSL-1.0", + "apsl-1.0": "APSL-1.0", + "apsl-1.0.0": "APSL-1.0", + "apsl-1.1": "APSL-1.1", + "apsl-1.1.0": "APSL-1.1", + "apsl-1.2": "APSL-1.2", + "apsl-1.2.0": "APSL-1.2", + "apsl-2": "APSL-2.0", + "apsl-2.0": "APSL-2.0", + "apsl-2.0.0": "APSL-2.0", + "artistic-1": "Artistic-1.0", + "artistic-1-cl8": "Artistic-1.0-cl8", + "artistic-1-perl": "Artistic-1.0-Perl", + "artistic-1.0": "Artistic-1.0", + "artistic-1.0-cl8": "Artistic-1.0-cl8", + "artistic-1.0-perl": "Artistic-1.0-Perl", + "artistic-1.0.0": "Artistic-1.0", + "artistic-1.0.0-cl8": "Artistic-1.0-cl8", + "artistic-1.0.0-perl": "Artistic-1.0-Perl", + "artistic-2": "Artistic-2.0", + "artistic-2.0": "Artistic-2.0", + "artistic-2.0.0": "Artistic-2.0", + "bahyph": "Bahyph", + "barr": "Barr", + "beerware": "Beerware", + "bittorrent-1": "BitTorrent-1.0", + "bittorrent-1.0": "BitTorrent-1.0", + "bittorrent-1.0.0": "BitTorrent-1.0", + "bittorrent-1.1": "BitTorrent-1.1", + "bittorrent-1.1.0": "BitTorrent-1.1", + "blessing": "blessing", + "blueoak-1": "BlueOak-1.0.0", + "blueoak-1.0": "BlueOak-1.0.0", + "blueoak-1.0.0": "BlueOak-1.0.0", + "borceux": "Borceux", + "bsd-1-clause": "BSD-1-Clause", + "bsd-1.0-clause": "BSD-1-Clause", + "bsd-1.0.0-clause": "BSD-1-Clause", + "bsd-2-clause": "BSD-2-Clause", + "bsd-2-clause-freebsd": "BSD-2-Clause-FreeBSD", + "bsd-2-clause-netbsd": "BSD-2-Clause-NetBSD", + "bsd-2-clause-patent": "BSD-2-Clause-Patent", + "bsd-2-clause-views": "BSD-2-Clause-Views", + "bsd-2.0-clause": "BSD-2-Clause", + "bsd-2.0-clause-freebsd": "BSD-2-Clause-FreeBSD", + "bsd-2.0-clause-netbsd": "BSD-2-Clause-NetBSD", + "bsd-2.0-clause-patent": "BSD-2-Clause-Patent", + "bsd-2.0-clause-views": "BSD-2-Clause-Views", + "bsd-2.0.0-clause": "BSD-2-Clause", + "bsd-2.0.0-clause-freebsd": "BSD-2-Clause-FreeBSD", + "bsd-2.0.0-clause-netbsd": "BSD-2-Clause-NetBSD", + "bsd-2.0.0-clause-patent": "BSD-2-Clause-Patent", + "bsd-2.0.0-clause-views": "BSD-2-Clause-Views", + "bsd-3-clause": "BSD-3-Clause", + "bsd-3-clause-attribution": "BSD-3-Clause-Attribution", + "bsd-3-clause-clear": "BSD-3-Clause-Clear", + "bsd-3-clause-lbnl": "BSD-3-Clause-LBNL", + "bsd-3-clause-modification": "BSD-3-Clause-Modification", + "bsd-3-clause-no-military-license": "BSD-3-Clause-No-Military-License", + "bsd-3-clause-no-nuclear-license": "BSD-3-Clause-No-Nuclear-License", + "bsd-3-clause-no-nuclear-license-2014": "BSD-3-Clause-No-Nuclear-License-2014", + "bsd-3-clause-no-nuclear-warranty": "BSD-3-Clause-No-Nuclear-Warranty", + "bsd-3-clause-open-mpi": "BSD-3-Clause-Open-MPI", + "bsd-3.0-clause": "BSD-3-Clause", + "bsd-3.0-clause-attribution": "BSD-3-Clause-Attribution", + "bsd-3.0-clause-clear": "BSD-3-Clause-Clear", + "bsd-3.0-clause-lbnl": "BSD-3-Clause-LBNL", + "bsd-3.0-clause-modification": "BSD-3-Clause-Modification", + "bsd-3.0-clause-no-military-license": "BSD-3-Clause-No-Military-License", + "bsd-3.0-clause-no-nuclear-license": "BSD-3-Clause-No-Nuclear-License", + "bsd-3.0-clause-no-nuclear-license-2014": "BSD-3-Clause-No-Nuclear-License-2014", + "bsd-3.0-clause-no-nuclear-warranty": "BSD-3-Clause-No-Nuclear-Warranty", + "bsd-3.0-clause-open-mpi": "BSD-3-Clause-Open-MPI", + "bsd-3.0.0-clause": "BSD-3-Clause", + "bsd-3.0.0-clause-attribution": "BSD-3-Clause-Attribution", + "bsd-3.0.0-clause-clear": "BSD-3-Clause-Clear", + "bsd-3.0.0-clause-lbnl": "BSD-3-Clause-LBNL", + "bsd-3.0.0-clause-modification": "BSD-3-Clause-Modification", + "bsd-3.0.0-clause-no-military-license": "BSD-3-Clause-No-Military-License", + "bsd-3.0.0-clause-no-nuclear-license": "BSD-3-Clause-No-Nuclear-License", + "bsd-3.0.0-clause-no-nuclear-license-2014": "BSD-3-Clause-No-Nuclear-License-2014", + "bsd-3.0.0-clause-no-nuclear-warranty": "BSD-3-Clause-No-Nuclear-Warranty", + "bsd-3.0.0-clause-open-mpi": "BSD-3-Clause-Open-MPI", + "bsd-4-clause": "BSD-4-Clause", + "bsd-4-clause-shortened": "BSD-4-Clause-Shortened", + "bsd-4-clause-uc": "BSD-4-Clause-UC", + "bsd-4.0-clause": "BSD-4-Clause", + "bsd-4.0-clause-shortened": "BSD-4-Clause-Shortened", + "bsd-4.0-clause-uc": "BSD-4-Clause-UC", + "bsd-4.0.0-clause": "BSD-4-Clause", + "bsd-4.0.0-clause-shortened": "BSD-4-Clause-Shortened", + "bsd-4.0.0-clause-uc": "BSD-4-Clause-UC", + "bsd-protection": "BSD-Protection", + "bsd-source-code": "BSD-Source-Code", + "bsl-1": "BSL-1.0", + "bsl-1.0": "BSL-1.0", + "bsl-1.0.0": "BSL-1.0", + "busl-1": "BUSL-1.1", + "busl-1.1": "BUSL-1.1", + "busl-1.1.0": "BUSL-1.1", + "bzip2-1": "bzip2-1.0.5", + "bzip2-1.0": "bzip2-1.0.5", + "bzip2-1.0.5": "bzip2-1.0.5", + "bzip2-1.0.6": "bzip2-1.0.6", + "c-uda-1": "C-UDA-1.0", + "c-uda-1.0": "C-UDA-1.0", + "c-uda-1.0.0": "C-UDA-1.0", + "cal-1": "CAL-1.0", + "cal-1-combined-work-exception": "CAL-1.0-Combined-Work-Exception", + "cal-1.0": "CAL-1.0", + "cal-1.0-combined-work-exception": "CAL-1.0-Combined-Work-Exception", + "cal-1.0.0": "CAL-1.0", + "cal-1.0.0-combined-work-exception": "CAL-1.0-Combined-Work-Exception", + "caldera": "Caldera", + "catosl-1": "CATOSL-1.1", + "catosl-1.1": "CATOSL-1.1", + "catosl-1.1.0": "CATOSL-1.1", + "cc-by-1": "CC-BY-1.0", + "cc-by-1.0": "CC-BY-1.0", + "cc-by-1.0.0": "CC-BY-1.0", + "cc-by-2": "CC-BY-2.0", + "cc-by-2-au": "CC-BY-2.5-AU", + "cc-by-2.0": "CC-BY-2.0", + "cc-by-2.0.0": "CC-BY-2.0", + "cc-by-2.5": "CC-BY-2.5", + "cc-by-2.5-au": "CC-BY-2.5-AU", + "cc-by-2.5.0": "CC-BY-2.5", + "cc-by-2.5.0-au": "CC-BY-2.5-AU", + "cc-by-3": "CC-BY-3.0", + "cc-by-3-at": "CC-BY-3.0-AT", + "cc-by-3-de": "CC-BY-3.0-DE", + "cc-by-3-nl": "CC-BY-3.0-NL", + "cc-by-3-us": "CC-BY-3.0-US", + "cc-by-3.0": "CC-BY-3.0", + "cc-by-3.0-at": "CC-BY-3.0-AT", + "cc-by-3.0-de": "CC-BY-3.0-DE", + "cc-by-3.0-nl": "CC-BY-3.0-NL", + "cc-by-3.0-us": "CC-BY-3.0-US", + "cc-by-3.0.0": "CC-BY-3.0", + "cc-by-3.0.0-at": "CC-BY-3.0-AT", + "cc-by-3.0.0-de": "CC-BY-3.0-DE", + "cc-by-3.0.0-nl": "CC-BY-3.0-NL", + "cc-by-3.0.0-us": "CC-BY-3.0-US", + "cc-by-4": "CC-BY-4.0", + "cc-by-4.0": "CC-BY-4.0", + "cc-by-4.0.0": "CC-BY-4.0", + "cc-by-nc-1": "CC-BY-NC-1.0", + "cc-by-nc-1.0": "CC-BY-NC-1.0", + "cc-by-nc-1.0.0": "CC-BY-NC-1.0", + "cc-by-nc-2": "CC-BY-NC-2.0", + "cc-by-nc-2.0": "CC-BY-NC-2.0", + "cc-by-nc-2.0.0": "CC-BY-NC-2.0", + "cc-by-nc-2.5": "CC-BY-NC-2.5", + "cc-by-nc-2.5.0": "CC-BY-NC-2.5", + "cc-by-nc-3": "CC-BY-NC-3.0", + "cc-by-nc-3-de": "CC-BY-NC-3.0-DE", + "cc-by-nc-3.0": "CC-BY-NC-3.0", + "cc-by-nc-3.0-de": "CC-BY-NC-3.0-DE", + "cc-by-nc-3.0.0": "CC-BY-NC-3.0", + "cc-by-nc-3.0.0-de": "CC-BY-NC-3.0-DE", + "cc-by-nc-4": "CC-BY-NC-4.0", + "cc-by-nc-4.0": "CC-BY-NC-4.0", + "cc-by-nc-4.0.0": "CC-BY-NC-4.0", + "cc-by-nc-nd-1": "CC-BY-NC-ND-1.0", + "cc-by-nc-nd-1.0": "CC-BY-NC-ND-1.0", + "cc-by-nc-nd-1.0.0": "CC-BY-NC-ND-1.0", + "cc-by-nc-nd-2": "CC-BY-NC-ND-2.0", + "cc-by-nc-nd-2.0": "CC-BY-NC-ND-2.0", + "cc-by-nc-nd-2.0.0": "CC-BY-NC-ND-2.0", + "cc-by-nc-nd-2.5": "CC-BY-NC-ND-2.5", + "cc-by-nc-nd-2.5.0": "CC-BY-NC-ND-2.5", + "cc-by-nc-nd-3": "CC-BY-NC-ND-3.0", + "cc-by-nc-nd-3-de": "CC-BY-NC-ND-3.0-DE", + "cc-by-nc-nd-3-igo": "CC-BY-NC-ND-3.0-IGO", + "cc-by-nc-nd-3.0": "CC-BY-NC-ND-3.0", + "cc-by-nc-nd-3.0-de": "CC-BY-NC-ND-3.0-DE", + "cc-by-nc-nd-3.0-igo": "CC-BY-NC-ND-3.0-IGO", + "cc-by-nc-nd-3.0.0": "CC-BY-NC-ND-3.0", + "cc-by-nc-nd-3.0.0-de": "CC-BY-NC-ND-3.0-DE", + "cc-by-nc-nd-3.0.0-igo": "CC-BY-NC-ND-3.0-IGO", + "cc-by-nc-nd-4": "CC-BY-NC-ND-4.0", + "cc-by-nc-nd-4.0": "CC-BY-NC-ND-4.0", + "cc-by-nc-nd-4.0.0": "CC-BY-NC-ND-4.0", + "cc-by-nc-sa-1": "CC-BY-NC-SA-1.0", + "cc-by-nc-sa-1.0": "CC-BY-NC-SA-1.0", + "cc-by-nc-sa-1.0.0": "CC-BY-NC-SA-1.0", + "cc-by-nc-sa-2": "CC-BY-NC-SA-2.0", + "cc-by-nc-sa-2-fr": "CC-BY-NC-SA-2.0-FR", + "cc-by-nc-sa-2-uk": "CC-BY-NC-SA-2.0-UK", + "cc-by-nc-sa-2.0": "CC-BY-NC-SA-2.0", + "cc-by-nc-sa-2.0-fr": "CC-BY-NC-SA-2.0-FR", + "cc-by-nc-sa-2.0-uk": "CC-BY-NC-SA-2.0-UK", + "cc-by-nc-sa-2.0.0": "CC-BY-NC-SA-2.0", + "cc-by-nc-sa-2.0.0-fr": "CC-BY-NC-SA-2.0-FR", + "cc-by-nc-sa-2.0.0-uk": "CC-BY-NC-SA-2.0-UK", + "cc-by-nc-sa-2.5": "CC-BY-NC-SA-2.5", + "cc-by-nc-sa-2.5.0": "CC-BY-NC-SA-2.5", + "cc-by-nc-sa-3": "CC-BY-NC-SA-3.0", + "cc-by-nc-sa-3-de": "CC-BY-NC-SA-3.0-DE", + "cc-by-nc-sa-3-igo": "CC-BY-NC-SA-3.0-IGO", + "cc-by-nc-sa-3.0": "CC-BY-NC-SA-3.0", + "cc-by-nc-sa-3.0-de": "CC-BY-NC-SA-3.0-DE", + "cc-by-nc-sa-3.0-igo": "CC-BY-NC-SA-3.0-IGO", + "cc-by-nc-sa-3.0.0": "CC-BY-NC-SA-3.0", + "cc-by-nc-sa-3.0.0-de": "CC-BY-NC-SA-3.0-DE", + "cc-by-nc-sa-3.0.0-igo": "CC-BY-NC-SA-3.0-IGO", + "cc-by-nc-sa-4": "CC-BY-NC-SA-4.0", + "cc-by-nc-sa-4.0": "CC-BY-NC-SA-4.0", + "cc-by-nc-sa-4.0.0": "CC-BY-NC-SA-4.0", + "cc-by-nd-1": "CC-BY-ND-1.0", + "cc-by-nd-1.0": "CC-BY-ND-1.0", + "cc-by-nd-1.0.0": "CC-BY-ND-1.0", + "cc-by-nd-2": "CC-BY-ND-2.0", + "cc-by-nd-2.0": "CC-BY-ND-2.0", + "cc-by-nd-2.0.0": "CC-BY-ND-2.0", + "cc-by-nd-2.5": "CC-BY-ND-2.5", + "cc-by-nd-2.5.0": "CC-BY-ND-2.5", + "cc-by-nd-3": "CC-BY-ND-3.0", + "cc-by-nd-3-de": "CC-BY-ND-3.0-DE", + "cc-by-nd-3.0": "CC-BY-ND-3.0", + "cc-by-nd-3.0-de": "CC-BY-ND-3.0-DE", + "cc-by-nd-3.0.0": "CC-BY-ND-3.0", + "cc-by-nd-3.0.0-de": "CC-BY-ND-3.0-DE", + "cc-by-nd-4": "CC-BY-ND-4.0", + "cc-by-nd-4.0": "CC-BY-ND-4.0", + "cc-by-nd-4.0.0": "CC-BY-ND-4.0", + "cc-by-sa-1": "CC-BY-SA-1.0", + "cc-by-sa-1.0": "CC-BY-SA-1.0", + "cc-by-sa-1.0.0": "CC-BY-SA-1.0", + "cc-by-sa-2": "CC-BY-SA-2.0", + "cc-by-sa-2-jp": "CC-BY-SA-2.1-JP", + "cc-by-sa-2-uk": "CC-BY-SA-2.0-UK", + "cc-by-sa-2.0": "CC-BY-SA-2.0", + "cc-by-sa-2.0-uk": "CC-BY-SA-2.0-UK", + "cc-by-sa-2.0.0": "CC-BY-SA-2.0", + "cc-by-sa-2.0.0-uk": "CC-BY-SA-2.0-UK", + "cc-by-sa-2.1-jp": "CC-BY-SA-2.1-JP", + "cc-by-sa-2.1.0-jp": "CC-BY-SA-2.1-JP", + "cc-by-sa-2.5": "CC-BY-SA-2.5", + "cc-by-sa-2.5.0": "CC-BY-SA-2.5", + "cc-by-sa-3": "CC-BY-SA-3.0", + "cc-by-sa-3-at": "CC-BY-SA-3.0-AT", + "cc-by-sa-3-de": "CC-BY-SA-3.0-DE", + "cc-by-sa-3.0": "CC-BY-SA-3.0", + "cc-by-sa-3.0-at": "CC-BY-SA-3.0-AT", + "cc-by-sa-3.0-de": "CC-BY-SA-3.0-DE", + "cc-by-sa-3.0.0": "CC-BY-SA-3.0", + "cc-by-sa-3.0.0-at": "CC-BY-SA-3.0-AT", + "cc-by-sa-3.0.0-de": "CC-BY-SA-3.0-DE", + "cc-by-sa-4": "CC-BY-SA-4.0", + "cc-by-sa-4.0": "CC-BY-SA-4.0", + "cc-by-sa-4.0.0": "CC-BY-SA-4.0", + "cc-pddc": "CC-PDDC", + "cc0-1": "CC0-1.0", + "cc0-1.0": "CC0-1.0", + "cc0-1.0.0": "CC0-1.0", + "cddl-1": "CDDL-1.0", + "cddl-1.0": "CDDL-1.0", + "cddl-1.0.0": "CDDL-1.0", + "cddl-1.1": "CDDL-1.1", + "cddl-1.1.0": "CDDL-1.1", + "cdl-1": "CDL-1.0", + "cdl-1.0": "CDL-1.0", + "cdl-1.0.0": "CDL-1.0", + "cdla-permissive-1": "CDLA-Permissive-1.0", + "cdla-permissive-1.0": "CDLA-Permissive-1.0", + "cdla-permissive-1.0.0": "CDLA-Permissive-1.0", + "cdla-permissive-2": "CDLA-Permissive-2.0", + "cdla-permissive-2.0": "CDLA-Permissive-2.0", + "cdla-permissive-2.0.0": "CDLA-Permissive-2.0", + "cdla-sharing-1": "CDLA-Sharing-1.0", + "cdla-sharing-1.0": "CDLA-Sharing-1.0", + "cdla-sharing-1.0.0": "CDLA-Sharing-1.0", + "cecill-1": "CECILL-1.0", + "cecill-1.0": "CECILL-1.0", + "cecill-1.0.0": "CECILL-1.0", + "cecill-1.1": "CECILL-1.1", + "cecill-1.1.0": "CECILL-1.1", + "cecill-2": "CECILL-2.0", + "cecill-2.0": "CECILL-2.0", + "cecill-2.0.0": "CECILL-2.0", + "cecill-2.1": "CECILL-2.1", + "cecill-2.1.0": "CECILL-2.1", + "cecill-b": "CECILL-B", + "cecill-c": "CECILL-C", + "cern-ohl-1": "CERN-OHL-1.1", + "cern-ohl-1.1": "CERN-OHL-1.1", + "cern-ohl-1.1.0": "CERN-OHL-1.1", + "cern-ohl-1.2": "CERN-OHL-1.2", + "cern-ohl-1.2.0": "CERN-OHL-1.2", + "cern-ohl-p-2": "CERN-OHL-P-2.0", + "cern-ohl-p-2.0": "CERN-OHL-P-2.0", + "cern-ohl-p-2.0.0": "CERN-OHL-P-2.0", + "cern-ohl-s-2": "CERN-OHL-S-2.0", + "cern-ohl-s-2.0": "CERN-OHL-S-2.0", + "cern-ohl-s-2.0.0": "CERN-OHL-S-2.0", + "cern-ohl-w-2": "CERN-OHL-W-2.0", + "cern-ohl-w-2.0": "CERN-OHL-W-2.0", + "cern-ohl-w-2.0.0": "CERN-OHL-W-2.0", + "clartistic": "ClArtistic", + "cnri-jython": "CNRI-Jython", + "cnri-python": "CNRI-Python", + "cnri-python-gpl-compatible": "CNRI-Python-GPL-Compatible", + "coil-1": "COIL-1.0", + "coil-1.0": "COIL-1.0", + "coil-1.0.0": "COIL-1.0", + "community-spec-1": "Community-Spec-1.0", + "community-spec-1.0": "Community-Spec-1.0", + "community-spec-1.0.0": "Community-Spec-1.0", + "condor-1": "Condor-1.1", + "condor-1.1": "Condor-1.1", + "condor-1.1.0": "Condor-1.1", + "copyleft-next-0.3": "copyleft-next-0.3.0", + "copyleft-next-0.3.0": "copyleft-next-0.3.0", + "copyleft-next-0.3.1": "copyleft-next-0.3.1", + "cpal-1": "CPAL-1.0", + "cpal-1.0": "CPAL-1.0", + "cpal-1.0.0": "CPAL-1.0", + "cpl-1": "CPL-1.0", + "cpl-1.0": "CPL-1.0", + "cpl-1.0.0": "CPL-1.0", + "cpol-1": "CPOL-1.02", + "cpol-1.02": "CPOL-1.02", + "cpol-1.02.0": "CPOL-1.02", + "crossword": "Crossword", + "crystalstacker": "CrystalStacker", + "cua-opl-1": "CUA-OPL-1.0", + "cua-opl-1.0": "CUA-OPL-1.0", + "cua-opl-1.0.0": "CUA-OPL-1.0", + "cube": "Cube", + "curl": "curl", + "d-fsl-1": "D-FSL-1.0", + "d-fsl-1.0": "D-FSL-1.0", + "d-fsl-1.0.0": "D-FSL-1.0", + "diffmark": "diffmark", + "dl-de-by-2": "DL-DE-BY-2.0", + "dl-de-by-2.0": "DL-DE-BY-2.0", + "dl-de-by-2.0.0": "DL-DE-BY-2.0", + "doc": "DOC", + "dotseqn": "Dotseqn", + "drl-1": "DRL-1.0", + "drl-1.0": "DRL-1.0", + "drl-1.0.0": "DRL-1.0", + "dsdp": "DSDP", + "dvipdfm": "dvipdfm", + "ecl-1": "ECL-1.0", + "ecl-1.0": "ECL-1.0", + "ecl-1.0.0": "ECL-1.0", + "ecl-2": "ECL-2.0", + "ecl-2.0": "ECL-2.0", + "ecl-2.0.0": "ECL-2.0", + "ecos-2": "eCos-2.0", + "ecos-2.0": "eCos-2.0", + "ecos-2.0.0": "eCos-2.0", + "efl-1": "EFL-1.0", + "efl-1.0": "EFL-1.0", + "efl-1.0.0": "EFL-1.0", + "efl-2": "EFL-2.0", + "efl-2.0": "EFL-2.0", + "efl-2.0.0": "EFL-2.0", + "egenix": "eGenix", + "elastic-2": "Elastic-2.0", + "elastic-2.0": "Elastic-2.0", + "elastic-2.0.0": "Elastic-2.0", + "entessa": "Entessa", + "epics": "EPICS", + "epl-1": "EPL-1.0", + "epl-1.0": "EPL-1.0", + "epl-1.0.0": "EPL-1.0", + "epl-2": "EPL-2.0", + "epl-2.0": "EPL-2.0", + "epl-2.0.0": "EPL-2.0", + "erlpl-1": "ErlPL-1.1", + "erlpl-1.1": "ErlPL-1.1", + "erlpl-1.1.0": "ErlPL-1.1", + "etalab-2": "etalab-2.0", + "etalab-2.0": "etalab-2.0", + "etalab-2.0.0": "etalab-2.0", + "eudatagrid": "EUDatagrid", + "eupl-1": "EUPL-1.0", + "eupl-1.0": "EUPL-1.0", + "eupl-1.0.0": "EUPL-1.0", + "eupl-1.1": "EUPL-1.1", + "eupl-1.1.0": "EUPL-1.1", + "eupl-1.2": "EUPL-1.2", + "eupl-1.2.0": "EUPL-1.2", + "eurosym": "Eurosym", + "fair": "Fair", + "fdk-aac": "FDK-AAC", + "frameworx-1": "Frameworx-1.0", + "frameworx-1.0": "Frameworx-1.0", + "frameworx-1.0.0": "Frameworx-1.0", + "freebsd-doc": "FreeBSD-DOC", + "freeimage": "FreeImage", + "fsfap": "FSFAP", + "fsful": "FSFUL", + "fsfullr": "FSFULLR", + "ftl": "FTL", + "gd": "GD", + "gfdl-1": "GFDL-1.1", + "gfdl-1-invariants-only": "GFDL-1.1-invariants-only", + "gfdl-1-invariants-or-later": "GFDL-1.1-invariants-or-later", + "gfdl-1-no-invariants-only": "GFDL-1.1-no-invariants-only", + "gfdl-1-no-invariants-or-later": "GFDL-1.1-no-invariants-or-later", + "gfdl-1-only": "GFDL-1.1-only", + "gfdl-1-or-later": "GFDL-1.1-or-later", + "gfdl-1.1": "GFDL-1.1", + "gfdl-1.1-invariants-only": "GFDL-1.1-invariants-only", + "gfdl-1.1-invariants-or-later": "GFDL-1.1-invariants-or-later", + "gfdl-1.1-no-invariants-only": "GFDL-1.1-no-invariants-only", + "gfdl-1.1-no-invariants-or-later": "GFDL-1.1-no-invariants-or-later", + "gfdl-1.1-only": "GFDL-1.1-only", + "gfdl-1.1-or-later": "GFDL-1.1-or-later", + "gfdl-1.1.0": "GFDL-1.1", + "gfdl-1.1.0-invariants-only": "GFDL-1.1-invariants-only", + "gfdl-1.1.0-invariants-or-later": "GFDL-1.1-invariants-or-later", + "gfdl-1.1.0-no-invariants-only": "GFDL-1.1-no-invariants-only", + "gfdl-1.1.0-no-invariants-or-later": "GFDL-1.1-no-invariants-or-later", + "gfdl-1.1.0-only": "GFDL-1.1-only", + "gfdl-1.1.0-or-later": "GFDL-1.1-or-later", + "gfdl-1.2": "GFDL-1.2", + "gfdl-1.2-invariants-only": "GFDL-1.2-invariants-only", + "gfdl-1.2-invariants-or-later": "GFDL-1.2-invariants-or-later", + "gfdl-1.2-no-invariants-only": "GFDL-1.2-no-invariants-only", + "gfdl-1.2-no-invariants-or-later": "GFDL-1.2-no-invariants-or-later", + "gfdl-1.2-only": "GFDL-1.2-only", + "gfdl-1.2-or-later": "GFDL-1.2-or-later", + "gfdl-1.2.0": "GFDL-1.2", + "gfdl-1.2.0-invariants-only": "GFDL-1.2-invariants-only", + "gfdl-1.2.0-invariants-or-later": "GFDL-1.2-invariants-or-later", + "gfdl-1.2.0-no-invariants-only": "GFDL-1.2-no-invariants-only", + "gfdl-1.2.0-no-invariants-or-later": "GFDL-1.2-no-invariants-or-later", + "gfdl-1.2.0-only": "GFDL-1.2-only", + "gfdl-1.2.0-or-later": "GFDL-1.2-or-later", + "gfdl-1.3": "GFDL-1.3", + "gfdl-1.3-invariants-only": "GFDL-1.3-invariants-only", + "gfdl-1.3-invariants-or-later": "GFDL-1.3-invariants-or-later", + "gfdl-1.3-no-invariants-only": "GFDL-1.3-no-invariants-only", + "gfdl-1.3-no-invariants-or-later": "GFDL-1.3-no-invariants-or-later", + "gfdl-1.3-only": "GFDL-1.3-only", + "gfdl-1.3-or-later": "GFDL-1.3-or-later", + "gfdl-1.3.0": "GFDL-1.3", + "gfdl-1.3.0-invariants-only": "GFDL-1.3-invariants-only", + "gfdl-1.3.0-invariants-or-later": "GFDL-1.3-invariants-or-later", + "gfdl-1.3.0-no-invariants-only": "GFDL-1.3-no-invariants-only", + "gfdl-1.3.0-no-invariants-or-later": "GFDL-1.3-no-invariants-or-later", + "gfdl-1.3.0-only": "GFDL-1.3-only", + "gfdl-1.3.0-or-later": "GFDL-1.3-or-later", + "giftware": "Giftware", + "gl2ps": "GL2PS", + "glide": "Glide", + "glulxe": "Glulxe", + "glwtpl": "GLWTPL", + "gnuplot": "gnuplot", + "gpl-1": "GPL-1.0", + "gpl-1+": "GPL-1.0+", + "gpl-1-only": "GPL-1.0-only", + "gpl-1-or-later": "GPL-1.0-or-later", + "gpl-1.0": "GPL-1.0", + "gpl-1.0+": "GPL-1.0+", + "gpl-1.0-only": "GPL-1.0-only", + "gpl-1.0-or-later": "GPL-1.0-or-later", + "gpl-1.0.0": "GPL-1.0", + "gpl-1.0.0+": "GPL-1.0+", + "gpl-1.0.0-only": "GPL-1.0-only", + "gpl-1.0.0-or-later": "GPL-1.0-or-later", + "gpl-2": "GPL-2.0", + "gpl-2+": "GPL-2.0+", + "gpl-2-only": "GPL-2.0-only", + "gpl-2-or-later": "GPL-2.0-or-later", + "gpl-2-with-autoconf-exception": "GPL-2.0-with-autoconf-exception", + "gpl-2-with-bison-exception": "GPL-2.0-with-bison-exception", + "gpl-2-with-classpath-exception": "GPL-2.0-with-classpath-exception", + "gpl-2-with-font-exception": "GPL-2.0-with-font-exception", + "gpl-2-with-gcc-exception": "GPL-2.0-with-GCC-exception", + "gpl-2.0": "GPL-2.0", + "gpl-2.0+": "GPL-2.0+", + "gpl-2.0-only": "GPL-2.0-only", + "gpl-2.0-or-later": "GPL-2.0-or-later", + "gpl-2.0-with-autoconf-exception": "GPL-2.0-with-autoconf-exception", + "gpl-2.0-with-bison-exception": "GPL-2.0-with-bison-exception", + "gpl-2.0-with-classpath-exception": "GPL-2.0-with-classpath-exception", + "gpl-2.0-with-font-exception": "GPL-2.0-with-font-exception", + "gpl-2.0-with-gcc-exception": "GPL-2.0-with-GCC-exception", + "gpl-2.0.0": "GPL-2.0", + "gpl-2.0.0+": "GPL-2.0+", + "gpl-2.0.0-only": "GPL-2.0-only", + "gpl-2.0.0-or-later": "GPL-2.0-or-later", + "gpl-2.0.0-with-autoconf-exception": "GPL-2.0-with-autoconf-exception", + "gpl-2.0.0-with-bison-exception": "GPL-2.0-with-bison-exception", + "gpl-2.0.0-with-classpath-exception": "GPL-2.0-with-classpath-exception", + "gpl-2.0.0-with-font-exception": "GPL-2.0-with-font-exception", + "gpl-2.0.0-with-gcc-exception": "GPL-2.0-with-GCC-exception", + "gpl-3": "GPL-3.0", + "gpl-3+": "GPL-3.0+", + "gpl-3-only": "GPL-3.0-only", + "gpl-3-or-later": "GPL-3.0-or-later", + "gpl-3-with-autoconf-exception": "GPL-3.0-with-autoconf-exception", + "gpl-3-with-gcc-exception": "GPL-3.0-with-GCC-exception", + "gpl-3.0": "GPL-3.0", + "gpl-3.0+": "GPL-3.0+", + "gpl-3.0-only": "GPL-3.0-only", + "gpl-3.0-or-later": "GPL-3.0-or-later", + "gpl-3.0-with-autoconf-exception": "GPL-3.0-with-autoconf-exception", + "gpl-3.0-with-gcc-exception": "GPL-3.0-with-GCC-exception", + "gpl-3.0.0": "GPL-3.0", + "gpl-3.0.0+": "GPL-3.0+", + "gpl-3.0.0-only": "GPL-3.0-only", + "gpl-3.0.0-or-later": "GPL-3.0-or-later", + "gpl-3.0.0-with-autoconf-exception": "GPL-3.0-with-autoconf-exception", + "gpl-3.0.0-with-gcc-exception": "GPL-3.0-with-GCC-exception", + "gsoap-1.3.0b": "gSOAP-1.3b", + "gsoap-1.3b": "gSOAP-1.3b", + "gsoap-1b": "gSOAP-1.3b", + "haskellreport": "HaskellReport", + "hippocratic-2": "Hippocratic-2.1", + "hippocratic-2.1": "Hippocratic-2.1", + "hippocratic-2.1.0": "Hippocratic-2.1", + "hpnd": "HPND", + "hpnd-sell-variant": "HPND-sell-variant", + "htmltidy": "HTMLTIDY", + "ibm-pibs": "IBM-pibs", + "icu": "ICU", + "ijg": "IJG", + "imagemagick": "ImageMagick", + "imatix": "iMatix", + "imlib2": "Imlib2", + "info-zip": "Info-ZIP", + "intel": "Intel", + "intel-acpi": "Intel-ACPI", + "interbase-1": "Interbase-1.0", + "interbase-1.0": "Interbase-1.0", + "interbase-1.0.0": "Interbase-1.0", + "ipa": "IPA", + "ipl-1": "IPL-1.0", + "ipl-1.0": "IPL-1.0", + "ipl-1.0.0": "IPL-1.0", + "isc": "ISC", + "jam": "Jam", + "jasper-2": "JasPer-2.0", + "jasper-2.0": "JasPer-2.0", + "jasper-2.0.0": "JasPer-2.0", + "jpnic": "JPNIC", + "json": "JSON", + "lal-1": "LAL-1.2", + "lal-1.2": "LAL-1.2", + "lal-1.2.0": "LAL-1.2", + "lal-1.3": "LAL-1.3", + "lal-1.3.0": "LAL-1.3", + "latex2e": "Latex2e", + "leptonica": "Leptonica", + "lgpl-2": "LGPL-2.0", + "lgpl-2+": "LGPL-2.0+", + "lgpl-2-only": "LGPL-2.0-only", + "lgpl-2-or-later": "LGPL-2.0-or-later", + "lgpl-2.0": "LGPL-2.0", + "lgpl-2.0+": "LGPL-2.0+", + "lgpl-2.0-only": "LGPL-2.0-only", + "lgpl-2.0-or-later": "LGPL-2.0-or-later", + "lgpl-2.0.0": "LGPL-2.0", + "lgpl-2.0.0+": "LGPL-2.0+", + "lgpl-2.0.0-only": "LGPL-2.0-only", + "lgpl-2.0.0-or-later": "LGPL-2.0-or-later", + "lgpl-2.1": "LGPL-2.1", + "lgpl-2.1+": "LGPL-2.1+", + "lgpl-2.1-only": "LGPL-2.1-only", + "lgpl-2.1-or-later": "LGPL-2.1-or-later", + "lgpl-2.1.0": "LGPL-2.1", + "lgpl-2.1.0+": "LGPL-2.1+", + "lgpl-2.1.0-only": "LGPL-2.1-only", + "lgpl-2.1.0-or-later": "LGPL-2.1-or-later", + "lgpl-3": "LGPL-3.0", + "lgpl-3+": "LGPL-3.0+", + "lgpl-3-only": "LGPL-3.0-only", + "lgpl-3-or-later": "LGPL-3.0-or-later", + "lgpl-3.0": "LGPL-3.0", + "lgpl-3.0+": "LGPL-3.0+", + "lgpl-3.0-only": "LGPL-3.0-only", + "lgpl-3.0-or-later": "LGPL-3.0-or-later", + "lgpl-3.0.0": "LGPL-3.0", + "lgpl-3.0.0+": "LGPL-3.0+", + "lgpl-3.0.0-only": "LGPL-3.0-only", + "lgpl-3.0.0-or-later": "LGPL-3.0-or-later", + "lgpllr": "LGPLLR", + "libpng": "Libpng", + "libpng-2": "libpng-2.0", + "libpng-2.0": "libpng-2.0", + "libpng-2.0.0": "libpng-2.0", + "libselinux-1": "libselinux-1.0", + "libselinux-1.0": "libselinux-1.0", + "libselinux-1.0.0": "libselinux-1.0", + "libtiff": "libtiff", + "liliq-p-1": "LiLiQ-P-1.1", + "liliq-p-1.1": "LiLiQ-P-1.1", + "liliq-p-1.1.0": "LiLiQ-P-1.1", + "liliq-r-1": "LiLiQ-R-1.1", + "liliq-r-1.1": "LiLiQ-R-1.1", + "liliq-r-1.1.0": "LiLiQ-R-1.1", + "liliq-rplus-1": "LiLiQ-Rplus-1.1", + "liliq-rplus-1.1": "LiLiQ-Rplus-1.1", + "liliq-rplus-1.1.0": "LiLiQ-Rplus-1.1", + "linux-man-pages-copyleft": "Linux-man-pages-copyleft", + "linux-openib": "Linux-OpenIB", + "lpl-1": "LPL-1.0", + "lpl-1.0": "LPL-1.0", + "lpl-1.0.0": "LPL-1.0", + "lpl-1.02": "LPL-1.02", + "lpl-1.02.0": "LPL-1.02", + "lppl-1": "LPPL-1.0", + "lppl-1.0": "LPPL-1.0", + "lppl-1.0.0": "LPPL-1.0", + "lppl-1.1": "LPPL-1.1", + "lppl-1.1.0": "LPPL-1.1", + "lppl-1.2": "LPPL-1.2", + "lppl-1.2.0": "LPPL-1.2", + "lppl-1.3.0a": "LPPL-1.3a", + "lppl-1.3.0c": "LPPL-1.3c", + "lppl-1.3a": "LPPL-1.3a", + "lppl-1.3c": "LPPL-1.3c", + "lppl-1a": "LPPL-1.3a", + "lppl-1c": "LPPL-1.3c", + "makeindex": "MakeIndex", + "miros": "MirOS", + "mit": "MIT", + "mit-0": "MIT-0", + "mit-advertising": "MIT-advertising", + "mit-cmu": "MIT-CMU", + "mit-enna": "MIT-enna", + "mit-feh": "MIT-feh", + "mit-modern-variant": "MIT-Modern-Variant", + "mit-open-group": "MIT-open-group", + "mitnfa": "MITNFA", + "motosoto": "Motosoto", + "mpich2": "mpich2", + "mpl-1": "MPL-1.0", + "mpl-1.0": "MPL-1.0", + "mpl-1.0.0": "MPL-1.0", + "mpl-1.1": "MPL-1.1", + "mpl-1.1.0": "MPL-1.1", + "mpl-2": "MPL-2.0", + "mpl-2-no-copyleft-exception": "MPL-2.0-no-copyleft-exception", + "mpl-2.0": "MPL-2.0", + "mpl-2.0-no-copyleft-exception": "MPL-2.0-no-copyleft-exception", + "mpl-2.0.0": "MPL-2.0", + "mpl-2.0.0-no-copyleft-exception": "MPL-2.0-no-copyleft-exception", + "ms-pl": "MS-PL", + "ms-rl": "MS-RL", + "mtll": "MTLL", + "mulanpsl-1": "MulanPSL-1.0", + "mulanpsl-1.0": "MulanPSL-1.0", + "mulanpsl-1.0.0": "MulanPSL-1.0", + "mulanpsl-2": "MulanPSL-2.0", + "mulanpsl-2.0": "MulanPSL-2.0", + "mulanpsl-2.0.0": "MulanPSL-2.0", + "multics": "Multics", + "mup": "Mup", + "naist-2003": "NAIST-2003", + "naist-2003.0": "NAIST-2003", + "naist-2003.0.0": "NAIST-2003", + "nasa-1": "NASA-1.3", + "nasa-1.3": "NASA-1.3", + "nasa-1.3.0": "NASA-1.3", + "naumen": "Naumen", + "nbpl-1": "NBPL-1.0", + "nbpl-1.0": "NBPL-1.0", + "nbpl-1.0.0": "NBPL-1.0", + "ncgl-uk-2": "NCGL-UK-2.0", + "ncgl-uk-2.0": "NCGL-UK-2.0", + "ncgl-uk-2.0.0": "NCGL-UK-2.0", + "ncsa": "NCSA", + "net-snmp": "Net-SNMP", + "netcdf": "NetCDF", + "newsletr": "Newsletr", + "ngpl": "NGPL", + "nist-pd": "NIST-PD", + "nist-pd-fallback": "NIST-PD-fallback", + "nlod-1": "NLOD-1.0", + "nlod-1.0": "NLOD-1.0", + "nlod-1.0.0": "NLOD-1.0", + "nlod-2": "NLOD-2.0", + "nlod-2.0": "NLOD-2.0", + "nlod-2.0.0": "NLOD-2.0", + "nlpl": "NLPL", + "nokia": "Nokia", + "nosl": "NOSL", + "noweb": "Noweb", + "npl-1": "NPL-1.0", + "npl-1.0": "NPL-1.0", + "npl-1.0.0": "NPL-1.0", + "npl-1.1": "NPL-1.1", + "npl-1.1.0": "NPL-1.1", + "nposl-3": "NPOSL-3.0", + "nposl-3.0": "NPOSL-3.0", + "nposl-3.0.0": "NPOSL-3.0", + "nrl": "NRL", + "ntp": "NTP", + "ntp-0": "NTP-0", + "nunit": "Nunit", + "o-uda-1": "O-UDA-1.0", + "o-uda-1.0": "O-UDA-1.0", + "o-uda-1.0.0": "O-UDA-1.0", + "occt-pl": "OCCT-PL", + "oclc-2": "OCLC-2.0", + "oclc-2.0": "OCLC-2.0", + "oclc-2.0.0": "OCLC-2.0", + "odbl-1": "ODbL-1.0", + "odbl-1.0": "ODbL-1.0", + "odbl-1.0.0": "ODbL-1.0", + "odc-by-1": "ODC-By-1.0", + "odc-by-1.0": "ODC-By-1.0", + "odc-by-1.0.0": "ODC-By-1.0", + "ofl-1": "OFL-1.0", + "ofl-1-no-rfn": "OFL-1.0-no-RFN", + "ofl-1-rfn": "OFL-1.0-RFN", + "ofl-1.0": "OFL-1.0", + "ofl-1.0-no-rfn": "OFL-1.0-no-RFN", + "ofl-1.0-rfn": "OFL-1.0-RFN", + "ofl-1.0.0": "OFL-1.0", + "ofl-1.0.0-no-rfn": "OFL-1.0-no-RFN", + "ofl-1.0.0-rfn": "OFL-1.0-RFN", + "ofl-1.1": "OFL-1.1", + "ofl-1.1-no-rfn": "OFL-1.1-no-RFN", + "ofl-1.1-rfn": "OFL-1.1-RFN", + "ofl-1.1.0": "OFL-1.1", + "ofl-1.1.0-no-rfn": "OFL-1.1-no-RFN", + "ofl-1.1.0-rfn": "OFL-1.1-RFN", + "ogc-1": "OGC-1.0", + "ogc-1.0": "OGC-1.0", + "ogc-1.0.0": "OGC-1.0", + "ogdl-taiwan-1": "OGDL-Taiwan-1.0", + "ogdl-taiwan-1.0": "OGDL-Taiwan-1.0", + "ogdl-taiwan-1.0.0": "OGDL-Taiwan-1.0", + "ogl-canada-2": "OGL-Canada-2.0", + "ogl-canada-2.0": "OGL-Canada-2.0", + "ogl-canada-2.0.0": "OGL-Canada-2.0", + "ogl-uk-1": "OGL-UK-1.0", + "ogl-uk-1.0": "OGL-UK-1.0", + "ogl-uk-1.0.0": "OGL-UK-1.0", + "ogl-uk-2": "OGL-UK-2.0", + "ogl-uk-2.0": "OGL-UK-2.0", + "ogl-uk-2.0.0": "OGL-UK-2.0", + "ogl-uk-3": "OGL-UK-3.0", + "ogl-uk-3.0": "OGL-UK-3.0", + "ogl-uk-3.0.0": "OGL-UK-3.0", + "ogtsl": "OGTSL", + "oldap-1": "OLDAP-1.1", + "oldap-1.1": "OLDAP-1.1", + "oldap-1.1.0": "OLDAP-1.1", + "oldap-1.2": "OLDAP-1.2", + "oldap-1.2.0": "OLDAP-1.2", + "oldap-1.3": "OLDAP-1.3", + "oldap-1.3.0": "OLDAP-1.3", + "oldap-1.4": "OLDAP-1.4", + "oldap-1.4.0": "OLDAP-1.4", + "oldap-2": "OLDAP-2.0", + "oldap-2.0": "OLDAP-2.0", + "oldap-2.0.0": "OLDAP-2.0", + "oldap-2.0.1": "OLDAP-2.0.1", + "oldap-2.1": "OLDAP-2.1", + "oldap-2.1.0": "OLDAP-2.1", + "oldap-2.2": "OLDAP-2.2", + "oldap-2.2.0": "OLDAP-2.2", + "oldap-2.2.1": "OLDAP-2.2.1", + "oldap-2.2.2": "OLDAP-2.2.2", + "oldap-2.3": "OLDAP-2.3", + "oldap-2.3.0": "OLDAP-2.3", + "oldap-2.4": "OLDAP-2.4", + "oldap-2.4.0": "OLDAP-2.4", + "oldap-2.5": "OLDAP-2.5", + "oldap-2.5.0": "OLDAP-2.5", + "oldap-2.6": "OLDAP-2.6", + "oldap-2.6.0": "OLDAP-2.6", + "oldap-2.7": "OLDAP-2.7", + "oldap-2.7.0": "OLDAP-2.7", + "oldap-2.8": "OLDAP-2.8", + "oldap-2.8.0": "OLDAP-2.8", + "oml": "OML", + "openssl": "OpenSSL", + "opl-1": "OPL-1.0", + "opl-1.0": "OPL-1.0", + "opl-1.0.0": "OPL-1.0", + "opubl-1": "OPUBL-1.0", + "opubl-1.0": "OPUBL-1.0", + "opubl-1.0.0": "OPUBL-1.0", + "oset-pl-2": "OSET-PL-2.1", + "oset-pl-2.1": "OSET-PL-2.1", + "oset-pl-2.1.0": "OSET-PL-2.1", + "osl-1": "OSL-1.0", + "osl-1.0": "OSL-1.0", + "osl-1.0.0": "OSL-1.0", + "osl-1.1": "OSL-1.1", + "osl-1.1.0": "OSL-1.1", + "osl-2": "OSL-2.0", + "osl-2.0": "OSL-2.0", + "osl-2.0.0": "OSL-2.0", + "osl-2.1": "OSL-2.1", + "osl-2.1.0": "OSL-2.1", + "osl-3": "OSL-3.0", + "osl-3.0": "OSL-3.0", + "osl-3.0.0": "OSL-3.0", + "parity-6": "Parity-6.0.0", + "parity-6.0": "Parity-6.0.0", + "parity-6.0.0": "Parity-6.0.0", + "parity-7": "Parity-7.0.0", + "parity-7.0": "Parity-7.0.0", + "parity-7.0.0": "Parity-7.0.0", + "pddl-1": "PDDL-1.0", + "pddl-1.0": "PDDL-1.0", + "pddl-1.0.0": "PDDL-1.0", + "php-3": "PHP-3.0", + "php-3.0": "PHP-3.0", + "php-3.0.0": "PHP-3.0", + "php-3.01": "PHP-3.01", + "php-3.01.0": "PHP-3.01", + "plexus": "Plexus", + "polyform-noncommercial-1": "PolyForm-Noncommercial-1.0.0", + "polyform-noncommercial-1.0": "PolyForm-Noncommercial-1.0.0", + "polyform-noncommercial-1.0.0": "PolyForm-Noncommercial-1.0.0", + "polyform-small-business-1": "PolyForm-Small-Business-1.0.0", + "polyform-small-business-1.0": "PolyForm-Small-Business-1.0.0", + "polyform-small-business-1.0.0": "PolyForm-Small-Business-1.0.0", + "postgresql": "PostgreSQL", + "psf-2": "PSF-2.0", + "psf-2.0": "PSF-2.0", + "psf-2.0.0": "PSF-2.0", + "psfrag": "psfrag", + "psutils": "psutils", + "python-2": "Python-2.0", + "python-2.0": "Python-2.0", + "python-2.0.0": "Python-2.0", + "qhull": "Qhull", + "qpl-1": "QPL-1.0", + "qpl-1.0": "QPL-1.0", + "qpl-1.0.0": "QPL-1.0", + "rdisc": "Rdisc", + "rhecos-1": "RHeCos-1.1", + "rhecos-1.1": "RHeCos-1.1", + "rhecos-1.1.0": "RHeCos-1.1", + "rpl-1": "RPL-1.1", + "rpl-1.1": "RPL-1.1", + "rpl-1.1.0": "RPL-1.1", + "rpl-1.5": "RPL-1.5", + "rpl-1.5.0": "RPL-1.5", + "rpsl-1": "RPSL-1.0", + "rpsl-1.0": "RPSL-1.0", + "rpsl-1.0.0": "RPSL-1.0", + "rsa-md": "RSA-MD", + "rscpl": "RSCPL", + "ruby": "Ruby", + "sax-pd": "SAX-PD", + "saxpath": "Saxpath", + "scea": "SCEA", + "schemereport": "SchemeReport", + "sendmail": "Sendmail", + "sendmail-8": "Sendmail-8.23", + "sendmail-8.23": "Sendmail-8.23", + "sendmail-8.23.0": "Sendmail-8.23", + "sgi-b-1": "SGI-B-1.0", + "sgi-b-1.0": "SGI-B-1.0", + "sgi-b-1.0.0": "SGI-B-1.0", + "sgi-b-1.1": "SGI-B-1.1", + "sgi-b-1.1.0": "SGI-B-1.1", + "sgi-b-2": "SGI-B-2.0", + "sgi-b-2.0": "SGI-B-2.0", + "sgi-b-2.0.0": "SGI-B-2.0", + "shl-0.5": "SHL-0.5", + "shl-0.5.0": "SHL-0.5", + "shl-0.51": "SHL-0.51", + "shl-0.51.0": "SHL-0.51", + "simpl-2": "SimPL-2.0", + "simpl-2.0": "SimPL-2.0", + "simpl-2.0.0": "SimPL-2.0", + "sissl": "SISSL", + "sissl-1": "SISSL-1.2", + "sissl-1.2": "SISSL-1.2", + "sissl-1.2.0": "SISSL-1.2", + "sleepycat": "Sleepycat", + "smlnj": "SMLNJ", + "smppl": "SMPPL", + "snia": "SNIA", + "spencer-86": "Spencer-86", + "spencer-86.0": "Spencer-86", + "spencer-86.0.0": "Spencer-86", + "spencer-94": "Spencer-94", + "spencer-94.0": "Spencer-94", + "spencer-94.0.0": "Spencer-94", + "spencer-99": "Spencer-99", + "spencer-99.0": "Spencer-99", + "spencer-99.0.0": "Spencer-99", + "spl-1": "SPL-1.0", + "spl-1.0": "SPL-1.0", + "spl-1.0.0": "SPL-1.0", + "ssh-openssh": "SSH-OpenSSH", + "ssh-short": "SSH-short", + "sspl-1": "SSPL-1.0", + "sspl-1.0": "SSPL-1.0", + "sspl-1.0.0": "SSPL-1.0", + "standardml-nj": "StandardML-NJ", + "sugarcrm-1": "SugarCRM-1.1.3", + "sugarcrm-1.1": "SugarCRM-1.1.3", + "sugarcrm-1.1.3": "SugarCRM-1.1.3", + "swl": "SWL", + "tapr-ohl-1": "TAPR-OHL-1.0", + "tapr-ohl-1.0": "TAPR-OHL-1.0", + "tapr-ohl-1.0.0": "TAPR-OHL-1.0", + "tcl": "TCL", + "tcp-wrappers": "TCP-wrappers", + "tmate": "TMate", + "torque-1": "TORQUE-1.1", + "torque-1.1": "TORQUE-1.1", + "torque-1.1.0": "TORQUE-1.1", + "tosl": "TOSL", + "tu-berlin-1": "TU-Berlin-1.0", + "tu-berlin-1.0": "TU-Berlin-1.0", + "tu-berlin-1.0.0": "TU-Berlin-1.0", + "tu-berlin-2": "TU-Berlin-2.0", + "tu-berlin-2.0": "TU-Berlin-2.0", + "tu-berlin-2.0.0": "TU-Berlin-2.0", + "ucl-1": "UCL-1.0", + "ucl-1.0": "UCL-1.0", + "ucl-1.0.0": "UCL-1.0", + "unicode-dfs-2015": "Unicode-DFS-2015", + "unicode-dfs-2015.0": "Unicode-DFS-2015", + "unicode-dfs-2015.0.0": "Unicode-DFS-2015", + "unicode-dfs-2016": "Unicode-DFS-2016", + "unicode-dfs-2016.0": "Unicode-DFS-2016", + "unicode-dfs-2016.0.0": "Unicode-DFS-2016", + "unicode-tou": "Unicode-TOU", + "unlicense": "Unlicense", + "upl-1": "UPL-1.0", + "upl-1.0": "UPL-1.0", + "upl-1.0.0": "UPL-1.0", + "vim": "Vim", + "vostrom": "VOSTROM", + "vsl-1": "VSL-1.0", + "vsl-1.0": "VSL-1.0", + "vsl-1.0.0": "VSL-1.0", + "w3c": "W3C", + "w3c-19980720": "W3C-19980720", + "w3c-19980720.0": "W3C-19980720", + "w3c-19980720.0.0": "W3C-19980720", + "w3c-20150513": "W3C-20150513", + "w3c-20150513.0": "W3C-20150513", + "w3c-20150513.0.0": "W3C-20150513", + "watcom-1": "Watcom-1.0", + "watcom-1.0": "Watcom-1.0", + "watcom-1.0.0": "Watcom-1.0", + "wsuipa": "Wsuipa", + "wtfpl": "WTFPL", + "wxwindows": "wxWindows", + "x11": "X11", + "x11-distribute-modifications-variant": "X11-distribute-modifications-variant", + "xerox": "Xerox", + "xfree86-1": "XFree86-1.1", + "xfree86-1.1": "XFree86-1.1", + "xfree86-1.1.0": "XFree86-1.1", + "xinetd": "xinetd", + "xnet": "Xnet", + "xpp": "xpp", + "xskat": "XSkat", + "ypl-1": "YPL-1.0", + "ypl-1.0": "YPL-1.0", + "ypl-1.0.0": "YPL-1.0", + "ypl-1.1": "YPL-1.1", + "ypl-1.1.0": "YPL-1.1", + "zed": "Zed", + "zend-2": "Zend-2.0", + "zend-2.0": "Zend-2.0", + "zend-2.0.0": "Zend-2.0", + "zimbra-1": "Zimbra-1.3", + "zimbra-1.3": "Zimbra-1.3", + "zimbra-1.3.0": "Zimbra-1.3", + "zimbra-1.4": "Zimbra-1.4", + "zimbra-1.4.0": "Zimbra-1.4", + "zlib": "Zlib", + "zlib-acknowledgement": "zlib-acknowledgement", + "zpl-1": "ZPL-1.1", + "zpl-1.1": "ZPL-1.1", + "zpl-1.1.0": "ZPL-1.1", + "zpl-2": "ZPL-2.0", + "zpl-2.0": "ZPL-2.0", + "zpl-2.0.0": "ZPL-2.0", + "zpl-2.1": "ZPL-2.1", + "zpl-2.1.0": "ZPL-2.1", +} diff --git a/sbom/internal/spdxlicense/license_list_test.go b/sbom/internal/spdxlicense/license_list_test.go new file mode 100644 index 00000000..9b67902d --- /dev/null +++ b/sbom/internal/spdxlicense/license_list_test.go @@ -0,0 +1,15 @@ +package spdxlicense + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLicenceListIDs(t *testing.T) { + // do a sanity check on the generated data + assert.Equal(t, "0BSD", licenseIDs["0bsd"]) + assert.Equal(t, "ZPL-2.1", licenseIDs["zpl-2.1"]) + assert.Equal(t, "GPL-2.0", licenseIDs["gpl-2"]) + assert.NotEmpty(t, Version) +} diff --git a/sbom/internal/spdxlicense/license_test.go b/sbom/internal/spdxlicense/license_test.go new file mode 100644 index 00000000..ff236c17 --- /dev/null +++ b/sbom/internal/spdxlicense/license_test.go @@ -0,0 +1,60 @@ +package spdxlicense + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIDParse(t *testing.T) { + var tests = []struct { + shortName string + spdx string + }{ + { + "GPL-1-only", + "GPL-1.0-only", + }, + { + "GPL-2", + "GPL-2.0", + }, + { + "GPL-2+", + "GPL-2.0+", + }, + { + "GPL-3.0.0-or-later", + "GPL-3.0-or-later", + }, + { + "GPL-3-with-autoconf-exception", + "GPL-3.0-with-autoconf-exception", + }, + { + "CC-by-nc-3-de", + "CC-BY-NC-3.0-DE", + }, + // the below few cases are NOT expected, however, seem unavoidable given the current approach + { + "w3c-20150513.0.0", + "W3C-20150513", + }, + { + "spencer-86.0.0", + "Spencer-86", + }, + { + "unicode-dfs-2015.0.0", + "Unicode-DFS-2015", + }, + } + + for _, test := range tests { + t.Run(test.shortName, func(t *testing.T) { + got, exists := ID(test.shortName) + assert.True(t, exists) + assert.Equal(t, test.spdx, got) + }) + } +} diff --git a/sbom/internal/version/README.md b/sbom/internal/version/README.md new file mode 100644 index 00000000..986cd357 --- /dev/null +++ b/sbom/internal/version/README.md @@ -0,0 +1,5 @@ +# Source +The contents of this directory has been copied from anchore/syft's internal +`spdxlicense` package. The version copied is current as of [syft +0.42.3](https://github.com/anchore/syft/blob/cc2c0e57a0d02a1719b4e34d0793f09e9699c8b0/internal/version/). + diff --git a/sbom/internal/version/build.go b/sbom/internal/version/build.go new file mode 100644 index 00000000..6aec1458 --- /dev/null +++ b/sbom/internal/version/build.go @@ -0,0 +1,55 @@ +/* +Package version contains all build time metadata (version, build time, git commit, etc). +*/ +package version + +import ( + "fmt" + "runtime" + "strings" + + // "github.com/anchore/syft/internal" + "github.com/paketo-buildpacks/packit/v2/sbom/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/sbom/internal/version/update.go b/sbom/internal/version/update.go new file mode 100644 index 00000000..80ed0253 --- /dev/null +++ b/sbom/internal/version/update.go @@ -0,0 +1,74 @@ +package version + +import ( + "fmt" + "io" + "net/http" + "strings" + + hashiVersion "github.com/anchore/go-version" + // "github.com/anchore/syft/internal" + "github.com/paketo-buildpacks/packit/v2/sbom/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/sbom/internal/version/update_test.go b/sbom/internal/version/update_test.go new file mode 100644 index 00000000..c7975054 --- /dev/null +++ b/sbom/internal/version/update_test.go @@ -0,0 +1,218 @@ +package version + +import ( + "net/http" + "net/http/httptest" + "testing" + + hashiVersion "github.com/anchore/go-version" +) + +func TestIsUpdateAvailable(t *testing.T) { + tests := []struct { + name string + buildVersion string + latestVersion string + code int + isAvailable bool + newVersion string + err bool + }{ + { + name: "equal", + buildVersion: "1.0.0", + latestVersion: "1.0.0", + code: 200, + isAvailable: false, + newVersion: "", + err: false, + }, + { + name: "hasUpdate", + buildVersion: "1.0.0", + latestVersion: "1.2.0", + code: 200, + isAvailable: true, + newVersion: "1.2.0", + err: false, + }, + { + name: "aheadOfLatest", + buildVersion: "1.2.0", + latestVersion: "1.0.0", + code: 200, + isAvailable: false, + newVersion: "", + err: false, + }, + { + name: "EmptyUpdate", + buildVersion: "1.0.0", + latestVersion: "", + code: 200, + isAvailable: false, + newVersion: "", + err: true, + }, + { + name: "GarbageUpdate", + buildVersion: "1.0.0", + latestVersion: "hdfjksdhfhkj", + code: 200, + isAvailable: false, + newVersion: "", + err: true, + }, + { + name: "BadUpdate", + buildVersion: "1.0.0", + latestVersion: "1.0.", + code: 500, + isAvailable: false, + newVersion: "", + err: true, + }, + { + name: "NoBuildVersion", + buildVersion: valueNotProvided, + latestVersion: "1.0.0", + code: 200, + isAvailable: false, + newVersion: "", + err: false, + }, + { + name: "SnapshotBuildVersion", + buildVersion: "2.0.0-SHAPSHOT-a78bf9c", + latestVersion: "1.0.0", + code: 200, + isAvailable: false, + newVersion: "", + err: false, + }, + { + name: "BadUpdateValidVersion", + buildVersion: "1.0.0", + latestVersion: "2.0.0", + code: 404, + isAvailable: false, + newVersion: "", + err: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup mocks + // local... + version = test.buildVersion + // remote... + handler := http.NewServeMux() + handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.code) + _, _ = w.Write([]byte(test.latestVersion)) + }) + mockSrv := httptest.NewServer(handler) + latestAppVersionURL.host = mockSrv.URL + defer mockSrv.Close() + + isAvailable, newVersion, err := IsUpdateAvailable() + if err != nil && !test.err { + t.Fatalf("got error but expected none: %+v", err) + } else if err == nil && test.err { + t.Fatalf("expected error but got none") + } + + if newVersion != test.newVersion { + t.Errorf("unexpected NEW version: %+v", newVersion) + } + + if isAvailable != test.isAvailable { + t.Errorf("unexpected result: %+v", isAvailable) + } + }) + } + +} + +func TestFetchLatestApplicationVersion(t *testing.T) { + tests := []struct { + name string + response string + code int + err bool + expected *hashiVersion.Version + }{ + { + name: "gocase", + response: "1.0.0", + code: 200, + expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")), + }, + { + name: "garbage", + response: "garbage", + code: 200, + expected: nil, + err: true, + }, + { + name: "http 500", + response: "1.0.0", + code: 500, + expected: nil, + err: true, + }, + { + name: "http 404", + response: "1.0.0", + code: 404, + expected: nil, + err: true, + }, + { + name: "empty", + response: "", + 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, + expected: nil, + err: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // setup mock + handler := http.NewServeMux() + handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.code) + _, _ = w.Write([]byte(test.response)) + }) + mockSrv := httptest.NewServer(handler) + latestAppVersionURL.host = mockSrv.URL + defer mockSrv.Close() + + actual, err := fetchLatestApplicationVersion() + if err != nil && !test.err { + t.Fatalf("got error but expected none: %+v", err) + } else if err == nil && test.err { + t.Fatalf("expected error but got none") + } + + if err != nil { + return + } + + if actual.String() != test.expected.String() { + t.Errorf("unexpected version: %+v", actual.String()) + } + }) + } + +} diff --git a/sbom/sbom.go b/sbom/sbom.go index 473c8943..ab1e7bc7 100644 --- a/sbom/sbom.go +++ b/sbom/sbom.go @@ -1,15 +1,25 @@ +// Package sbom implements standardized SBoM tooling that allows multiple SBoM +// formats to be generated from the same scanning information. package sbom import ( "fmt" + "os" "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" - "github.com/paketo-buildpacks/packit/postal" + "github.com/paketo-buildpacks/packit/v2/postal" ) +// UnknownCPE is a Common Platform Enumeration (CPE) that uses the NA (Not +// applicable) logical operator for all components of its name. It is designed +// not to match with other CPEs, to avoid false positive CPE matches. +const UnknownCPE = "cpe:2.3:-:-:-:-:-:-:-:-:-:-:-" + // SBOM holds the internal representation of the generated software // bill-of-materials. This type can be combined with a FormattedReader to // output the SBoM in a number of file formats. @@ -17,14 +27,36 @@ type SBOM struct { syft sbom.SBOM } +func NewSBOM(syft sbom.SBOM) SBOM { + return SBOM{syft: syft} +} + // Generate returns a populated SBOM given a path to a directory to scan. func Generate(path string) (SBOM, error) { - src, err := source.NewFromDirectory(path) + info, err := os.Stat(path) if err != nil { return SBOM{}, err } - catalog, _, distro, err := syft.CatalogPackages(&src, source.UnknownScope) + var src source.Source + if info.IsDir() { + src, err = source.NewFromDirectory(path) + if err != nil { + return SBOM{}, err + } + } else { + var cleanup func() + src, cleanup = source.NewFromFile(path) + defer cleanup() + } + + config := cataloger.Config{ + Search: cataloger.SearchConfig{ + Scope: source.UnknownScope, + }, + } + + catalog, _, release, err := syft.CatalogPackages(&src, config) if err != nil { return SBOM{}, err } @@ -32,8 +64,8 @@ func Generate(path string) (SBOM, error) { return SBOM{ syft: sbom.SBOM{ Artifacts: sbom.Artifacts{ - PackageCatalog: catalog, - Distro: distro, + Packages: catalog, + LinuxDistribution: release, }, Source: src.Metadata, }, @@ -43,24 +75,40 @@ func Generate(path string) (SBOM, error) { // GenerateFromDependency returns a populated SBOM given a postal.Dependency // and the directory path where the dependency will be located within the // application image. + +//nolint Ignore SA1019, informed usage of deprecated package func GenerateFromDependency(dependency postal.Dependency, path string) (SBOM, error) { - cpe, err := pkg.NewCPE(dependency.CPE) - if err != nil { - return SBOM{}, err + + //nolint Ignore SA1019, informed usage of deprecated package + if dependency.CPE == "" { + dependency.CPE = UnknownCPE + } + if len(dependency.CPEs) == 0 { + //nolint Ignore SA1019, informed usage of deprecated package + dependency.CPEs = []string{dependency.CPE} + } + + var cpes []cpe.CPE + for _, cpeString := range dependency.CPEs { + cpe, err := cpe.New(cpeString) + if err != nil { + return SBOM{}, err + } + cpes = append(cpes, cpe) } catalog := pkg.NewCatalog(pkg.Package{ Name: dependency.Name, Version: dependency.Version, Licenses: dependency.Licenses, - CPEs: []pkg.CPE{cpe}, + CPEs: cpes, PURL: dependency.PURL, }) return SBOM{ syft: sbom.SBOM{ Artifacts: sbom.Artifacts{ - PackageCatalog: catalog, + Packages: catalog, }, Source: source.Metadata{ Scheme: source.DirectoryScheme, @@ -71,16 +119,20 @@ func GenerateFromDependency(dependency postal.Dependency, path string) (SBOM, er } // InFormats returns a Formatter containing mappings for the given Formats. -func (s SBOM) InFormats(formats ...string) (Formatter, error) { - var fs []Format - for _, f := range formats { - format := Format(f) +func (s SBOM) InFormats(mediaTypes ...string) (Formatter, error) { + var fs []sbom.FormatID + for _, m := range mediaTypes { + format, err := sbomFormatByMediaType(m) + if err != nil { + return Formatter{}, err + } + if format.Extension() == "" { - return Formatter{}, fmt.Errorf("%q is not a supported SBOM format", f) + return Formatter{}, fmt.Errorf("unable to determine file extension for SBOM format '%s'", format.ID()) } - fs = append(fs, format) + fs = append(fs, format.ID()) } - return Formatter{sbom: s, formats: fs}, nil + return Formatter{sbom: s, formatIDs: fs}, nil } diff --git a/sbom/sbom_format.go b/sbom/sbom_format.go new file mode 100644 index 00000000..ccc3b97e --- /dev/null +++ b/sbom/sbom_format.go @@ -0,0 +1,108 @@ +package sbom + +import ( + "fmt" + "mime" + + "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/sbom" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/cyclonedx13" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/spdx22" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft2" + "github.com/paketo-buildpacks/packit/v2/sbom/internal/formats/syft301" +) + +// TODO: refactor the version lookup part +var syftFormats map[string]sbom.FormatID = map[string]sbom.FormatID{ + "default": syft301.ID, + "3.0.1": syft301.ID, + "2.0.2": syft2.ID, +} + +var cyclonedxFormats map[string]sbom.FormatID = map[string]sbom.FormatID{ + "default": cyclonedx13.ID, + "1.4": syft.CycloneDxJSONFormatID, + "1.3": cyclonedx13.ID, +} + +var spdxFormats map[string]sbom.FormatID = map[string]sbom.FormatID{ + "default": spdx22.ID, + "2.2": spdx22.ID, +} + +var additionalFormats []sbomFormat + +func init() { + additionalFormats = []sbomFormat{ + newSBOMFormat(cyclonedx13.Format()), + newSBOMFormat(syft2.Format()), + newSBOMFormat(syft301.Format()), + newSBOMFormat(spdx22.Format()), + } +} + +// An experimental type added to support more SBOM formats +// It extends the Syft sbom.Format interface +type sbomFormat struct { + sbom.Format +} + +func newSBOMFormat(format sbom.Format) sbomFormat { + return sbomFormat{ + Format: format, + } +} + +func (f sbomFormat) Extension() string { + switch f.ID() { + case syft.CycloneDxJSONFormatID, cyclonedx13.ID: + return "cdx.json" + case syft.SPDXJSONFormatID, spdx22.ID: + return "spdx.json" + case syft.JSONFormatID, syft2.ID, syft301.ID: + return "syft.json" + default: + return "" + } +} + +func sbomFormatByMediaType(mediaType string) (sbomFormat, error) { + baseType, params, err := mime.ParseMediaType(mediaType) + if err != nil { + return sbomFormat{}, fmt.Errorf("failed to parse SBOM media type: %w", err) + } + // TODO: semver version parsing? + version, ok := params["version"] + if !ok { + version = "default" + } + var selected sbom.FormatID + switch baseType { + case CycloneDXFormat: + selected = cyclonedxFormats[version] + case SPDXFormat: + selected = spdxFormats[version] + case SyftFormat: + selected = syftFormats[version] + default: + return sbomFormat{}, fmt.Errorf("unsupported SBOM format: '%s'", mediaType) + } + + if selected == sbom.FormatID("") { + return sbomFormat{}, fmt.Errorf("version '%s' is not supported for SBOM format '%s'", version, baseType) + } + return sbomFormatByID(selected) +} + +func sbomFormatByID(id sbom.FormatID) (sbomFormat, error) { + for _, f := range additionalFormats { + if f.ID() == id { + return f, nil + } + } + format := syft.FormatByID(id) + if format == nil { + return sbomFormat{}, fmt.Errorf("'%s' is not a valid SBOM format identifier", id) + } + return newSBOMFormat(format), nil +} diff --git a/sbom/sbom_outputs_test.go b/sbom/sbom_outputs_test.go new file mode 100644 index 00000000..22264b41 --- /dev/null +++ b/sbom/sbom_outputs_test.go @@ -0,0 +1,75 @@ +package sbom_test + +import "time" + +/* A set of structs that are used to unmarshal SBOM JSON output in tests */ + +type license struct { + License struct { + ID string `json:"id"` + } `json:"license"` +} + +type component struct { + Type string `json:"type"` + Name string `json:"name"` + Version string `json:"version"` + Licenses []license `json:"licenses"` + PURL string `json:"purl"` +} + +type cdxOutput struct { + BOMFormat string `json:"bomFormat"` + SpecVersion string `json:"specVersion"` + SerialNumber string `json:"serialNumber"` + Metadata struct { + Timestamp string `json:"timestamp"` + Component struct { + Type string `json:"type"` + Name string `json:"name"` + } `json:"component"` + } `json:"metadata"` + Components []component `json:"components"` +} + +type artifact struct { + Name string `json:"name"` + Version string `json:"version"` + Licenses []string `json:"licenses"` + CPEs []string `json:"cpes"` + PURL string `json:"purl"` +} + +type syftOutput struct { + Artifacts []artifact `json:"artifacts"` + Source struct { + Type string `json:"type"` + Target string `json:"target"` + } `json:"source"` + Schema struct { + Version string `json:"version"` + } `json:"schema"` +} + +type externalRef struct { + Category string `json:"referenceCategory"` + Locator string `json:"referenceLocator"` + Type string `json:"referenceType"` +} + +type pkg struct { + ExternalRefs []externalRef `json:"externalRefs"` + LicenseConcluded string `json:"licenseConcluded"` + LicenseDeclared string `json:"licenseDeclared"` + Name string `json:"name"` + Version string `json:"versionInfo"` +} + +type spdxOutput struct { + Packages []pkg `json:"packages"` + SPDXVersion string `json:"spdxVersion"` + DocumentNamespace string `json:"documentNamespace"` + CreationInfo struct { + Created time.Time `json:"created"` + } `json:"creationInfo"` +} diff --git a/sbom/sbom_test.go b/sbom/sbom_test.go index ceb4caac..e26414a5 100644 --- a/sbom/sbom_test.go +++ b/sbom/sbom_test.go @@ -7,8 +7,9 @@ import ( "io" "testing" - "github.com/paketo-buildpacks/packit/postal" - "github.com/paketo-buildpacks/packit/sbom" + syftsbom "github.com/anchore/syft/syft/sbom" + "github.com/paketo-buildpacks/packit/v2/postal" + "github.com/paketo-buildpacks/packit/v2/sbom" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -17,20 +18,87 @@ import ( func testSBOM(t *testing.T, context spec.G, it spec.S) { var Expect = NewWithT(t).Expect + context("NewSBOM", func() { + it("constructs an SBOM given a syft.SBOM", func() { + bom := sbom.NewSBOM(syftsbom.SBOM{}) + formatter, err := bom.InFormats(sbom.SyftFormat) + Expect(err).NotTo(HaveOccurred()) + + buffer := bytes.NewBuffer(nil) + _, err = io.Copy(buffer, formatter.Formats()[0].Content) + Expect(err).NotTo(HaveOccurred()) + + var output syftOutput + err = json.NewDecoder(buffer).Decode(&output) + Expect(err).NotTo(HaveOccurred()) + Expect(output.Schema.Version).To(Equal(`3.0.1`), buffer.String()) + Expect(output.Artifacts).To(HaveLen(0)) + }) + }) + + context("Generate", func() { + context("when given a directory", func() { + it("generates an SBOM for that directory", func() { + bom, err := sbom.Generate("testdata/") + Expect(err).NotTo(HaveOccurred()) + + formatter, err := bom.InFormats(sbom.SyftFormat) + Expect(err).NotTo(HaveOccurred()) + + syft := bytes.NewBuffer(nil) + _, err = io.Copy(syft, formatter.Formats()[0].Content) + Expect(err).NotTo(HaveOccurred()) + + var syftOutput syftOutput + err = json.Unmarshal(syft.Bytes(), &syftOutput) + Expect(err).NotTo(HaveOccurred(), syft.String()) + Expect(syftOutput.Source.Type).To(Equal("directory"), syft.String()) + }) + }) + + context("when given a file", func() { + it("generates an SBOM for that file", func() { + bom, err := sbom.Generate("testdata/package-lock.json") + Expect(err).NotTo(HaveOccurred()) + + formatter, err := bom.InFormats(sbom.SyftFormat) + Expect(err).NotTo(HaveOccurred()) + + syft := bytes.NewBuffer(nil) + _, err = io.Copy(syft, formatter.Formats()[0].Content) + Expect(err).NotTo(HaveOccurred()) + + var syftOutput syftOutput + err = json.Unmarshal(syft.Bytes(), &syftOutput) + Expect(err).NotTo(HaveOccurred(), syft.String()) + Expect(syftOutput.Source.Type).To(Equal("file"), syft.String()) + }) + }) + + context("failure cases", func() { + context("when given a nonexistent path", func() { + it("returns an error", func() { + _, err := sbom.Generate("no/such/path") + Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) + }) + }) + }) + }) + context("GenerateFromDependency", func() { - it("generates a SBOM from a dependency", func() { + it("generates a SBOM from a dependency for latest schema versions", func() { bom, err := sbom.GenerateFromDependency(postal.Dependency{ - CPE: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", - ID: "go", - Licenses: []string{"BSD-3-Clause"}, - Name: "Go", - PURL: "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", - SHA256: "ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", - Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", - SourceSHA256: "0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", - Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, - URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", - Version: "1.16.9", + CPE: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", + ID: "go", + Licenses: []string{"BSD-3-Clause"}, + Name: "Go", + PURL: "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", + Checksum: "sha256:ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", + Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", + SourceChecksum: "sha256:0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", + Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, + URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", + Version: "1.16.9", }, "some-path") Expect(err).NotTo(HaveOccurred()) @@ -47,46 +115,21 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { } } - Expect(syft.String()).To(MatchJSON(`{ - "artifacts": [ - { - "id": "b0a2cd11c0e13e43", - "name": "Go", - "version": "1.16.9", - "type": "", - "foundBy": "", - "locations": [], - "licenses": [ - "BSD-3-Clause" - ], - "language": "", - "cpes": [ - "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*" - ], - "purl": "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", - "metadataType": "", - "metadata": null - } - ], - "artifactRelationships": [], - "source": { - "type": "directory", - "target": "some-path" - }, - "distro": { - "name": "", - "version": "", - "idLike": "" - }, - "descriptor": { - "name": "", - "version": "" - }, - "schema": { - "version": "2.0.0", - "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json" - } - }`)) + var syftDefaultOutput syftOutput + + err = json.NewDecoder(syft).Decode(&syftDefaultOutput) + Expect(err).NotTo(HaveOccurred(), syft.String()) + + Expect(syftDefaultOutput.Schema.Version).To(Equal(`3.0.1`), syft.String()) + + goArtifact := syftDefaultOutput.Artifacts[0] + Expect(goArtifact.Name).To(Equal("Go"), syft.String()) + Expect(goArtifact.Version).To(Equal("1.16.9"), syft.String()) + Expect(goArtifact.Licenses).To(Equal([]string{"BSD-3-Clause"}), syft.String()) + Expect(goArtifact.CPEs).To(Equal([]string{"cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*"}), syft.String()) + Expect(goArtifact.PURL).To(Equal("pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz"), syft.String()) + Expect(syftDefaultOutput.Source.Type).To(Equal("directory"), syft.String()) + Expect(syftDefaultOutput.Source.Target).To(Equal("some-path"), syft.String()) cdx := bytes.NewBuffer(nil) for _, format := range formats { @@ -96,51 +139,23 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { } } - var cdxOutput struct { - SerialNumber string `json:"serialNumber"` - Metadata struct { - Timestamp string `json:"timestamp"` - } `json:"metadata"` - } - err = json.Unmarshal(cdx.Bytes(), &cdxOutput) - Expect(err).NotTo(HaveOccurred()) + var cdxDefaultOutput cdxOutput - Expect(cdx.String()).To(MatchJSON(fmt.Sprintf(`{ - "bomFormat": "CycloneDX", - "specVersion": "1.3", - "version": 1, - "serialNumber": "%s", - "metadata": { - "timestamp": "%s", - "tools": [ - { - "vendor": "anchore", - "name": "syft", - "version": "[not provided]" - } - ], - "component": { - "type": "file", - "name": "some-path", - "version": "" - } - }, - "components": [ - { - "type": "library", - "name": "Go", - "version": "1.16.9", - "licenses": [ - { - "license": { - "name": "BSD-3-Clause" - } - } - ], - "purl": "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz" - } - ] - }`, cdxOutput.SerialNumber, cdxOutput.Metadata.Timestamp))) + err = json.Unmarshal(cdx.Bytes(), &cdxDefaultOutput) + Expect(err).NotTo(HaveOccurred(), cdx.String()) + + Expect(cdxDefaultOutput.BOMFormat).To(Equal("CycloneDX")) + Expect(cdxDefaultOutput.SpecVersion).To(Equal("1.3")) + + goComponent := cdxDefaultOutput.Components[0] + Expect(goComponent.Name).To(Equal("Go"), cdx.String()) + Expect(goComponent.Version).To(Equal("1.16.9"), cdx.String()) + Expect(goComponent.Licenses).To(HaveLen(1), cdx.String()) + Expect(goComponent.Licenses[0].License.ID).To(Equal("BSD-3-Clause"), cdx.String()) + Expect(goComponent.PURL).To(Equal("pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz"), cdx.String()) + + Expect(cdxDefaultOutput.Metadata.Component.Type).To(Equal("file"), cdx.String()) + Expect(cdxDefaultOutput.Metadata.Component.Name).To(Equal("some-path"), cdx.String()) spdx := bytes.NewBuffer(nil) for _, format := range formats { @@ -150,54 +165,268 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { } } - var spdxOutput struct { - CreationInfo struct { - Created string `json:"created"` - } `json:"creationInfo"` - DocumentNamespace string `json:"documentNamespace"` + var spdxDefaultOutput spdxOutput + + err = json.Unmarshal(spdx.Bytes(), &spdxDefaultOutput) + Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred(), spdx.String()) + + Expect(spdxDefaultOutput.SPDXVersion).To(Equal("SPDX-2.2"), spdx.String()) + + goPackage := spdxDefaultOutput.Packages[0] + Expect(goPackage.Name).To(Equal("Go"), spdx.String()) + Expect(goPackage.Version).To(Equal("1.16.9"), spdx.String()) + Expect(goPackage.LicenseConcluded).To(Equal("BSD-3-Clause"), spdx.String()) + Expect(goPackage.LicenseDeclared).To(Equal("BSD-3-Clause"), spdx.String()) + Expect(goPackage.ExternalRefs).To(ContainElement(externalRef{ + Category: "SECURITY", + Locator: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", + Type: "cpe23Type", + }), spdx.String()) + Expect(goPackage.ExternalRefs).To(ContainElement(externalRef{ + Category: "PACKAGE_MANAGER", + Locator: "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", + Type: "purl", + }), spdx.String()) + }) + + it("generates a SBOM from a dependency as syft2 JSON", func() { + bom, err := sbom.GenerateFromDependency(postal.Dependency{ + CPE: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", + ID: "go", + Licenses: []string{"BSD-3-Clause"}, + Name: "Go", + PURL: "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", + Checksum: "sha256:ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", + Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", + SourceChecksum: "sha256:0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", + Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, + URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", + Version: "1.16.9", + }, "some-path") + Expect(err).NotTo(HaveOccurred()) + + formatter, err := bom.InFormats(fmt.Sprintf("%s;version=2.0.2", sbom.SyftFormat)) + Expect(err).NotTo(HaveOccurred()) + + formats := formatter.Formats() + + syft := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "syft.json" { + _, err = io.Copy(syft, format.Content) + Expect(err).NotTo(HaveOccurred()) + } } - err = json.Unmarshal(spdx.Bytes(), &spdxOutput) + + var syft2Output syftOutput + + err = json.Unmarshal(syft.Bytes(), &syft2Output) + Expect(err).NotTo(HaveOccurred(), syft.String()) + + Expect(syft2Output.Schema.Version).To(Equal("2.0.2"), syft.String()) + + goArtifact := syft2Output.Artifacts[0] + Expect(goArtifact.Name).To(Equal("Go"), syft.String()) + Expect(goArtifact.Version).To(Equal("1.16.9"), syft.String()) + Expect(goArtifact.Licenses).To(Equal([]string{"BSD-3-Clause"}), syft.String()) + Expect(goArtifact.CPEs).To(Equal([]string{"cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*"}), syft.String()) + Expect(goArtifact.PURL).To(Equal("pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz"), syft.String()) + Expect(syft2Output.Source.Type).To(Equal("directory"), syft.String()) + Expect(syft2Output.Source.Target).To(Equal("some-path"), syft.String()) + }) + + it("generates a SBOM from a dependency in CycloneDX 1.4 JSON", func() { + bom, err := sbom.GenerateFromDependency(postal.Dependency{ + CPE: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", + ID: "go", + Licenses: []string{"BSD-3-Clause"}, + Name: "Go", + PURL: "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", + Checksum: "sha256:ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", + Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", + SourceChecksum: "sha256:0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", + Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, + URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", + Version: "1.16.9", + }, "some-path") Expect(err).NotTo(HaveOccurred()) - Expect(spdx.String()).To(MatchJSON(fmt.Sprintf(`{ - "SPDXID": "SPDXRef-DOCUMENT", - "name": "some-path", - "spdxVersion": "SPDX-2.2", - "creationInfo": { - "created": "%s", - "creators": [ - "Organization: Anchore, Inc", - "Tool: syft-[not provided]" - ], - "licenseListVersion": "3.15" - }, - "dataLicense": "CC0-1.0", - "documentNamespace": "%s", - "packages": [ - { - "SPDXID": "SPDXRef-b0a2cd11c0e13e43", - "name": "Go", - "licenseConcluded": "BSD-3-Clause", - "downloadLocation": "NOASSERTION", - "externalRefs": [ - { - "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", - "referenceType": "cpe23Type" - }, - { - "referenceCategory": "PACKAGE_MANAGER", - "referenceLocator": "pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz", - "referenceType": "purl" - } - ], - "filesAnalyzed": false, - "licenseDeclared": "BSD-3-Clause", - "sourceInfo": "acquired package info from the following paths: ", - "versionInfo": "1.16.9" + formatter, err := bom.InFormats(fmt.Sprintf("%s;version=1.4", sbom.CycloneDXFormat)) + Expect(err).NotTo(HaveOccurred()) + + formats := formatter.Formats() + + cdx := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "cdx.json" { + _, err = io.Copy(cdx, format.Content) + Expect(err).NotTo(HaveOccurred()) + } + } + + var cdx14Output cdxOutput + + err = json.Unmarshal(cdx.Bytes(), &cdx14Output) + Expect(err).NotTo(HaveOccurred(), cdx.String()) + + Expect(cdx14Output.BOMFormat).To(Equal("CycloneDX")) + Expect(cdx14Output.SpecVersion).To(Equal("1.4")) + + goComponent := cdx14Output.Components[0] + Expect(goComponent.Name).To(Equal("Go"), cdx.String()) + Expect(goComponent.Version).To(Equal("1.16.9"), cdx.String()) + Expect(goComponent.Licenses).To(HaveLen(1), cdx.String()) + Expect(goComponent.Licenses[0].License.ID).To(Equal("BSD-3-Clause"), cdx.String()) + Expect(goComponent.PURL).To(Equal("pkg:generic/go@go1.16.9?checksum=0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d&download_url=https://dl.google.com/go/go1.16.9.src.tar.gz"), cdx.String()) + + Expect(cdx14Output.Metadata.Component.Type).To(Equal("file"), cdx.String()) + Expect(cdx14Output.Metadata.Component.Name).To(Equal("some-path"), cdx.String()) + }) + + context("when the input dependency does not have a CPE or a PURL", func() { + it("succeeds in generating an SBOM without CPEs", func() { + bom, err := sbom.GenerateFromDependency(postal.Dependency{ + ID: "go", + Licenses: []string{"BSD-3-Clause"}, + Name: "Go", + Checksum: "sha256:ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", + Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", + SourceChecksum: "sha256:0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", + Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, + URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", + Version: "1.16.9", + }, "some-path") + Expect(err).NotTo(HaveOccurred()) + + formatter, err := bom.InFormats(sbom.SyftFormat, sbom.CycloneDXFormat, sbom.SPDXFormat) + Expect(err).NotTo(HaveOccurred()) + + formats := formatter.Formats() + + syft := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "syft.json" { + _, err = io.Copy(syft, format.Content) + Expect(err).NotTo(HaveOccurred()) } - ] - }`, spdxOutput.CreationInfo.Created, spdxOutput.DocumentNamespace))) + } + + var syftDefaultOutput syftOutput + err = json.NewDecoder(syft).Decode(&syftDefaultOutput) + Expect(err).NotTo(HaveOccurred(), syft.String()) + + Expect(syftDefaultOutput.Schema.Version).To(Equal(`3.0.1`), syft.String()) + + goArtifact := syftDefaultOutput.Artifacts[0] + Expect(goArtifact.Name).To(Equal("Go"), syft.String()) + Expect(goArtifact.Version).To(Equal("1.16.9"), syft.String()) + Expect(goArtifact.Licenses).To(Equal([]string{"BSD-3-Clause"}), syft.String()) + Expect(syftDefaultOutput.Source.Type).To(Equal("directory"), syft.String()) + Expect(syftDefaultOutput.Source.Target).To(Equal("some-path"), syft.String()) + Expect(goArtifact.PURL).To(BeEmpty()) + Expect(goArtifact.CPEs).To(Equal([]string{"cpe:2.3:-:-:-:-:-:-:-:-:-:-:-"})) + + cdx := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "cdx.json" { + _, err = io.Copy(cdx, format.Content) + Expect(err).NotTo(HaveOccurred()) + } + } + + var cdxDefaultOutput cdxOutput + err = json.Unmarshal(cdx.Bytes(), &cdxDefaultOutput) + Expect(err).NotTo(HaveOccurred(), cdx.String()) + + Expect(cdxDefaultOutput.BOMFormat).To(Equal("CycloneDX")) + Expect(cdxDefaultOutput.SpecVersion).To(Equal("1.3")) + + goComponent := cdxDefaultOutput.Components[0] + Expect(goComponent.Name).To(Equal("Go"), cdx.String()) + Expect(goComponent.Version).To(Equal("1.16.9"), cdx.String()) + Expect(goComponent.Licenses).To(HaveLen(1), cdx.String()) + Expect(goComponent.Licenses[0].License.ID).To(Equal("BSD-3-Clause"), cdx.String()) + Expect(goComponent.PURL).To(BeEmpty()) + + Expect(cdxDefaultOutput.Metadata.Component.Type).To(Equal("file"), cdx.String()) + Expect(cdxDefaultOutput.Metadata.Component.Name).To(Equal("some-path"), cdx.String()) + + spdx := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "spdx.json" { + _, err = io.Copy(spdx, format.Content) + Expect(err).NotTo(HaveOccurred()) + } + } + + var spdxDefaultOutput spdxOutput + err = json.Unmarshal(spdx.Bytes(), &spdxDefaultOutput) + Expect(err).NotTo(HaveOccurred()) + Expect(err).NotTo(HaveOccurred(), spdx.String()) + + Expect(spdxDefaultOutput.SPDXVersion).To(Equal("SPDX-2.2"), spdx.String()) + + goPackage := spdxDefaultOutput.Packages[0] + Expect(goPackage.Name).To(Equal("Go"), spdx.String()) + Expect(goPackage.Version).To(Equal("1.16.9"), spdx.String()) + Expect(goPackage.LicenseConcluded).To(Equal("BSD-3-Clause"), spdx.String()) + Expect(goPackage.LicenseDeclared).To(Equal("BSD-3-Clause"), spdx.String()) + Expect(goPackage.ExternalRefs).To(Equal([]externalRef{{ + Category: "SECURITY", + Locator: "cpe:2.3:-:-:-:-:-:-:-:-:-:-:-", + Type: "cpe23Type", + }}), spdx.String()) + }) + }) + context("when the input dependency has CPEs and CPE", func() { + it("uses CPEs, not CPE", func() { + bom, err := sbom.GenerateFromDependency(postal.Dependency{ + CPE: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", + CPEs: []string{"cpe:2.3:a:some:other:cpe:*:*:*:*:*:*:*", "cpe:2.3:a:another:cpe:to:include:*:*:*:*:*:*"}, + ID: "go", + Licenses: []string{"BSD-3-Clause"}, + Name: "Go", + Checksum: "sha256:ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", + Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", + SourceChecksum: "sha256:0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", + Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, + URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", + Version: "1.16.9", + }, "some-path") + Expect(err).NotTo(HaveOccurred()) + + formatter, err := bom.InFormats(sbom.SyftFormat, sbom.CycloneDXFormat, sbom.SPDXFormat) + Expect(err).NotTo(HaveOccurred()) + + formats := formatter.Formats() + + syft := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "syft.json" { + _, err = io.Copy(syft, format.Content) + Expect(err).NotTo(HaveOccurred()) + } + } + + var syftDefaultOutput syftOutput + err = json.NewDecoder(syft).Decode(&syftDefaultOutput) + Expect(err).NotTo(HaveOccurred(), syft.String()) + + Expect(syftDefaultOutput.Schema.Version).To(Equal(`3.0.1`), syft.String()) + + goArtifact := syftDefaultOutput.Artifacts[0] + Expect(goArtifact.Name).To(Equal("Go"), syft.String()) + Expect(goArtifact.Version).To(Equal("1.16.9"), syft.String()) + Expect(goArtifact.Licenses).To(Equal([]string{"BSD-3-Clause"}), syft.String()) + Expect(syftDefaultOutput.Source.Type).To(Equal("directory"), syft.String()) + Expect(syftDefaultOutput.Source.Target).To(Equal("some-path"), syft.String()) + Expect(goArtifact.PURL).To(BeEmpty()) + Expect(goArtifact.CPEs).To(Equal([]string{ + "cpe:2.3:a:some:other:cpe:*:*:*:*:*:*:*", + "cpe:2.3:a:another:cpe:to:include:*:*:*:*:*:*", + })) + }) }) context("failure cases", func() { @@ -217,7 +446,13 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { context("when a format is not supported", func() { it("returns an error", func() { _, err := sbom.SBOM{}.InFormats("unknown-format") - Expect(err).To(MatchError(`"unknown-format" is not a supported SBOM format`)) + Expect(err).To(MatchError(`unsupported SBOM format: 'unknown-format'`)) + }) + }) + context("when a requested version is not supported", func() { + it("returns an error", func() { + _, err := sbom.SBOM{}.InFormats(fmt.Sprintf("%s;version=0.0.0", sbom.SyftFormat)) + Expect(err).To(MatchError(fmt.Sprintf(`version '0.0.0' is not supported for SBOM format '%s'`, sbom.SyftFormat))) }) }) }) diff --git a/scribe/color_test.go b/scribe/color_test.go index 07167378..18a82279 100644 --- a/scribe/color_test.go +++ b/scribe/color_test.go @@ -3,7 +3,7 @@ package scribe_test import ( "testing" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/scribe/emitter.go b/scribe/emitter.go index f2ec5206..c90bfb24 100644 --- a/scribe/emitter.go +++ b/scribe/emitter.go @@ -7,10 +7,51 @@ import ( "strings" "time" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/postal" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/postal" ) +type launchProcess interface { + GetType() string + GetCommand() []string + GetArgs() []string + GetDefault() bool +} + +type indirectProcess struct { + packit.Process +} + +func (p indirectProcess) GetType() string { + return p.Type +} +func (p indirectProcess) GetCommand() []string { + return strings.Split(p.Command, " ") +} +func (p indirectProcess) GetArgs() []string { + return p.Args +} +func (p indirectProcess) GetDefault() bool { + return p.Default +} + +type directProcess struct { + packit.DirectProcess +} + +func (p directProcess) GetType() string { + return p.Type +} +func (p directProcess) GetCommand() []string { + return p.Command +} +func (p directProcess) GetArgs() []string { + return p.Args +} +func (p directProcess) GetDefault() bool { + return p.Default +} + // An Emitter embeds the scribe.Logger type to provide an interface for // complicated shared logging tasks. type Emitter struct { @@ -26,6 +67,13 @@ func NewEmitter(output io.Writer) Emitter { } } +// WithLevel takes in a log level string and configures the underlying Logger +// log level. To enable debug logging the log level must be set to "DEBUG". +func (e Emitter) WithLevel(level string) Emitter { + e.Logger = e.Logger.WithLevel(level) + return e +} + // SelectedDependency takes in a buildpack plan entry, a postal dependency, and // the current time, and prints out a message giving the name and version of // the dependency as well as the source of the request for that given @@ -99,11 +147,31 @@ Entries: e.Break() } -// LaunchProcesses take a list of processes and a map of process specific +// LaunchProcesses take a list of (indirect) processes and a map of process specific // enivronment varables and prints out a formatted table including the type // name, whether or not it is a default process, the command, arguments, and // any process specific environment variables. func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[string]packit.Environment) { + launchProcesses := []launchProcess{} + for _, process := range processes { + launchProcesses = append(launchProcesses, indirectProcess{process}) + } + e.launch(launchProcesses, processEnvs...) +} + +// LaunchDirectProcesses take a list of direct processes and a map of process specific +// enivronment varables and prints out a formatted table including the type +// name, whether or not it is a default process, the command, arguments, and +// any process specific environment variables. +func (e Emitter) LaunchDirectProcesses(processes []packit.DirectProcess, processEnvs ...map[string]packit.Environment) { + launchProcesses := []launchProcess{} + for _, process := range processes { + launchProcesses = append(launchProcesses, directProcess{process}) + } + e.launch(launchProcesses, processEnvs...) +} + +func (e Emitter) launch(processes []launchProcess, processEnvs ...map[string]packit.Environment) { e.Process("Assigning launch processes:") var ( @@ -111,8 +179,8 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ ) for _, process := range processes { - pType := process.Type - if process.Default { + pType := process.GetType() + if process.GetDefault() { pType += " " + "(default)" } @@ -122,16 +190,17 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ } for _, process := range processes { - pType := process.Type - if process.Default { + pType := process.GetType() + if process.GetDefault() { pType += " " + "(default)" } - pad := typePadding + len(process.Command) - len(pType) - p := fmt.Sprintf("%s: %*s", pType, pad, process.Command) + command := strings.Join(process.GetCommand(), " ") + pad := typePadding + len(command) - len(pType) + p := fmt.Sprintf("%s: %*s", pType, pad, command) - if process.Args != nil { - p += " " + strings.Join(process.Args, " ") + if process.GetArgs() != nil { + p += " " + strings.Join(process.GetArgs(), " ") } e.Subprocess(p) @@ -140,7 +209,7 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ // matter the order of the process envs map list processEnv := packit.Environment{} for _, pEnvs := range processEnvs { - if env, ok := pEnvs[process.Type]; ok { + if env, ok := pEnvs[process.GetType()]; ok { for key, value := range env { processEnv[key] = value } @@ -155,7 +224,7 @@ func (e Emitter) LaunchProcesses(processes []packit.Process, processEnvs ...map[ e.Break() } -// EnvironmentVariables take a layer and prints out a formatted table of the +// EnvironmentVariables takes a layer and prints out a formatted table of the // build and launch time environment variables set in the layer. func (e Emitter) EnvironmentVariables(layer packit.Layer) { buildEnv := packit.Environment{} @@ -189,3 +258,40 @@ func (e Emitter) EnvironmentVariables(layer packit.Layer) { e.Break() } } + +// LayerFlags takes a layer and prints out the state of the build, launch, +// and cache layer flags in human-readable language. +func (e Emitter) LayerFlags(layer packit.Layer) { + e.Debug.Process("Setting up layer '%s'", layer.Name) + e.Debug.Subprocess("Available at app launch: %t", layer.Launch) + e.Debug.Subprocess("Available to other buildpacks: %t", layer.Build) + e.Debug.Subprocess("Cached for rebuilds: %t", layer.Cache) + e.Debug.Break() +} + +// GeneratingSBOM takes a path to a directory and logs that an SBOM is +// being generated for that directory. +func (e Emitter) GeneratingSBOM(path string) { + e.Process("Generating SBOM for %s", path) +} + +// FormattingSBOM takes a list of SBOM formats and logs that an SBOM is +// generated in each format. Note: Only logs when the emitter is in DEBUG +// mode. +func (e Emitter) FormattingSBOM(formats ...string) { + e.Debug.Process("Writing SBOM in the following format(s):") + for _, f := range formats { + e.Debug.Subprocess(f) + } + e.Debug.Break() +} + +// BuildConfiguration takes a map representing environment variables +// that will configure a buildpack build and prints them in a formatted +// table. +func (e Emitter) BuildConfiguration(envVars map[string]string) { + formatted := NewFormattedMapFromEnvironment(envVars) + e.Debug.Process("Build configuration:") + e.Debug.Subprocess(formatted.String()) + e.Debug.Break() +} diff --git a/scribe/emitter_test.go b/scribe/emitter_test.go index 3d60b700..b02b45af 100644 --- a/scribe/emitter_test.go +++ b/scribe/emitter_test.go @@ -5,13 +5,13 @@ import ( "testing" "time" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/postal" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/postal" + "github.com/paketo-buildpacks/packit/v2/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" - . "github.com/paketo-buildpacks/packit/matchers" + . "github.com/paketo-buildpacks/packit/v2/matchers" ) func testEmitter(t *testing.T, context spec.G, it spec.S) { @@ -149,6 +149,38 @@ func testEmitter(t *testing.T, context spec.G, it spec.S) { }) }) + context("WithLevel", func() { + context("default", func() { + it("output includes debug level logs", func() { + emitter.Title("non-debug title") + emitter.Debug.Title("debug title") + + Expect(buffer.String()).To(ContainLines( + "non-debug title", + )) + Expect(buffer.String()).ToNot(ContainLines( + "debug title", + )) + }) + }) + + context("DEBUG", func() { + it.Before(func() { + emitter = emitter.WithLevel("DEBUG") + }) + + it("output includes debug level logs", func() { + emitter.Title("non-debug title") + emitter.Debug.Title("debug title") + + Expect(buffer.String()).To(ContainLines( + "non-debug title", + "debug title", + )) + }) + }) + }) + context("Candidates", func() { it("logs the candidate entries", func() { emitter.Candidates([]packit.BuildpackPlanEntry{ @@ -291,6 +323,74 @@ func testEmitter(t *testing.T, context spec.G, it spec.S) { }) }) + context("LaunchDirectProcesses", func() { + var processes []packit.DirectProcess + + it.Before(func() { + processes = []packit.DirectProcess{ + { + Type: "some-type", + Command: []string{"some-command"}, + }, + { + Type: "web", + Command: []string{"web-command"}, + Default: true, + }, + { + Type: "some-other-type", + Command: []string{"some-other-command"}, + Args: []string{"some", "args"}, + }, + } + }) + + it("prints a list of launch processes", func() { + emitter.LaunchDirectProcesses(processes) + + Expect(buffer.String()).To(ContainLines( + " Assigning launch processes:", + " some-type: some-command", + " web (default): web-command", + " some-other-type: some-other-command some args", + "", + )) + }) + + context("when passed process specific environment variables", func() { + var processEnvs []map[string]packit.Environment + + it.Before(func() { + processEnvs = []map[string]packit.Environment{ + { + "web": packit.Environment{ + "WEB_VAR.default": "some-env", + }, + }, + { + "web": packit.Environment{ + "ANOTHER_WEB_VAR.default": "another-env", + }, + }, + } + }) + + it("prints a list of the launch processes and their processes specific env vars", func() { + emitter.LaunchDirectProcesses(processes, processEnvs...) + + Expect(buffer.String()).To(ContainLines( + " Assigning launch processes:", + " some-type: some-command", + " web (default): web-command", + ` ANOTHER_WEB_VAR -> "another-env"`, + ` WEB_VAR -> "some-env"`, + " some-other-type: some-other-command some args", + "", + )) + }) + }) + }) + context("EnvironmentVariables", func() { it("prints a list of environment variables available during launch and build", func() { emitter.EnvironmentVariables(packit.Layer{ @@ -367,4 +467,90 @@ func testEmitter(t *testing.T, context spec.G, it spec.S) { }) }) }) + context("LayerFlags", func() { + context("when log level is INFO", func() { + it("prints information about launch, cache, build flags on layer", func() { + emitter.LayerFlags(packit.Layer{Name: "some-layer"}) + Expect(buffer.String()).To(BeEmpty()) + }) + }) + context("when log level is DEBUG", func() { + it.Before(func() { + emitter = scribe.NewEmitter(buffer).WithLevel("DEBUG") + }) + it("prints information about launch, cache, build flags on layer", func() { + emitter.LayerFlags(packit.Layer{ + Name: "some-layer", + Launch: false, + Build: true, + Cache: false, + }) + Expect(buffer.String()).To(ContainLines( + " Setting up layer 'some-layer'", + " Available at app launch: false", + " Available to other buildpacks: true", + " Cached for rebuilds: false", + )) + }) + }) + }) + + context("GeneratingSBOM", func() { + it("prints the correct log line with the inputted path", func() { + emitter.GeneratingSBOM("/some/path") + + Expect(buffer.String()).To(ContainSubstring("Generating SBOM for /some/path")) + }) + }) + + context("FormattingSBOM", func() { + context("when log level is INFO", func() { + it("does not print anything", func() { + emitter.FormattingSBOM("format1", "format2") + Expect(buffer.String()).To(BeEmpty()) + }) + + context("when the log level is DEBUG", func() { + it.Before(func() { + emitter = scribe.NewEmitter(buffer).WithLevel("DEBUG") + }) + + it("lists the inputted SBOM formats", func() { + emitter.FormattingSBOM("format1", "format2") + Expect(buffer.String()).To(ContainLines( + " Writing SBOM in the following format(s):", + " format1", + " format2", + )) + }) + }) + }) + }) + + context("BuildConfiguration", func() { + context("when log level is INFO", func() { + it("does not print anything", func() { + emitter.BuildConfiguration(map[string]string{"ENV_VAR": "value"}) + Expect(buffer.String()).To(BeEmpty()) + }) + + context("when the log level is DEBUG", func() { + it.Before(func() { + emitter = scribe.NewEmitter(buffer).WithLevel("DEBUG") + }) + + it("lists the environment variables of the build configuration and their values", func() { + emitter.BuildConfiguration(map[string]string{ + "OTHER_ENV_VAR": "another-value", + "ENV_VAR": "value", + }) + Expect(buffer.String()).To(ContainLines( + " Build configuration:", + ` ENV_VAR -> "value"`, + ` OTHER_ENV_VAR -> "another-value"`, + )) + }) + }) + }) + }) } diff --git a/scribe/example_test.go b/scribe/example_test.go index 23689267..2b26c13f 100644 --- a/scribe/example_test.go +++ b/scribe/example_test.go @@ -6,9 +6,9 @@ import ( "os" "time" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/postal" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/postal" + "github.com/paketo-buildpacks/packit/v2/scribe" ) func ExampleEmitter() { @@ -136,7 +136,7 @@ func ExampleEmitter_LaunchProcesses() { // some-type: some-command // web (default): web-command // some-other-type: some-other-command some args - + // // Assigning launch processes: // some-type: some-command // web (default): web-command diff --git a/scribe/formatted_list_test.go b/scribe/formatted_list_test.go index 7075408c..0df4a0ba 100644 --- a/scribe/formatted_list_test.go +++ b/scribe/formatted_list_test.go @@ -3,7 +3,7 @@ package scribe_test import ( "testing" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/scribe/formatted_map.go b/scribe/formatted_map.go index 16f19629..16acda14 100644 --- a/scribe/formatted_map.go +++ b/scribe/formatted_map.go @@ -4,8 +4,6 @@ import ( "fmt" "sort" "strings" - - "github.com/paketo-buildpacks/packit" ) // A FormattedMap is a wrapper for map[string]interface{} to extend functionality. @@ -46,18 +44,31 @@ func (m FormattedMap) String() string { // NewFormattedMapFromEnvironment take an environment and returns a // FormattedMap with the appropriate environment variable information added. -func NewFormattedMapFromEnvironment(environment packit.Environment) FormattedMap { +func NewFormattedMapFromEnvironment(environment map[string]string) FormattedMap { envMap := FormattedMap{} for key, value := range environment { parts := strings.SplitN(key, ".", 2) + if len(parts) < 2 { + envMap[key] = value + continue + } + switch { case parts[1] == "override" || parts[1] == "default": envMap[parts[0]] = value case parts[1] == "prepend": - envMap[parts[0]] = strings.Join([]string{value, "$" + parts[0]}, environment[parts[0]+".delim"]) + existingValue, ok := envMap[parts[0]] + if !ok { + existingValue = "$" + parts[0] + } + envMap[parts[0]] = strings.Join([]string{value, fmt.Sprintf("%v", existingValue)}, environment[parts[0]+".delim"]) case parts[1] == "append": - envMap[parts[0]] = strings.Join([]string{"$" + parts[0], value}, environment[parts[0]+".delim"]) + existingValue, ok := envMap[parts[0]] + if !ok { + existingValue = "$" + parts[0] + } + envMap[parts[0]] = strings.Join([]string{fmt.Sprintf("%v", existingValue), value}, environment[parts[0]+".delim"]) } } diff --git a/scribe/formatted_map_test.go b/scribe/formatted_map_test.go index 1a79b730..89daa32a 100644 --- a/scribe/formatted_map_test.go +++ b/scribe/formatted_map_test.go @@ -3,8 +3,8 @@ package scribe_test import ( "testing" - "github.com/paketo-buildpacks/packit" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2" + "github.com/paketo-buildpacks/packit/v2/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -24,7 +24,7 @@ func testFormattedMap(t *testing.T, context spec.G, it spec.S) { }) context("NewFormattedMapFromEnvironment", func() { - context("when the operation is override", func() { + context("for all packit env var operations", func() { it("prints the env in a well formatted map", func() { Expect(scribe.NewFormattedMapFromEnvironment(packit.Environment{ "OVERRIDE.override": "some-value", @@ -33,11 +33,26 @@ func testFormattedMap(t *testing.T, context spec.G, it spec.S) { "PREPEND.delim": ":", "APPEND.append": "some-value", "APPEND.delim": ":", + "BOTH.append": "appended-value", + "BOTH.delim": ":", + "BOTH.prepend": "prepended-value", })).To(Equal(scribe.FormattedMap{ "OVERRIDE": "some-value", "DEFAULT": "some-value", "PREPEND": "some-value:$PREPEND", "APPEND": "$APPEND:some-value", + "BOTH": "prepended-value:$BOTH:appended-value", + })) + }) + }) + context("for a standard string map", func() { + it("prints the env in a well formatted map", func() { + Expect(scribe.NewFormattedMapFromEnvironment(map[string]string{ + "SOME_ENV_VAR": "some-value", + "SOME_OTHER_ENV_VAR": "some-other-value", + })).To(Equal(scribe.FormattedMap{ + "SOME_ENV_VAR": "some-value", + "SOME_OTHER_ENV_VAR": "some-other-value", })) }) }) diff --git a/scribe/logger.go b/scribe/logger.go index e58edcce..bbb65273 100644 --- a/scribe/logger.go +++ b/scribe/logger.go @@ -45,66 +45,66 @@ func (l Logger) WithLevel(level string) Logger { // A LeveledLogger provides a standard interface for basic formatted logging. type LeveledLogger struct { - title io.Writer - process io.Writer - subprocess io.Writer - action io.Writer - detail io.Writer - subdetail io.Writer + TitleWriter io.Writer + ProcessWriter io.Writer + SubprocessWriter io.Writer + ActionWriter io.Writer + DetailWriter io.Writer + SubdetailWriter io.Writer } // NewLeveledLogger takes a writer and returns a LeveledLogger that writes to the given // writer. func NewLeveledLogger(writer io.Writer) LeveledLogger { return LeveledLogger{ - title: NewWriter(writer), - process: NewWriter(writer, WithIndent(1)), - subprocess: NewWriter(writer, WithIndent(2)), - action: NewWriter(writer, WithIndent(3)), - detail: NewWriter(writer, WithIndent(4)), - subdetail: NewWriter(writer, WithIndent(5)), + TitleWriter: NewWriter(writer), + ProcessWriter: NewWriter(writer, WithIndent(1)), + SubprocessWriter: NewWriter(writer, WithIndent(2)), + ActionWriter: NewWriter(writer, WithIndent(3)), + DetailWriter: NewWriter(writer, WithIndent(4)), + SubdetailWriter: NewWriter(writer, WithIndent(5)), } } // Title takes a string and optional formatting, and prints a formatted string // with zero levels of indentation. func (l LeveledLogger) Title(format string, v ...interface{}) { - l.printf(l.title, format, v...) + l.printf(l.TitleWriter, format, v...) } // Process takes a string and optional formatting, and prints a formatted string // with one level of indentation. func (l LeveledLogger) Process(format string, v ...interface{}) { - l.printf(l.process, format, v...) + l.printf(l.ProcessWriter, format, v...) } // Subprocess takes a string and optional formatting, and prints a formatted string // with two levels of indentation. func (l LeveledLogger) Subprocess(format string, v ...interface{}) { - l.printf(l.subprocess, format, v...) + l.printf(l.SubprocessWriter, format, v...) } // Action takes a string and optional formatting, and prints a formatted string // with three levels of indentation. func (l LeveledLogger) Action(format string, v ...interface{}) { - l.printf(l.action, format, v...) + l.printf(l.ActionWriter, format, v...) } // Detail takes a string and optional formatting, and prints a formatted string // with four levels of indentation. func (l LeveledLogger) Detail(format string, v ...interface{}) { - l.printf(l.detail, format, v...) + l.printf(l.DetailWriter, format, v...) } // Subdetail takes a string and optional formatting, and prints a formatted string // with five levels of indentation. func (l LeveledLogger) Subdetail(format string, v ...interface{}) { - l.printf(l.subdetail, format, v...) + l.printf(l.SubdetailWriter, format, v...) } // Break inserts a line break in the log output func (l LeveledLogger) Break() { - l.printf(l.title, "\n") + l.printf(l.TitleWriter, "\n") } func (l LeveledLogger) printf(writer io.Writer, format string, v ...interface{}) { diff --git a/scribe/logger_test.go b/scribe/logger_test.go index 0a9a43d3..ee8222b3 100644 --- a/scribe/logger_test.go +++ b/scribe/logger_test.go @@ -4,11 +4,11 @@ import ( "bytes" "testing" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" - . "github.com/paketo-buildpacks/packit/matchers" + . "github.com/paketo-buildpacks/packit/v2/matchers" ) func testLogger(t *testing.T, context spec.G, it spec.S) { diff --git a/scribe/writer.go b/scribe/writer.go index 4e249836..026ac271 100644 --- a/scribe/writer.go +++ b/scribe/writer.go @@ -27,28 +27,40 @@ func WithIndent(indent int) Option { } } +// WithPrefix takes a prefix string and returns an Option which can be passed +// in while creating a new Writer to configure a prefix to be prepended to the +// output of the Writer. +func WithPrefix(prefix string) Option { + return func(l Writer) Writer { + l.prefix = prefix + return l + } +} + // A Writer conforms to the io.Writer interface and allows for configuration of // output from the writter such as the color or indentation through Options. type Writer struct { - writer io.Writer - color Color - indent int + writer io.Writer + color Color + indent int + prefix string + linestart bool } // NewWriter takes a Writer and Options and returns a Writer that will format // output according to the options given. -func NewWriter(writer io.Writer, options ...Option) Writer { - w := Writer{writer: writer} +func NewWriter(writer io.Writer, options ...Option) *Writer { + w := Writer{writer: writer, linestart: true} for _, option := range options { w = option(w) } - return w + return &w } // Write takes the given byte array and formats it in accordance with the // options on the writer and then outputs that formated text. -func (w Writer) Write(b []byte) (int, error) { +func (w *Writer) Write(b []byte) (int, error) { var ( prefix, suffix []byte reset = []byte("\r") @@ -69,15 +81,23 @@ func (w Writer) Write(b []byte) (int, error) { lines := bytes.Split(b, newline) var indentedLines [][]byte - for _, line := range lines { - for i := 0; i < w.indent; i++ { - line = append([]byte(" "), line...) + for index, line := range lines { + if !(index == 0 && !w.linestart) { + line = append([]byte(w.prefix), line...) + + for i := 0; i < w.indent; i++ { + line = append([]byte(" "), line...) + } } indentedLines = append(indentedLines, line) } b = bytes.Join(indentedLines, newline) + if n > 0 { + w.linestart = false + } + if w.color != nil { b = []byte(w.color(string(b))) } @@ -88,6 +108,7 @@ func (w Writer) Write(b []byte) (int, error) { if suffix != nil { b = append(b, suffix...) + w.linestart = true } _, err := w.writer.Write(b) diff --git a/scribe/writer_test.go b/scribe/writer_test.go index 369b7479..2ca2b7e1 100644 --- a/scribe/writer_test.go +++ b/scribe/writer_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/paketo-buildpacks/packit/scribe" + "github.com/paketo-buildpacks/packit/v2/scribe" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -16,7 +16,7 @@ func testWriter(t *testing.T, context spec.G, it spec.S) { context("Writer", func() { var ( buffer *bytes.Buffer - writer scribe.Writer + writer *scribe.Writer ) it.Before(func() { @@ -53,29 +53,71 @@ func testWriter(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(buffer.String()).To(Equal(" some-text\n other-text")) }) + + context("when sequential write inputs are not newline terminated", func() { + it("handles the indentation correctly", func() { + _, err := writer.Write([]byte("some-text")) + Expect(err).NotTo(HaveOccurred()) + + _, err = writer.Write([]byte(" followed by other-text\n")) + Expect(err).NotTo(HaveOccurred()) + + _, err = writer.Write([]byte("followed by\neven-more-text\n")) + Expect(err).NotTo(HaveOccurred()) + + Expect(buffer.String()).To(Equal(" some-text followed by other-text\n followed by\n even-more-text\n")) + }) + }) + }) + + context("when the writer has a prefix", func() { + it.Before(func() { + writer = scribe.NewWriter(buffer, scribe.WithPrefix("[some-prefix] ")) + }) + + it("prints to the writer with the given prefix", func() { + _, err := writer.Write([]byte("some-text\nother-text")) + Expect(err).NotTo(HaveOccurred()) + Expect(buffer.String()).To(Equal("[some-prefix] some-text\n[some-prefix] other-text")) + }) + + context("when sequential write inputs are not newline terminated", func() { + it("handles the indentation correctly", func() { + _, err := writer.Write([]byte("some-text")) + Expect(err).NotTo(HaveOccurred()) + + _, err = writer.Write([]byte(" followed by other-text\n")) + Expect(err).NotTo(HaveOccurred()) + + _, err = writer.Write([]byte("followed by\neven-more-text\n")) + Expect(err).NotTo(HaveOccurred()) + + Expect(buffer.String()).To(Equal("[some-prefix] some-text followed by other-text\n[some-prefix] followed by\n[some-prefix] even-more-text\n")) + }) + }) }) context("when the writer has a return prefix", func() { it.Before(func() { - writer = scribe.NewWriter(buffer, scribe.WithColor(scribe.RedColor), scribe.WithIndent(2)) + writer = scribe.NewWriter(buffer, scribe.WithColor(scribe.RedColor), scribe.WithIndent(2), scribe.WithPrefix("[some-prefix] ")) }) it("prints to the writer with the correct indentation", func() { _, err := writer.Write([]byte("\rsome-text")) Expect(err).NotTo(HaveOccurred()) - Expect(buffer.String()).To(Equal("\r\x1b[0;38;5;1m some-text\x1b[0m")) + Expect(buffer.String()).To(Equal("\r\x1b[0;38;5;1m [some-prefix] some-text\x1b[0m")) }) }) context("when the writer has a newline suffix", func() { it.Before(func() { - writer = scribe.NewWriter(buffer, scribe.WithColor(scribe.RedColor), scribe.WithIndent(2)) + writer = scribe.NewWriter(buffer, scribe.WithColor(scribe.RedColor), scribe.WithIndent(2), scribe.WithPrefix("[some-prefix] ")) }) it("prints to the writer with the correct indentation", func() { _, err := writer.Write([]byte("some-text\n")) Expect(err).NotTo(HaveOccurred()) - Expect(buffer.String()).To(Equal("\x1b[0;38;5;1m some-text\x1b[0m\n")) + Expect(buffer.String()).To(Equal("\x1b[0;38;5;1m [some-prefix] some-text\x1b[0m\n")) }) }) diff --git a/scripts/.util/print.sh b/scripts/.util/print.sh new file mode 100644 index 00000000..0c5a49e8 --- /dev/null +++ b/scripts/.util/print.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +function util::print::title() { + local blue reset message + blue="\033[0;34m" + reset="\033[0;39m" + message="${1}" + + echo -e "\n${blue}${message}${reset}" >&2 +} + +function util::print::info() { + local message + message="${1}" + + echo -e "${message}" >&2 +} + +function util::print::error() { + local message red reset + message="${1}" + red="\033[0;31m" + reset="\033[0;39m" + + echo -e "${red}${message}${reset}" >&2 + exit 1 +} + +function util::print::success() { + local message green reset + message="${1}" + green="\033[0;32m" + reset="\033[0;39m" + + echo -e "${green}${message}${reset}" >&2 + exitcode="${2:-0}" + exit "${exitcode}" +} + +function util::print::warn() { + local message yellow reset + message="${1}" + yellow="\033[0;33m" + reset="\033[0;39m" + + echo -e "${yellow}${message}${reset}" >&2 + exit 0 +} diff --git a/scripts/.util/tools.sh b/scripts/.util/tools.sh new file mode 100644 index 00000000..53abd49e --- /dev/null +++ b/scripts/.util/tools.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +# shellcheck source=SCRIPTDIR/print.sh +source "$(dirname "${BASH_SOURCE[0]}")/print.sh" + +function util::tools::tests::checkfocus() { + testout="${1}" + if grep -q 'Focused: [1-9]' "${testout}"; then + echo "Detected Focused Test(s) - setting exit code to 197" + rm "${testout}" + util::print::success "** GO Test Succeeded **" 197 + fi + rm "${testout}" +} diff --git a/scripts/unit.sh b/scripts/unit.sh new file mode 100755 index 00000000..f8c40ca7 --- /dev/null +++ b/scripts/unit.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +readonly PROGDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly BUILDPACKDIR="$(cd "${PROGDIR}/.." && pwd)" + +# shellcheck source=SCRIPTDIR/.util/tools.sh +source "${PROGDIR}/.util/tools.sh" + +# shellcheck source=SCRIPTDIR/.util/print.sh +source "${PROGDIR}/.util/print.sh" + +function main() { + while [[ "${#}" != 0 ]]; do + case "${1}" in + --help|-h) + shift 1 + usage + exit 0 + ;; + + "") + # skip if the argument is empty + shift 1 + ;; + + *) + util::print::error "unknown argument \"${1}\"" + esac + done + + unit::run +} + +function usage() { + cat <<-USAGE +unit.sh [OPTIONS] + +Runs the unit test suite. + +OPTIONS + --help -h prints the command usage +USAGE +} + +function unit::run() { + util::print::title "Run Library pack Unit and Example Tests" + + testout=$(mktemp) + pushd "${BUILDPACKDIR}" > /dev/null + if go test ./... -v | tee "${testout}"; then + util::tools::tests::checkfocus "${testout}" + util::print::success "** GO Test Succeeded **" + else + util::print::error "** GO Test Failed **" + fi + popd > /dev/null +} + +main "${@:-}" diff --git a/servicebindings/entry.go b/servicebindings/entry.go index 6df41045..4613b860 100644 --- a/servicebindings/entry.go +++ b/servicebindings/entry.go @@ -1,13 +1,16 @@ package servicebindings import ( + "bytes" + "io" "os" ) // Entry represents the read-only content of a binding entry. type Entry struct { - path string - file *os.File + path string + file *os.File + value *bytes.Reader } // NewEntry returns a new Entry whose content is given by the file at the provided path. @@ -17,18 +20,39 @@ func NewEntry(path string) *Entry { } } +// NewWithValue returns a new Entry with predefined value. +func NewWithValue(value []byte) *Entry { + return &Entry{ + value: bytes.NewReader(value), + } +} + // ReadBytes reads the entire raw content of the entry. There is no need to call Close after calling ReadBytes. func (e *Entry) ReadBytes() ([]byte, error) { + if e.value != nil { + return io.ReadAll(e.value) + } return os.ReadFile(e.path) } // ReadString reads the entire content of the entry as a string. There is no need to call Close after calling // ReadString. func (e *Entry) ReadString() (string, error) { - bytes, err := e.ReadBytes() - if err != nil { - return "", err + var bytes []byte + var err error + + if e.value != nil { + bytes, err = io.ReadAll(e.value) + if err != nil { + return "", err + } + } else { + bytes, err = e.ReadBytes() + if err != nil { + return "", err + } } + return string(bytes), nil } @@ -36,6 +60,10 @@ func (e *Entry) ReadString() (string, error) { // of entry data, Read returns 0, io.EOF. // Close must be called when all read operations are complete. func (e *Entry) Read(b []byte) (int, error) { + if e.value != nil { + return e.value.Read(b) + } + if e.file == nil { file, err := os.Open(e.path) if err != nil { @@ -49,11 +77,16 @@ func (e *Entry) Read(b []byte) (int, error) { // Close closes the entry and resets it for reading. After calling Close, any subsequent calls to Read will read entry // data from the beginning. Close may be called on a closed entry without error. func (e *Entry) Close() error { - if e.file == nil { - return nil - } defer func() { e.file = nil }() - return e.file.Close() + + if e.value != nil { + _, err := e.value.Seek(0, io.SeekStart) + return err + } else if e.file == nil { + return nil + } else { + return e.file.Close() + } } diff --git a/servicebindings/entry_test.go b/servicebindings/entry_test.go index 83fb990d..7178b7ce 100644 --- a/servicebindings/entry_test.go +++ b/servicebindings/entry_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/servicebindings" + "github.com/paketo-buildpacks/packit/v2/servicebindings" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -14,9 +14,10 @@ import ( func testEntry(t *testing.T, context spec.G, it spec.S) { var ( - Expect = NewWithT(t).Expect - entry *servicebindings.Entry - tmpDir string + Expect = NewWithT(t).Expect + entry *servicebindings.Entry + entryWithValue *servicebindings.Entry + tmpDir string ) it.Before(func() { @@ -26,6 +27,7 @@ func testEntry(t *testing.T, context spec.G, it spec.S) { entryPath := filepath.Join(tmpDir, "entry") Expect(os.WriteFile(entryPath, []byte("some data"), os.ModePerm)).To(Succeed()) entry = servicebindings.NewEntry(entryPath) + entryWithValue = servicebindings.NewWithValue([]byte("value from env")) }) it.After(func() { @@ -35,12 +37,14 @@ func testEntry(t *testing.T, context spec.G, it spec.S) { context("ReadBytes", func() { it("returns the raw bytes of the entry", func() { Expect(entry.ReadBytes()).To(Equal([]byte("some data"))) + Expect(entryWithValue.ReadBytes()).To(Equal([]byte("value from env"))) }) }) context("ReadString", func() { it("returns the string value of the entry", func() { Expect(entry.ReadString()).To(Equal("some data")) + Expect(entryWithValue.ReadString()).To(Equal("value from env")) }) }) @@ -59,6 +63,16 @@ func testEntry(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(entry.Close()).To(Succeed()) Expect(data).To(Equal([]byte("some data"))) + + data, err = io.ReadAll(entryWithValue) + Expect(err).NotTo(HaveOccurred()) + Expect(entryWithValue.Close()).To(Succeed()) + Expect(data).To(Equal([]byte("value from env"))) + + data, err = io.ReadAll(entryWithValue) + Expect(err).NotTo(HaveOccurred()) + Expect(entryWithValue.Close()).To(Succeed()) + Expect(data).To(Equal([]byte("value from env"))) }) it("can be closed multiple times in a row", func() { @@ -66,10 +80,16 @@ func testEntry(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(entry.Close()).To(Succeed()) Expect(entry.Close()).To(Succeed()) + + _, err = io.ReadAll(entryWithValue) + Expect(err).NotTo(HaveOccurred()) + Expect(entryWithValue.Close()).To(Succeed()) + Expect(entryWithValue.Close()).To(Succeed()) }) it("can be closed if never read from", func() { Expect(entry.Close()).To(Succeed()) + Expect(entryWithValue.Close()).To(Succeed()) }) }) } diff --git a/servicebindings/resolver.go b/servicebindings/resolver.go index c81eb72a..2b46f763 100644 --- a/servicebindings/resolver.go +++ b/servicebindings/resolver.go @@ -1,6 +1,7 @@ package servicebindings import ( + "encoding/json" "errors" "fmt" "os" @@ -47,9 +48,9 @@ func NewResolver() *Resolver { // // The location of bindings is given by one of the following, in order of precedence: // -// 1. SERVICE_BINDING_ROOT environment variable -// 2. CNB_BINDINGS environment variable, if above is not set -// 3. `/bindings`, if both above are not set +// 1. SERVICE_BINDING_ROOT environment variable +// 2. CNB_BINDINGS environment variable, if above is not set +// 3. `/bindings`, if both above are not set func (r *Resolver) Resolve(typ, provider, platformDir string) ([]Binding, error) { if newRoot := bindingRoot(platformDir); r.bindingRoot != newRoot { r.bindingRoot = newRoot @@ -92,6 +93,10 @@ func (r *Resolver) ResolveOne(typ, provider, platformDir string) (Binding, error } func loadBindings(bindingRoot string) ([]Binding, error) { + if vcapEnv, ok := os.LookupEnv("VCAP_SERVICES"); ok { + return loadvcapservicesbinding(vcapEnv) + } + files, err := os.ReadDir(bindingRoot) if os.IsNotExist(err) { return nil, nil @@ -165,6 +170,7 @@ func loadBinding(bindingRoot, name string) (Binding, error) { if err != nil { return Binding{}, err } + binding.Type = strings.TrimSpace(binding.Type) delete(entries, "type") provider, ok := entries["provider"] @@ -173,6 +179,7 @@ func loadBinding(bindingRoot, name string) (Binding, error) { if err != nil { return Binding{}, err } + binding.Provider = strings.TrimSpace(binding.Provider) delete(entries, "provider") } @@ -202,6 +209,7 @@ func loadLegacyBinding(bindingRoot, name string) (Binding, error) { if err != nil { return Binding{}, err } + binding.Type = strings.TrimSpace(binding.Type) delete(metadata, "kind") provider, ok := metadata["provider"] @@ -212,6 +220,7 @@ func loadLegacyBinding(bindingRoot, name string) (Binding, error) { if err != nil { return Binding{}, err } + binding.Provider = strings.TrimSpace(binding.Provider) delete(metadata, "provider") binding.Entries = metadata @@ -229,6 +238,55 @@ func loadLegacyBinding(bindingRoot, name string) (Binding, error) { return binding, nil } +func loadvcapservicesbinding(content string) ([]Binding, error) { + var contentTyped map[string][]vcapServicesBinding + + err := json.Unmarshal([]byte(content), &contentTyped) + if err != nil { + return []Binding{}, err + } + + bindings := []Binding{} + for p, bArray := range contentTyped { + for _, b := range bArray { + entries := map[string]*Entry{} + for k, v := range b.Credentials { + entries[k], err = toJSONString(v) + if err != nil { + return nil, err + } + } + bindings = append(bindings, Binding{ + Name: b.Name, + Type: b.Label, + Provider: p, + Entries: entries, + }) + } + } + + return bindings, nil +} + +type vcapServicesBinding struct { + Name string `json:"name"` + Label string `json:"label"` + Credentials map[string]interface{} `json:"credentials"` +} + +func toJSONString(input interface{}) (*Entry, error) { + switch in := input.(type) { + case string: + return NewWithValue([]byte(in)), nil + default: + jsonProperty, err := json.Marshal(in) + if err != nil { + return nil, err + } + return NewWithValue(jsonProperty), nil + } +} + func loadEntries(path string) (map[string]*Entry, error) { entries := map[string]*Entry{} files, err := os.ReadDir(path) diff --git a/servicebindings/resolver_test.go b/servicebindings/resolver_test.go index f6893d2d..a56b94aa 100644 --- a/servicebindings/resolver_test.go +++ b/servicebindings/resolver_test.go @@ -5,10 +5,10 @@ import ( "path/filepath" "testing" - . "github.com/onsi/gomega" + "github.com/paketo-buildpacks/packit/v2/servicebindings" "github.com/sclevine/spec" - "github.com/paketo-buildpacks/packit/servicebindings" + . "github.com/onsi/gomega" ) func testResolver(t *testing.T, context spec.G, it spec.S) { @@ -147,6 +147,43 @@ func testResolver(t *testing.T, context spec.G, it spec.S) { }) }) }) + + context("VCAP_SERVICES env var is set", func() { + it.Before(func() { + content, err := os.ReadFile("testdata/vcap_services.json") + Expect(err).NotTo(HaveOccurred()) + Expect(os.Setenv("VCAP_SERVICES", string(content))).To(Succeed()) + }) + + it.After(func() { + Expect(os.Unsetenv("VCAP_SERVICES")).To(Succeed()) + }) + + context("SERVICE_BINDING_ROOT env var is set", func() { + it.Before(func() { + Expect(os.Setenv("SERVICE_BINDING_ROOT", bindingRootK8s)).To(Succeed()) + }) + + it("resolves bindings from VCAP_SERVICES", func() { + resolver := servicebindings.NewResolver() + bindings, err := resolver.Resolve("postgres", "", platformDir) + Expect(err).NotTo(HaveOccurred()) + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "postgres", + Path: "", + Type: "postgres", + Provider: "postgres", + Entries: map[string]*servicebindings.Entry{ + "username": servicebindings.NewWithValue([]byte("foo")), + "password": servicebindings.NewWithValue([]byte("bar")), + "urls": servicebindings.NewWithValue([]byte("{\"example\":\"http://example.com\"}")), + }, + }, + )) + }) + }) + }) }) context("resolving bindings", func() { @@ -205,6 +242,19 @@ func testResolver(t *testing.T, context spec.G, it spec.S) { err = os.WriteFile(filepath.Join(bindingRoot, "binding-2", "password"), nil, os.ModePerm) Expect(err).NotTo(HaveOccurred()) + + err = os.MkdirAll(filepath.Join(bindingRoot, "binding-3"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-3", "type"), []byte("\n type-3\n"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-3", "provider"), []byte("\tprovider-3\n"), os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + + err = os.WriteFile(filepath.Join(bindingRoot, "binding-3", "value"), nil, os.ModePerm) + Expect(err).NotTo(HaveOccurred()) + }) it.After(func() { @@ -259,6 +309,23 @@ func testResolver(t *testing.T, context spec.G, it spec.S) { )) }) + it("resolves by type/provider files that contains whitespace", func() { + bindings, err := resolver.Resolve("type-3", "provider-3", "") + Expect(err).NotTo(HaveOccurred()) + + Expect(bindings).To(ConsistOf( + servicebindings.Binding{ + Name: "binding-3", + Path: filepath.Join(bindingRoot, "binding-3"), + Type: "type-3", + Provider: "provider-3", + Entries: map[string]*servicebindings.Entry{ + "value": servicebindings.NewEntry(filepath.Join(bindingRoot, "binding-3", "value")), + }, + }, + )) + }) + it("allows 'metadata' as an entry name", func() { err := os.MkdirAll(filepath.Join(bindingRoot, "binding-metadata"), os.ModePerm) Expect(err).NotTo(HaveOccurred()) diff --git a/servicebindings/servicebinding.go b/servicebindings/servicebinding.go new file mode 100644 index 00000000..2e88b3bd --- /dev/null +++ b/servicebindings/servicebinding.go @@ -0,0 +1,3 @@ +// Package servicebindings provides a service for inspecting and retrieving +// data from service binding. +package servicebindings diff --git a/servicebindings/testdata/vcap_services.json b/servicebindings/testdata/vcap_services.json new file mode 100644 index 00000000..48d96b52 --- /dev/null +++ b/servicebindings/testdata/vcap_services.json @@ -0,0 +1,69 @@ +{ + "elephantsql-provider": [ + { + "name": "elephantsql-binding-c6c60", + "binding_guid": "44ceb72f-100b-4f50-87a2-7809c8b42b8d", + "binding_name": "elephantsql-binding-c6c60", + "instance_guid": "391308e8-8586-4c42-b464-c7831aa2ad22", + "instance_name": "elephantsql-c6c60", + "label": "elephantsql-type", + "tags": [ + "postgres", + "postgresql", + "relational" + ], + "plan": "turtle", + "credentials": { + "uri": "postgres://exampleuser:examplepass@postgres.example.com:5432/exampleuser", + "int": 1, + "bool": true + }, + "syslog_drain_url": null, + "volume_mounts": [] + } + ], + "sendgrid-provider": [ + { + "name": "mysendgrid", + "binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081", + "binding_name": null, + "instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d", + "instance_name": "mysendgrid", + "label": "sendgrid-type", + "tags": [ + "smtp" + ], + "plan": "free", + "credentials": { + "hostname": "smtp.example.com", + "username": "QvsXMbJ3rK", + "password": "HCHMOYluTv" + }, + "syslog_drain_url": null, + "volume_mounts": [] + } + ], + "postgres": [ + { + "name": "postgres", + "label": "postgres", + "plan": "default", + "tags": [ + "postgres" + ], + "binding_guid": "6533b1b6-7916-488d-b286-ca33d3fa0081", + "binding_name": null, + "instance_guid": "8c907d0f-ec0f-44e4-87cf-e23c9ba3925d", + "credentials": { + "username": "foo", + "password": "bar", + "urls": { + "example": "http://example.com" + } + }, + "syslog_drain_url": null, + "volume_mounts": [] + } + ] + } + \ No newline at end of file diff --git a/vacation/archive.go b/vacation/archive.go index dde99e07..ac5aab0b 100644 --- a/vacation/archive.go +++ b/vacation/archive.go @@ -30,9 +30,13 @@ func NewArchive(inputReader io.Reader) Archive { // Decompress reads from Archive, determines the archive type of the input // stream, and writes files into the destination specified. // -// Archive decompression will also handle files that are types "text/plain; -// charset=utf-8" and write the contents of the input stream to a file name -// specified by the `Archive.WithName()` option in the destination directory. +// Archive decompression will also handle files that are types +// - "application/x-executable" +// - "text/plain; charset=utf-8" +// - "application/jar" +// - "application/octet-stream" +// and write the contents of the input stream to a file name specified by the +// `Archive.WithName()` option in the destination directory. func (a Archive) Decompress(destination string) error { // Convert reader into a buffered read so that the header can be peeked to // determine the type. @@ -48,8 +52,7 @@ func (a Archive) Decompress(destination string) error { mime := mimetype.Detect(header) - // This switch case is reponsible for determining what the decompression - // strategy should be. + // This switch case is responsible for determining the decompression strategy var decompressor Decompressor switch mime.String() { case "application/x-tar": @@ -64,7 +67,9 @@ func (a Archive) Decompress(destination string) error { decompressor = NewZipArchive(bufferedReader).StripComponents(a.components) case "application/x-executable": decompressor = NewExecutable(bufferedReader).WithName(a.name) - case "text/plain; charset=utf-8", "application/jar": + case "text/plain; charset=utf-8", + "application/jar", + "application/octet-stream": decompressor = NewNopArchive(bufferedReader).WithName(a.name) default: return fmt.Errorf("unsupported archive type: %s", mime.String()) diff --git a/vacation/archive_test.go b/vacation/archive_test.go index af3df9fb..17a04400 100644 --- a/vacation/archive_test.go +++ b/vacation/archive_test.go @@ -6,14 +6,14 @@ import ( "bytes" "compress/gzip" "encoding/base64" + "io" "io/fs" - "io/ioutil" "os" "path/filepath" "testing" dsnetBzip2 "github.com/dsnet/compress/bzip2" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" "github.com/ulikunitz/xz" @@ -363,7 +363,7 @@ func testArchive(t *testing.T, context spec.G, it spec.S) { tempDir, err = os.MkdirTemp("", "vacation") Expect(err).NotTo(HaveOccurred()) - literalContents, err = ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(encodedContents))) + literalContents, err = io.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(encodedContents))) Expect(err).NotTo(HaveOccurred()) archive = vacation.NewArchive(bytes.NewBuffer(literalContents)) diff --git a/vacation/bzip2_archive_test.go b/vacation/bzip2_archive_test.go index 499c0b54..545c018e 100644 --- a/vacation/bzip2_archive_test.go +++ b/vacation/bzip2_archive_test.go @@ -9,7 +9,7 @@ import ( "testing" dsnetBzip2 "github.com/dsnet/compress/bzip2" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -22,7 +22,7 @@ func testBzip2Archive(t *testing.T, context spec.G, it spec.S) { context("Decompress", func() { var ( - tempDir string + tempDir string bzip2Archive vacation.Bzip2Archive ) diff --git a/vacation/example_test.go b/vacation/example_test.go index 9bfcf15a..3418d1a8 100644 --- a/vacation/example_test.go +++ b/vacation/example_test.go @@ -11,7 +11,7 @@ import ( "path/filepath" dsnetBzip2 "github.com/dsnet/compress/bzip2" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/ulikunitz/xz" ) diff --git a/vacation/executable_test.go b/vacation/executable_test.go index cae6d32d..c9f5029d 100644 --- a/vacation/executable_test.go +++ b/vacation/executable_test.go @@ -3,13 +3,13 @@ package vacation_test import ( "bytes" "encoding/base64" + "io" "io/fs" - "io/ioutil" "os" "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -32,7 +32,7 @@ func testExecutable(t *testing.T, context spec.G, it spec.S) { tempDir, err = os.MkdirTemp("", "vacation") Expect(err).NotTo(HaveOccurred()) - literalContents, err = ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(encodedContents))) + literalContents, err = io.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(encodedContents))) Expect(err).NotTo(HaveOccurred()) archive = vacation.NewExecutable(bytes.NewBuffer(literalContents)) diff --git a/vacation/gzip_archive_test.go b/vacation/gzip_archive_test.go index a8a996cd..c8ebc8c4 100644 --- a/vacation/gzip_archive_test.go +++ b/vacation/gzip_archive_test.go @@ -9,7 +9,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega" @@ -22,7 +22,7 @@ func testGzipArchive(t *testing.T, context spec.G, it spec.S) { context("Decompress", func() { var ( - tempDir string + tempDir string gzipArchive vacation.GzipArchive ) diff --git a/vacation/link_sorting_test.go b/vacation/link_sorting_test.go index 63c2985b..352e5c5e 100644 --- a/vacation/link_sorting_test.go +++ b/vacation/link_sorting_test.go @@ -9,7 +9,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/vacation/nop_archive_test.go b/vacation/nop_archive_test.go index 2a863de9..25a1f993 100644 --- a/vacation/nop_archive_test.go +++ b/vacation/nop_archive_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/vacation/tar_archive_test.go b/vacation/tar_archive_test.go index eb540217..96e3f088 100644 --- a/vacation/tar_archive_test.go +++ b/vacation/tar_archive_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega" diff --git a/vacation/xz_archive_test.go b/vacation/xz_archive_test.go index 77f3ca9a..c99b905b 100644 --- a/vacation/xz_archive_test.go +++ b/vacation/xz_archive_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" "github.com/ulikunitz/xz" diff --git a/vacation/zip_archive_test.go b/vacation/zip_archive_test.go index 1e1c660b..91d04124 100644 --- a/vacation/zip_archive_test.go +++ b/vacation/zip_archive_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/paketo-buildpacks/packit/vacation" + "github.com/paketo-buildpacks/packit/v2/vacation" "github.com/sclevine/spec" . "github.com/onsi/gomega"