diff --git a/.github/workflows/sbom-diff-and-risk-ci.yml b/.github/workflows/sbom-diff-and-risk-ci.yml index 5ab79c8..118c2e2 100644 --- a/.github/workflows/sbom-diff-and-risk-ci.yml +++ b/.github/workflows/sbom-diff-and-risk-ci.yml @@ -11,9 +11,14 @@ on: - ".github/workflows/sbom-diff-and-risk-ci.yml" - "tools/sbom-diff-and-risk/**" +env: + SBOM_DIFF_RISK_DIST_ARTIFACT_NAME: sbom-diff-and-risk-dist + jobs: test: runs-on: ubuntu-latest + permissions: + contents: read defaults: run: working-directory: tools/sbom-diff-and-risk @@ -49,3 +54,48 @@ jobs: test -f "$tmpdir/report.md" diff -u examples/sample-report.json "$tmpdir/report.json" diff -u examples/sample-report.md "$tmpdir/report.md" + + build-and-attest: + # Keep provenance publication on trusted non-PR runs so consumers verify + # workflow-produced wheel and sdist artifacts from this repository workflow. + if: github.event_name != 'pull_request' + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + attestations: write + defaults: + run: + working-directory: tools/sbom-diff-and-risk + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install build tooling + run: python -m pip install build + + - name: Build distributable artifacts + run: python -m build + + - name: Upload wheel and source distribution artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.SBOM_DIFF_RISK_DIST_ARTIFACT_NAME }} + path: | + tools/sbom-diff-and-risk/dist/*.whl + tools/sbom-diff-and-risk/dist/*.tar.gz + if-no-files-found: error + + - name: Generate artifact attestation for built distributions + uses: actions/attest@v4 + with: + subject-path: ${{ github.workspace }}/tools/sbom-diff-and-risk/dist/* diff --git a/tools/sbom-diff-and-risk/README.md b/tools/sbom-diff-and-risk/README.md index c61e75e..85dd36c 100644 --- a/tools/sbom-diff-and-risk/README.md +++ b/tools/sbom-diff-and-risk/README.md @@ -228,6 +228,17 @@ sbom-diff-risk compare \ For GitHub code scanning integration guidance and a minimal upload workflow, see [docs/github-code-scanning.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/docs/github-code-scanning.md). +## Self-provenance + +This repository also records provenance for `sbom-diff-and-risk` itself by generating GitHub artifact attestations for the wheel and source distribution produced by the `sbom-diff-and-risk-ci` workflow. + +- the attested files are the wheel and source distribution built by `python -m build` from `tools/sbom-diff-and-risk` +- the build files are uploaded together as the `sbom-diff-and-risk-dist` workflow artifact +- only trusted non-PR runs publish the attestation +- consumers can verify provenance with GitHub's attestation tooling after downloading one of those artifacts +- this complements the tool's analysis of third-party supply-chain inputs, but it does not replace that analysis + +See [docs/self-provenance.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/docs/self-provenance.md) for the exact attested filenames, where the evidence appears in GitHub, and a run-by-run verification flow for consumers. ## Parser Boundaries Deterministic local mode intentionally supports a conservative subset of packaging syntax. The detailed matrix lives in [docs/parser-boundaries.md](D:/OneDrive/Code/scientific-computing-toolkit/tools/sbom-diff-and-risk/docs/parser-boundaries.md). diff --git a/tools/sbom-diff-and-risk/docs/self-provenance.md b/tools/sbom-diff-and-risk/docs/self-provenance.md new file mode 100644 index 0000000..96db8ee --- /dev/null +++ b/tools/sbom-diff-and-risk/docs/self-provenance.md @@ -0,0 +1,100 @@ +# Self-provenance and artifact attestations + +`sbom-diff-and-risk` analyzes third-party dependency changes, but consumers should also be able to verify where the tool itself came from. This repository generates GitHub artifact attestations for the packaged build outputs produced by the `sbom-diff-and-risk-ci` workflow. + +## What is attested in this repository + +The attested subjects are the exact Python distributables built from `tools/sbom-diff-and-risk` via `python -m build`: + +- the wheel: `dist/sbom_diff_and_risk--py3-none-any.whl` +- the source distribution: `dist/sbom_diff_and_risk-.tar.gz` + +Those two files are uploaded together as the workflow artifact named `sbom-diff-and-risk-dist`. The attestation applies to the built files themselves, not just to the artifact bundle name shown in the Actions UI. + +Current attestations cover workflow-built wheel and sdist artifacts, not GitHub Release assets or PyPI-published distributions. + +## Workflow and permissions + +The attestation is generated in `.github/workflows/sbom-diff-and-risk-ci.yml` by the `build-and-attest` job in the `sbom-diff-and-risk-ci` workflow. + +That job runs only for trusted non-PR events in this repository: + +- `push` +- `workflow_dispatch` + +Pull request runs still execute the `test` job, but they do not publish artifact attestations. + +The `build-and-attest` job uses the minimum explicit permissions required for GitHub-hosted build provenance: + +- `contents: read` for repository checkout +- `id-token: write` for GitHub's signing identity +- `attestations: write` to publish the attestation + +## Where provenance evidence appears in GitHub + +After a successful non-PR run of `sbom-diff-and-risk-ci`, consumers can find the evidence in two useful places: + +1. On the workflow run page: + - the uploaded artifact appears as `sbom-diff-and-risk-dist` + - this is the run consumers should use to confirm the workflow name, job name, and downloaded artifact bundle before verification +2. In the repository-wide attestations view: + - open **Actions** + - in the left sidebar, under **Management**, open **Attestations** + - search for `sbom_diff_and_risk-` or filter by recent creation date + +On the **Attestations** page, the relevant subjects are the wheel and sdist filenames, not the workflow artifact bundle name. On the workflow run page, the main visible bundle name is still `sbom-diff-and-risk-dist`. + +## Manual verification for one workflow run + +Use this path after a merge to the default branch or an intentional `workflow_dispatch` run. + +1. Open the repository's **Actions** tab. +2. Open a successful `sbom-diff-and-risk-ci` run triggered by `push` or `workflow_dispatch`. +3. Confirm that the `build-and-attest` job ran successfully. +4. Download the `sbom-diff-and-risk-dist` artifact from that run. +5. Confirm the downloaded archive contains exactly the expected build outputs for that version: + - `sbom_diff_and_risk--py3-none-any.whl` + - `sbom_diff_and_risk-.tar.gz` +6. Verify one of the files with the GitHub CLI: + +```bash +gh attestation verify path/to/sbom_diff_and_risk--py3-none-any.whl \ + --repo OWNER/scientific-computing-toolkit \ + --signer-workflow OWNER/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml +``` + +You can verify the source distribution the same way: + +```bash +gh attestation verify path/to/sbom_diff_and_risk-.tar.gz \ + --repo OWNER/scientific-computing-toolkit \ + --signer-workflow OWNER/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml +``` + +If you want more inspection detail during review, ask the CLI for structured output: + +```bash +gh attestation verify path/to/sbom_diff_and_risk--py3-none-any.whl \ + --repo OWNER/scientific-computing-toolkit \ + --signer-workflow OWNER/scientific-computing-toolkit/.github/workflows/sbom-diff-and-risk-ci.yml \ + --format json +``` + +A successful verification confirms that: + +- the downloaded file matches an attested subject +- the attestation was linked to `OWNER/scientific-computing-toolkit` +- the attestation was signed by `.github/workflows/sbom-diff-and-risk-ci.yml` + +## Release-consumer note + +If these same wheel or source distribution bytes are later attached to a GitHub release, consumers should verify the downloaded release asset file itself with the same `gh attestation verify` flow. In the current setup, the provenance source of truth is still the workflow-produced build artifact and its attestation, not a separate release-attestation workflow. + +## How this complements the tool's own analysis + +Self-provenance and dependency analysis solve different problems: + +- artifact attestations help consumers verify where `sbom-diff-and-risk` itself was built +- `sbom-diff-and-risk` helps users review and gate third-party dependency changes in their own projects + +These attestations strengthen trust in the tool's own distributable artifacts, but they do not replace the tool's analysis of external SBOM inputs, policy decisions, or trust-signal reporting for third-party packages. diff --git a/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.json b/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.json index b28f3d5..3469a60 100644 --- a/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.json +++ b/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.json @@ -1,564 +1,564 @@ -{ - "summary": { - "added": 1, - "removed": 0, - "changed": 1, - "risk_counts": { - "new_package": 1, - "major_upgrade": 0, - "version_change_unclassified": 1, - "unknown_license": 0, - "stale_package": 0, - "suspicious_source": 0, - "not_evaluated": 2 - } - }, - "components": { - "added": [ - { - "name": "urllib3", - "version": "2.2.1", - "ecosystem": "pypi", - "purl": "pkg:pypi/urllib3@2.2.1", - "license_id": "MIT", - "supplier": null, - "source_url": "https://pypi.org/project/urllib3/", - "bom_ref": "pkg:pypi/urllib3@2.2.1", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/urllib3@2.2.1", - "type": "library", - "name": "urllib3", - "version": "2.2.1", - "purl": "pkg:pypi/urllib3@2.2.1", - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/urllib3/" - } - ] - } - } - } - ], - "removed": [], - "changed": [ - { - "key": "purl:pkg:pypi/requests", - "classification": "version_changed", - "before": { - "name": "requests", - "version": "2.31.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.31.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.31.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.31.0", - "type": "library", - "name": "requests", - "version": "2.31.0", - "purl": "pkg:pypi/requests@2.31.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - }, - { - "type": "vcs", - "url": "https://github.com/psf/requests" - } - ] - } - } - }, - "after": { - "name": "requests", - "version": "2.32.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.32.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.32.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.32.0", - "type": "library", - "name": "requests", - "version": "2.32.0", - "purl": "pkg:pypi/requests@2.32.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - } - ] - } - } - } - } - ] - }, - "risks": [ - { - "bucket": "new_package", - "component_key": "purl:pkg:pypi/urllib3", - "component": { - "name": "urllib3", - "version": "2.2.1", - "ecosystem": "pypi", - "purl": "pkg:pypi/urllib3@2.2.1", - "license_id": "MIT", - "supplier": null, - "source_url": "https://pypi.org/project/urllib3/", - "bom_ref": "pkg:pypi/urllib3@2.2.1", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/urllib3@2.2.1", - "type": "library", - "name": "urllib3", - "version": "2.2.1", - "purl": "pkg:pypi/urllib3@2.2.1", - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/urllib3/" - } - ] - } - } - }, - "rationale": "Component was not present in the before input." - }, - { - "bucket": "not_evaluated", - "component_key": "purl:pkg:pypi/requests", - "component": { - "name": "requests", - "version": "2.32.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.32.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.32.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.32.0", - "type": "library", - "name": "requests", - "version": "2.32.0", - "purl": "pkg:pypi/requests@2.32.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - } - ] - } - } - }, - "rationale": "stale_package was not evaluated because enrichment mode is disabled." - }, - { - "bucket": "not_evaluated", - "component_key": "purl:pkg:pypi/urllib3", - "component": { - "name": "urllib3", - "version": "2.2.1", - "ecosystem": "pypi", - "purl": "pkg:pypi/urllib3@2.2.1", - "license_id": "MIT", - "supplier": null, - "source_url": "https://pypi.org/project/urllib3/", - "bom_ref": "pkg:pypi/urllib3@2.2.1", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/urllib3@2.2.1", - "type": "library", - "name": "urllib3", - "version": "2.2.1", - "purl": "pkg:pypi/urllib3@2.2.1", - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/urllib3/" - } - ] - } - } - }, - "rationale": "stale_package was not evaluated because enrichment mode is disabled." - }, - { - "bucket": "version_change_unclassified", - "component_key": "purl:pkg:pypi/requests", - "component": { - "name": "requests", - "version": "2.32.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.32.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.32.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.32.0", - "type": "library", - "name": "requests", - "version": "2.32.0", - "purl": "pkg:pypi/requests@2.32.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - } - ] - } - } - }, - "rationale": "Version changed but did not qualify as a parseable SemVer major upgrade." - } - ], - "policy_evaluation": { - "applied": true, +{ + "summary": { + "added": 1, + "removed": 0, + "changed": 1, + "risk_counts": { + "new_package": 1, + "major_upgrade": 0, + "version_change_unclassified": 1, + "unknown_license": 0, + "stale_package": 0, + "suspicious_source": 0, + "not_evaluated": 2 + } + }, + "components": { + "added": [ + { + "name": "urllib3", + "version": "2.2.1", + "ecosystem": "pypi", + "purl": "pkg:pypi/urllib3@2.2.1", + "license_id": "MIT", + "supplier": null, + "source_url": "https://pypi.org/project/urllib3/", + "bom_ref": "pkg:pypi/urllib3@2.2.1", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/urllib3@2.2.1", + "type": "library", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/urllib3/" + } + ] + } + } + } + ], + "removed": [], + "changed": [ + { + "key": "purl:pkg:pypi/requests", + "classification": "version_changed", + "before": { + "name": "requests", + "version": "2.31.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.31.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.31.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.31.0", + "type": "library", + "name": "requests", + "version": "2.31.0", + "purl": "pkg:pypi/requests@2.31.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + }, + { + "type": "vcs", + "url": "https://github.com/psf/requests" + } + ] + } + } + }, + "after": { + "name": "requests", + "version": "2.32.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.32.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.32.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.32.0", + "type": "library", + "name": "requests", + "version": "2.32.0", + "purl": "pkg:pypi/requests@2.32.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + } + ] + } + } + } + } + ] + }, + "risks": [ + { + "bucket": "new_package", + "component_key": "purl:pkg:pypi/urllib3", + "component": { + "name": "urllib3", + "version": "2.2.1", + "ecosystem": "pypi", + "purl": "pkg:pypi/urllib3@2.2.1", + "license_id": "MIT", + "supplier": null, + "source_url": "https://pypi.org/project/urllib3/", + "bom_ref": "pkg:pypi/urllib3@2.2.1", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/urllib3@2.2.1", + "type": "library", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/urllib3/" + } + ] + } + } + }, + "rationale": "Component was not present in the before input." + }, + { + "bucket": "not_evaluated", + "component_key": "purl:pkg:pypi/requests", + "component": { + "name": "requests", + "version": "2.32.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.32.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.32.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.32.0", + "type": "library", + "name": "requests", + "version": "2.32.0", + "purl": "pkg:pypi/requests@2.32.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + } + ] + } + } + }, + "rationale": "stale_package was not evaluated because enrichment mode is disabled." + }, + { + "bucket": "not_evaluated", + "component_key": "purl:pkg:pypi/urllib3", + "component": { + "name": "urllib3", + "version": "2.2.1", + "ecosystem": "pypi", + "purl": "pkg:pypi/urllib3@2.2.1", + "license_id": "MIT", + "supplier": null, + "source_url": "https://pypi.org/project/urllib3/", + "bom_ref": "pkg:pypi/urllib3@2.2.1", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/urllib3@2.2.1", + "type": "library", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/urllib3/" + } + ] + } + } + }, + "rationale": "stale_package was not evaluated because enrichment mode is disabled." + }, + { + "bucket": "version_change_unclassified", + "component_key": "purl:pkg:pypi/requests", + "component": { + "name": "requests", + "version": "2.32.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.32.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.32.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.32.0", + "type": "library", + "name": "requests", + "version": "2.32.0", + "purl": "pkg:pypi/requests@2.32.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + } + ] + } + } + }, + "rationale": "Version changed but did not qualify as a parseable SemVer major upgrade." + } + ], + "policy_evaluation": { + "applied": true, "policy_path": "examples/policy-strict.yml", - "effective_policy": { - "version": 1, - "block_on": [ - "unknown_license", - "suspicious_source", - "stale_package", - "max_added_packages", - "allow_sources" - ], - "warn_on": [ - "new_package", - "major_upgrade" - ], - "max_added_packages": 0, - "allow_sources": [ - "pypi.org", - "files.pythonhosted.org", - "github.com" - ], - "ignore_rules": [] - }, - "blocking_violations": [ - { - "rule_id": "max_added_packages", - "level": "block", - "message": "Added package count 1 exceeds max_added_packages=0.", - "component_key": null, - "component_name": null, - "finding_bucket": null, - "suppression_reason": null - }, - { - "rule_id": "stale_package", - "level": "block", - "message": "stale_package was not evaluated because enrichment mode is disabled.", - "component_key": "purl:pkg:pypi/requests", - "component_name": "requests", - "finding_bucket": "not_evaluated", - "suppression_reason": null - }, - { - "rule_id": "stale_package", - "level": "block", - "message": "stale_package was not evaluated because enrichment mode is disabled.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "not_evaluated", - "suppression_reason": null - } - ], - "warning_violations": [ - { - "rule_id": "new_package", - "level": "warn", - "message": "Component was not present in the before input.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "new_package", - "suppression_reason": null - } - ], - "suppressed_violations": [], - "totals": { - "blocking": 3, - "warning": 1, - "suppressed": 0, - "ignored_checks": 0 - }, - "exit_code": 1 - }, - "blocking_findings": [ - { - "rule_id": "max_added_packages", - "level": "block", - "message": "Added package count 1 exceeds max_added_packages=0.", - "component_key": null, - "component_name": null, - "finding_bucket": null, - "suppression_reason": null - }, - { - "rule_id": "stale_package", - "level": "block", - "message": "stale_package was not evaluated because enrichment mode is disabled.", - "component_key": "purl:pkg:pypi/requests", - "component_name": "requests", - "finding_bucket": "not_evaluated", - "suppression_reason": null - }, - { - "rule_id": "stale_package", - "level": "block", - "message": "stale_package was not evaluated because enrichment mode is disabled.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "not_evaluated", - "suppression_reason": null - } - ], - "warning_findings": [ - { - "rule_id": "new_package", - "level": "warn", - "message": "Component was not present in the before input.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "new_package", - "suppression_reason": null - } - ], - "suppressed_findings": [], - "rule_catalog": { - "new_package": { - "rule_id": "new_package", - "kind": "risk_finding", - "description": "Component is present only in the after input.", - "finding_buckets": [ - "new_package" - ] - }, - "major_upgrade": { - "rule_id": "major_upgrade", - "kind": "risk_finding", - "description": "Version change is a parseable SemVer major upgrade.", - "finding_buckets": [ - "major_upgrade" - ] - }, - "version_change_unclassified": { - "rule_id": "version_change_unclassified", - "kind": "risk_finding", - "description": "Version changed but could not be classified as a reliable major SemVer upgrade.", - "finding_buckets": [ - "version_change_unclassified" - ] - }, - "unknown_license": { - "rule_id": "unknown_license", - "kind": "risk_finding", - "description": "License metadata is missing, empty, UNKNOWN, or NOASSERTION.", - "finding_buckets": [ - "unknown_license" - ] - }, - "suspicious_source": { - "rule_id": "suspicious_source", - "kind": "risk_finding", - "description": "Source provenance is missing or points to a suspicious scheme, path, or host.", - "finding_buckets": [ - "suspicious_source" - ] - }, - "stale_package": { - "rule_id": "stale_package", - "kind": "risk_finding", - "description": "Staleness check result. Offline mode maps this rule to not_evaluated instead of guessing.", - "finding_buckets": [ - "stale_package", - "not_evaluated" - ] - }, - "max_added_packages": { - "rule_id": "max_added_packages", - "kind": "policy_check", - "description": "Added package count exceeded the configured deterministic threshold.", - "finding_buckets": [] - }, - "allow_sources": { - "rule_id": "allow_sources", - "kind": "policy_check", - "description": "Component source host was not present in the configured allow_sources list.", - "finding_buckets": [] - } - }, - "metadata": { - "before_format": "cyclonedx-json", - "after_format": "cyclonedx-json", - "generated_at": null, - "strict": false, - "stub": false, - "policy_evaluation": { - "applied": true, + "effective_policy": { + "version": 1, + "block_on": [ + "unknown_license", + "suspicious_source", + "stale_package", + "max_added_packages", + "allow_sources" + ], + "warn_on": [ + "new_package", + "major_upgrade" + ], + "max_added_packages": 0, + "allow_sources": [ + "pypi.org", + "files.pythonhosted.org", + "github.com" + ], + "ignore_rules": [] + }, + "blocking_violations": [ + { + "rule_id": "max_added_packages", + "level": "block", + "message": "Added package count 1 exceeds max_added_packages=0.", + "component_key": null, + "component_name": null, + "finding_bucket": null, + "suppression_reason": null + }, + { + "rule_id": "stale_package", + "level": "block", + "message": "stale_package was not evaluated because enrichment mode is disabled.", + "component_key": "purl:pkg:pypi/requests", + "component_name": "requests", + "finding_bucket": "not_evaluated", + "suppression_reason": null + }, + { + "rule_id": "stale_package", + "level": "block", + "message": "stale_package was not evaluated because enrichment mode is disabled.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "not_evaluated", + "suppression_reason": null + } + ], + "warning_violations": [ + { + "rule_id": "new_package", + "level": "warn", + "message": "Component was not present in the before input.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "new_package", + "suppression_reason": null + } + ], + "suppressed_violations": [], + "totals": { + "blocking": 3, + "warning": 1, + "suppressed": 0, + "ignored_checks": 0 + }, + "exit_code": 1 + }, + "blocking_findings": [ + { + "rule_id": "max_added_packages", + "level": "block", + "message": "Added package count 1 exceeds max_added_packages=0.", + "component_key": null, + "component_name": null, + "finding_bucket": null, + "suppression_reason": null + }, + { + "rule_id": "stale_package", + "level": "block", + "message": "stale_package was not evaluated because enrichment mode is disabled.", + "component_key": "purl:pkg:pypi/requests", + "component_name": "requests", + "finding_bucket": "not_evaluated", + "suppression_reason": null + }, + { + "rule_id": "stale_package", + "level": "block", + "message": "stale_package was not evaluated because enrichment mode is disabled.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "not_evaluated", + "suppression_reason": null + } + ], + "warning_findings": [ + { + "rule_id": "new_package", + "level": "warn", + "message": "Component was not present in the before input.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "new_package", + "suppression_reason": null + } + ], + "suppressed_findings": [], + "rule_catalog": { + "new_package": { + "rule_id": "new_package", + "kind": "risk_finding", + "description": "Component is present only in the after input.", + "finding_buckets": [ + "new_package" + ] + }, + "major_upgrade": { + "rule_id": "major_upgrade", + "kind": "risk_finding", + "description": "Version change is a parseable SemVer major upgrade.", + "finding_buckets": [ + "major_upgrade" + ] + }, + "version_change_unclassified": { + "rule_id": "version_change_unclassified", + "kind": "risk_finding", + "description": "Version changed but could not be classified as a reliable major SemVer upgrade.", + "finding_buckets": [ + "version_change_unclassified" + ] + }, + "unknown_license": { + "rule_id": "unknown_license", + "kind": "risk_finding", + "description": "License metadata is missing, empty, UNKNOWN, or NOASSERTION.", + "finding_buckets": [ + "unknown_license" + ] + }, + "suspicious_source": { + "rule_id": "suspicious_source", + "kind": "risk_finding", + "description": "Source provenance is missing or points to a suspicious scheme, path, or host.", + "finding_buckets": [ + "suspicious_source" + ] + }, + "stale_package": { + "rule_id": "stale_package", + "kind": "risk_finding", + "description": "Staleness check result. Offline mode maps this rule to not_evaluated instead of guessing.", + "finding_buckets": [ + "stale_package", + "not_evaluated" + ] + }, + "max_added_packages": { + "rule_id": "max_added_packages", + "kind": "policy_check", + "description": "Added package count exceeded the configured deterministic threshold.", + "finding_buckets": [] + }, + "allow_sources": { + "rule_id": "allow_sources", + "kind": "policy_check", + "description": "Component source host was not present in the configured allow_sources list.", + "finding_buckets": [] + } + }, + "metadata": { + "before_format": "cyclonedx-json", + "after_format": "cyclonedx-json", + "generated_at": null, + "strict": false, + "stub": false, + "policy_evaluation": { + "applied": true, "policy_path": "examples/policy-strict.yml", - "effective_policy": { - "version": 1, - "block_on": [ - "unknown_license", - "suspicious_source", - "stale_package", - "max_added_packages", - "allow_sources" - ], - "warn_on": [ - "new_package", - "major_upgrade" - ], - "max_added_packages": 0, - "allow_sources": [ - "pypi.org", - "files.pythonhosted.org", - "github.com" - ], - "ignore_rules": [] - }, - "blocking_violations": [ - { - "rule_id": "max_added_packages", - "level": "block", - "message": "Added package count 1 exceeds max_added_packages=0.", - "component_key": null, - "component_name": null, - "finding_bucket": null, - "suppression_reason": null - }, - { - "rule_id": "stale_package", - "level": "block", - "message": "stale_package was not evaluated because enrichment mode is disabled.", - "component_key": "purl:pkg:pypi/requests", - "component_name": "requests", - "finding_bucket": "not_evaluated", - "suppression_reason": null - }, - { - "rule_id": "stale_package", - "level": "block", - "message": "stale_package was not evaluated because enrichment mode is disabled.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "not_evaluated", - "suppression_reason": null - } - ], - "warning_violations": [ - { - "rule_id": "new_package", - "level": "warn", - "message": "Component was not present in the before input.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "new_package", - "suppression_reason": null - } - ], - "suppressed_violations": [], - "totals": { - "blocking": 3, - "warning": 1, - "suppressed": 0, - "ignored_checks": 0 - }, - "exit_code": 1 - } - }, - "notes": [ - "This tool uses heuristic risk classification.", - "No network enrichment was performed." - ] -} + "effective_policy": { + "version": 1, + "block_on": [ + "unknown_license", + "suspicious_source", + "stale_package", + "max_added_packages", + "allow_sources" + ], + "warn_on": [ + "new_package", + "major_upgrade" + ], + "max_added_packages": 0, + "allow_sources": [ + "pypi.org", + "files.pythonhosted.org", + "github.com" + ], + "ignore_rules": [] + }, + "blocking_violations": [ + { + "rule_id": "max_added_packages", + "level": "block", + "message": "Added package count 1 exceeds max_added_packages=0.", + "component_key": null, + "component_name": null, + "finding_bucket": null, + "suppression_reason": null + }, + { + "rule_id": "stale_package", + "level": "block", + "message": "stale_package was not evaluated because enrichment mode is disabled.", + "component_key": "purl:pkg:pypi/requests", + "component_name": "requests", + "finding_bucket": "not_evaluated", + "suppression_reason": null + }, + { + "rule_id": "stale_package", + "level": "block", + "message": "stale_package was not evaluated because enrichment mode is disabled.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "not_evaluated", + "suppression_reason": null + } + ], + "warning_violations": [ + { + "rule_id": "new_package", + "level": "warn", + "message": "Component was not present in the before input.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "new_package", + "suppression_reason": null + } + ], + "suppressed_violations": [], + "totals": { + "blocking": 3, + "warning": 1, + "suppressed": 0, + "ignored_checks": 0 + }, + "exit_code": 1 + } + }, + "notes": [ + "This tool uses heuristic risk classification.", + "No network enrichment was performed." + ] +} diff --git a/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.md b/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.md index 4524518..f725fc7 100644 --- a/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.md +++ b/tools/sbom-diff-and-risk/examples/sample-policy-fail-report.md @@ -1,64 +1,64 @@ -# sbom-diff-and-risk report - -## Summary -- Before format: cyclonedx-json -- After format: cyclonedx-json -- Added: 1 -- Removed: 0 -- Version changes: 1 - -## Risk buckets -- new_package: 1 -- major_upgrade: 0 -- version_change_unclassified: 1 -- unknown_license: 0 -- stale_package: 0 -- suspicious_source: 0 -- not_evaluated: 2 - -## Policy summary -- Applied: yes +# sbom-diff-and-risk report + +## Summary +- Before format: cyclonedx-json +- After format: cyclonedx-json +- Added: 1 +- Removed: 0 +- Version changes: 1 + +## Risk buckets +- new_package: 1 +- major_upgrade: 0 +- version_change_unclassified: 1 +- unknown_license: 0 +- stale_package: 0 +- suspicious_source: 0 +- not_evaluated: 2 + +## Policy summary +- Applied: yes - Policy path: examples/policy-strict.yml -- Exit code: 1 -- Blocking findings: 3 -- Warnings: 1 -- Suppressed findings: 0 - -## Added components -| name | version | ecosystem | risk buckets | -|------|---------|-----------|--------------| -| urllib3 | 2.2.1 | pypi | new_package, not_evaluated | - -## Removed components -| name | version | ecosystem | -|------|---------|-----------| -| _none_ | | | - -## Version changes -| name | before | after | classification | risk buckets | -|------|--------|-------|----------------|--------------| -| requests | 2.31.0 | 2.32.0 | version_changed | not_evaluated, version_change_unclassified | - -## Risk findings -| bucket | component | version | rationale | -|--------|-----------|---------|-----------| -| new_package | urllib3 | 2.2.1 | Component was not present in the before input. | -| not_evaluated | requests | 2.32.0 | stale_package was not evaluated because enrichment mode is disabled. | -| not_evaluated | urllib3 | 2.2.1 | stale_package was not evaluated because enrichment mode is disabled. | -| version_change_unclassified | requests | 2.32.0 | Version changed but did not qualify as a parseable SemVer major upgrade. | - -## Blocking violations -| rule id | component | level | message | -|---------|-----------|-------|---------| -| max_added_packages | | block | Added package count 1 exceeds max_added_packages=0. | -| stale_package | requests | block | stale_package was not evaluated because enrichment mode is disabled. | -| stale_package | urllib3 | block | stale_package was not evaluated because enrichment mode is disabled. | - -## Warnings -| rule id | component | level | message | -|---------|-----------|-------|---------| -| new_package | urllib3 | warn | Component was not present in the before input. | - -## Notes -- This tool uses heuristic risk classification. -- No network enrichment was performed. +- Exit code: 1 +- Blocking findings: 3 +- Warnings: 1 +- Suppressed findings: 0 + +## Added components +| name | version | ecosystem | risk buckets | +|------|---------|-----------|--------------| +| urllib3 | 2.2.1 | pypi | new_package, not_evaluated | + +## Removed components +| name | version | ecosystem | +|------|---------|-----------| +| _none_ | | | + +## Version changes +| name | before | after | classification | risk buckets | +|------|--------|-------|----------------|--------------| +| requests | 2.31.0 | 2.32.0 | version_changed | not_evaluated, version_change_unclassified | + +## Risk findings +| bucket | component | version | rationale | +|--------|-----------|---------|-----------| +| new_package | urllib3 | 2.2.1 | Component was not present in the before input. | +| not_evaluated | requests | 2.32.0 | stale_package was not evaluated because enrichment mode is disabled. | +| not_evaluated | urllib3 | 2.2.1 | stale_package was not evaluated because enrichment mode is disabled. | +| version_change_unclassified | requests | 2.32.0 | Version changed but did not qualify as a parseable SemVer major upgrade. | + +## Blocking violations +| rule id | component | level | message | +|---------|-----------|-------|---------| +| max_added_packages | | block | Added package count 1 exceeds max_added_packages=0. | +| stale_package | requests | block | stale_package was not evaluated because enrichment mode is disabled. | +| stale_package | urllib3 | block | stale_package was not evaluated because enrichment mode is disabled. | + +## Warnings +| rule id | component | level | message | +|---------|-----------|-------|---------| +| new_package | urllib3 | warn | Component was not present in the before input. | + +## Notes +- This tool uses heuristic risk classification. +- No network enrichment was performed. diff --git a/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json b/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json index 4b10e88..ba0d7c7 100644 --- a/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json +++ b/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.json @@ -1,462 +1,462 @@ -{ - "summary": { - "added": 1, - "removed": 0, - "changed": 1, - "risk_counts": { - "new_package": 1, - "major_upgrade": 0, - "version_change_unclassified": 1, - "unknown_license": 0, - "stale_package": 0, - "suspicious_source": 0, - "not_evaluated": 2 - } - }, - "components": { - "added": [ - { - "name": "urllib3", - "version": "2.2.1", - "ecosystem": "pypi", - "purl": "pkg:pypi/urllib3@2.2.1", - "license_id": "MIT", - "supplier": null, - "source_url": "https://pypi.org/project/urllib3/", - "bom_ref": "pkg:pypi/urllib3@2.2.1", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/urllib3@2.2.1", - "type": "library", - "name": "urllib3", - "version": "2.2.1", - "purl": "pkg:pypi/urllib3@2.2.1", - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/urllib3/" - } - ] - } - } - } - ], - "removed": [], - "changed": [ - { - "key": "purl:pkg:pypi/requests", - "classification": "version_changed", - "before": { - "name": "requests", - "version": "2.31.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.31.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.31.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.31.0", - "type": "library", - "name": "requests", - "version": "2.31.0", - "purl": "pkg:pypi/requests@2.31.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - }, - { - "type": "vcs", - "url": "https://github.com/psf/requests" - } - ] - } - } - }, - "after": { - "name": "requests", - "version": "2.32.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.32.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.32.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.32.0", - "type": "library", - "name": "requests", - "version": "2.32.0", - "purl": "pkg:pypi/requests@2.32.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - } - ] - } - } - } - } - ] - }, - "risks": [ - { - "bucket": "new_package", - "component_key": "purl:pkg:pypi/urllib3", - "component": { - "name": "urllib3", - "version": "2.2.1", - "ecosystem": "pypi", - "purl": "pkg:pypi/urllib3@2.2.1", - "license_id": "MIT", - "supplier": null, - "source_url": "https://pypi.org/project/urllib3/", - "bom_ref": "pkg:pypi/urllib3@2.2.1", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/urllib3@2.2.1", - "type": "library", - "name": "urllib3", - "version": "2.2.1", - "purl": "pkg:pypi/urllib3@2.2.1", - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/urllib3/" - } - ] - } - } - }, - "rationale": "Component was not present in the before input." - }, - { - "bucket": "not_evaluated", - "component_key": "purl:pkg:pypi/requests", - "component": { - "name": "requests", - "version": "2.32.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.32.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.32.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.32.0", - "type": "library", - "name": "requests", - "version": "2.32.0", - "purl": "pkg:pypi/requests@2.32.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - } - ] - } - } - }, - "rationale": "stale_package was not evaluated because enrichment mode is disabled." - }, - { - "bucket": "not_evaluated", - "component_key": "purl:pkg:pypi/urllib3", - "component": { - "name": "urllib3", - "version": "2.2.1", - "ecosystem": "pypi", - "purl": "pkg:pypi/urllib3@2.2.1", - "license_id": "MIT", - "supplier": null, - "source_url": "https://pypi.org/project/urllib3/", - "bom_ref": "pkg:pypi/urllib3@2.2.1", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/urllib3@2.2.1", - "type": "library", - "name": "urllib3", - "version": "2.2.1", - "purl": "pkg:pypi/urllib3@2.2.1", - "licenses": [ - { - "license": { - "id": "MIT" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/urllib3/" - } - ] - } - } - }, - "rationale": "stale_package was not evaluated because enrichment mode is disabled." - }, - { - "bucket": "version_change_unclassified", - "component_key": "purl:pkg:pypi/requests", - "component": { - "name": "requests", - "version": "2.32.0", - "ecosystem": "pypi", - "purl": "pkg:pypi/requests@2.32.0", - "license_id": "Apache-2.0", - "supplier": "Python Software Foundation", - "source_url": "https://pypi.org/project/requests/", - "bom_ref": "pkg:pypi/requests@2.32.0", - "raw_type": "library", - "evidence": { - "source_format": "cyclonedx-json", - "component": { - "bom-ref": "pkg:pypi/requests@2.32.0", - "type": "library", - "name": "requests", - "version": "2.32.0", - "purl": "pkg:pypi/requests@2.32.0", - "supplier": { - "name": "Python Software Foundation" - }, - "licenses": [ - { - "license": { - "id": "Apache-2.0" - } - } - ], - "externalReferences": [ - { - "type": "website", - "url": "https://pypi.org/project/requests/" - } - ] - } - } - }, - "rationale": "Version changed but did not qualify as a parseable SemVer major upgrade." - } - ], - "policy_evaluation": { - "applied": true, +{ + "summary": { + "added": 1, + "removed": 0, + "changed": 1, + "risk_counts": { + "new_package": 1, + "major_upgrade": 0, + "version_change_unclassified": 1, + "unknown_license": 0, + "stale_package": 0, + "suspicious_source": 0, + "not_evaluated": 2 + } + }, + "components": { + "added": [ + { + "name": "urllib3", + "version": "2.2.1", + "ecosystem": "pypi", + "purl": "pkg:pypi/urllib3@2.2.1", + "license_id": "MIT", + "supplier": null, + "source_url": "https://pypi.org/project/urllib3/", + "bom_ref": "pkg:pypi/urllib3@2.2.1", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/urllib3@2.2.1", + "type": "library", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/urllib3/" + } + ] + } + } + } + ], + "removed": [], + "changed": [ + { + "key": "purl:pkg:pypi/requests", + "classification": "version_changed", + "before": { + "name": "requests", + "version": "2.31.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.31.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.31.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.31.0", + "type": "library", + "name": "requests", + "version": "2.31.0", + "purl": "pkg:pypi/requests@2.31.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + }, + { + "type": "vcs", + "url": "https://github.com/psf/requests" + } + ] + } + } + }, + "after": { + "name": "requests", + "version": "2.32.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.32.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.32.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.32.0", + "type": "library", + "name": "requests", + "version": "2.32.0", + "purl": "pkg:pypi/requests@2.32.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + } + ] + } + } + } + } + ] + }, + "risks": [ + { + "bucket": "new_package", + "component_key": "purl:pkg:pypi/urllib3", + "component": { + "name": "urllib3", + "version": "2.2.1", + "ecosystem": "pypi", + "purl": "pkg:pypi/urllib3@2.2.1", + "license_id": "MIT", + "supplier": null, + "source_url": "https://pypi.org/project/urllib3/", + "bom_ref": "pkg:pypi/urllib3@2.2.1", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/urllib3@2.2.1", + "type": "library", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/urllib3/" + } + ] + } + } + }, + "rationale": "Component was not present in the before input." + }, + { + "bucket": "not_evaluated", + "component_key": "purl:pkg:pypi/requests", + "component": { + "name": "requests", + "version": "2.32.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.32.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.32.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.32.0", + "type": "library", + "name": "requests", + "version": "2.32.0", + "purl": "pkg:pypi/requests@2.32.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + } + ] + } + } + }, + "rationale": "stale_package was not evaluated because enrichment mode is disabled." + }, + { + "bucket": "not_evaluated", + "component_key": "purl:pkg:pypi/urllib3", + "component": { + "name": "urllib3", + "version": "2.2.1", + "ecosystem": "pypi", + "purl": "pkg:pypi/urllib3@2.2.1", + "license_id": "MIT", + "supplier": null, + "source_url": "https://pypi.org/project/urllib3/", + "bom_ref": "pkg:pypi/urllib3@2.2.1", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/urllib3@2.2.1", + "type": "library", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/urllib3/" + } + ] + } + } + }, + "rationale": "stale_package was not evaluated because enrichment mode is disabled." + }, + { + "bucket": "version_change_unclassified", + "component_key": "purl:pkg:pypi/requests", + "component": { + "name": "requests", + "version": "2.32.0", + "ecosystem": "pypi", + "purl": "pkg:pypi/requests@2.32.0", + "license_id": "Apache-2.0", + "supplier": "Python Software Foundation", + "source_url": "https://pypi.org/project/requests/", + "bom_ref": "pkg:pypi/requests@2.32.0", + "raw_type": "library", + "evidence": { + "source_format": "cyclonedx-json", + "component": { + "bom-ref": "pkg:pypi/requests@2.32.0", + "type": "library", + "name": "requests", + "version": "2.32.0", + "purl": "pkg:pypi/requests@2.32.0", + "supplier": { + "name": "Python Software Foundation" + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "type": "website", + "url": "https://pypi.org/project/requests/" + } + ] + } + } + }, + "rationale": "Version changed but did not qualify as a parseable SemVer major upgrade." + } + ], + "policy_evaluation": { + "applied": true, "policy_path": "examples/policy-minimal.yml", - "effective_policy": { - "version": 1, - "block_on": [ - "unknown_license" - ], - "warn_on": [ - "new_package" - ], - "max_added_packages": null, - "allow_sources": [], - "ignore_rules": [] - }, - "blocking_violations": [], - "warning_violations": [ - { - "rule_id": "new_package", - "level": "warn", - "message": "Component was not present in the before input.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "new_package", - "suppression_reason": null - } - ], - "suppressed_violations": [], - "totals": { - "blocking": 0, - "warning": 1, - "suppressed": 0, - "ignored_checks": 0 - }, - "exit_code": 0 - }, - "blocking_findings": [], - "warning_findings": [ - { - "rule_id": "new_package", - "level": "warn", - "message": "Component was not present in the before input.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "new_package", - "suppression_reason": null - } - ], - "suppressed_findings": [], - "rule_catalog": { - "new_package": { - "rule_id": "new_package", - "kind": "risk_finding", - "description": "Component is present only in the after input.", - "finding_buckets": [ - "new_package" - ] - }, - "major_upgrade": { - "rule_id": "major_upgrade", - "kind": "risk_finding", - "description": "Version change is a parseable SemVer major upgrade.", - "finding_buckets": [ - "major_upgrade" - ] - }, - "version_change_unclassified": { - "rule_id": "version_change_unclassified", - "kind": "risk_finding", - "description": "Version changed but could not be classified as a reliable major SemVer upgrade.", - "finding_buckets": [ - "version_change_unclassified" - ] - }, - "unknown_license": { - "rule_id": "unknown_license", - "kind": "risk_finding", - "description": "License metadata is missing, empty, UNKNOWN, or NOASSERTION.", - "finding_buckets": [ - "unknown_license" - ] - }, - "suspicious_source": { - "rule_id": "suspicious_source", - "kind": "risk_finding", - "description": "Source provenance is missing or points to a suspicious scheme, path, or host.", - "finding_buckets": [ - "suspicious_source" - ] - }, - "stale_package": { - "rule_id": "stale_package", - "kind": "risk_finding", - "description": "Staleness check result. Offline mode maps this rule to not_evaluated instead of guessing.", - "finding_buckets": [ - "stale_package", - "not_evaluated" - ] - }, - "max_added_packages": { - "rule_id": "max_added_packages", - "kind": "policy_check", - "description": "Added package count exceeded the configured deterministic threshold.", - "finding_buckets": [] - }, - "allow_sources": { - "rule_id": "allow_sources", - "kind": "policy_check", - "description": "Component source host was not present in the configured allow_sources list.", - "finding_buckets": [] - } - }, - "metadata": { - "before_format": "cyclonedx-json", - "after_format": "cyclonedx-json", - "generated_at": null, - "strict": false, - "stub": false, - "policy_evaluation": { - "applied": true, + "effective_policy": { + "version": 1, + "block_on": [ + "unknown_license" + ], + "warn_on": [ + "new_package" + ], + "max_added_packages": null, + "allow_sources": [], + "ignore_rules": [] + }, + "blocking_violations": [], + "warning_violations": [ + { + "rule_id": "new_package", + "level": "warn", + "message": "Component was not present in the before input.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "new_package", + "suppression_reason": null + } + ], + "suppressed_violations": [], + "totals": { + "blocking": 0, + "warning": 1, + "suppressed": 0, + "ignored_checks": 0 + }, + "exit_code": 0 + }, + "blocking_findings": [], + "warning_findings": [ + { + "rule_id": "new_package", + "level": "warn", + "message": "Component was not present in the before input.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "new_package", + "suppression_reason": null + } + ], + "suppressed_findings": [], + "rule_catalog": { + "new_package": { + "rule_id": "new_package", + "kind": "risk_finding", + "description": "Component is present only in the after input.", + "finding_buckets": [ + "new_package" + ] + }, + "major_upgrade": { + "rule_id": "major_upgrade", + "kind": "risk_finding", + "description": "Version change is a parseable SemVer major upgrade.", + "finding_buckets": [ + "major_upgrade" + ] + }, + "version_change_unclassified": { + "rule_id": "version_change_unclassified", + "kind": "risk_finding", + "description": "Version changed but could not be classified as a reliable major SemVer upgrade.", + "finding_buckets": [ + "version_change_unclassified" + ] + }, + "unknown_license": { + "rule_id": "unknown_license", + "kind": "risk_finding", + "description": "License metadata is missing, empty, UNKNOWN, or NOASSERTION.", + "finding_buckets": [ + "unknown_license" + ] + }, + "suspicious_source": { + "rule_id": "suspicious_source", + "kind": "risk_finding", + "description": "Source provenance is missing or points to a suspicious scheme, path, or host.", + "finding_buckets": [ + "suspicious_source" + ] + }, + "stale_package": { + "rule_id": "stale_package", + "kind": "risk_finding", + "description": "Staleness check result. Offline mode maps this rule to not_evaluated instead of guessing.", + "finding_buckets": [ + "stale_package", + "not_evaluated" + ] + }, + "max_added_packages": { + "rule_id": "max_added_packages", + "kind": "policy_check", + "description": "Added package count exceeded the configured deterministic threshold.", + "finding_buckets": [] + }, + "allow_sources": { + "rule_id": "allow_sources", + "kind": "policy_check", + "description": "Component source host was not present in the configured allow_sources list.", + "finding_buckets": [] + } + }, + "metadata": { + "before_format": "cyclonedx-json", + "after_format": "cyclonedx-json", + "generated_at": null, + "strict": false, + "stub": false, + "policy_evaluation": { + "applied": true, "policy_path": "examples/policy-minimal.yml", - "effective_policy": { - "version": 1, - "block_on": [ - "unknown_license" - ], - "warn_on": [ - "new_package" - ], - "max_added_packages": null, - "allow_sources": [], - "ignore_rules": [] - }, - "blocking_violations": [], - "warning_violations": [ - { - "rule_id": "new_package", - "level": "warn", - "message": "Component was not present in the before input.", - "component_key": "purl:pkg:pypi/urllib3", - "component_name": "urllib3", - "finding_bucket": "new_package", - "suppression_reason": null - } - ], - "suppressed_violations": [], - "totals": { - "blocking": 0, - "warning": 1, - "suppressed": 0, - "ignored_checks": 0 - }, - "exit_code": 0 - } - }, - "notes": [ - "This tool uses heuristic risk classification.", - "No network enrichment was performed." - ] -} + "effective_policy": { + "version": 1, + "block_on": [ + "unknown_license" + ], + "warn_on": [ + "new_package" + ], + "max_added_packages": null, + "allow_sources": [], + "ignore_rules": [] + }, + "blocking_violations": [], + "warning_violations": [ + { + "rule_id": "new_package", + "level": "warn", + "message": "Component was not present in the before input.", + "component_key": "purl:pkg:pypi/urllib3", + "component_name": "urllib3", + "finding_bucket": "new_package", + "suppression_reason": null + } + ], + "suppressed_violations": [], + "totals": { + "blocking": 0, + "warning": 1, + "suppressed": 0, + "ignored_checks": 0 + }, + "exit_code": 0 + } + }, + "notes": [ + "This tool uses heuristic risk classification.", + "No network enrichment was performed." + ] +} diff --git a/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md b/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md index de4540b..d4ce8d1 100644 --- a/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md +++ b/tools/sbom-diff-and-risk/examples/sample-policy-warn-report.md @@ -1,62 +1,62 @@ -# sbom-diff-and-risk report - -## Summary -- Before format: cyclonedx-json -- After format: cyclonedx-json -- Added: 1 -- Removed: 0 -- Version changes: 1 - -## Risk buckets -- new_package: 1 -- major_upgrade: 0 -- version_change_unclassified: 1 -- unknown_license: 0 -- stale_package: 0 -- suspicious_source: 0 -- not_evaluated: 2 - -## Policy summary -- Applied: yes +# sbom-diff-and-risk report + +## Summary +- Before format: cyclonedx-json +- After format: cyclonedx-json +- Added: 1 +- Removed: 0 +- Version changes: 1 + +## Risk buckets +- new_package: 1 +- major_upgrade: 0 +- version_change_unclassified: 1 +- unknown_license: 0 +- stale_package: 0 +- suspicious_source: 0 +- not_evaluated: 2 + +## Policy summary +- Applied: yes - Policy path: examples/policy-minimal.yml -- Exit code: 0 -- Blocking findings: 0 -- Warnings: 1 -- Suppressed findings: 0 - -## Added components -| name | version | ecosystem | risk buckets | -|------|---------|-----------|--------------| -| urllib3 | 2.2.1 | pypi | new_package, not_evaluated | - -## Removed components -| name | version | ecosystem | -|------|---------|-----------| -| _none_ | | | - -## Version changes -| name | before | after | classification | risk buckets | -|------|--------|-------|----------------|--------------| -| requests | 2.31.0 | 2.32.0 | version_changed | not_evaluated, version_change_unclassified | - -## Risk findings -| bucket | component | version | rationale | -|--------|-----------|---------|-----------| -| new_package | urllib3 | 2.2.1 | Component was not present in the before input. | -| not_evaluated | requests | 2.32.0 | stale_package was not evaluated because enrichment mode is disabled. | -| not_evaluated | urllib3 | 2.2.1 | stale_package was not evaluated because enrichment mode is disabled. | -| version_change_unclassified | requests | 2.32.0 | Version changed but did not qualify as a parseable SemVer major upgrade. | - -## Blocking violations -| rule id | component | level | message | -|---------|-----------|-------|---------| -| _none_ | | | | - -## Warnings -| rule id | component | level | message | -|---------|-----------|-------|---------| -| new_package | urllib3 | warn | Component was not present in the before input. | - -## Notes -- This tool uses heuristic risk classification. -- No network enrichment was performed. +- Exit code: 0 +- Blocking findings: 0 +- Warnings: 1 +- Suppressed findings: 0 + +## Added components +| name | version | ecosystem | risk buckets | +|------|---------|-----------|--------------| +| urllib3 | 2.2.1 | pypi | new_package, not_evaluated | + +## Removed components +| name | version | ecosystem | +|------|---------|-----------| +| _none_ | | | + +## Version changes +| name | before | after | classification | risk buckets | +|------|--------|-------|----------------|--------------| +| requests | 2.31.0 | 2.32.0 | version_changed | not_evaluated, version_change_unclassified | + +## Risk findings +| bucket | component | version | rationale | +|--------|-----------|---------|-----------| +| new_package | urllib3 | 2.2.1 | Component was not present in the before input. | +| not_evaluated | requests | 2.32.0 | stale_package was not evaluated because enrichment mode is disabled. | +| not_evaluated | urllib3 | 2.2.1 | stale_package was not evaluated because enrichment mode is disabled. | +| version_change_unclassified | requests | 2.32.0 | Version changed but did not qualify as a parseable SemVer major upgrade. | + +## Blocking violations +| rule id | component | level | message | +|---------|-----------|-------|---------| +| _none_ | | | | + +## Warnings +| rule id | component | level | message | +|---------|-----------|-------|---------| +| new_package | urllib3 | warn | Component was not present in the before input. | + +## Notes +- This tool uses heuristic risk classification. +- No network enrichment was performed. diff --git a/tools/sbom-diff-and-risk/pyproject.toml b/tools/sbom-diff-and-risk/pyproject.toml index 2fadb50..ddc6b3c 100644 --- a/tools/sbom-diff-and-risk/pyproject.toml +++ b/tools/sbom-diff-and-risk/pyproject.toml @@ -8,7 +8,7 @@ version = "0.2.0" description = "Local, deterministic SBOM diff and heuristic risk reporting." readme = "README.md" requires-python = ">=3.11" -license = { text = "MIT" } +license = "MIT" authors = [ { name = "OpenAI Codex" } ]