import json
import sys
import re
from typing import Set, List, Dict, Tuple
ATTRIBUTION_REQUIRED_LICENSES: Set[str] = {
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSD-4-Clause",
"MIT",
"ISC",
"IJG",
}
PROPRIETARY_LICENSES: Set[str] = {
"NXP-Proprietary",
"NXP Proprietary",
"Proprietary",
}
KNOWN_ATTRIBUTION_PACKAGES: Set[str] = {
"dma-buf", "dma-heap", }
def extract_license_from_component(component: Dict) -> Set[str]:
licenses: Set[str] = set()
if "licenses" not in component:
return licenses
for lic_entry in component["licenses"]:
if "license" in lic_entry:
if "id" in lic_entry["license"]:
licenses.add(lic_entry["license"]["id"])
elif "name" in lic_entry["license"]:
licenses.add(lic_entry["license"]["name"])
if "expression" in lic_entry:
expr = lic_entry["expression"]
parts = expr.replace("(", "").replace(")", "")
parts = parts.replace(" OR ", " ").replace(" AND ", " ").replace(" WITH ", " ")
for part in parts.split():
if part and not part.isspace():
licenses.add(part)
return licenses
def get_first_level_dependencies(sbom: Dict) -> Set[str]:
first_level: Set[str] = set()
metadata = sbom.get("metadata", {})
project_component = metadata.get("component", {})
project_ref = project_component.get("bom-ref", "")
dependencies = sbom.get("dependencies", [])
first_level_refs = []
for dep_entry in dependencies:
if dep_entry.get("ref") == project_ref:
first_level_refs = dep_entry.get("dependsOn", [])
break
if not first_level_refs and not dependencies:
print("⚠️ WARNING: No dependency graph found in SBOM.")
print(" Skipping NOTICE validation - SBOM may be incomplete.")
print(" Ensure cargo-cyclonedx runs with --all flag.")
return set()
components = sbom.get("components", [])
ref_to_component = {c.get("bom-ref"): c for c in components}
for ref in first_level_refs:
if ref in ref_to_component:
component = ref_to_component[ref]
name = component.get("name", "unknown")
version = component.get("version", "unknown")
licenses = extract_license_from_component(component)
if (licenses.intersection(ATTRIBUTION_REQUIRED_LICENSES) or
licenses.intersection(PROPRIETARY_LICENSES) or
name in KNOWN_ATTRIBUTION_PACKAGES):
first_level.add(f"{name} {version}")
return first_level
def parse_notice_file(notice_path: str) -> Set[str]:
listed_deps: Set[str] = set()
try:
with open(notice_path, 'r') as f:
content = f.read()
except Exception as e:
print(f"Error reading NOTICE file: {e}", file=sys.stderr)
sys.exit(1)
pattern = r'^\s*\*\s+(\S+)\s+([\d.]+(?:-[\w.]+)?)\s+\(.*?\)'
proprietary_pattern = r'^\s*\*\s+(.+?)\s+-\s+.*Proprietary'
for line in content.split('\n'):
match = re.match(pattern, line)
if match:
name = match.group(1)
version = match.group(2)
listed_deps.add(f"{name} {version}")
else:
prop_match = re.match(proprietary_pattern, line)
if prop_match:
listed_deps.add(prop_match.group(1))
return listed_deps
def validate_notice(notice_path: str, sbom_path: str) -> Tuple[bool, List[str], List[str]]:
try:
with open(sbom_path, 'r') as f:
sbom = json.load(f)
except Exception as e:
print(f"Error reading SBOM: {e}", file=sys.stderr)
sys.exit(1)
sbom_first_level = get_first_level_dependencies(sbom)
notice_deps = parse_notice_file(notice_path)
missing_deps = list(sbom_first_level - notice_deps)
extra_deps = list(notice_deps - sbom_first_level)
missing_deps.sort()
extra_deps.sort()
passed = len(missing_deps) == 0 and len(extra_deps) == 0
return passed, missing_deps, extra_deps
def main():
if len(sys.argv) != 3:
print("Usage: validate_notice.py <NOTICE> <sbom.json>", file=sys.stderr)
sys.exit(1)
notice_path = sys.argv[1]
sbom_path = sys.argv[2]
passed, missing_deps, extra_deps = validate_notice(notice_path, sbom_path)
print("=" * 80)
print("NOTICE File Validation")
print("=" * 80)
print()
if missing_deps:
print("MISSING FROM NOTICE FILE:")
print("-" * 80)
print("The following first-level dependencies require attribution but are")
print("missing from the NOTICE file:")
print()
for dep in missing_deps:
print(f" • {dep}")
print()
if extra_deps:
print("EXTRA IN NOTICE FILE:")
print("-" * 80)
print("The following dependencies are listed in NOTICE but are not")
print("first-level dependencies in sbom.json:")
print()
for dep in extra_deps:
print(f" • {dep}")
print()
if passed:
print("✓ NOTICE file validation PASSED!")
print(" All first-level dependencies are properly documented.")
print()
sys.exit(0)
elif len(missing_deps) == 0 and len(extra_deps) == 0:
print("⚠️ NOTICE file validation SKIPPED!")
print(" SBOM does not contain dependency graph.")
print(" This is a warning, not a failure.")
print()
sys.exit(0)
else:
print("✗ NOTICE file validation FAILED!")
print()
print("ACTION REQUIRED:")
print(" 1. Review the missing/extra dependencies above")
print(" 2. Manually update the NOTICE file to match first-level dependencies")
print(" 3. Re-run this validation script")
print()
sys.exit(1)
if __name__ == "__main__":
main()