From 81e79dd5f9ec760539c34a2b0f8ce444cd6d1e28 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 21 Jun 2026 11:03:12 +1000 Subject: [PATCH 1/2] ci: fix silently-broken SBOM attestation in release workflow The attest job's SBOM steps all carried continue-on-error, so the job reported success while producing no SBOM. Root cause: `cyclonedx-py requirements` was invoked with no input file (this is a uv project, no requirements.txt), so python-sbom.cdx.json was never created, the merge `cp` fallback failed, and attest-sbom hit ENOENT. Second latent bug: actions/checkout deletes the workspace (including the downloaded dist/), so the post-checkout attest-sbom subject-path would have found nothing even if the SBOM had been generated. Replace the cyclonedx-py + cargo-sbom + cyclonedx-cli merge pipeline with a single anchore/sbom-action (syft) pass that catalogs both uv.lock and Cargo.lock statically. syft never executes Cargo/build code to enumerate packages, which is the correct posture for a supply-chain security product. Check source into ./source so dist/ survives for attestation. Drop continue-on-error so a missing SBOM gates publish rather than shipping silently. Build provenance was unaffected (runs before checkout, never continue-on-error); PyPI publish and the weekly attestation health check both verify provenance only, which is why this went unnoticed. --- .github/workflows/release-please.yml | 53 +++++++++++----------------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 6211e47..8ffed0b 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -243,48 +243,37 @@ jobs: with: subject-path: dist/* + # Check out into a subdirectory: actions/checkout deletes the contents of its + # target path before cloning, and the wheels/sdist we just downloaded live in + # ./dist at the workspace root. Isolating the checkout in ./source keeps dist + # intact for the SBOM attestation below. - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ needs.release-please.outputs.tag_name || needs.validate-inputs.outputs.release_tag }} - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + path: source + + # One syft pass catalogs both the Python (uv.lock) and Rust (Cargo.lock) + # dependency trees. syft parses lockfiles statically and never executes build + # code — unlike cargo-sbom, which runs Cargo (and therefore build.rs / proc + # macros from the dependency tree) just to enumerate packages. For a supply- + # chain security product, generating the SBOM without executing untrusted code + # is the correct posture. This replaces the previous cyclonedx-py + cargo-sbom + # + cyclonedx-cli merge pipeline, which silently produced no SBOM at all. + - name: Generate SBOM (Python + Rust) + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: - python-version: '3.12' - - - name: Install Rust toolchain - run: rustup toolchain install stable --profile minimal - - - name: Generate Python SBOM - run: | - pip install cyclonedx-bom - cyclonedx-py requirements --outfile python-sbom.cdx.json --format json - continue-on-error: true - - - name: Generate Rust SBOM - run: | - cargo install cargo-sbom --locked - cd rust && cargo sbom --output-format cyclonedx_json_v1_6 > ../rust-sbom.cdx.json - continue-on-error: true - - - name: Merge SBOMs - run: | - pip install cyclonedx-cli || true - # If cyclonedx-cli merge is available, merge; otherwise use the Python SBOM - if command -v cyclonedx &> /dev/null; then - cyclonedx merge --input-files python-sbom.cdx.json rust-sbom.cdx.json --output-file sbom.cdx.json - else - # Fallback: use Python SBOM (Rust SBOM still attested separately if needed) - cp python-sbom.cdx.json sbom.cdx.json - fi - continue-on-error: true + path: source + format: cyclonedx-json + output-file: sbom.cdx.json + # No continue-on-error: a security product must not ship wheels without a + # verified SBOM. If this step fails, the publish job (needs: attest) is skipped + # rather than shipping silently. Releases are re-runnable via workflow_dispatch. - name: Attest SBOM uses: actions/attest-sbom@10926c72720ffc3f7b666661c8e55b1344e2a365 # v2 with: subject-path: dist/* sbom-path: sbom.cdx.json - continue-on-error: true publish: name: Publish to PyPI From c02c9d5c14d7b7518378b0fdab8839cbef63f32d Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 21 Jun 2026 11:11:05 +1000 Subject: [PATCH 2/2] ci: drop persisted checkout credentials before third-party SBOM action --- .github/workflows/release-please.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8ffed0b..9c5b95d 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -251,6 +251,9 @@ jobs: with: ref: ${{ needs.release-please.outputs.tag_name || needs.validate-inputs.outputs.release_tag }} path: source + # No git operations follow, and the next step runs a third-party action + # (syft) against this tree — don't persist the job token in source/.git/config. + persist-credentials: false # One syft pass catalogs both the Python (uv.lock) and Rust (Cargo.lock) # dependency trees. syft parses lockfiles statically and never executes build