From 0628d641fa3815f12bcb0af4b5da8eb7f228a266 Mon Sep 17 00:00:00 2001 From: stacknil Date: Wed, 15 Apr 2026 15:41:37 +0800 Subject: [PATCH] Normalize policy report paths for deterministic CI --- .../examples/sample-policy-fail-report.json | 1128 ++++++++--------- .../examples/sample-policy-fail-report.md | 128 +- .../examples/sample-policy-warn-report.json | 924 +++++++------- .../examples/sample-policy-warn-report.md | 124 +- .../src/sbom_diff_risk/policy_parser.py | 10 +- tools/sbom-diff-and-risk/tests/test_policy.py | 13 +- 6 files changed, 1173 insertions(+), 1154 deletions(-) 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 a5b7271..b28f3d5 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, - "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, - "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." - ] -} +{ + "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, + "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." + ] +} 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 20485a7..4524518 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 -- 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. +# 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. 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 4410c59..4b10e88 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, - "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, - "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." - ] -} +{ + "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, + "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." + ] +} 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 9422e2b..de4540b 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 -- 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. +# 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. diff --git a/tools/sbom-diff-and-risk/src/sbom_diff_risk/policy_parser.py b/tools/sbom-diff-and-risk/src/sbom_diff_risk/policy_parser.py index 6b417bf..eb11b5c 100644 --- a/tools/sbom-diff-and-risk/src/sbom_diff_risk/policy_parser.py +++ b/tools/sbom-diff-and-risk/src/sbom_diff_risk/policy_parser.py @@ -72,7 +72,7 @@ def build_policy( rendered_path: str | None = None if policy_path is not None: base_policy = load_policy(policy_path) - rendered_path = str(policy_path) + rendered_path = _render_policy_path(policy_path) cli_block_on = parse_rule_csv(fail_on, "--fail-on") cli_warn_on = parse_rule_csv(warn_on, "--warn-on") @@ -154,3 +154,11 @@ def _validate_rule_ids(rule_ids: Iterable[str], context: str) -> tuple[str, ...] def _merge_strings(base: tuple[str, ...], extra: tuple[str, ...]) -> tuple[str, ...]: return tuple(dict.fromkeys((*base, *extra))) + + +def _render_policy_path(policy_path: Path) -> str: + resolved_policy_path = policy_path.resolve() + try: + return resolved_policy_path.relative_to(Path.cwd().resolve()).as_posix() + except ValueError: + return resolved_policy_path.as_posix() diff --git a/tools/sbom-diff-and-risk/tests/test_policy.py b/tools/sbom-diff-and-risk/tests/test_policy.py index d6aed05..67f6f51 100644 --- a/tools/sbom-diff-and-risk/tests/test_policy.py +++ b/tools/sbom-diff-and-risk/tests/test_policy.py @@ -5,7 +5,7 @@ import pytest from sbom_diff_risk.errors import PolicyError -from sbom_diff_risk.models import Component, ComponentChange, RiskBucket, RiskFinding +from sbom_diff_risk.models import Component, RiskBucket, RiskFinding from sbom_diff_risk.policy_evaluator import evaluate_policy from sbom_diff_risk.policy_models import PolicyConfig, PolicyLevel from sbom_diff_risk.policy_parser import build_policy, load_policy @@ -57,6 +57,15 @@ def test_build_policy_merges_cli_rules() -> None: assert "new_package" in policy.warn_on +def test_build_policy_renders_path_relative_to_cwd(monkeypatch: pytest.MonkeyPatch) -> None: + project_root = Path(__file__).resolve().parents[1] + monkeypatch.chdir(project_root) + + _, policy_path_str = build_policy(policy_path=project_root / "examples" / "policy-minimal.yml") + + assert policy_path_str == "examples/policy-minimal.yml" + + def test_policy_evaluator_blocks_on_finding_bucket() -> None: policy = PolicyConfig(version=1, block_on=("unknown_license",)) component = Component(name="requests", version="2.32.0", ecosystem="pypi") @@ -134,6 +143,8 @@ def test_policy_ignore_rules_suppresses_violations() -> None: assert evaluation.exit_code == 0 assert evaluation.blocking_violations == [] assert evaluation.ignored_checks == 1 + assert len(evaluation.suppressed_violations) == 1 + assert evaluation.suppressed_violations[0].suppression_reason == "ignored_by_policy" def _example_path(name: str) -> Path: