From e320b36b7a78f3146bf8247c4454094dac6553ad Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Mon, 25 May 2026 10:15:27 -0500 Subject: [PATCH 1/4] =?UTF-8?q?ci:=20release-crate=20provenance=20?= =?UTF-8?q?=E2=80=94=20harden-runner=20+=20SBOM=20+=20build=20attestation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publish stays HELD (workflow_dispatch). Adds: step-security/harden-runner@v2.19.4 (egress audit) on all 3 jobs; a CycloneDX SBOM (cargo-cyclonedx --manifest-path Cargo.toml) uploaded as a build artifact (crates.io doesn't host SBOMs); and actions/attest-build-provenance@v4.1.0 SLSA provenance for the packaged .crate, run BEFORE cargo publish so a failed attestation fails the release closed. attestations: write added to the publish job. OIDC auth, cargo publish, and the verify/require-ci-green gates are unchanged. All actions SHA-pinned. --- .github/workflows/release-crate.yml | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/release-crate.yml b/.github/workflows/release-crate.yml index 8364dea2..b81db480 100644 --- a/.github/workflows/release-crate.yml +++ b/.github/workflows/release-crate.yml @@ -34,6 +34,10 @@ jobs: name: build + test + publish dry-run runs-on: ubuntu-latest steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (2026-03-27) with: @@ -57,6 +61,10 @@ jobs: contents: read actions: read steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - name: assert ci.yml is green for this commit env: GH_TOKEN: ${{ github.token }} @@ -84,7 +92,14 @@ jobs: permissions: contents: read id-token: write + # Required by actions/attest-build-provenance to write the SLSA + # provenance attestation for the published .crate. + attestations: write steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (2026-03-27) with: @@ -92,6 +107,35 @@ jobs: - name: Mint a short-lived crates.io credential (OIDC) id: auth uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 + # Supply-chain attestation + SBOM, run BEFORE the publish so a failed + # attestation fails the release closed (the .crate is never pushed if we + # can't attest it). crates.io does not host SBOMs, so the CycloneDX SBOM + # is uploaded as a build artifact rather than shipped with the crate; the + # attestation below is GitHub-issued SLSA build provenance bound to the + # exact published .crate. + - name: Package the crate (produces the artifact to attest) + # Emits target/package/ordvec-.crate — the precise byte-for-byte + # tarball that `cargo publish` uploads, so the provenance covers the + # published artifact. + run: cargo package -p ordvec --locked + - name: Generate CycloneDX SBOM for the crate + # cargo-cyclonedx writes ordvec.cdx.json (named .cdx.json) at the + # repo root for the core crate. Verified locally: the command rejects + # `-p`; package scoping is via --manifest-path against the root manifest, + # which is the `ordvec` package. + run: | + cargo install cargo-cyclonedx --locked + cargo cyclonedx --manifest-path Cargo.toml --format json + - name: Attest build provenance for the .crate + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: target/package/*.crate + - name: Upload SBOM as a build artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom-crate + path: ordvec.cdx.json + if-no-files-found: error - name: cargo publish run: cargo publish -p ordvec --locked env: From 9bbd46c0f15eeb82d057945cc35119abb09d2fc4 Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Mon, 25 May 2026 10:15:27 -0500 Subject: [PATCH 2/4] =?UTF-8?q?ci:=20release-python=20provenance=20?= =?UTF-8?q?=E2=80=94=20harden-runner=20+=20SBOM=20+=20build=20attestation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publish stays HELD. Adds: harden-runner@v2.19.4 (egress audit) on all 4 jobs; a CycloneDX SBOM (cargo-cyclonedx on ordvec-python/Cargo.toml — the wheel is the compiled Rust ext) uploaded from build-sdist; and actions/attest-build-provenance@v4.1.0 GitHub SLSA provenance for the wheels + sdist, run BEFORE the pypa publish (fail-closed) — complementary to the PyPI-side PEP 740 attestations the pypa action already emits. attestations: write added to the publish job. The wheel matrix, maturin, pytest, require-ci-green, and pypa publish are unchanged. All actions SHA-pinned. --- .github/workflows/release-python.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index caf409e8..c5c26384 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -40,6 +40,10 @@ jobs: - { runner: macos-latest, target: aarch64, manylinux: auto } - { runner: windows-latest, target: x64, manylinux: auto } steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build abi3 wheel (covers CPython 3.9+) uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 @@ -76,6 +80,10 @@ jobs: name: sdist runs-on: ubuntu-latest steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build the sdist uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 @@ -107,6 +115,23 @@ jobs: with: name: sdist path: ordvec-python/dist + # CycloneDX SBOM — generated ONCE here, not per-wheel. The wheel is the + # compiled Rust extension, so the binding crate's Cargo dependency tree is + # the meaningful, platform-independent bill of materials. cargo-cyclonedx + # walks the manifest and writes `.cdx.json` next to it, i.e. + # `ordvec-python/ordvec-python.cdx.json`. The dtolnay toolchain above + # provides cargo. + - name: Install cargo-cyclonedx + shell: bash + run: cargo install cargo-cyclonedx --locked + - name: Generate CycloneDX SBOM for the binding crate + shell: bash + run: cargo cyclonedx --manifest-path ordvec-python/Cargo.toml --format json + - name: Upload the SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom-python + path: ordvec-python/ordvec-python.cdx.json require-ci-green: # The wheel must not ship from a commit whose core (`ci.yml`) or binding @@ -120,6 +145,10 @@ jobs: contents: read actions: read steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - name: assert ci.yml and python.yml are green for this commit env: GH_TOKEN: ${{ github.token }} @@ -151,10 +180,25 @@ jobs: permissions: contents: read id-token: write + attestations: write steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true + # GitHub SLSA build provenance for the wheels + sdist, complementary to the + # PyPI-side PEP 740 attestations the pypa publish step emits below. Attest + # runs BEFORE publish so a failed attestation fails the release closed — + # nothing is pushed to PyPI without provenance recorded first. + - name: Attest build provenance for the wheels + sdist + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: | + dist/*.whl + dist/*.tar.gz - name: Publish to PyPI (Trusted Publishing; PEP 740 attestations on by default) uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 From 4145425fbdfd78d1205c56745ea6fb88be29f59e Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Mon, 25 May 2026 10:26:12 -0500 Subject: [PATCH 3/4] ci: pin cargo-cyclonedx + fail-closed SBOM uploads (#43 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qodo/copilot/Codex(P1): the SBOM tool was installed unpinned — 'cargo install cargo-cyclonedx --locked' selects whatever is latest at runtime (--locked only pins the tool's own deps), which is non-reproducible and a supply-chain risk in a release pipeline. Pin to --version 0.5.9 (the version verified locally) in both release workflows. copilot/Codex(P2): the SBOM upload steps lacked 'if-no-files-found: error', so a missing/failed SBOM could publish silently — add it to both (and collapse a duplicate on the crate upload that the count check caught). --- .github/workflows/release-crate.yml | 2 +- .github/workflows/release-python.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-crate.yml b/.github/workflows/release-crate.yml index b81db480..52018ebe 100644 --- a/.github/workflows/release-crate.yml +++ b/.github/workflows/release-crate.yml @@ -124,7 +124,7 @@ jobs: # `-p`; package scoping is via --manifest-path against the root manifest, # which is the `ordvec` package. run: | - cargo install cargo-cyclonedx --locked + cargo install cargo-cyclonedx --version 0.5.9 --locked cargo cyclonedx --manifest-path Cargo.toml --format json - name: Attest build provenance for the .crate uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index c5c26384..b9ee77cc 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -123,7 +123,7 @@ jobs: # provides cargo. - name: Install cargo-cyclonedx shell: bash - run: cargo install cargo-cyclonedx --locked + run: cargo install cargo-cyclonedx --version 0.5.9 --locked - name: Generate CycloneDX SBOM for the binding crate shell: bash run: cargo cyclonedx --manifest-path ordvec-python/Cargo.toml --format json @@ -132,6 +132,7 @@ jobs: with: name: sbom-python path: ordvec-python/ordvec-python.cdx.json + if-no-files-found: error require-ci-green: # The wheel must not ship from a commit whose core (`ci.yml`) or binding From 4060b879a9e8c8e8ef98ab20f83bad3faf3341f8 Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Mon, 25 May 2026 10:33:52 -0500 Subject: [PATCH 4/4] ci: mint crates.io OIDC credential last, just before publish (#43 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qodo: the short-lived crates.io token was minted right after checkout, then sat live through cargo package + the 'cargo install cargo-cyclonedx' SBOM build (third-party code) + attestation before cargo publish used it — a needlessly wide exposure window. Move the crates-io-auth-action step to immediately before cargo publish so the token is minted last. The id:auth -> steps.auth.outputs.token reference is unchanged (auth still runs before publish). --- .github/workflows/release-crate.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-crate.yml b/.github/workflows/release-crate.yml index 52018ebe..ef5269f0 100644 --- a/.github/workflows/release-crate.yml +++ b/.github/workflows/release-crate.yml @@ -104,9 +104,6 @@ jobs: - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (2026-03-27) with: toolchain: stable - - name: Mint a short-lived crates.io credential (OIDC) - id: auth - uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 # Supply-chain attestation + SBOM, run BEFORE the publish so a failed # attestation fails the release closed (the .crate is never pushed if we # can't attest it). crates.io does not host SBOMs, so the CycloneDX SBOM @@ -136,6 +133,13 @@ jobs: name: sbom-crate path: ordvec.cdx.json if-no-files-found: error + # Mint the crates.io OIDC credential LAST — only here, immediately before + # publish, so the short-lived token is never live during the + # `cargo install cargo-cyclonedx` step (third-party code) or the + # attestation/upload steps. Minimises the token's exposure window. + - name: Mint a short-lived crates.io credential (OIDC) + id: auth + uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 - name: cargo publish run: cargo publish -p ordvec --locked env: