Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions tools/sbom-diff-and-risk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ report. It includes `policy_evaluation`, policy finding lists, `rule_catalog`,
and `summary.policy` when policy evaluation is applied. For CI job-summary
examples, see
[docs/policy-decision-ci-cookbook.md](docs/policy-decision-ci-cookbook.md).
The checked-in [examples/sample-policy.json](examples/sample-policy.json)
artifact shows the standalone policy sidecar shape.

## Dependency Provenance Analysis (Opt-in)

Expand Down Expand Up @@ -309,6 +311,7 @@ The [examples/](examples/) directory includes:
- 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)
- a sample policy-only sidecar at [sample-policy.json](examples/sample-policy.json)
- a sample SARIF export at [sample-sarif.sarif](examples/sample-sarif.sarif)
- provenance-aware sample reports at [sample-provenance-report.json](examples/sample-provenance-report.json), [sample-provenance-report.md](examples/sample-provenance-report.md), and [sample-provenance-report.sarif](examples/sample-provenance-report.sarif)
- Scorecard-aware sample reports at [sample-scorecard-report.json](examples/sample-scorecard-report.json), [sample-scorecard-report.md](examples/sample-scorecard-report.md), and [sample-scorecard-report.sarif](examples/sample-scorecard-report.sarif)
Expand Down
3 changes: 3 additions & 0 deletions tools/sbom-diff-and-risk/docs/policy-decision-ci-cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ The strict example policy can make the command return a policy failure exit
code. In CI, keep the generated `outputs/policy.json` artifact so the policy
decision metadata remains available for review.

For a checked-in reference artifact generated from this path, see
[sample-policy.json](../examples/sample-policy.json).

## Python consumer

This example reads the policy-only JSON sidecar, prints compact policy status,
Expand Down
3 changes: 3 additions & 0 deletions tools/sbom-diff-and-risk/docs/report-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ same policy-related sections from the full JSON report:
- `provenance_policy` and `provenance_policy_impact` when provenance policy
fields are relevant

The checked-in [sample-policy.json](../examples/sample-policy.json) artifact
locks the standalone policy sidecar shape for a strict policy example.

## Summary contract

`summary` is the stable, compact entry point for automation that needs counts
Expand Down
257 changes: 257 additions & 0 deletions tools/sbom-diff-and-risk/examples/sample-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
{
"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.",
"decision_reason": "added_package_count_exceeded_threshold",
"policy_rule": "max_added_packages",
"severity_source": "block_on",
"matched_threshold": 0,
"observed_value": 1,
"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.",
"decision_reason": "risk_finding_matched_policy_rule",
"policy_rule": "stale_package",
"severity_source": "block_on",
"matched_threshold": null,
"observed_value": "not_evaluated",
"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.",
"decision_reason": "risk_finding_matched_policy_rule",
"policy_rule": "stale_package",
"severity_source": "block_on",
"matched_threshold": null,
"observed_value": "not_evaluated",
"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.",
"decision_reason": "risk_finding_matched_policy_rule",
"policy_rule": "new_package",
"severity_source": "warn_on",
"matched_threshold": null,
"observed_value": "new_package",
"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.",
"decision_reason": "added_package_count_exceeded_threshold",
"policy_rule": "max_added_packages",
"severity_source": "block_on",
"matched_threshold": 0,
"observed_value": 1,
"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.",
"decision_reason": "risk_finding_matched_policy_rule",
"policy_rule": "stale_package",
"severity_source": "block_on",
"matched_threshold": null,
"observed_value": "not_evaluated",
"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.",
"decision_reason": "risk_finding_matched_policy_rule",
"policy_rule": "stale_package",
"severity_source": "block_on",
"matched_threshold": null,
"observed_value": "not_evaluated",
"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.",
"decision_reason": "risk_finding_matched_policy_rule",
"policy_rule": "new_package",
"severity_source": "warn_on",
"matched_threshold": null,
"observed_value": "new_package",
"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": []
},
"missing_attestation": {
"rule_id": "missing_attestation",
"kind": "provenance_signal",
"description": "PyPI release metadata was fetched, but no attestations were published for the package release.",
"finding_buckets": []
},
"unverified_provenance": {
"rule_id": "unverified_provenance",
"kind": "provenance_signal",
"description": "PyPI attestations were present, but provenance could not be verified against publisher metadata.",
"finding_buckets": []
},
"provenance_unavailable": {
"rule_id": "provenance_unavailable",
"kind": "provenance_signal",
"description": "PyPI provenance evidence was unavailable because enrichment was disabled, unsupported, or errored.",
"finding_buckets": []
},
"provenance_required": {
"rule_id": "provenance_required",
"kind": "policy_check",
"description": "A configured provenance requirement was not satisfied for the component.",
"finding_buckets": []
},
"scorecard_below_threshold": {
"rule_id": "scorecard_below_threshold",
"kind": "policy_check",
"description": "A mapped repository's OpenSSF Scorecard score was below the configured minimum threshold.",
"finding_buckets": []
}
},
"summary": {
"policy": {
"status": "fail",
"blocking": 3,
"warning": 1,
"suppressed": 0
}
}
}
35 changes: 34 additions & 1 deletion tools/sbom-diff-and-risk/tests/test_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from sbom_diff_risk.policy_models import PolicyConfig
from sbom_diff_risk.policy_parser import build_policy
from sbom_diff_risk.normalize import normalize_input
from sbom_diff_risk.report_json import render_report_json, render_summary_json
from sbom_diff_risk.report_json import render_policy_json, render_report_json, render_summary_json
from sbom_diff_risk.report_md import render_report_markdown
from sbom_diff_risk.risk import evaluate_risks, summarize_risks

Expand Down Expand Up @@ -78,6 +78,18 @@ def test_report_json_matches_cyclonedx_policy_fail_golden() -> None:
assert rendered == expected


def test_policy_json_matches_cyclonedx_policy_fail_golden() -> None:
report = _build_report("cdx_before.json", "cdx_after.json", policy_name="policy-strict.yml")

rendered = render_policy_json(report)
expected = _read_example("sample-policy.json")

assert rendered == expected
assert json.loads(rendered) == _policy_sidecar_from_full_report(
json.loads(_read_example("sample-policy-fail-report.json"))
)


def test_report_markdown_matches_cyclonedx_policy_fail_golden() -> None:
report = _build_report("cdx_before.json", "cdx_after.json", policy_name="policy-strict.yml")

Expand Down Expand Up @@ -397,3 +409,24 @@ def _build_report(
def _read_example(name: str) -> str:
examples = Path(__file__).resolve().parents[1] / "examples"
return (examples / name).read_text(encoding="utf-8")


def _policy_sidecar_from_full_report(report_payload: dict[str, object]) -> dict[str, object]:
policy_payload = {
"policy_evaluation": report_payload["policy_evaluation"],
"blocking_findings": report_payload["blocking_findings"],
"warning_findings": report_payload["warning_findings"],
"suppressed_findings": report_payload["suppressed_findings"],
"rule_catalog": report_payload["rule_catalog"],
}

summary = report_payload["summary"]
assert isinstance(summary, dict)
if "policy" in summary:
policy_payload["summary"] = {"policy": summary["policy"]}

if "provenance_policy" in report_payload:
policy_payload["provenance_policy"] = report_payload["provenance_policy"]
policy_payload["provenance_policy_impact"] = report_payload["provenance_policy_impact"]

return policy_payload