diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ec79f7..6f430cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,11 +132,14 @@ jobs: cp /tmp/shim-cli-evidence-source/tests/corpus/guard-tools-v1.json dist/guard-tools-v1.json - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: - path: dist/packages + path: /tmp/shim-cli-benchmark format: cyclonedx-json output-file: dist/shim-cli.sbom.cdx.json upload-artifact: false upload-release-assets: false + - name: Require the SBOM to list the package and every locked runtime dependency + run: | + python -c 'import json, os, re; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); found = {(re.sub(r"[-_.]+", "-", component["name"]).lower(), component.get("version")) for component in json.loads(Path("dist/shim-cli.sbom.cdx.json").read_text())["components"] if component["type"] == "library"}; locked = {(re.sub(r"[-_.]+", "-", name).lower(), pin) for name, pin in re.findall(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==(\S+) \\$", Path("dist/requirements.lock").read_text(), re.MULTILINE)}; assert ("shim", version) in found; assert locked and locked <= found, sorted(locked - found)' - name: Recheck the package artifact allowlist run: | python -c 'import os; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); expected = {f"shim-{version}-py3-none-any.whl", f"shim-{version}.tar.gz"}; assert {path.name for path in Path("dist/packages").iterdir()} == expected' @@ -150,12 +153,6 @@ jobs: cd .. sha256sum COMPATIBILITY.md shim.pyz guard-v2.json guard-v2-metrics.json guard-tools-v1.json benchmark-hook.json requirements.lock shim-cli.sbom.cdx.json ) > dist/SHA256SUMS - - name: Verify checksums from a flat download directory - run: | - mkdir dist/download - cp "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/shim-cli.sbom.cdx.json dist/SHA256SUMS dist/download/ - cd dist/download - sha256sum -c SHA256SUMS - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-dist @@ -187,18 +184,48 @@ jobs: - name: Verify the package artifact allowlist run: python -c 'import os; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); expected = {f"shim-{version}-py3-none-any.whl", f"shim-{version}.tar.gz"}; assert {path.name for path in Path("dist/packages").iterdir()} == expected' - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + id: provenance with: - subject-path: dist/packages/* + subject-path: | + dist/packages/* + dist/shim.pyz - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + id: sbom with: - subject-path: dist/packages/* + subject-path: | + dist/packages/* + dist/shim.pyz sbom-path: dist/shim-cli.sbom.cdx.json + - name: Attach and hash the attestation bundles + env: + PROVENANCE_BUNDLE: ${{ steps.provenance.outputs.bundle-path }} + SBOM_BUNDLE: ${{ steps.sbom.outputs.bundle-path }} + run: | + cp "$PROVENANCE_BUNDLE" "dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl" + cp "$SBOM_BUNDLE" "dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" + cp "$PROVENANCE_BUNDLE" dist/shim.pyz.sigstore.json + ( + cd dist + sha256sum "shim-${GITHUB_REF_NAME#v}.intoto.jsonl" "shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" shim.pyz.sigstore.json + ) >> dist/SHA256SUMS + - name: Verify checksums from a flat download directory + run: | + mkdir /tmp/shim-cli-download + cp "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/shim-cli.sbom.cdx.json "dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl" "dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" dist/shim.pyz.sigstore.json dist/SHA256SUMS /tmp/shim-cli-download/ + cd /tmp/shim-cli-download + sha256sum -c SHA256SUMS + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: attested-dist + path: dist/ + if-no-files-found: error release: needs: [build, attest] runs-on: ubuntu-latest environment: release permissions: + attestations: read contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -211,13 +238,20 @@ jobs: test -f "docs/releases/${GITHUB_REF_NAME#v}.md" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: release-dist + name: attested-dist path: dist - name: Verify the package artifact allowlist run: python -c 'import os; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); expected = {f"shim-{version}-py3-none-any.whl", f"shim-{version}.tar.gz"}; assert {path.name for path in Path("dist/packages").iterdir()} == expected' + - name: Verify the attestations before publishing + env: + GH_TOKEN: ${{ github.token }} + run: | + gh attestation verify "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" --repo "$GITHUB_REPOSITORY" + gh attestation verify "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" --repo "$GITHUB_REPOSITORY" + gh attestation verify dist/shim.pyz --repo "$GITHUB_REPOSITORY" - env: GH_TOKEN: ${{ github.token }} - run: gh release create "$GITHUB_REF_NAME" "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/SHA256SUMS dist/shim-cli.sbom.cdx.json --notes-file "docs/releases/${GITHUB_REF_NAME#v}.md" --repo "$GITHUB_REPOSITORY" + run: gh release create "$GITHUB_REF_NAME" "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/SHA256SUMS dist/shim-cli.sbom.cdx.json "dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl" "dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" dist/shim.pyz.sigstore.json --notes-file "docs/releases/${GITHUB_REF_NAME#v}.md" --repo "$GITHUB_REPOSITORY" publish: needs: [build, attest, release] diff --git a/README.md b/README.md index 2c0259c..a24508a 100644 --- a/README.md +++ b/README.md @@ -469,6 +469,7 @@ Every figure here was measured on the released build, not estimated. | Hook cost | **67 ms** median end to end, interpreter start included; **41 ms** for a session summary | | With 32 custom patterns | **+0.8 ms** median against the same prompt with none | | Detector corpus | **570 cases**, graded on exact redacted output rather than category presence | +| Release evidence | SBOM, provenance and Sigstore bundles on the release page from 0.3.2, with the `gh attestation verify` command in [the compatibility record](https://github.com/GetSHIM/shim-cli/blob/main/docs/compatibility.md#030-release-evidence) |
None:
assert text.count("persist-credentials: false") == checkouts, (
f"{workflow.name}: a checkout keeps its credentials"
)
+
+
+RELEASE = WORKFLOWS / "release.yml"
+
+
+def _step(marker: str) -> str:
+ text = RELEASE.read_text(encoding="utf-8")
+ start = text.rindex("\n - ", 0, text.index(marker))
+ end = text.find("\n - ", start + 1)
+ return text[start:end]
+
+
+def test_the_sbom_scans_the_installed_wheel_and_is_checked() -> None:
+ text = RELEASE.read_text(encoding="utf-8")
+ sbom = _step("uses: anchore/sbom-action@")
+ check = _step("name: Require the SBOM to list")
+
+ assert "path: /tmp/shim-cli-benchmark" in sbom
+ assert "dist/packages" not in sbom
+ assert text.index('--no-deps "dist/packages/shim-') < text.index(sbom)
+ assert text.index(sbom) + len(sbom) == text.index(check)
+ assert '["components"]' in check and "dist/requirements.lock" in check
+
+
+def test_the_attestation_bundles_are_hashed_and_attached() -> None:
+ bundles = (
+ '"dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl"',
+ '"dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json"',
+ "dist/shim.pyz.sigstore.json",
+ )
+ hashed = _step("name: Attach and hash the attestation bundles")
+ flat = _step("name: Verify checksums from a flat download directory")
+ create = _step("gh release create")
+
+ assert all(bundle in flat and bundle in create for bundle in bundles)
+ assert ">> dist/SHA256SUMS" in hashed
+ assert all(
+ name in hashed
+ for name in (".intoto.jsonl", ".whl.sigstore.json", " shim.pyz.sigstore.json")
+ )
+ attest = [_step("id: provenance"), _step("id: sbom")]
+ assert all("dist/packages/*" in step and "dist/shim.pyz" in step for step in attest)
+
+
+def test_the_release_verifies_attestations_before_it_publishes() -> None:
+ text = RELEASE.read_text(encoding="utf-8")
+ verify = _step("gh attestation verify")
+
+ assert text.index(verify) < text.index("gh release create")
+ assert (
+ "attestations: read"
+ in text[text.index("\n release:") : text.index("\n publish:")]
+ )
+ assert verify.count("gh attestation verify ") == 3
+ for subject in (
+ '"dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl"',
+ '"dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz"',
+ "dist/shim.pyz",
+ ):
+ assert f'gh attestation verify {subject} --repo "$GITHUB_REPOSITORY"' in verify