use affidavit::ocel::SeqCounter;
use affidavit::sbom::{parse_sbom_json, SbomFormat};
use affidavit::{sbom_compliance, sbom_ocel, sbom_supply_chain, sbom_vulnerability};
use std::fs;
fn cyclonedx() -> affidavit::sbom::Sbom {
let raw = fs::read_to_string("fixtures/sbom/cyclonedx-sample.json").expect("fixture present");
parse_sbom_json(&raw).expect("cyclonedx parses")
}
fn spdx() -> affidavit::sbom::Sbom {
let raw = fs::read_to_string("fixtures/sbom/spdx-sample.json").expect("fixture present");
parse_sbom_json(&raw).expect("spdx parses")
}
#[test]
fn cyclonedx_fixture_ingests_with_expected_shape() {
let sbom = cyclonedx();
assert_eq!(sbom.format, SbomFormat::CycloneDx16);
assert_eq!(sbom.components.len(), 6);
assert!(!sbom.dependencies.is_empty());
assert!(sbom.component("pkg:cargo/serde@1.0.197").is_some());
}
#[test]
fn spdx_fixture_ingests_and_resolves_relationships() {
let sbom = spdx();
assert_eq!(sbom.format, SbomFormat::Spdx23);
assert_eq!(sbom.components.len(), 3);
assert!(!sbom.dependencies.is_empty());
}
#[test]
fn both_formats_are_ntia_conformant() {
assert!(
cyclonedx().ntia_minimum_elements().is_conformant(),
"cyclonedx fixture should be NTIA-conformant"
);
assert!(
spdx().ntia_minimum_elements().is_conformant(),
"spdx fixture should be NTIA-conformant"
);
}
#[test]
fn ocel_event_stream_is_deterministic_and_nonempty() {
let sbom = cyclonedx();
let mut c1 = SeqCounter::new();
let mut c2 = SeqCounter::new();
let a = sbom_ocel::sbom_to_ocel_events(&sbom, &mut c1).expect("events");
let b = sbom_ocel::sbom_to_ocel_events(&sbom, &mut c2).expect("events");
assert!(!a.is_empty());
assert_eq!(a.len(), b.len(), "event count is deterministic");
for (x, y) in a.iter().zip(b.iter()) {
assert_eq!(x.event.payload_commitment, y.event.payload_commitment);
assert_eq!(x.sbom_event_type, y.sbom_event_type);
}
}
#[test]
fn compliance_assessment_runs_all_frameworks() {
let sbom = cyclonedx();
let results = sbom_compliance::assess_all(&sbom).expect("assess");
assert!(results.len() >= 8, "all major frameworks assessed");
let ntia = results
.iter()
.find(|r| r.framework.to_ascii_lowercase().contains("ntia"))
.expect("ntia result present");
assert!(ntia.passed, "complete fixture passes NTIA");
}
#[test]
fn vulnerability_scan_matches_and_vex_suppresses() {
let sbom = cyclonedx();
let adv = fs::read_to_string("fixtures/sbom/advisories-sample.json").expect("advisories");
let doc: serde_json::Value = serde_json::from_str(&adv).unwrap();
let vulns: Vec<sbom_vulnerability::Vulnerability> =
serde_json::from_value(doc["vulnerabilities"].clone()).unwrap();
let vex: Vec<sbom_vulnerability::VexStatement> =
serde_json::from_value(doc["vex"].clone()).unwrap();
let raw_matches = sbom_vulnerability::match_vulnerabilities(&sbom, &vulns);
assert!(raw_matches.len() >= 2, "openssl + log advisories match");
let report = sbom_vulnerability::build_report(&sbom, &vulns, &vex);
assert!(
report.exploitable_after_vex < report.total_matches,
"VEX NotAffected must reduce exploitable count"
);
assert_eq!(report.max_severity, sbom_vulnerability::Severity::High);
}
#[test]
fn risk_propagates_along_the_dependency_chain() {
let sbom = cyclonedx();
let adv = fs::read_to_string("fixtures/sbom/advisories-sample.json").expect("advisories");
let doc: serde_json::Value = serde_json::from_str(&adv).unwrap();
let vulns: Vec<sbom_vulnerability::Vulnerability> =
serde_json::from_value(doc["vulnerabilities"].clone()).unwrap();
let matches = sbom_vulnerability::match_vulnerabilities(&sbom, &vulns);
let propagated = sbom_vulnerability::propagate_risk(&sbom, &matches);
assert!(
propagated.iter().any(
|(bom_ref, vuln, _)| bom_ref == "pkg:cargo/affidavit@26.6.22"
&& vuln == "CVE-2023-0464"
),
"root application inherits the openssl CVE through the dependency edge"
);
}
#[test]
fn blast_radius_identifies_transitive_dependents() {
let sbom = cyclonedx();
let graph = sbom_supply_chain::DependencyGraph::from_sbom(&sbom);
let radius = sbom_supply_chain::blast_radius(&graph, "pkg:cargo/log@0.4.20").expect("radius");
assert!(
radius.transitively_impacted >= 2,
"log's compromise impacts multiple upstream components"
);
}
#[test]
fn supplier_concentration_is_reported() {
let sbom = cyclonedx();
let conc = sbom_supply_chain::supplier_concentration(&sbom);
assert!(!conc.is_empty());
assert!(
conc.iter()
.any(|c| c.supplier == "serde-rs" && c.component_count >= 2),
"serde-rs supplies multiple components"
);
}
#[test]
fn provenance_attestation_binds_address_and_receipt() {
let sbom = cyclonedx();
let attestation = sbom_supply_chain::attest_provenance(&sbom, Some("receipt://abc"));
assert!(!attestation.sbom_address.is_empty());
assert_eq!(
attestation.generated_from_receipt.as_deref(),
Some("receipt://abc")
);
assert!(attestation.dependency_edges > 0);
}
#[test]
fn content_address_is_format_stable_within_a_format() {
let a = cyclonedx().content_address();
let b = cyclonedx().content_address();
assert_eq!(a, b);
}