From 5a9928696f19772969d5fcc53880fd6deae9770a Mon Sep 17 00:00:00 2001 From: stacknil Date: Sat, 20 Jun 2026 14:41:21 +0800 Subject: [PATCH] docs(sbom): add minimal CI consumer demo --- .gitattributes | 1 + scripts/validate-reviewer-routes.py | 16 ++ tools/sbom-diff-and-risk/README.md | 3 +- .../docs/github-actions-consumer-example.md | 106 +++----- .../docs/policy-decision-ci-cookbook.md | 6 +- .../docs/reviewer-evidence-pack.md | 254 +++++++++--------- .../sbom-diff-and-risk/docs/reviewer-path.md | 2 +- .../examples/github-actions-consumer.yml | 85 ------ .../github-actions-policy-consumer.yml | 76 +----- .../tests/test_ci_consumer_demo.py | 52 ++++ 10 files changed, 257 insertions(+), 344 deletions(-) delete mode 100644 tools/sbom-diff-and-risk/examples/github-actions-consumer.yml create mode 100644 tools/sbom-diff-and-risk/tests/test_ci_consumer_demo.py diff --git a/.gitattributes b/.gitattributes index 050bb8e..6e8dffa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ /.gitattributes text eol=lf .github/workflows/*.yml text eol=lf +tools/sbom-diff-and-risk/examples/*.yml text eol=lf *.json text eol=lf *.md text eol=lf *.py text eol=lf diff --git a/scripts/validate-reviewer-routes.py b/scripts/validate-reviewer-routes.py index 571055b..d8a3d7d 100644 --- a/scripts/validate-reviewer-routes.py +++ b/scripts/validate-reviewer-routes.py @@ -16,6 +16,7 @@ Path("docs/repo-scope-map.md"), Path("docs/risk-model-boundary.md"), Path("tools/sbom-diff-and-risk/docs/report-schema.md"), + Path("tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md"), Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"), Path("projects/precipitation-anomaly-diagnostics/docs/reviewer-path.md"), Path("projects/precipitation-anomaly-diagnostics-lab/docs/reviewer-path.md"), @@ -77,10 +78,15 @@ "tools/sbom-diff-and-risk/examples/sample-report.json", "tools/sbom-diff-and-risk/examples/sample-summary.json", }, + Path("tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md"): { + "tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md", + "tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml", + }, Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): { ".github/workflows/reviewer-route-contract-ci.yml", "docs/risk-model-boundary.md", "scripts/validate-reviewer-routes.py", + "tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml", "tools/sbom-diff-and-risk/docs/reviewer-brief.md", "tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md", "tools/sbom-diff-and-risk/docs/verification.md", @@ -189,6 +195,15 @@ "not a package safety verdict", "not a CVE result", ), + Path("tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md"): ( + "minimal GitHub Actions consumer workflow", + "outputs/policy.json", + "Upload policy JSON", + "Pass or fail based on local policy", + "tool's own exit code", + "not a CVE scanner", + "not a dependency safety oracle", + ), Path("tools/sbom-diff-and-risk/docs/reviewer-path.md"): ( "Artifact evidence map", "Reviewer route contract", @@ -202,6 +217,7 @@ "python scripts/validate-reviewer-routes.py", "No network", "summary.evidence_confidence", + "runs the tool, uploads `policy.json`, and fails or passes from the policy exit code", "not current PyPI package truth", "not current repository reputation", "It does not decide whether a dependency is safe.", diff --git a/tools/sbom-diff-and-risk/README.md b/tools/sbom-diff-and-risk/README.md index 195ac97..dd2693b 100644 --- a/tools/sbom-diff-and-risk/README.md +++ b/tools/sbom-diff-and-risk/README.md @@ -313,8 +313,7 @@ The [examples/](examples/) directory includes: - a Scorecard-aware policy example at `examples/policy-scorecard-minimal.yml` - a sample pass JSON report at [sample-report.json](examples/sample-report.json) - a sample summary-only JSON artifact at [sample-summary.json](examples/sample-summary.json) -- a consumer GitHub Actions workflow example at [github-actions-consumer.yml](examples/github-actions-consumer.yml) -- a policy-gated consumer GitHub Actions workflow example at [github-actions-policy-consumer.yml](examples/github-actions-policy-consumer.yml) +- a minimal policy-gated consumer GitHub Actions workflow example at [github-actions-policy-consumer.yml](examples/github-actions-policy-consumer.yml) - a sample pass Markdown report at [sample-report.md](examples/sample-report.md) - sample policy-warn reports at [sample-policy-warn-report.json](examples/sample-policy-warn-report.json) and [sample-policy-warn-report.md](examples/sample-policy-warn-report.md) - sample policy-fail reports at [sample-policy-fail-report.json](examples/sample-policy-fail-report.json) and [sample-policy-fail-report.md](examples/sample-policy-fail-report.md) diff --git a/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md b/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md index e5800e5..a2dc5d3 100644 --- a/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md +++ b/tools/sbom-diff-and-risk/docs/github-actions-consumer-example.md @@ -1,7 +1,9 @@ -# GitHub Actions consumer example +# GitHub Actions policy consumer demo -This page shows how another repository could run `sbom-diff-risk` from GitHub -Actions and upload the generated review artifacts. +This page documents the minimal GitHub Actions consumer workflow for +`sbom-diff-risk`. The workflow runs the tool with a local policy, uploads +`outputs/policy.json`, and then passes or fails based on the tool's policy exit +code. It is documentation only. It is not a workflow for this repository, and it does not change the `sbom-diff-risk` CLI or publishing model. @@ -10,24 +12,15 @@ Production PyPI publishing is intentionally deferred, so consumers should not install `sbom-diff-and-risk` from production PyPI. Use a GitHub Release asset or a local checkout instead. -## Example workflow +## Minimal policy workflow -This example downloads the released wheel from the public GitHub Release, runs a -local comparison, writes JSON, Markdown, summary JSON, and SARIF outputs, applies -an explicit local threshold to `summary.json`, and uploads the outputs as CI -artifacts. - -Replace the placeholder input paths with files from the consumer repository. -The same workflow is also checked in as -[../examples/github-actions-consumer.yml](../examples/github-actions-consumer.yml) +Replace the placeholder input and policy paths with files from the consumer +repository. The same workflow is checked in as +[../examples/github-actions-policy-consumer.yml](../examples/github-actions-policy-consumer.yml) for copying into consumer repositories. -For a policy-gated variant that writes `outputs/policy.json` with -`--policy-json PATH`, see -[../examples/github-actions-policy-consumer.yml](../examples/github-actions-policy-consumer.yml). - ```yaml -name: Dependency diff review +name: Dependency policy review on: pull_request: @@ -37,7 +30,7 @@ permissions: contents: read jobs: - dependency-diff: + dependency-policy: runs-on: ubuntu-latest steps: @@ -64,50 +57,42 @@ jobs: python -m pip install \ .tooling/sbom-diff-risk/sbom_diff_and_risk-0.9.0-py3-none-any.whl - - name: Compare dependency evidence + - name: Run dependency policy + id: compare + shell: bash run: | mkdir -p outputs + set +e sbom-diff-risk compare \ --before path/to/before-sbom.json \ --after path/to/after-sbom.json \ --format auto \ - --out-json outputs/report.json \ - --out-md outputs/report.md \ - --summary-json outputs/summary.json \ - --out-sarif outputs/report.sarif - - - name: Apply local summary threshold - run: | - python - <<'PY' - import json - from pathlib import Path - - summary = json.loads( - Path("outputs/summary.json").read_text(encoding="utf-8") - ) - risk_counts = summary["risk_counts"] - - max_new_packages = 2 - new_package_count = risk_counts.get("new_package", 0) - print(f"new_package={new_package_count}") - - if new_package_count > max_new_packages: - raise SystemExit( - f"new_package count exceeds local threshold: {max_new_packages}" - ) - PY - - - name: Upload dependency diff outputs + --policy path/to/policy.yml \ + --policy-json outputs/policy.json + status=$? + set -e + echo "exit_code=$status" >> "$GITHUB_OUTPUT" + + - name: Upload policy JSON + if: always() uses: actions/upload-artifact@v7 with: - name: dependency-diff-outputs - path: | - outputs/report.json - outputs/report.md - outputs/summary.json - outputs/report.sarif + name: dependency-policy-json + path: outputs/policy.json + if-no-files-found: error + + - name: Pass or fail based on local policy + run: exit "${{ steps.compare.outputs.exit_code }}" ``` +The upload step runs before the final pass/fail step, so reviewers can inspect +`outputs/policy.json` even when the local policy blocks the job. The final step +uses the tool's own exit code: + +- `0`: report written and local policy passed +- `1`: report written and local policy produced blocking findings +- `2`: usage, parse, or runtime error before a successful policy decision + ## Local checkout variant If the consumer repository vendors or checks out this toolkit repository, install @@ -120,17 +105,14 @@ from that local checkout instead of downloading a release wheel: path/to/scientific-computing-toolkit/tools/sbom-diff-and-risk ``` -## What the example proves +## What the demo proves -- The consumer workflow runs deterministic local diff analysis over files the +- The consumer workflow runs deterministic local policy analysis over files the consumer repository provides. -- `outputs/report.json` contains the full machine-readable report. -- `outputs/report.md` contains the human-readable review report. -- `outputs/summary.json` contains the same object as `report.json["summary"]`. -- `outputs/report.sarif` can be uploaded or inspected by consumers that want - SARIF output. -- The threshold step is a local consumer policy choice, not a built-in security - verdict. +- `outputs/policy.json` contains policy status, blocking/warning/suppressed + findings, and rule metadata. +- CI pass/fail is based on the `sbom-diff-risk compare` exit code. +- The workflow does not invent a second policy decision after the tool runs. ## Boundaries @@ -145,7 +127,5 @@ from that local checkout instead of downloading a release wheel: - Replace all placeholder input paths with non-private paths from the consumer repository. -For compact summary consumption patterns, see -[summary-json-ci-cookbook.md](summary-json-ci-cookbook.md). For policy sidecar consumption patterns, see [policy-decision-ci-cookbook.md](policy-decision-ci-cookbook.md). diff --git a/tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md b/tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md index b13bf32..84e39f5 100644 --- a/tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md +++ b/tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md @@ -29,9 +29,9 @@ For compact consumer examples that distinguish `pass`, `warn`, `fail`, and `needs-review` review outcomes, see [examples/policy-decisions](../examples/policy-decisions/README.md). -For a full GitHub Actions consumer workflow example that captures -`outputs/policy.json`, uploads it even when local policy fails, and then fails -the job based on `summary.policy`, see +For a minimal GitHub Actions consumer workflow example that captures +`outputs/policy.json`, uploads it before the final pass/fail step, and then +uses the tool's exit code as the CI result, see [github-actions-policy-consumer.yml](../examples/github-actions-policy-consumer.yml). ## Python consumer diff --git a/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md b/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md index 5be90e2..bdcf028 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-evidence-pack.md @@ -1,5 +1,5 @@ -# Reviewer evidence pack - +# Reviewer evidence pack + This page is a reproducible evidence checklist for reviewing `sbom-diff-and-risk`. It focuses on what can be verified from the repository, examples, GitHub release assets, and TestPyPI dry-run documentation. It does not introduce new CLI behavior. For the shortest ordered route through these materials, start with @@ -9,33 +9,33 @@ For interpreting checked-in examples, use the [artifact evidence map](reviewer-path.md#artifact-evidence-map). It separates deterministic no-network examples, mocked enrichment snapshots, and consumer workflow templates. - -## Project Identity - + +## Project Identity + `sbom-diff-and-risk` is a local-first deterministic CLI for comparing SBOMs and dependency manifests. It is designed to produce stable review evidence for dependency changes. Current released version: `v0.9.0`. - -Core identity: - -- local deterministic SBOM/dependency diffing -- JSON, Markdown, and SARIF output -- local policy checks over diff and risk findings -- optional provenance-aware reporting through explicit PyPI enrichment -- optional OpenSSF Scorecard evidence when repository mapping is explicit enough -- release and distribution documentation that separates tool behavior from artifact provenance - -## Reproducible Demo Path - -From `tools/sbom-diff-and-risk`, install the package in editable development mode: - -```powershell -python -m pip install -e .[dev] -``` - -Generate the default CycloneDX example reports: - -```powershell + +Core identity: + +- local deterministic SBOM/dependency diffing +- JSON, Markdown, and SARIF output +- local policy checks over diff and risk findings +- optional provenance-aware reporting through explicit PyPI enrichment +- optional OpenSSF Scorecard evidence when repository mapping is explicit enough +- release and distribution documentation that separates tool behavior from artifact provenance + +## Reproducible Demo Path + +From `tools/sbom-diff-and-risk`, install the package in editable development mode: + +```powershell +python -m pip install -e .[dev] +``` + +Generate the default CycloneDX example reports: + +```powershell sbom-diff-risk compare ` --before examples/cdx_before.json ` --after examples/cdx_after.json ` @@ -44,16 +44,16 @@ sbom-diff-risk compare ` --summary-json outputs/summary.json ` --out-md outputs/report.md ``` - + Expected output files: - `outputs/report.json` - `outputs/summary.json` - `outputs/report.md` - -Compare the outputs against the checked-in sample reports: - -```powershell + +Compare the outputs against the checked-in sample reports: + +```powershell Compare-Object (Get-Content examples/sample-report.json) (Get-Content outputs/report.json) Compare-Object (Get-Content examples/sample-summary.json) (Get-Content outputs/summary.json) Compare-Object (Get-Content examples/sample-report.md) (Get-Content outputs/report.md) @@ -105,21 +105,21 @@ lists, and `rule_catalog`. It intentionally omits full report `components` and `risks`. Generate the strict-policy SARIF sample: - -```powershell -sbom-diff-risk compare ` - --before examples/sarif_before.json ` - --after examples/sarif_after.json ` - --policy examples/policy-strict.yml ` - --out-sarif outputs/report.sarif -``` - -Compare the SARIF output against the sample: - -```powershell -Compare-Object (Get-Content examples/sample-sarif.sarif) (Get-Content outputs/report.sarif) -``` - + +```powershell +sbom-diff-risk compare ` + --before examples/sarif_before.json ` + --after examples/sarif_after.json ` + --policy examples/policy-strict.yml ` + --out-sarif outputs/report.sarif +``` + +Compare the SARIF output against the sample: + +```powershell +Compare-Object (Get-Content examples/sample-sarif.sarif) (Get-Content outputs/report.sarif) +``` + The SARIF sample is intentionally conservative. It covers selected high-signal findings and explicit policy violations, not every enrichment fact. For consumers of the JSON output, see [report-schema.md](report-schema.md). It @@ -142,7 +142,7 @@ For CI dashboard, job-summary, and local-threshold examples that consume [summary-json-ci-cookbook.md](summary-json-ci-cookbook.md). ## Release Verification Path - + Start with the GitHub Release for the version under review. For `v0.9.0`, inspect the release and assets: @@ -184,95 +184,95 @@ and source distribution. For workflow-built artifacts downloaded from a trusted workflow run, verify artifact attestations with the signer workflow: - -```powershell + +```powershell gh attestation verify path/to/sbom_diff_and_risk-0.9.0-py3-none-any.whl ` - --repo stacknil/scientific-computing-toolkit ` - --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml -``` - -```powershell + --repo stacknil/scientific-computing-toolkit ` + --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml +``` + +```powershell gh attestation verify path/to/sbom_diff_and_risk-0.9.0.tar.gz ` - --repo stacknil/scientific-computing-toolkit ` - --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml -``` - + --repo stacknil/scientific-computing-toolkit ` + --signer-workflow stacknil/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml +``` + `gh release verify` and `gh release verify-asset` are conditional on immutable releases. Use them only when the repository release is immutable and GitHub has generated release attestations: - -```powershell + +```powershell gh release view v0.9.0 --repo stacknil/scientific-computing-toolkit --json isImmutable,assets,url -``` - +``` + If `isImmutable` is true, release verification can check the release record and downloaded release assets: - -```powershell + +```powershell gh release verify v0.9.0 --repo stacknil/scientific-computing-toolkit gh release verify-asset v0.9.0 path/to/sbom_diff_and_risk-0.9.0-py3-none-any.whl --repo stacknil/scientific-computing-toolkit -``` - -If `isImmutable` is false, use the workflow artifact attestation path as the primary artifact verification story. - -## TestPyPI Evidence Path - -The TestPyPI Trusted Publishing dry-run completed for `sbom-diff-and-risk`. See `pypi-trusted-publishing-readiness.md` for the exact workflow identity and setup notes. - -What this proves: - -- the package metadata can render on TestPyPI -- the TestPyPI upload path can use Trusted Publishing / OIDC -- the workflow separates build/check from upload -- TestPyPI upload was manually gated - -What this does not prove: - -- production PyPI publishing is ready -- production PyPI has a project, pending publisher, or trusted publisher -- future production distributions will be byte-identical to GitHub Release assets -- dependency analysis results are safety verdicts - -Production PyPI is intentionally deferred. See `pypi-production-publishing-decision.md` before making any production publishing decision. - -## Code Scanning / SARIF Evidence Path - -The SARIF output is designed for GitHub code scanning consumption. Start with: - -- `docs/github-code-scanning.md` -- `examples/sample-sarif.sarif` -- `examples/sample-provenance-report.sarif` -- `examples/sample-scorecard-report.sarif` - -The SARIF renderer intentionally emits a conservative subset: - -- selected heuristic findings such as suspicious source, unknown license, and major upgrade -- explicit blocking policy decisions -- selected provenance or Scorecard policy violations when policy turns them into findings - -Avoid overclaiming: - -- SARIF output is not a CVE scanner -- SARIF output is not a malware or reputation verdict -- missing provenance is an evidence gap, not proof of compromise -- Scorecard evidence is advisory unless policy explicitly gates it - -## Non-Claims - -- No hidden network access occurs by default. -- No production PyPI package exists yet. -- No dependency safety verdicts are produced. -- No CVE resolution is performed. -- No advisory database or exploitability analysis is performed. -- No production PyPI publishing workflow is enabled. -- TestPyPI validation is not production PyPI readiness. - -## 30-Second Reviewer Checklist - -- Can I identify what the tool does? Read `README.md` and `reviewer-brief.md`. +``` + +If `isImmutable` is false, use the workflow artifact attestation path as the primary artifact verification story. + +## TestPyPI Evidence Path + +The TestPyPI Trusted Publishing dry-run completed for `sbom-diff-and-risk`. See `pypi-trusted-publishing-readiness.md` for the exact workflow identity and setup notes. + +What this proves: + +- the package metadata can render on TestPyPI +- the TestPyPI upload path can use Trusted Publishing / OIDC +- the workflow separates build/check from upload +- TestPyPI upload was manually gated + +What this does not prove: + +- production PyPI publishing is ready +- production PyPI has a project, pending publisher, or trusted publisher +- future production distributions will be byte-identical to GitHub Release assets +- dependency analysis results are safety verdicts + +Production PyPI is intentionally deferred. See `pypi-production-publishing-decision.md` before making any production publishing decision. + +## Code Scanning / SARIF Evidence Path + +The SARIF output is designed for GitHub code scanning consumption. Start with: + +- `docs/github-code-scanning.md` +- `examples/sample-sarif.sarif` +- `examples/sample-provenance-report.sarif` +- `examples/sample-scorecard-report.sarif` + +The SARIF renderer intentionally emits a conservative subset: + +- selected heuristic findings such as suspicious source, unknown license, and major upgrade +- explicit blocking policy decisions +- selected provenance or Scorecard policy violations when policy turns them into findings + +Avoid overclaiming: + +- SARIF output is not a CVE scanner +- SARIF output is not a malware or reputation verdict +- missing provenance is an evidence gap, not proof of compromise +- Scorecard evidence is advisory unless policy explicitly gates it + +## Non-Claims + +- No hidden network access occurs by default. +- No production PyPI package exists yet. +- No dependency safety verdicts are produced. +- No CVE resolution is performed. +- No advisory database or exploitability analysis is performed. +- No production PyPI publishing workflow is enabled. +- TestPyPI validation is not production PyPI readiness. + +## 30-Second Reviewer Checklist + +- Can I identify what the tool does? Read `README.md` and `reviewer-brief.md`. - Can I reproduce a deterministic demo? Run the CycloneDX example and compare `outputs/report.*` to `examples/sample-report.*`. - Can I see machine-readable security output? Inspect or regenerate `examples/sample-sarif.sarif`, and read `report-schema.md` for JSON report shape. -- Can I verify release/distribution evidence? Read `verification.md`, `self-provenance.md`, and `release-provenance.md`. -- Can I distinguish TestPyPI from production PyPI? Read `pypi-trusted-publishing-readiness.md` and `pypi-production-publishing-decision.md`. -- Can I state the non-claims? No CVE scanner, no reputation oracle, no dependency safety verdicts, no production PyPI package yet. - +- Can I verify release/distribution evidence? Read `verification.md`, `self-provenance.md`, and `release-provenance.md`. +- Can I distinguish TestPyPI from production PyPI? Read `pypi-trusted-publishing-readiness.md` and `pypi-production-publishing-decision.md`. +- Can I state the non-claims? No CVE scanner, no reputation oracle, no dependency safety verdicts, no production PyPI package yet. + diff --git a/tools/sbom-diff-and-risk/docs/reviewer-path.md b/tools/sbom-diff-and-risk/docs/reviewer-path.md index 98fb422..c03857e 100644 --- a/tools/sbom-diff-and-risk/docs/reviewer-path.md +++ b/tools/sbom-diff-and-risk/docs/reviewer-path.md @@ -95,7 +95,7 @@ consumer workflow templates. They answer different review questions. | Strict-policy SARIF | [sample-sarif.sarif](../examples/sample-sarif.sarif) | `scripts/regenerate-example-artifacts.py --only sarif` with normalized source root | No network | Inspect conservative code-scanning output for selected findings. | | PyPI provenance snapshot | [sample-provenance-report.json](../examples/sample-provenance-report.json), [sample-provenance-report.md](../examples/sample-provenance-report.md), [sample-provenance-report.sarif](../examples/sample-provenance-report.sarif) | Focused golden tests with constructed provenance evidence | No live lookup while testing | Review opt-in provenance rendering and policy semantics, not current PyPI package truth. | | OpenSSF Scorecard snapshot | [sample-scorecard-report.json](../examples/sample-scorecard-report.json), [sample-scorecard-report.md](../examples/sample-scorecard-report.md), [sample-scorecard-report.sarif](../examples/sample-scorecard-report.sarif) | Focused golden tests with constructed Scorecard evidence | No live lookup while testing | Review opt-in Scorecard rendering and explicit policy gating, not current repository reputation. | -| Consumer workflows | [github-actions-consumer.yml](../examples/github-actions-consumer.yml), [github-actions-policy-consumer.yml](../examples/github-actions-policy-consumer.yml) | Checked-in examples | Not executed by this repository | Copy or inspect CI consumption patterns. | +| Policy consumer workflow | [github-actions-policy-consumer.yml](../examples/github-actions-policy-consumer.yml) | Checked-in example | Not executed by this repository | Copy or inspect the minimal CI path that runs the tool, uploads `policy.json`, and fails or passes from the policy exit code. | Reviewer rule: diff --git a/tools/sbom-diff-and-risk/examples/github-actions-consumer.yml b/tools/sbom-diff-and-risk/examples/github-actions-consumer.yml deleted file mode 100644 index 95e2388..0000000 --- a/tools/sbom-diff-and-risk/examples/github-actions-consumer.yml +++ /dev/null @@ -1,85 +0,0 @@ -# Example only. -# Copy this file into a consumer repository under .github/workflows/ if useful. -# This repository does not run this file as a workflow. -# Production PyPI publishing for sbom-diff-and-risk is intentionally deferred; -# install from a GitHub Release asset or local checkout instead. - -name: Dependency diff review - -on: - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - dependency-diff: - runs-on: ubuntu-latest - - steps: - - name: Check out consumer repository - uses: actions/checkout@v6 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.x" - - - name: Download sbom-diff-and-risk release wheel - env: - GH_TOKEN: ${{ github.token }} - run: | - mkdir -p .tooling/sbom-diff-risk - gh release download v0.9.0 \ - --repo stacknil/scientific-computing-toolkit \ - --pattern "sbom_diff_and_risk-0.9.0-py3-none-any.whl" \ - --dir .tooling/sbom-diff-risk - - - name: Install sbom-diff-risk - run: | - python -m pip install \ - .tooling/sbom-diff-risk/sbom_diff_and_risk-0.9.0-py3-none-any.whl - - - name: Compare dependency evidence - run: | - mkdir -p outputs - sbom-diff-risk compare \ - --before path/to/before-sbom.json \ - --after path/to/after-sbom.json \ - --format auto \ - --out-json outputs/report.json \ - --out-md outputs/report.md \ - --summary-json outputs/summary.json \ - --out-sarif outputs/report.sarif - - - name: Apply local summary threshold - run: | - python - <<'PY' - import json - from pathlib import Path - - summary = json.loads( - Path("outputs/summary.json").read_text(encoding="utf-8") - ) - risk_counts = summary["risk_counts"] - - max_new_packages = 2 - new_package_count = risk_counts.get("new_package", 0) - print(f"new_package={new_package_count}") - - if new_package_count > max_new_packages: - raise SystemExit( - f"new_package count exceeds local threshold: {max_new_packages}" - ) - PY - - - name: Upload dependency diff outputs - uses: actions/upload-artifact@v7 - with: - name: dependency-diff-outputs - path: | - outputs/report.json - outputs/report.md - outputs/summary.json - outputs/report.sarif diff --git a/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml b/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml index c896100..0e92a4a 100644 --- a/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml +++ b/tools/sbom-diff-and-risk/examples/github-actions-policy-consumer.yml @@ -1,8 +1,6 @@ -# Example only. -# Copy this file into a consumer repository under .github/workflows/ if useful. -# This repository does not run this file as a workflow. -# Production PyPI publishing for sbom-diff-and-risk is intentionally deferred; -# install from a GitHub Release asset or local checkout instead. +# Example only. Copy this file into a consumer repository under +# .github/workflows/ if useful. This repository does not run this file as a +# workflow. name: Dependency policy review @@ -41,8 +39,9 @@ jobs: python -m pip install \ .tooling/sbom-diff-risk/sbom_diff_and_risk-0.9.0-py3-none-any.whl - - name: Compare dependency evidence with local policy + - name: Run dependency policy id: compare + shell: bash run: | mkdir -p outputs set +e @@ -51,67 +50,18 @@ jobs: --after path/to/after-sbom.json \ --format auto \ --policy path/to/policy.yml \ - --out-json outputs/report.json \ - --out-md outputs/report.md \ - --policy-json outputs/policy.json \ - --out-sarif outputs/report.sarif + --policy-json outputs/policy.json status=$? set -e - echo "$status" > outputs/policy-exit-code.txt echo "exit_code=$status" >> "$GITHUB_OUTPUT" - - name: Summarize local policy decision - run: | - python - <<'PY' - import json - from pathlib import Path - - policy_report = json.loads( - Path("outputs/policy.json").read_text(encoding="utf-8") - ) - policy = policy_report.get("summary", {}).get("policy") - - if policy is None: - print("policy=not-used") - raise SystemExit(0) - - print( - "policy=" - f"{policy['status']} " - f"blocking={policy['blocking']} " - f"warning={policy['warning']} " - f"suppressed={policy['suppressed']}" - ) - - findings = ( - policy_report.get("blocking_findings", []) - + policy_report.get("warning_findings", []) - + policy_report.get("suppressed_findings", []) - ) - - for finding in findings: - print( - "policy-finding " - f"level={finding.get('level')} " - f"rule={finding.get('policy_rule')} " - f"reason={finding.get('decision_reason')} " - f"severity_source={finding.get('severity_source')} " - f"observed={finding.get('observed_value')} " - f"threshold={finding.get('matched_threshold')}" - ) - - if policy["status"] == "fail": - raise SystemExit("local policy failed") - PY - - - name: Upload dependency policy outputs + - name: Upload policy JSON if: always() uses: actions/upload-artifact@v7 with: - name: dependency-policy-outputs - path: | - outputs/report.json - outputs/report.md - outputs/policy.json - outputs/policy-exit-code.txt - outputs/report.sarif + name: dependency-policy-json + path: outputs/policy.json + if-no-files-found: error + + - name: Pass or fail based on local policy + run: exit "${{ steps.compare.outputs.exit_code }}" diff --git a/tools/sbom-diff-and-risk/tests/test_ci_consumer_demo.py b/tools/sbom-diff-and-risk/tests/test_ci_consumer_demo.py new file mode 100644 index 0000000..6bf4b7c --- /dev/null +++ b/tools/sbom-diff-and-risk/tests/test_ci_consumer_demo.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml + +EXAMPLES = Path(__file__).resolve().parents[1] / "examples" +POLICY_CONSUMER = EXAMPLES / "github-actions-policy-consumer.yml" + + +def test_only_minimal_github_actions_policy_consumer_is_checked_in() -> None: + workflow_examples = sorted( + path.name for path in EXAMPLES.glob("github-actions*consumer.yml") + ) + + assert workflow_examples == ["github-actions-policy-consumer.yml"] + + +def test_policy_consumer_uploads_policy_json_before_enforcing_exit_code() -> None: + text = POLICY_CONSUMER.read_text(encoding="utf-8") + payload = yaml.safe_load(text) + + assert payload["name"] == "Dependency policy review" + + steps = payload["jobs"]["dependency-policy"]["steps"] + step_names = [step["name"] for step in steps] + + assert step_names == [ + "Check out consumer repository", + "Set up Python", + "Download sbom-diff-and-risk release wheel", + "Install sbom-diff-risk", + "Run dependency policy", + "Upload policy JSON", + "Pass or fail based on local policy", + ] + assert "--policy-json outputs/policy.json" in text + assert "--summary-json" not in text + assert "--out-json" not in text + assert "--out-md" not in text + assert "--out-sarif" not in text + assert "tee " not in text + + run_index = step_names.index("Run dependency policy") + upload_index = step_names.index("Upload policy JSON") + enforce_index = step_names.index("Pass or fail based on local policy") + + assert run_index < upload_index < enforce_index + assert steps[upload_index]["if"] == "always()" + assert steps[upload_index]["with"]["path"] == "outputs/policy.json" + assert steps[upload_index]["with"]["if-no-files-found"] == "error" + assert steps[enforce_index]["run"] == 'exit "${{ steps.compare.outputs.exit_code }}"'