chore(deps): bump k8s-openapi from 0.27.1 to 0.28.0 #192
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) 2025 Erick Bourgeois, firestoned | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: Build | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - 'src/**' | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| - 'Dockerfile*' | |
| - '.github/workflows/build.yaml' | |
| - '.github/actions/**' | |
| - '.vex/**' | |
| push: | |
| branches: | |
| - main | |
| release: | |
| types: | |
| - published | |
| # Top-level GITHUB_TOKEN is read-only by default (OpenSSF Scorecard | |
| # Token-Permissions rule). Jobs that need elevated scopes — id-token:write | |
| # for keyless Cosign / Sigstore, security-events:write for SARIF upload to | |
| # Code Scanning, packages:write for GHCR push, attestations:write for | |
| # actions/attest-build-provenance, contents:write for release-asset upload | |
| # — declare them explicitly at job level. See the docker / attest / | |
| # build-vex / iac-scan / grype / sign-artifacts / slsa-provenance / | |
| # upload-release-assets jobs below. (Semgrep SAST runs from sast.yaml.) | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUST_BACKTRACE: 1 | |
| # Override the cross-compilation linkers set in .cargo/config.toml. | |
| # config.toml specifies the Homebrew cross-compilers needed when a macOS | |
| # developer runs `cargo build --target x86_64-unknown-linux-gnu` locally. | |
| # On Linux CI runners the native target IS x86_64/aarch64-unknown-linux-gnu, | |
| # so cargo would try to invoke those cross-compilers — which are not | |
| # installed. Setting these vars to `cc` restores the default system linker. | |
| CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: cc | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: cc | |
| jobs: | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # License headers — always runs (PRs are pre-filtered by path at the trigger). | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| license-check: | |
| name: ⚖️ Verify SPDX License Headers | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Check license headers | |
| uses: firestoned/github-actions/security/license-check@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| copyright-holder: "Erick Bourgeois, firestoned" | |
| license-id: "Apache-2.0" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Commit signatures — verify-mode differs by event type. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| verify-commits: | |
| name: 🔐 Verify Signed Commits | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify commits (PR) | |
| if: github.event_name == 'pull_request' | |
| uses: firestoned/github-actions/security/verify-signed-commits@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| base-ref: origin/${{ github.base_ref }} | |
| verify-mode: pr | |
| - name: Verify commits (push to main) | |
| if: github.event_name == 'push' | |
| uses: firestoned/github-actions/security/verify-signed-commits@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| verify-mode: push | |
| - name: Verify commits (release) | |
| if: github.event_name == 'release' | |
| uses: firestoned/github-actions/security/verify-signed-commits@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| verify-mode: release | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # PR only: formatting + clippy. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| format: | |
| name: 🎨 Check Formatting | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [license-check, verify-commits] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| with: | |
| components: rustfmt | |
| - name: Check formatting | |
| run: cargo fmt -- --check | |
| clippy: | |
| name: 📎 Clippy | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [format] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| with: | |
| components: clippy | |
| - name: Cache cargo dependencies | |
| uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| - name: Run clippy | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # VEX parse gate — PR only. | |
| # Every .vex/<id>.json is a single-statement OpenVEX document (the native | |
| # OpenVEX shape). vexctl merge parses each one as input; any malformed file | |
| # fails the merge, giving us free schema + enum + structural validation | |
| # from upstream tooling. No custom validator to maintain. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| validate-vex: | |
| name: 🧾 Validate VEX Statements | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [verify-commits] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install vexctl | |
| run: make vexctl-install | |
| - name: Parse every .vex/*.json via vexctl merge | |
| run: make vex-validate | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Extract version — all events; downstream build jobs depend on this. | |
| # format/clippy are independent PR quality checks and do not gate the build. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| extract-version: | |
| name: 🏷️ Extract Version Information | |
| runs-on: ubuntu-latest | |
| needs: [license-check, verify-commits] | |
| outputs: | |
| version: ${{ steps.version-chainguard.outputs.version }} | |
| tag_name: ${{ steps.version-chainguard.outputs.tag-name }} | |
| image-tag-chainguard: ${{ steps.version-chainguard.outputs.image-tag }} | |
| image-tag-distroless: ${{ steps.version-distroless.outputs.image-tag }} | |
| image-repository-chainguard: ${{ steps.version-chainguard.outputs.image-repository }} | |
| image-repository-distroless: ${{ steps.version-distroless.outputs.image-repository }} | |
| short-sha: ${{ steps.version-chainguard.outputs.short-sha }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Extract version for Chainguard | |
| id: version-chainguard | |
| uses: ./.github/actions/extract-version | |
| with: | |
| repository: ${{ github.repository }} | |
| workflow-type: ${{ github.event_name == 'pull_request' && 'pr' || github.event_name == 'push' && 'main' || 'release' }} | |
| pr-number: ${{ github.event.pull_request.number }} | |
| release-tag: ${{ github.event.release.tag_name }} | |
| image-tag-suffix: "" | |
| - name: Extract version for Distroless | |
| id: version-distroless | |
| uses: ./.github/actions/extract-version | |
| with: | |
| repository: ${{ github.repository }} | |
| workflow-type: ${{ github.event_name == 'pull_request' && 'pr' || github.event_name == 'push' && 'main' || 'release' }} | |
| pr-number: ${{ github.event.pull_request.number }} | |
| release-tag: ${{ github.event.release.tag_name }} | |
| image-tag-suffix: "-distroless" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Build binary — all events. | |
| # On release the Cargo.toml version is bumped to match the release tag. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| build: | |
| name: 🦀 Build - ${{ matrix.platform.name }} | |
| runs-on: ${{ matrix.platform.runner }} | |
| needs: [extract-version] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform: | |
| - name: Linux x86_64 | |
| runner: ubuntu-24.04 | |
| artifact_name: 5spot-linux-amd64 | |
| binary_name: 5spot | |
| - name: Linux ARM64 | |
| runner: ubuntu-24.04-arm | |
| artifact_name: 5spot-linux-arm64 | |
| binary_name: 5spot | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Update Cargo.toml version | |
| if: github.event_name == 'release' | |
| run: | | |
| perl -i -pe 's/^version = ".*"/version = "${{ needs.extract-version.outputs.version }}"/' Cargo.toml | |
| echo "Updated Cargo.toml to version ${{ needs.extract-version.outputs.version }}" | |
| grep '^version = ' Cargo.toml | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| - name: Cache cargo dependencies | |
| uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| - name: Build binary | |
| run: cargo build --release | |
| - name: Install cargo-cyclonedx | |
| run: cargo install cargo-cyclonedx --locked --quiet | |
| - name: Generate SBOM | |
| run: | | |
| cargo cyclonedx --format json | |
| find . -maxdepth 1 -name "*.cdx.json" -exec mv {} target/release/ \; | |
| # PR and push artifacts expire after 1 day — they are not release assets. | |
| - name: Upload artifacts (PR / push) | |
| if: github.event_name != 'release' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ matrix.platform.artifact_name }} | |
| path: | | |
| target/release/${{ matrix.platform.binary_name }} | |
| target/release/*.cdx.json | |
| retention-days: 1 | |
| # Release artifacts keep the default retention so re-runs remain possible. | |
| - name: Upload artifacts (release) | |
| if: github.event_name == 'release' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ matrix.platform.artifact_name }} | |
| path: | | |
| target/release/${{ matrix.platform.binary_name }} | |
| target/release/*.cdx.json | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Tests — all events; running unconditionally lets docker depend on it simply. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| test: | |
| name: 🧪 Test | |
| runs-on: ubuntu-latest | |
| needs: [build] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| - name: Download x86_64 build artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: 5spot-linux-amd64 | |
| path: target/release/ | |
| - name: Cache cargo dependencies | |
| uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| - name: Run tests | |
| run: cargo test --all --verbose | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Docker build and push — all events. | |
| # | |
| # Metadata tags vary by event (three separate metadata steps, only one runs | |
| # per invocation; empty outputs from skipped steps are filtered out by | |
| # docker/build-push-action). On release, Cosign signing and SBOM generation | |
| # steps are also enabled. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| docker: | |
| name: 🐳 Build and Push Docker Image - ${{ matrix.variant.name }} | |
| runs-on: ubuntu-latest | |
| needs: [build, extract-version, test] | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| outputs: | |
| digest-chainguard: ${{ steps.export-digest.outputs.digest-chainguard }} | |
| digest-distroless: ${{ steps.export-digest.outputs.digest-distroless }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: | |
| - name: Chainguard | |
| dockerfile: Dockerfile.chainguard | |
| suffix: "" | |
| description: "5-Spot Chainguard Zero-CVE (glibc, FIPS-ready)" | |
| image-tag: ${{ needs.extract-version.outputs.image-tag-chainguard }} | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }} | |
| - name: Distroless | |
| dockerfile: Dockerfile | |
| suffix: "-distroless" | |
| description: "5-Spot Google Distroless (glibc)" | |
| image-tag: ${{ needs.extract-version.outputs.image-tag-distroless }} | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Prepare Docker binaries | |
| uses: ./.github/actions/prepare-docker-binaries | |
| - name: Setup Docker environment | |
| uses: firestoned/github-actions/docker/setup-docker@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| # Only one of the three metadata steps below runs per job instance. | |
| # docker/build-push-action concatenates all tags/labels outputs and | |
| # silently drops the empty lines produced by the two skipped steps. | |
| - name: Extract metadata (PR) | |
| id: meta-pr | |
| if: github.event_name == 'pull_request' | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ghcr.io/${{ matrix.variant.image-repository }} | |
| tags: | | |
| type=raw,value=${{ matrix.variant.image-tag }} | |
| type=raw,value=sha-${{ needs.extract-version.outputs.short-sha }} | |
| flavor: | | |
| latest=false | |
| - name: Extract metadata (push to main) | |
| id: meta-main | |
| if: github.event_name == 'push' | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ghcr.io/${{ matrix.variant.image-repository }} | |
| tags: | | |
| type=raw,value=${{ matrix.variant.image-tag }} | |
| type=raw,value=main-{{date 'YYYY-MM-DD'}} | |
| type=raw,value=sha-${{ needs.extract-version.outputs.short-sha }} | |
| type=raw,value=latest | |
| flavor: | | |
| latest=false | |
| - name: Extract metadata (release) | |
| id: meta-release | |
| if: github.event_name == 'release' | |
| uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 | |
| with: | |
| images: ghcr.io/${{ matrix.variant.image-repository }} | |
| tags: | | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{major}}.{{minor}} | |
| type=semver,pattern={{major}} | |
| type=raw,value=sha-${{ needs.extract-version.outputs.short-sha }}${{ matrix.variant.suffix }} | |
| flavor: | | |
| latest=false | |
| prefix=v | |
| suffix=${{ matrix.variant.suffix }} | |
| - name: Build and push Docker image (${{ matrix.variant.name }}) | |
| id: docker_build | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| with: | |
| context: . | |
| file: ${{ matrix.variant.dockerfile }} | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: | | |
| ${{ steps.meta-pr.outputs.tags }} | |
| ${{ steps.meta-main.outputs.tags }} | |
| ${{ steps.meta-release.outputs.tags }} | |
| labels: | | |
| ${{ steps.meta-pr.outputs.labels }} | |
| ${{ steps.meta-main.outputs.labels }} | |
| ${{ steps.meta-release.outputs.labels }} | |
| org.opencontainers.image.description=${{ matrix.variant.description }} | |
| build-args: | | |
| ${{ github.event_name == 'release' && format('VERSION={0}', needs.extract-version.outputs.version) || '' }} | |
| cache-from: type=gha,scope=${{ matrix.variant.name }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.variant.name }} | |
| sbom: true | |
| provenance: true | |
| # Push to main and release: sign the image with Cosign (keyless, Sigstore). | |
| # PR images are ephemeral and not deployed, so signing them is skipped. | |
| # Sign by digest (not tag) to guarantee we sign exactly what was pushed. | |
| - name: Install Cosign | |
| if: github.event_name != 'pull_request' | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Sign container image with Cosign | |
| if: github.event_name != 'pull_request' | |
| run: | | |
| cosign sign --yes \ | |
| ghcr.io/${{ matrix.variant.image-repository }}@${{ steps.docker_build.outputs.digest }} | |
| # Push + release: generate a CycloneDX SBOM for the published image. | |
| # Needed on push (not just release) because auto-vex-presence | |
| # (roadmap Phase 2) uses it to decide whether a Grype finding's purl | |
| # is actually present in the product. | |
| - name: Generate Docker SBOM for ${{ matrix.variant.name }} | |
| if: github.event_name != 'pull_request' | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| image: ghcr.io/${{ matrix.variant.image-repository }}:${{ matrix.variant.image-tag }} | |
| format: cyclonedx-json | |
| output-file: docker-sbom-${{ matrix.variant.name }}.json | |
| upload-release-assets: false | |
| - name: Upload Docker SBOM as artifact | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: docker-sbom-${{ matrix.variant.name }} | |
| path: docker-sbom-${{ matrix.variant.name }}.json | |
| - name: Export image digest | |
| id: export-digest | |
| run: | | |
| case "${{ matrix.variant.name }}" in | |
| Chainguard) echo "digest-chainguard=${{ steps.docker_build.outputs.digest }}" >> "$GITHUB_OUTPUT" ;; | |
| Distroless) echo "digest-distroless=${{ steps.docker_build.outputs.digest }}" >> "$GITHUB_OUTPUT" ;; | |
| esac | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # GitHub Artifact Attestation — all events. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| attest: | |
| name: 🔏 Attest Docker Image - ${{ matrix.variant.name }} | |
| runs-on: ubuntu-latest | |
| needs: [docker, extract-version] | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: | |
| - name: Chainguard | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }} | |
| digest: ${{ needs.docker.outputs.digest-chainguard }} | |
| - name: Distroless | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }} | |
| digest: ${{ needs.docker.outputs.digest-distroless }} | |
| steps: | |
| - name: Log in to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Attest ${{ matrix.variant.name }} image provenance | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ghcr.io/${{ matrix.variant.image-repository }} | |
| subject-digest: ${{ matrix.variant.digest }} | |
| push-to-registry: true | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Grype triage scan — push + release. | |
| # Runs Grype against each image variant WITHOUT VEX suppression and emits | |
| # JSON (not SARIF). Its output feeds auto-vex-presence so we can see the | |
| # raw set of CVEs before any suppression. The subsequent `grype` job | |
| # (after build-vex) still runs with the final VEX for SARIF upload to | |
| # Code Scanning. Two passes; costs ~30 s per variant; keeps the | |
| # dependency graph acyclic (presence-check needs grype output; final | |
| # grype needs the VEX that includes the presence-check output). | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| grype-triage: | |
| name: 🔭 Grype Triage Scan - ${{ matrix.variant.name }} | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [docker, extract-version] | |
| permissions: | |
| contents: read | |
| packages: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: | |
| - name: Chainguard | |
| image-tag: ${{ needs.extract-version.outputs.image-tag-chainguard }} | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }} | |
| - name: Distroless | |
| image-tag: ${{ needs.extract-version.outputs.image-tag-distroless }} | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }} | |
| steps: | |
| - name: Log in to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Pinned version matches the later grype job. Bump both together. | |
| - name: Install Grype | |
| env: | |
| GRYPE_VERSION: '0.87.0' | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz" | |
| curl -fsSLo grype.tgz "$url" | |
| tar -xzf grype.tgz grype | |
| sudo install -m 0755 grype /usr/local/bin/grype | |
| grype version | |
| - name: Run Grype triage (no VEX) | |
| env: | |
| IMAGE_REF: ghcr.io/${{ matrix.variant.image-repository }}:${{ matrix.variant.image-tag }} | |
| run: | | |
| grype "$IMAGE_REF" \ | |
| --output json \ | |
| --file grype-triage-${{ matrix.variant.name }}.json | |
| - name: Upload triage artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: grype-triage-${{ matrix.variant.name }} | |
| path: grype-triage-${{ matrix.variant.name }}.json | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Auto-VEX (presence-based) — push + release. Roadmap Phase 2. | |
| # | |
| # For each Grype triage finding whose affected purl is absent from every | |
| # image SBOM and whose CVE id is not already covered by a hand-authored | |
| # .vex/*.json statement, emit a `not_affected + component_not_present` | |
| # OpenVEX statement. The result is uploaded as the `vex-auto-presence` | |
| # artifact and is also merged unconditionally into the signed VEX | |
| # document by `build-vex`. Verification is downstream (Security team | |
| # counter-signs after their own re-evaluation); our side emits | |
| # aggressively. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| auto-vex-presence: | |
| name: 🤖 Auto-VEX (presence) | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [docker, extract-version, grype-triage] | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| - name: Cache cargo dependencies | |
| uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| - name: Build auto-vex-presence | |
| run: cargo build --release --bin auto-vex-presence | |
| - name: Download triage scan artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: grype-triage-* | |
| path: ./triage | |
| merge-multiple: true | |
| - name: Download Docker SBOM artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: docker-sbom-* | |
| path: ./sboms | |
| merge-multiple: true | |
| # One auto-presence document per image variant, then merged at the | |
| # end. We keep variants separate because Grype's triage scan is | |
| # per-image and we want each statement's `products` to correctly | |
| # scope to the image it was derived from. For simplicity in Phase 2, | |
| # both variants use the same `pkg:oci/5-spot` product purl (matching | |
| # the existing hand-authored statements). | |
| - name: Run auto-vex-presence | |
| run: | | |
| set -euo pipefail | |
| vex_id="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/auto-vex-presence" | |
| ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| per_variant=() | |
| for variant in Chainguard Distroless; do | |
| triage="triage/grype-triage-${variant}.json" | |
| sbom="sboms/docker-sbom-${variant}.json" | |
| if [ ! -f "$triage" ] || [ ! -f "$sbom" ]; then | |
| echo "missing $triage or $sbom; skipping variant $variant" | |
| continue | |
| fi | |
| out="vex.auto-presence-${variant}.json" | |
| ./target/release/auto-vex-presence \ | |
| --grype-json "$triage" \ | |
| --sbom "$sbom" \ | |
| --vex-dir .vex \ | |
| --product-purl "pkg:oci/5-spot" \ | |
| --id "${vex_id}/${variant}" \ | |
| --author "auto-vex-presence" \ | |
| --timestamp "$ts" \ | |
| --output "$out" | |
| per_variant+=("$out") | |
| done | |
| if [ ${#per_variant[@]} -eq 0 ]; then | |
| echo "ERROR: no variant produced an auto-presence document" | |
| exit 1 | |
| fi | |
| - name: Merge per-variant auto-presence documents | |
| run: | | |
| make vexctl-install | |
| vexctl merge \ | |
| --id "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/auto-vex-presence" \ | |
| --author "auto-vex-presence" \ | |
| vex.auto-presence-*.json > vex.auto-presence.json | |
| echo "── vex.auto-presence.json ─────────────────────────────────────────" | |
| cat vex.auto-presence.json | |
| - name: Upload auto-presence artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: vex-auto-presence | |
| path: vex.auto-presence.json | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Auto-VEX (symbol-import reachability) — push + release. Roadmap Phase 3. | |
| # | |
| # For each Grype triage finding whose CVE id maps to a list of | |
| # affected library function names in `.vex/.affected-functions.json`, | |
| # check whether the release binary's dynamic symbol-import table | |
| # contains any of those names. If none → emit a `not_affected + | |
| # vulnerable_code_not_in_execute_path` statement. The result is | |
| # uploaded as the `vex-auto-reachability` artifact and merged | |
| # unconditionally into the signed VEX document by `build-vex`. | |
| # | |
| # Why "symbol-import reachability" instead of full LLVM-IR call graph: | |
| # cargo audit reports zero open Rust-level vulnerabilities for this | |
| # codebase; every CVE we triage is a base-image glibc/zlib finding | |
| # surfaced by Grype. Those CVEs are not tracked in RustSec, so the | |
| # original roadmap's RustSec-affected-functions approach addresses | |
| # zero current findings. Symbol-import absence is the mechanical | |
| # equivalent for our actual data: if our Rust binary doesn't link | |
| # against `glob`/`scanf`/`iconv`, the affected glibc code paths | |
| # cannot be reached through documented entry points. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| auto-vex-reachability: | |
| name: 🤖 Auto-VEX (reachability) | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [build, extract-version, grype-triage] | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| - name: Cache cargo dependencies | |
| uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| - name: Build auto-vex-reachability | |
| run: cargo build --release --bin auto-vex-reachability | |
| - name: Download release binary (5spot-linux-amd64) | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: 5spot-linux-amd64 | |
| path: ./bin | |
| - name: Download triage scan artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: grype-triage-* | |
| path: ./triage | |
| merge-multiple: true | |
| # Capture the binary's dynamic symbol-import table. This is the | |
| # ELF view; on Linux runners we can use nm -D --undefined-only | |
| # directly. The output becomes the evidence the Security team | |
| # re-derives downstream during counter-signing. | |
| - name: Capture binary symbol imports | |
| run: | | |
| set -euo pipefail | |
| chmod +x bin/5spot | |
| nm -D --undefined-only bin/5spot > symbols.txt | |
| echo "── imported symbol count ──" | |
| wc -l symbols.txt | |
| echo "── first 20 symbols ──" | |
| head -20 symbols.txt | |
| # Run once per variant (binary symbols are identical across | |
| # variants but Grype findings differ), merge per-variant outputs | |
| # at the end. | |
| - name: Run auto-vex-reachability | |
| run: | | |
| set -euo pipefail | |
| vex_id="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/auto-vex-reachability" | |
| ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| per_variant=() | |
| for variant in Chainguard Distroless; do | |
| triage="triage/grype-triage-${variant}.json" | |
| if [ ! -f "$triage" ]; then | |
| echo "missing $triage; skipping variant $variant" | |
| continue | |
| fi | |
| out="vex.auto-reachability-${variant}.json" | |
| ./target/release/auto-vex-reachability \ | |
| --grype-json "$triage" \ | |
| --binary-symbols symbols.txt \ | |
| --affected-functions .vex/.affected-functions.json \ | |
| --vex-dir .vex \ | |
| --product-purl "pkg:oci/5-spot" \ | |
| --id "${vex_id}/${variant}" \ | |
| --author "auto-vex-reachability" \ | |
| --timestamp "$ts" \ | |
| --output "$out" | |
| per_variant+=("$out") | |
| done | |
| if [ ${#per_variant[@]} -eq 0 ]; then | |
| echo "ERROR: no variant produced an auto-reachability document" | |
| exit 1 | |
| fi | |
| - name: Merge per-variant auto-reachability documents | |
| run: | | |
| make vexctl-install | |
| vexctl merge \ | |
| --id "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/auto-vex-reachability" \ | |
| --author "auto-vex-reachability" \ | |
| vex.auto-reachability-*.json > vex.auto-reachability.json | |
| echo "── vex.auto-reachability.json ─────────────────────────────────" | |
| cat vex.auto-reachability.json | |
| - name: Upload auto-reachability artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: vex-auto-reachability | |
| path: vex.auto-reachability.json | |
| if-no-files-found: error | |
| # The symbols.txt evidence is uploaded separately so the Security | |
| # team can re-run the same reachability analysis against the | |
| # release-attested binary digest and confirm the auto-emitted | |
| # statements. | |
| - name: Upload symbol-imports evidence | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: vex-auto-reachability-evidence | |
| path: symbols.txt | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # VEX document — push to main + release. | |
| # | |
| # Reads hand-authored per-CVE triage from .vex/*.json (each file is a | |
| # single-statement native OpenVEX document), merges them into a single | |
| # document via `vexctl merge`, and (on push/release only) Cosign-attests | |
| # it to both image digests. The `openvex` artifact is always uploaded so | |
| # the `grype` container scan job can consume it via `--vex` and suppress | |
| # already-triaged findings from GitHub Code Scanning. On release, the | |
| # GitHub build-provenance bundle is also emitted and attached to the | |
| # release. | |
| # | |
| # vexctl merge fails on any malformed input, so there's no separate | |
| # validator step — a bad merge slipping past the PR gate would also fail | |
| # the assembly here. | |
| # | |
| # Trust model: this job emits and Cosign-attests the VEX document. The | |
| # auto-presence (Phase 2) and auto-reachability (Phase 3) artifacts are | |
| # merged unconditionally alongside the hand-authored `.vex/*.json` | |
| # statements. Verification of the resulting claims is performed | |
| # downstream by the Security team, which independently re-evaluates the | |
| # VEX against the attached evidence (SBOMs, symbol-imports, | |
| # Cosign attestations, SLSA provenance) and counter-signs if they | |
| # agree. There is no feature-flag gate on our side — automation is | |
| # aggressive, verification is external. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| build-vex: | |
| name: 🧾 Assemble OpenVEX | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [docker, extract-version, auto-vex-presence, auto-vex-reachability] | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| packages: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install vexctl | |
| run: make vexctl-install | |
| - name: Download auto-presence artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: vex-auto-presence | |
| path: ./auto | |
| - name: Download auto-reachability artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: vex-auto-reachability | |
| path: ./auto | |
| # Canonical @id varies by event so downstream consumers can link back | |
| # to the exact commit or release tag that produced the document. | |
| # Both auto-* documents are merged alongside hand-authored | |
| # statements on every build. | |
| - name: Assemble OpenVEX document | |
| id: assemble | |
| run: | | |
| set -euo pipefail | |
| case "${{ github.event_name }}" in | |
| release) | |
| vex_id="https://github.com/${{ github.repository }}/releases/tag/${{ needs.extract-version.outputs.tag_name }}/vex" | |
| ;; | |
| push) | |
| vex_id="https://github.com/${{ github.repository }}/commit/${{ github.sha }}/vex" | |
| ;; | |
| *) | |
| vex_id="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/vex" | |
| ;; | |
| esac | |
| test -f auto/vex.auto-presence.json || { echo "ERROR: auto-presence artifact missing"; exit 1; } | |
| test -f auto/vex.auto-reachability.json || { echo "ERROR: auto-reachability artifact missing"; exit 1; } | |
| vexctl merge \ | |
| --id "$vex_id" \ | |
| --author "${{ github.event.release.author.login || github.actor }}" \ | |
| .vex/*.json auto/vex.auto-presence.json auto/vex.auto-reachability.json > vex.openvex.json | |
| echo "── vex.openvex.json ─────────────────────────────────────────" | |
| cat vex.openvex.json | |
| # Always upload the assembled document so the `grype` scan job can | |
| # download it via actions/download-artifact@v4 regardless of event. | |
| - name: Upload OpenVEX document artifact | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: openvex | |
| path: vex.openvex.json | |
| if-no-files-found: error | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Log in to GHCR (for cosign attest) | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Cosign-attest the OpenVEX document to each image digest (keyless OIDC, | |
| # writes to the Sigstore transparency log + registry). Runs on both | |
| # main push and release so staging images have the same VEX posture | |
| # as release images. | |
| - name: Cosign attest OpenVEX to Chainguard image | |
| run: | | |
| cosign attest --yes \ | |
| --predicate vex.openvex.json \ | |
| --type openvex \ | |
| ghcr.io/${{ needs.extract-version.outputs.image-repository-chainguard }}@${{ needs.docker.outputs.digest-chainguard }} | |
| - name: Cosign attest OpenVEX to Distroless image | |
| run: | | |
| cosign attest --yes \ | |
| --predicate vex.openvex.json \ | |
| --type openvex \ | |
| ghcr.io/${{ needs.extract-version.outputs.image-repository-distroless }}@${{ needs.docker.outputs.digest-distroless }} | |
| # GitHub-native attestation parity with other release artifacts. | |
| # Release-only because the bundle is attached as a release asset and | |
| # the attestation links to the release tag. | |
| - name: Attest OpenVEX build provenance | |
| if: github.event_name == 'release' | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| id: attest-vex | |
| with: | |
| subject-path: vex.openvex.json | |
| # The attest-build-provenance action writes the sigstore bundle to a | |
| # path exposed via its `bundle-path` output. Copy it next to the VEX | |
| # document so the release-assets job can pick it up. | |
| - name: Stage VEX bundle beside document | |
| if: github.event_name == 'release' | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${{ steps.attest-vex.outputs.bundle-path }}" ]; then | |
| cp "${{ steps.attest-vex.outputs.bundle-path }}" vex.openvex.json.bundle | |
| fi | |
| ls -lh vex.openvex.json.bundle | |
| - name: Upload OpenVEX bundle artifact (release) | |
| if: github.event_name == 'release' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: openvex-bundle | |
| path: vex.openvex.json.bundle | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Security scan — all events. | |
| # Gitleaks secret scan only runs on PRs; release uploads the audit report. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| security: | |
| name: 🛡️ Security Vulnerability Scan | |
| runs-on: ubuntu-latest | |
| needs: [verify-commits] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| - name: Run security scan (PR / push) | |
| if: github.event_name != 'release' | |
| uses: firestoned/github-actions/rust/security-scan@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| cargo-audit-version: "0.22.0" | |
| - name: Run security scan (release) | |
| if: github.event_name == 'release' | |
| uses: firestoned/github-actions/rust/security-scan@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| cargo-audit-version: "0.22.0" | |
| upload-artifact-name: cargo-audit-report-release | |
| - name: Run gitleaks (secret scanning) | |
| if: github.event_name == 'pull_request' | |
| run: make gitleaks | |
| # Semgrep SAST moved to .github/workflows/sast.yaml so it runs on every | |
| # PR and push with no `paths:` filter (Scorecard's SAST check records | |
| # skipped commits as uncovered otherwise). | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # IaC security scan (Trivy config) — PR + push to main. | |
| # Scans Kubernetes manifests under deploy/ and the Dockerfiles for | |
| # misconfigurations. Uploads SARIF to the Code Scanning dashboard. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| iac-scan: | |
| name: 🏗️ IaC Security Scan (Trivy config) | |
| if: github.event_name != 'release' | |
| runs-on: ubuntu-latest | |
| needs: [verify-commits] | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Run Trivy config scan | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 | |
| with: | |
| scan-type: config | |
| scan-ref: . | |
| format: sarif | |
| output: trivy-iac.sarif | |
| severity: CRITICAL,HIGH,MEDIUM | |
| exit-code: '0' | |
| # Justified suppressions live in .trivyignore at the repo root; | |
| # each entry is commented with an architectural rationale. | |
| trivyignores: ./.trivyignore | |
| - name: Upload Trivy IaC SARIF to Code Scanning | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: trivy-iac.sarif | |
| category: trivy-iac | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # cargo-deny — PR + push to main. | |
| # Enforces the license allow-list, RustSec advisories (same DB as | |
| # cargo-audit), and forbids dependencies from unknown registries or git | |
| # URLs. Configured via deny.toml at repo root. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| cargo-deny: | |
| name: 🧪 cargo-deny (License + Advisory + Sources) | |
| if: github.event_name != 'release' | |
| runs-on: ubuntu-latest | |
| needs: [verify-commits] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Run cargo-deny | |
| uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 | |
| with: | |
| log-level: warn | |
| command: check | |
| arguments: --all-features | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Container security scan (Grype) — all events. | |
| # | |
| # Grype is the canonical Anchore/Chainguard scanner and natively consumes | |
| # OpenVEX via --vex to suppress already-triaged CVEs (Trivy's VEX support | |
| # is less mature and requires a different input shape). On push + release | |
| # the build-vex job assembles a VEX document; on PR there is no VEX yet, | |
| # so the scan runs without suppression. SARIF is uploaded to GitHub Code | |
| # Scanning under a category segmented by event + variant so historical | |
| # findings remain separable across branches. | |
| # | |
| # Ordering note: build-vex is listed in `needs` so the VEX artifact is | |
| # ready before grype runs. On PR events build-vex is skipped, so the `if` | |
| # guard lets grype proceed regardless of that skipped status. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| grype: | |
| name: 🔭 Container Security Scan - ${{ matrix.variant.name }} | |
| runs-on: ubuntu-latest | |
| needs: [docker, extract-version, build-vex] | |
| if: | | |
| always() && | |
| needs.docker.result == 'success' && | |
| needs.extract-version.result == 'success' && | |
| (needs.build-vex.result == 'success' || needs.build-vex.result == 'skipped') | |
| permissions: | |
| contents: read | |
| packages: read | |
| id-token: write | |
| security-events: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: | |
| - name: Chainguard | |
| image-tag: ${{ needs.extract-version.outputs.image-tag-chainguard }} | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-chainguard }} | |
| - name: Distroless | |
| image-tag: ${{ needs.extract-version.outputs.image-tag-distroless }} | |
| image-repository: ${{ needs.extract-version.outputs.image-repository-distroless }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Download the VEX document assembled by build-vex. Only present on | |
| # push + release events — PR events have no document to consume. | |
| - name: Download OpenVEX document | |
| if: github.event_name != 'pull_request' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: openvex | |
| path: ./vex | |
| # Pinned-version install from the Anchore release tarball. Bump via | |
| # GRYPE_VERSION and record in | |
| # ~/dev/roadmaps/5spot-vex-generation-and-signing.md § Dependencies. | |
| - name: Install Grype | |
| env: | |
| GRYPE_VERSION: '0.87.0' | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz" | |
| curl -fsSLo grype.tgz "$url" | |
| tar -xzf grype.tgz grype | |
| sudo install -m 0755 grype /usr/local/bin/grype | |
| grype version | |
| # Build --vex only when we actually have a document. Grype ignores | |
| # statements whose product purl does not match the scanned image, so | |
| # passing an over-inclusive VEX is safe. A missing VEX simply means | |
| # no suppressions apply (PR events). | |
| - name: Run Grype scan on ${{ matrix.variant.name }} image | |
| env: | |
| IMAGE_REF: ghcr.io/${{ matrix.variant.image-repository }}:${{ matrix.variant.image-tag }} | |
| SARIF_FILE: grype-${{ github.event_name }}-${{ matrix.variant.name }}.sarif | |
| run: | | |
| set -euo pipefail | |
| vex_args=() | |
| if [ -f vex/vex.openvex.json ]; then | |
| echo "Using VEX document: vex/vex.openvex.json" | |
| vex_args+=(--vex vex/vex.openvex.json) | |
| else | |
| echo "No VEX document available — scanning without suppressions." | |
| fi | |
| grype "$IMAGE_REF" \ | |
| "${vex_args[@]}" \ | |
| --output sarif \ | |
| --file "$SARIF_FILE" | |
| - name: Upload Grype SARIF to Code Scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 | |
| with: | |
| sarif_file: grype-${{ github.event_name }}-${{ matrix.variant.name }}.sarif | |
| category: grype-${{ github.event_name }}-${{ matrix.variant.name }} | |
| - name: Upload Grype SARIF artifact | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ github.event_name == 'release' && format('grype-release-{0}', matrix.variant.name) || format('grype-{0}-{1}-{2}', github.event_name, matrix.variant.name, github.run_number) }} | |
| path: grype-${{ github.event_name }}-${{ matrix.variant.name }}.sarif | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Push-to-main and release: sign binaries with Cosign and emit GitHub | |
| # artifact attestations. Skipped on PRs. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| sign-artifacts: | |
| name: ✍️ Sign Binary Artifacts | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [build, extract-version] | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform: | |
| - artifact_name: 5spot-linux-amd64 | |
| binary_name: 5spot | |
| - artifact_name: 5spot-linux-arm64 | |
| binary_name: 5spot | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Download binary artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.platform.artifact_name }} | |
| path: ./artifacts/${{ matrix.platform.artifact_name }} | |
| - name: Create tarball for signing | |
| run: | | |
| cd artifacts/${{ matrix.platform.artifact_name }} | |
| chmod +x ${{ matrix.platform.binary_name }} | |
| tar czf ${{ matrix.platform.artifact_name }}.tar.gz ${{ matrix.platform.binary_name }} | |
| ls -lh ${{ matrix.platform.artifact_name }}.tar.gz | |
| - name: Sign binary tarball with Cosign | |
| uses: firestoned/github-actions/security/cosign-sign@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| with: | |
| artifact-path: artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz | |
| - name: Attest binary tarball provenance | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-path: artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz | |
| - name: Upload signed artifacts | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ matrix.platform.artifact_name }}-signed | |
| path: | | |
| artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz | |
| artifacts/${{ matrix.platform.artifact_name }}/${{ matrix.platform.artifact_name }}.tar.gz.bundle | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Push-to-main and release: compute SHA-256 hashes of signed tarballs for SLSA. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| generate-provenance-subjects: | |
| name: 🔬 Generate SLSA Provenance Subjects | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| needs: [sign-artifacts, extract-version] | |
| outputs: | |
| hashes: ${{ steps.hash.outputs.hashes }} | |
| steps: | |
| - name: Download signed artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: 5spot-*-signed | |
| path: ./artifacts | |
| merge-multiple: true | |
| - name: Generate hashes for SLSA provenance | |
| id: hash | |
| run: | | |
| cd artifacts | |
| sha256sum *.tar.gz > checksums.txt | |
| cat checksums.txt | |
| echo "hashes=$(cat checksums.txt | base64 -w0)" >> "$GITHUB_OUTPUT" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Push-to-main and release: SLSA Level 3 provenance via the official generator. | |
| # `upload-assets` only applies on release (there is no GitHub Release object | |
| # on a push-to-main to attach assets to); on push the provenance lands as a | |
| # workflow artifact, verifiable with slsa-verifier or `gh attestation verify`. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| slsa-provenance: | |
| name: 🏛️ Generate SLSA Provenance | |
| if: github.event_name != 'pull_request' | |
| needs: [generate-provenance-subjects, extract-version] | |
| permissions: | |
| actions: read | |
| id-token: write | |
| contents: write | |
| # MUST remain on a semver tag, not a SHA: SLSA Level 3 verification | |
| # requires the generator ref to be one of the approved released versions. | |
| # SHA-pinning this line would break slsa-verifier attestation validation. | |
| # See https://github.com/slsa-framework/slsa-github-generator#verification-of-provenance | |
| uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 | |
| with: | |
| base64-subjects: "${{ needs.generate-provenance-subjects.outputs.hashes }}" | |
| upload-assets: ${{ github.event_name == 'release' }} | |
| provenance-name: "${{ needs.extract-version.outputs.version }}.intoto.jsonl" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Release only: regenerate and package the CRD deploy manifests. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| package-deploy-manifests: | |
| name: 📦 Package Deployment Manifests | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| needs: [extract-version] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (moving ref — re-pin quarterly) | |
| - name: Cache cargo dependencies | |
| uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6 | |
| - name: Regenerate CRDs | |
| run: cargo run --quiet --bin crdgen > deploy/crds/scheduledmachine.yaml | |
| - name: Package deploy manifests | |
| run: tar czf deploy-manifests.tar.gz deploy/ | |
| - name: Upload deploy manifests | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: deploy-manifests | |
| path: deploy-manifests.tar.gz | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Release only: collate all artifacts and upload to the GitHub Release. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| upload-release-assets: | |
| name: 🚀 Upload Release Assets | |
| if: github.event_name == 'release' | |
| runs-on: ubuntu-latest | |
| needs: [build, docker, attest, sign-artifacts, slsa-provenance, package-deploy-manifests, build-vex] | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: ./artifacts | |
| pattern: "{5spot-*,docker-sbom-*,*.intoto.jsonl,deploy-manifests,openvex,openvex-bundle}" | |
| - name: 🧹 Organize release artifacts | |
| run: | | |
| cd artifacts | |
| mkdir -p release sboms signatures provenance | |
| # Copy signed tarballs and signature bundles | |
| for dir in 5spot-*-signed; do | |
| if [ -d "$dir" ]; then | |
| platform="${dir%-signed}" | |
| if compgen -G "$dir/*.tar.gz" > /dev/null; then | |
| cp "$dir"/*.tar.gz release/ | |
| fi | |
| if compgen -G "$dir/*.tar.gz.bundle" > /dev/null; then | |
| cp "$dir"/*.tar.gz.bundle signatures/ | |
| fi | |
| fi | |
| done | |
| # Collect binary SBOMs (Linux builds only) | |
| for dir in 5spot-linux-amd64 5spot-linux-arm64; do | |
| if [ -d "$dir" ] && compgen -G "$dir/*.cdx.json" > /dev/null; then | |
| cp "$dir"/*.cdx.json "sboms/${dir}-sbom.json" | |
| fi | |
| done | |
| # Copy Docker SBOMs | |
| for variant in Chainguard Distroless; do | |
| if [ -d "docker-sbom-${variant}" ] && [ -f "docker-sbom-${variant}/docker-sbom-${variant}.json" ]; then | |
| cp "docker-sbom-${variant}/docker-sbom-${variant}.json" "sboms/docker-sbom-${variant}.json" | |
| fi | |
| done | |
| # Copy SLSA provenance | |
| find . -name "*.intoto.jsonl" -type f -exec cp {} provenance/ \; | |
| # Copy deploy manifests | |
| if [ -f "deploy-manifests/deploy-manifests.tar.gz" ]; then | |
| cp deploy-manifests/deploy-manifests.tar.gz release/ | |
| fi | |
| # Copy OpenVEX document (release/) and its attestation bundle | |
| # (signatures/). The release/ location keeps the document browseable | |
| # next to the binaries; the bundle sits with the other Sigstore | |
| # bundles so consumers can find them in one place. The document | |
| # ships via the always-uploaded `openvex` artifact; the signed | |
| # bundle ships via the release-only `openvex-bundle` artifact. | |
| if [ -f "openvex/vex.openvex.json" ]; then | |
| cp openvex/vex.openvex.json release/ | |
| fi | |
| if [ -f "openvex-bundle/vex.openvex.json.bundle" ]; then | |
| cp openvex-bundle/vex.openvex.json.bundle signatures/ | |
| fi | |
| # Generate SHA256 checksums | |
| cd release && sha256sum * > checksums.sha256 | |
| cd ../sboms && sha256sum *.json >> ../release/checksums.sha256 | |
| cd ../signatures && sha256sum *.bundle >> ../release/checksums.sha256 | |
| cd ../provenance && compgen -G "*.intoto.jsonl" > /dev/null && sha256sum *.intoto.jsonl >> ../release/checksums.sha256 || true | |
| echo "Release artifacts:" | |
| ls -lh ../release/ | |
| - name: Upload release assets | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 | |
| with: | |
| files: | | |
| artifacts/release/*.tar.gz | |
| artifacts/release/vex.openvex.json | |
| artifacts/sboms/*.json | |
| artifacts/signatures/*.bundle | |
| artifacts/provenance/*.intoto.jsonl | |
| artifacts/release/checksums.sha256 |