diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 527d2932..ade1ee34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,54 @@ jobs: echo "Test failed: Command succeeded when it should have failed." exit 1 + - name: Run Setup Cyclonedx Via Local Setup Cyclonedx Action + id: setup_cyclonedx + uses: ./setup-cyclonedx + + - name: Verify Cyclonedx Is On PATH With The Default Spec Version + env: + CYCLONEDX_PATH: ${{ steps.setup_cyclonedx.outputs.path }} + run: | + test "$(command -v cyclonedx)" = "$CYCLONEDX_PATH" + test "$CYCLONEDX_SPEC_VERSION" = v1_7 + + - name: Merge Image SBOMs Via Local Attest Chart Action + id: attest_chart + uses: ./attest-chart + with: + chart-path: testdata/attest-chart + chart-name: smoke + chart-version: 1.2.3 + attest: "false" + + - name: Verify Merged Chart SBOM + env: + SBOM_PATH: ${{ steps.attest_chart.outputs.sbom-path }} + # sboms/ also holds a busybox-c SBOM that bom.json does not reference, + # so the nested names prove a stale SBOM is left out. + run: | + test "$SBOM_PATH" = sbom-out/smoke-1.2.3.cdx.json + cyclonedx validate --input-file "$SBOM_PATH" --input-version v1_7 --fail-on-errors + jq -e '.specVersion == "1.7" + and .metadata.component.group == "codesphere" + and .metadata.component.name == "smoke" + and .metadata.component.version == "1.2.3" + and ([.components[].components[].name] | sort) == ["busybox-a", "busybox-b"]' "$SBOM_PATH" + + - name: Run Attest Chart On A Chart Without Image SBOMs + id: attest_chart_empty + uses: ./attest-chart + with: + chart-path: testdata/charts/e2e + chart-name: e2e + chart-version: 0.0.0 + attest: "false" + + - name: Verify No Chart SBOM Was Written + env: + SBOM_PATH: ${{ steps.attest_chart_empty.outputs.sbom-path }} + run: test -z "$SBOM_PATH" + attestation-smoke: if: github.event_name == 'push' runs-on: ubuntu-latest @@ -168,3 +216,47 @@ jobs: --certificate-identity "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/ci.yml@refs/heads/main" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ "$attestation_ref" + + - id: chart + name: Package and push smoke chart + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + repository="$(echo "oci://ghcr.io/${GITHUB_REPOSITORY}/attestation-smoke-charts" | tr '[:upper:]' '[:lower:]')" + version="0.0.0-${GITHUB_SHA::12}" + helm package testdata/attestation/chart --version "$version" -d "$RUNNER_TEMP/chart" + echo "$GH_TOKEN" | helm registry login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + push_log="$(helm push "$RUNNER_TEMP"/chart/*.tgz "$repository" 2>&1 | tee /dev/stderr)" + digest="$(printf '%s\n' "$push_log" | awk '/^Digest: /{print $2}' | head -n1)" + name="$(yq '.name' testdata/attestation/chart/Chart.yaml)" + test -n "$digest" + { + echo "name=$name" + echo "version=$version" + echo "ref=${repository#oci://}/${name}@${digest}" + } >> "$GITHUB_OUTPUT" + + - id: chart_sbom + name: Merge and attest chart SBOM + uses: ./attest-chart + with: + chart-path: testdata/attestation/chart + chart-name: ${{ steps.chart.outputs.name }} + chart-version: ${{ steps.chart.outputs.version }} + chart-ref: ${{ steps.chart.outputs.ref }} + + - name: Verify chart SBOM attestation + shell: bash + env: + CHART_REF: ${{ steps.chart.outputs.ref }} + CHART_NAME: ${{ steps.chart.outputs.name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cosign verify-attestation --k8s-keychain \ + --type cyclonedx \ + --certificate-identity "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/ci.yml@refs/heads/main" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + "$CHART_REF" | + jq -se --arg name "$CHART_NAME" \ + '.[0].payload | @base64d | fromjson | .predicate.specVersion == "1.7" and .predicate.metadata.component.name == $name' diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 00000000..01223f29 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,13 @@ +# Permissive on layout, strict on syntax: GitHub workflow and action files +# use long run: lines, `on:` keys and SHA-pin comments the defaults reject. +extends: default + +rules: + document-start: disable + truthy: + check-keys: false + line-length: + max: 200 + comments: + min-spaces-from-content: 1 + comments-indentation: disable diff --git a/README.md b/README.md index b4bd796d..59cf3845 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,17 @@ Choose the section that matches how you use this repository: ## Users Of The GitHub Action -This repository ships three composite Actions: +This repository ships five composite Actions: - [generate/action.yml](/Users/schrodit/dev/cs/bom/generate/action.yml) generates BOM files for Helm charts - [registry-login/action.yml](/Users/schrodit/dev/cs/bom/registry-login/action.yml) authenticates registry access for subsequent checks - [check/action.yml](/Users/schrodit/dev/cs/bom/check/action.yml) validates BOM files against upstream registries +- [attest-chart/action.yml](attest-chart/action.yml) merges a chart's image SBOMs into one chart SBOM and attests it on the pushed chart +- [setup-cyclonedx/action.yml](setup-cyclonedx/action.yml) installs the pinned `cyclonedx-cli` for callers that merge SBOMs themselves All Actions: -- run a prebuilt binary from `dist/linux-*` directly on the runner +- run a prebuilt binary from `dist/linux-*`, or a pinned release binary, directly on the runner - support Linux runners only ### Generate BOMs In CI @@ -300,6 +302,58 @@ Check outputs: All selected BOMs are checked even when some fail. The Action exits unsuccessfully after processing every BOM and writes a step-summary table containing each BOM and its validation status or error. Its metrics include the total number of image references outside the allowed registries and the total number that could not be found upstream. +### Attest Chart SBOMs In CI + +`generate` with `sbom: true` writes one SBOM per image. `attest-chart` runs after the chart is pushed: it merges the CycloneDX SBOMs the chart's `bom.json` references hierarchically into one CycloneDX document whose subject is the chart, then attests it on the chart's GHCR digest with keyless Cosign and reads the attestation back. SBOMs left in `sboms/` for digests the chart no longer uses are not merged. + +```yaml +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - uses: actions/checkout@v6 + + - uses: codesphere-cloud/bom/generate@main + with: + include-paths: chart + sbom: true + + - id: push + uses: codesphere-cloud/helm-action/push@main + with: + chart: chart + repository: oci://ghcr.io/codesphere-cloud/charts + + - uses: codesphere-cloud/bom/attest-chart@main + with: + chart-path: chart + chart-name: my-chart + chart-version: 1.2.3 + chart-ref: ghcr.io/codesphere-cloud/charts/my-chart@${{ steps.push.outputs.digest }} +``` + +Attest-chart inputs: + +- `chart-path`, `chart-name`, `chart-version`: the chart whose `bom.json` SBOMs are merged, and the subject the merged SBOM describes. Required unless `sbom-path` is set +- `chart-group`: group of the subject. Default: `codesphere` +- `chart-ref`: the pushed chart as `ghcr.io/@sha256:`. Required when `attest` is `true`. Only GHCR is supported: Cosign gets the registry credentials through its GitHub keychain +- `sbom-path`: attest an already merged SBOM instead of merging, for jobs that merge and attest separately +- `output-dir`: where the merged SBOM is written, as `-.cdx.json`. Default: `sbom-out` +- `spec-version`: CycloneDX version of the merged SBOM, as `cyclonedx-cli --output-version` takes it. Default: `v1_7` +- `attest`: attest and verify. Requires `id-token: write` and registry write access. Default: `true` +- `certificate-identity-regexp`: identity the attestation is verified against. Default: any workflow of the calling repository, matched literally +- `registry-token`: GHCR token Cosign uses for the registry. Default: `github.token` + +A chart without image SBOMs gets no chart SBOM: the `sbom-path` output is empty and nothing is attested. + +Callers that merge SBOMs in their own scripts use `setup-cyclonedx` instead. It puts `cyclonedx` on `PATH` and exports `CYCLONEDX_SPEC_VERSION` (input `spec-version`, default `v1_7`), so the version and the spec default stay defined in this repository. + +[setup-cyclonedx/install.sh](setup-cyclonedx/install.sh) is the only place the `cyclonedx-cli` version and checksums are pinned. Renovate bumps the version, and [scripts/pin-cyclonedx.sh](scripts/pin-cyclonedx.sh) rewrites the checksums in the same commit. + ## Users Of The CLI Use the CLI for local debugging, reproducing GitHub Action behavior, or developing chart/BOM logic outside CI. @@ -435,6 +489,7 @@ make dist - [internal/sbom](/Users/schrodit/dev/cs/bom/internal/sbom): SPDX and `csbom` / `csbom-v2` formatting and parsing - [internal/ghaction](/Users/schrodit/dev/cs/bom/internal/ghaction): shared action filtering, path resolution, and output handling - [generate/action.yml](/Users/schrodit/dev/cs/bom/generate/action.yml) and [check/action.yml](/Users/schrodit/dev/cs/bom/check/action.yml): composite Action entrypoints +- [attest-chart/action.yml](attest-chart/action.yml) and [setup-cyclonedx/action.yml](setup-cyclonedx/action.yml): composite Actions around the pinned `cyclonedx-cli`, which [setup-cyclonedx/install.sh](setup-cyclonedx/install.sh) installs - [scripts/build-dist.sh](/Users/schrodit/dev/cs/bom/scripts/build-dist.sh): dist artifact builder ### Action Development diff --git a/attest-chart/action.yml b/attest-chart/action.yml new file mode 100644 index 00000000..f0d3701e --- /dev/null +++ b/attest-chart/action.yml @@ -0,0 +1,184 @@ +name: bom attest chart +description: > + Merge the per-image CycloneDX SBOMs that a chart's bom.json references, written by generate + with sbom: true, into one chart-level SBOM and attest it on the pushed chart's GHCR digest + with keyless Cosign. +inputs: + chart-path: + description: Chart directory whose bom.json lists the per-image SBOMs to merge. Not used when sbom-path is set. + required: false + default: "" + chart-name: + description: Name of the software the merged SBOM describes, usually the chart name. + required: false + default: "" + chart-version: + description: Version of the software the merged SBOM describes, usually the chart version. + required: false + default: "" + chart-group: + description: Group of the software the merged SBOM describes. + required: false + default: codesphere + chart-ref: + description: Pushed chart to attest, as ghcr.io/@sha256:. Required when attest is true. + required: false + default: "" + sbom-path: + description: An already merged chart SBOM to attest. Skips the merge. + required: false + default: "" + output-dir: + description: Directory the merged SBOM is written to, as -.cdx.json. + required: false + default: sbom-out + spec-version: + description: CycloneDX specification version of the merged SBOM, as cyclonedx-cli's --output-version takes it. + required: false + default: v1_7 + attest: + description: Attest the SBOM on chart-ref and verify the attestation. Requires id-token write permission. + required: false + default: "true" + certificate-identity-regexp: + description: Signing identity the attestation is verified against. Defaults to any workflow of the calling repository. + required: false + default: "" + registry-token: + description: GHCR token cosign uses for the chart registry; the default needs the workflow permission packages write. + required: false + default: ${{ github.token }} +outputs: + sbom-path: + description: The merged or given chart SBOM. Empty when the chart has no image SBOMs. + value: ${{ steps.merge.outputs.sbom-path }} +runs: + using: composite + steps: + # GitHub only warns on a missing required input of a composite action, so + # these checks are the real enforcement, and they run before anything is + # downloaded or signed. + - name: Validate inputs + shell: bash + env: + CHART_PATH: ${{ inputs.chart-path }} + CHART_NAME: ${{ inputs.chart-name }} + CHART_VERSION: ${{ inputs.chart-version }} + CHART_REF: ${{ inputs.chart-ref }} + SBOM_PATH: ${{ inputs.sbom-path }} + SPEC_VERSION: ${{ inputs.spec-version }} + ATTEST: ${{ inputs.attest }} + run: | + if [ -n "$SBOM_PATH" ]; then + if [ ! -f "$SBOM_PATH" ]; then + echo "::error title=No SBOM::sbom-path '$SBOM_PATH' does not exist" + exit 1 + fi + elif [ -z "$CHART_PATH" ] || [ -z "$CHART_NAME" ] || [ -z "$CHART_VERSION" ]; then + echo "::error title=Missing inputs::chart-path, chart-name and chart-version are required unless sbom-path is set" + exit 1 + fi + if ! [[ "$SPEC_VERSION" =~ ^v1_[0-9]+$ ]]; then + echo "::error title=Invalid spec-version::expected v1_, got '$SPEC_VERSION'" + exit 1 + fi + # Registry credentials reach cosign only through the GHCR keychain (see + # the attest step), so another registry would fail to authorize. + if [ "$ATTEST" = "true" ] && ! [[ "$CHART_REF" =~ ^ghcr\.io/[^@[:space:]]+@sha256:[0-9a-f]{64}$ ]]; then + echo "::error title=Invalid chart-ref::attesting needs ghcr.io/@sha256:, got '$CHART_REF'" + exit 1 + fi + + - id: merge + name: Merge image SBOMs into the chart SBOM + shell: bash + env: + CHART_PATH: ${{ inputs.chart-path }} + CHART_NAME: ${{ inputs.chart-name }} + CHART_VERSION: ${{ inputs.chart-version }} + CHART_GROUP: ${{ inputs.chart-group }} + SBOM_PATH: ${{ inputs.sbom-path }} + OUTPUT_DIR: ${{ inputs.output-dir }} + SPEC_VERSION: ${{ inputs.spec-version }} + INSTALL_DIR: ${{ runner.temp }}/cyclonedx-cli + run: | + if [ -n "$SBOM_PATH" ]; then + echo "sbom-path=$SBOM_PATH" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # generate keeps SBOMs of digests the chart no longer uses, so only + # the ones the current bom.json references describe the chart. + bom_json="$CHART_PATH/bom.json" + image_sboms=() + if [ -f "$bom_json" ]; then + # Outside the loop, so a bom.json jq cannot read fails the step. + sbom_paths="$(jq -r '[.containerImages // {} | .[] | .sboms.cyclonedx.path // empty] | unique | .[]' "$bom_json")" + while IFS= read -r path; do + [ -n "$path" ] || continue + if ! [[ "$path" =~ ^sboms/sha256-[0-9a-f]{64}\.cdx\.json$ ]]; then + echo "::error title=Invalid SBOM path::$bom_json references '$path', expected sboms/sha256-.cdx.json" + exit 1 + fi + if [ ! -f "$CHART_PATH/$path" ]; then + echo "::error title=Missing SBOM::$bom_json references $path, which does not exist" + exit 1 + fi + image_sboms+=("$CHART_PATH/$path") + done <<< "$sbom_paths" + fi + if [ "${#image_sboms[@]}" -eq 0 ]; then + echo "::notice::$CHART_PATH has no image SBOMs in bom.json, so it gets no chart SBOM." + echo "sbom-path=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + cyclonedx="$("${{ github.action_path }}/../setup-cyclonedx/install.sh" "$INSTALL_DIR")" + out="$OUTPUT_DIR/${CHART_NAME}-${CHART_VERSION}.cdx.json" + mkdir -p "$OUTPUT_DIR" + "$cyclonedx" merge --hierarchical --output-version "$SPEC_VERSION" \ + --group "$CHART_GROUP" --name "$CHART_NAME" --version "$CHART_VERSION" \ + --input-files "${image_sboms[@]}" \ + --output-file "$out" + echo "sbom-path=$out" >> "$GITHUB_OUTPUT" + + - name: Set up Cosign + if: ${{ inputs.attest == 'true' && steps.merge.outputs.sbom-path != '' }} + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + with: + # The default is a shared path under $HOME, which concurrent jobs on a + # persistent runner would download the same binary into at once. + install-dir: ${{ runner.temp }}/cosign + + - name: Attest and verify the chart SBOM + if: ${{ inputs.attest == 'true' && steps.merge.outputs.sbom-path != '' }} + shell: bash + env: + CHART_REF: ${{ inputs.chart-ref }} + PREDICATE: ${{ steps.merge.outputs.sbom-path }} + IDENTITY_REGEXP: ${{ inputs.certificate-identity-regexp }} + # cosign has no env var for registry credentials. --k8s-keychain adds + # go-containerregistry's GitHub keychain, which reads GITHUB_TOKEN (and + # GITHUB_ACTOR) for ghcr.io, so the token is in neither argv nor a file. + GITHUB_TOKEN: ${{ inputs.registry-token }} + run: | + # An empty docker config of this step's own, so cosign cannot pick up + # a credential another step or job left in $HOME. Without any + # config.json, cosign's keychain falls back to $REGISTRY_AUTH_FILE and + # $XDG_RUNTIME_DIR/containers/auth.json, which may be shared. + DOCKER_CONFIG="$(mktemp -d "$RUNNER_TEMP/attest-chart-auth.XXXXXX")" + export DOCKER_CONFIG + echo '{}' > "$DOCKER_CONFIG/config.json" + + # A repository name may contain '.', which must match only itself. + repository_pattern="${GITHUB_REPOSITORY//./\\.}" + identity="${IDENTITY_REGEXP:-^https://github\\.com/${repository_pattern}/}" + + cosign attest --yes --k8s-keychain --type cyclonedx \ + --predicate "$PREDICATE" "$CHART_REF" + # Read it back, so an attestation that never landed fails the run + # instead of shipping a chart we only believe is attested. + cosign verify-attestation --k8s-keychain --type cyclonedx \ + --certificate-identity-regexp "$identity" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + "$CHART_REF" > /dev/null diff --git a/renovate.json b/renovate.json index 65b29cfb..4b0ae67a 100644 --- a/renovate.json +++ b/renovate.json @@ -8,6 +8,13 @@ "postUpgradeTasks": { "commands": ["go mod tidy", "make generate-notice"] } + }, + { + "matchDepNames": ["CycloneDX/cyclonedx-cli"], + "postUpgradeTasks": { + "commands": ["scripts/pin-cyclonedx.sh"], + "fileFilters": ["setup-cyclonedx/install.sh"] + } } ], "customManagers": [ @@ -17,6 +24,14 @@ "matchStrings": [ "# renovate: datasource=(?\\S+) depName=(?\\S+)\\n\\s+version: (?\\S+)" ] + }, + { + "customType": "regex", + "managerFilePatterns": ["/(^|/)setup-cyclonedx/install\\.sh$/"], + "matchStrings": [ + "# renovate: datasource=(?\\S+) depName=(?\\S+)\\n[A-Z_]+_VERSION=(?\\S+)" + ], + "extractVersionTemplate": "^v(?.+)$" } ] } diff --git a/scripts/pin-cyclonedx.sh b/scripts/pin-cyclonedx.sh new file mode 100755 index 00000000..7854b657 --- /dev/null +++ b/scripts/pin-cyclonedx.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Rewrites the cyclonedx-cli checksums in setup-cyclonedx/install.sh for the +# version that file pins. Renovate runs it after bumping CYCLONEDX_VERSION, so +# a version update and its checksums land in the same commit. + +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname "$0")/.." && pwd)" +INSTALL_SH="$ROOT_DIR/setup-cyclonedx/install.sh" + +version="$(sed -n 's/^CYCLONEDX_VERSION=//p' "$INSTALL_SH")" +if [ -z "$version" ]; then + echo "no CYCLONEDX_VERSION in $INSTALL_SH" >&2 + exit 1 +fi + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +for pair in LINUX_X64:cyclonedx-linux-x64 LINUX_ARM64:cyclonedx-linux-arm64; do + var="CYCLONEDX_SHA256_${pair%%:*}" + asset="${pair#*:}" + curl -fsSL --retry 3 \ + "https://github.com/CycloneDX/cyclonedx-cli/releases/download/v${version}/${asset}" \ + -o "$tmp/$asset" + sha="$(sha256sum "$tmp/$asset" | cut -d' ' -f1)" + sed -i.bak "s/^${var}=.*/${var}=${sha}/" "$INSTALL_SH" + rm -f "$INSTALL_SH.bak" + echo "$var=$sha" +done diff --git a/setup-cyclonedx/action.yml b/setup-cyclonedx/action.yml new file mode 100644 index 00000000..0973017f --- /dev/null +++ b/setup-cyclonedx/action.yml @@ -0,0 +1,35 @@ +name: bom setup cyclonedx +description: Install the pinned cyclonedx-cli and export the CycloneDX spec version chart SBOMs are written in. +inputs: + spec-version: + description: CycloneDX specification version merged SBOMs are written in, as cyclonedx-cli's --output-version takes it. + required: false + default: v1_7 +outputs: + path: + description: Absolute path of the installed cyclonedx binary. + value: ${{ steps.install.outputs.path }} + spec-version: + description: The validated spec version, also exported as CYCLONEDX_SPEC_VERSION. + value: ${{ steps.install.outputs.spec-version }} +runs: + using: composite + steps: + - id: install + name: Install cyclonedx-cli + shell: bash + env: + INPUT_SPEC_VERSION: ${{ inputs.spec-version }} + # Under the job's temp dir rather than /usr/local/bin: no sudo, and + # nothing left behind on a persistent self-hosted runner. + INSTALL_DIR: ${{ runner.temp }}/cyclonedx-cli + run: | + if ! [[ "$INPUT_SPEC_VERSION" =~ ^v1_[0-9]+$ ]]; then + echo "::error title=Invalid spec-version::expected v1_, got '$INPUT_SPEC_VERSION'" + exit 1 + fi + binary="$("${{ github.action_path }}/install.sh" "$INSTALL_DIR")" + dirname "$binary" >> "$GITHUB_PATH" + echo "CYCLONEDX_SPEC_VERSION=$INPUT_SPEC_VERSION" >> "$GITHUB_ENV" + echo "path=$binary" >> "$GITHUB_OUTPUT" + echo "spec-version=$INPUT_SPEC_VERSION" >> "$GITHUB_OUTPUT" diff --git a/setup-cyclonedx/install.sh b/setup-cyclonedx/install.sh new file mode 100755 index 00000000..ab6d8e72 --- /dev/null +++ b/setup-cyclonedx/install.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Installs the pinned cyclonedx-cli into the directory given as $1 and prints +# the binary's path. This file is the only place the version and checksums are +# defined: setup-cyclonedx and attest-chart both call it, and Renovate updates +# the version here (scripts/pin-cyclonedx.sh rewrites the checksums). +# +# A GitHub release asset is mutable, so the content is pinned and not just the +# version. + +set -euo pipefail + +# renovate: datasource=github-releases depName=CycloneDX/cyclonedx-cli +CYCLONEDX_VERSION=0.33.1 +CYCLONEDX_SHA256_LINUX_X64=bfc8b2538da86fe239bc53658bbb63c1c8c510a293c1e6891aa5bea5d3c58746 +CYCLONEDX_SHA256_LINUX_ARM64=b2e9fdf9665ef49868a2ec012171c6e785dcd69745bc5869e53e4f4bfb096a5f + +install_dir="${1:?usage: install.sh }" + +case "$(uname -s)/$(uname -m)" in + Linux/x86_64) + asset=cyclonedx-linux-x64 + sha256="$CYCLONEDX_SHA256_LINUX_X64" + ;; + Linux/aarch64 | Linux/arm64) + asset=cyclonedx-linux-arm64 + sha256="$CYCLONEDX_SHA256_LINUX_ARM64" + ;; + *) + echo "unsupported platform: $(uname -s)/$(uname -m)" >&2 + exit 1 + ;; +esac + +binary="$install_dir/cyclonedx" +mkdir -p "$install_dir" + +# Several steps of one job may call this; a binary that already passes the +# checksum is reused instead of downloaded again. +if ! echo "$sha256 $binary" | sha256sum --check --strict --status 2>/dev/null; then + curl -fsSL --retry 3 \ + "https://github.com/CycloneDX/cyclonedx-cli/releases/download/v${CYCLONEDX_VERSION}/${asset}" \ + -o "$binary.download" + echo "$sha256 $binary.download" | sha256sum --check --strict >&2 + mv "$binary.download" "$binary" + chmod 0755 "$binary" +fi + +"$binary" --version >&2 +echo "$binary" diff --git a/testdata/attest-chart/bom.json b/testdata/attest-chart/bom.json new file mode 100644 index 00000000..de3cacad --- /dev/null +++ b/testdata/attest-chart/bom.json @@ -0,0 +1,26 @@ +{ + "version": "2", + "name": "attest-chart", + "containerImages": { + "ghcr.io/codesphere-cloud/image-a": { + "ref": "ghcr.io/codesphere-cloud/image-a@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sboms": { + "cyclonedx": { + "path": "sboms/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.cdx.json", + "cosign": false + } + } + }, + "ghcr.io/codesphere-cloud/image-b": { + "ref": "ghcr.io/codesphere-cloud/image-b@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sboms": { + "cyclonedx": { + "path": "sboms/sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.cdx.json", + "cosign": false + } + } + } + } +} diff --git a/testdata/attest-chart/sboms/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.cdx.json b/testdata/attest-chart/sboms/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.cdx.json new file mode 100644 index 00000000..384c486c --- /dev/null +++ b/testdata/attest-chart/sboms/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.cdx.json @@ -0,0 +1,21 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": { + "bom-ref": "pkg:oci/image-a@sha256%3Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "type": "container", + "name": "ghcr.io/codesphere-cloud/image-a", + "version": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/busybox-a@1.37.0", + "type": "library", + "name": "busybox-a", + "version": "1.37.0" + } + ] +} diff --git a/testdata/attest-chart/sboms/sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.cdx.json b/testdata/attest-chart/sboms/sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.cdx.json new file mode 100644 index 00000000..dfd5baea --- /dev/null +++ b/testdata/attest-chart/sboms/sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.cdx.json @@ -0,0 +1,21 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": { + "bom-ref": "pkg:oci/image-b@sha256%3Abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "type": "container", + "name": "ghcr.io/codesphere-cloud/image-b", + "version": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/busybox-b@1.37.0", + "type": "library", + "name": "busybox-b", + "version": "1.37.0" + } + ] +} diff --git a/testdata/attest-chart/sboms/sha256-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.cdx.json b/testdata/attest-chart/sboms/sha256-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.cdx.json new file mode 100644 index 00000000..a99d2a06 --- /dev/null +++ b/testdata/attest-chart/sboms/sha256-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.cdx.json @@ -0,0 +1,21 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": { + "bom-ref": "pkg:oci/image-c@sha256%3Acccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "type": "container", + "name": "ghcr.io/codesphere-cloud/image-c", + "version": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/busybox-c@1.37.0", + "type": "library", + "name": "busybox-c", + "version": "1.37.0" + } + ] +}