use crate::sbom::Sbom;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ComplianceError {
#[error("empty sbom: no components to certify")]
EmptySbom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Framework {
Ntia,
ExecutiveOrder14028,
Slsa,
InToto,
Cisa,
Cscrm,
Iso27001,
Soc2,
Vex,
}
impl Framework {
pub fn tag(&self) -> &'static str {
match self {
Framework::Ntia => "ntia",
Framework::ExecutiveOrder14028 => "eo-14028",
Framework::Slsa => "slsa",
Framework::InToto => "in-toto",
Framework::Cisa => "cisa",
Framework::Cscrm => "cscrm-800-161",
Framework::Iso27001 => "iso-27001",
Framework::Soc2 => "soc-2",
Framework::Vex => "vex",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Framework::Ntia => "NTIA Minimum Elements",
Framework::ExecutiveOrder14028 => "Executive Order 14028",
Framework::Slsa => "SLSA",
Framework::InToto => "in-toto",
Framework::Cisa => "CISA SBOM",
Framework::Cscrm => "C-SCRM (NIST SP 800-161)",
Framework::Iso27001 => "ISO/IEC 27001",
Framework::Soc2 => "SOC 2",
Framework::Vex => "VEX Readiness",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ComplianceResult {
pub framework: String,
pub level: Option<String>,
pub passed: bool,
pub satisfied: Vec<String>,
pub failed: Vec<String>,
pub notes: Vec<String>,
}
impl ComplianceResult {
fn new(framework: &Framework) -> Self {
ComplianceResult {
framework: framework.display_name().to_string(),
level: None,
passed: false,
satisfied: Vec::new(),
failed: Vec::new(),
notes: Vec::new(),
}
}
fn record(&mut self, name: impl Into<String>, ok: bool) {
if ok {
self.satisfied.push(name.into());
} else {
self.failed.push(name.into());
}
}
fn note(&mut self, note: impl Into<String>) {
self.notes.push(note.into());
}
pub fn score(&self) -> f64 {
let total = self.satisfied.len() + self.failed.len();
if total == 0 {
0.0
} else {
self.satisfied.len() as f64 / total as f64
}
}
pub fn requirement_count(&self) -> usize {
self.satisfied.len() + self.failed.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SlsaLevel {
L0,
L1,
L2,
L3,
L4,
}
impl SlsaLevel {
pub fn tag(&self) -> &'static str {
match self {
SlsaLevel::L0 => "L0",
SlsaLevel::L1 => "L1",
SlsaLevel::L2 => "L2",
SlsaLevel::L3 => "L3",
SlsaLevel::L4 => "L4",
}
}
pub fn rank(&self) -> u8 {
match self {
SlsaLevel::L0 => 0,
SlsaLevel::L1 => 1,
SlsaLevel::L2 => 2,
SlsaLevel::L3 => 3,
SlsaLevel::L4 => 4,
}
}
}
fn coverage<F>(sbom: &Sbom, pred: F) -> f64
where
F: Fn(&crate::sbom::Component) -> bool,
{
let total = sbom.components.len();
if total == 0 {
return 0.0;
}
let hits = sbom.components.iter().filter(|c| pred(c)).count();
hits as f64 / total as f64
}
fn all_components<F>(sbom: &Sbom, pred: F) -> bool
where
F: Fn(&crate::sbom::Component) -> bool,
{
!sbom.components.is_empty() && sbom.components.iter().all(|c| pred(c))
}
fn has_author(sbom: &Sbom) -> bool {
sbom.metadata
.author
.as_ref()
.is_some_and(|a| !a.trim().is_empty())
}
fn has_tools(sbom: &Sbom) -> bool {
!sbom.metadata.tools.is_empty()
}
fn has_timestamp(sbom: &Sbom) -> bool {
sbom.metadata.timestamp > 0
}
fn has_stable_id(c: &crate::sbom::Component) -> bool {
c.purl.as_ref().is_some_and(|p| !p.trim().is_empty())
|| c.cpe.as_ref().is_some_and(|p| !p.trim().is_empty())
}
fn ratio_note(label: &str, hits: usize, total: usize) -> String {
let pct = if total == 0 {
0.0
} else {
hits as f64 / total as f64 * 100.0
};
format!("{label}: {hits}/{total} ({pct:.1}%)")
}
pub fn check_ntia(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let ntia = sbom.ntia_minimum_elements();
let mut result = ComplianceResult::new(&Framework::Ntia);
result.record("supplier_name", ntia.supplier_name);
result.record("component_name", ntia.component_name);
result.record("version", ntia.version);
result.record("unique_identifiers", ntia.unique_identifiers);
result.record("dependency_relationship", ntia.dependency_relationship);
result.record("author", ntia.author);
result.record("timestamp", ntia.timestamp);
result.passed = ntia.is_conformant();
if !result.passed {
result.note(format!("missing elements: {:?}", ntia.missing()));
}
result.note("certifies presence of NTIA fields; does not decide truthfulness");
Ok(result)
}
pub fn check_eo_14028(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let ntia = sbom.ntia_minimum_elements();
let mut result = ComplianceResult::new(&Framework::ExecutiveOrder14028);
let ntia_ok = ntia.is_conformant();
result.record("ntia_minimum_elements", ntia_ok);
if !ntia_ok {
result.note(format!("NTIA gaps: {:?}", ntia.missing()));
}
let tooling = has_tools(sbom);
result.record("sbom_tooling_provenance", tooling);
let author = has_author(sbom);
result.record("document_author", author);
let timestamp = has_timestamp(sbom);
result.record("document_timestamp", timestamp);
let primary = sbom
.metadata
.primary_component
.as_ref()
.is_some_and(|p| !p.trim().is_empty());
result.record("primary_component_identified", primary);
result.passed = ntia_ok && tooling && author && timestamp && primary;
result.note("EO 14028: NTIA baseline plus SBOM tooling provenance and data completeness");
Ok(result)
}
pub fn assess_slsa(sbom: &Sbom) -> Result<(SlsaLevel, ComplianceResult), ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let mut result = ComplianceResult::new(&Framework::Slsa);
let l1 = has_tools(sbom);
result.record("L1_provenance_exists", l1);
let has_serial = sbom
.serial_number
.as_ref()
.is_some_and(|s| !s.trim().is_empty());
let all_hashed = all_components(sbom, |c| !c.hashes.is_empty());
let l2 = l1 && has_serial && all_hashed;
result.record("L2_authenticated_document", has_serial);
result.record("L2_all_components_hashed", all_hashed);
let all_supplied = all_components(sbom, |c| {
c.supplier
.as_ref()
.is_some_and(|s| !s.name.trim().is_empty())
});
let l3 = l2 && all_supplied;
result.record("L3_supplier_on_every_component", all_supplied);
let has_deps = !sbom.dependencies.is_empty();
let l4 = l3 && has_deps;
result.record("L4_dependency_relationships", has_deps);
let level = if l4 {
SlsaLevel::L4
} else if l3 {
SlsaLevel::L3
} else if l2 {
SlsaLevel::L2
} else if l1 {
SlsaLevel::L1
} else {
SlsaLevel::L0
};
result.level = Some(level.tag().to_string());
result.passed = level.rank() >= SlsaLevel::L1.rank();
result.note(format!("achieved SLSA {}", level.tag()));
result
.note("structural proxy: SBOM evidence is necessary but not sufficient for SLSA controls");
Ok((level, result))
}
pub fn check_slsa(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
assess_slsa(sbom).map(|(_, result)| result)
}
pub fn check_in_toto(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let mut result = ComplianceResult::new(&Framework::InToto);
let functionaries = has_tools(sbom);
result.record("functionaries_present", functionaries);
let products = sbom
.metadata
.primary_component
.as_ref()
.is_some_and(|p| !p.trim().is_empty());
result.record("products_primary_component", products);
let linkage = !sbom.dependencies.is_empty();
result.record("materials_products_linkage", linkage);
result.passed = functionaries && products && linkage;
result.note("structural proxy: SBOM fields stand in for in-toto layout/link metadata");
Ok(result)
}
pub fn check_cisa(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let mut result = ComplianceResult::new(&Framework::Cisa);
let ntia_ok = sbom.ntia_minimum_elements().is_conformant();
result.record("minimum_ntia_elements", ntia_ok);
let total = sbom.components.len();
let id_hits = sbom.components.iter().filter(|c| has_stable_id(c)).count();
let lic_hits = sbom
.components
.iter()
.filter(|c| !c.licenses.is_empty())
.count();
let id_ratio = coverage(sbom, has_stable_id);
let lic_ratio = coverage(sbom, |c| !c.licenses.is_empty());
const RECOMMENDED_THRESHOLD: f64 = 0.80;
let id_ok = id_ratio >= RECOMMENDED_THRESHOLD;
let lic_ok = lic_ratio >= RECOMMENDED_THRESHOLD;
result.record("recommended_identifier_coverage", id_ok);
result.record("recommended_license_coverage", lic_ok);
result.note(ratio_note("identifier (purl/cpe) coverage", id_hits, total));
result.note(ratio_note("license coverage", lic_hits, total));
result.note(format!(
"recommended threshold: {:.0}%",
RECOMMENDED_THRESHOLD * 100.0
));
result.passed = ntia_ok;
Ok(result)
}
pub fn check_cscrm(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let mut result = ComplianceResult::new(&Framework::Cscrm);
let total = sbom.components.len();
let supplied = |c: &crate::sbom::Component| {
c.supplier
.as_ref()
.is_some_and(|s| !s.name.trim().is_empty())
};
let sup_hits = sbom.components.iter().filter(|c| supplied(c)).count();
let sup_ratio = coverage(sbom, supplied);
const SUPPLIER_THRESHOLD: f64 = 0.90;
let coverage_ok = sup_ratio >= SUPPLIER_THRESHOLD;
result.record("supplier_coverage", coverage_ok);
let primary_supplier = match sbom.metadata.primary_component.as_ref() {
Some(pref) if !pref.trim().is_empty() => {
sbom.component(pref).map(supplied).unwrap_or(false)
|| sbom
.metadata
.supplier
.as_ref()
.is_some_and(|s| !s.name.trim().is_empty())
}
_ => false,
};
result.record("primary_component_supplier", primary_supplier);
result.note(ratio_note("supplier coverage", sup_hits, total));
result.note(format!(
"supplier threshold: {:.0}%",
SUPPLIER_THRESHOLD * 100.0
));
result.passed = coverage_ok && primary_supplier;
Ok(result)
}
const INTEGRITY_THRESHOLD: f64 = 0.50;
pub fn check_iso_27001(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
evidence_presence_gate(sbom, &Framework::Iso27001)
}
pub fn check_soc2(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
evidence_presence_gate(sbom, &Framework::Soc2)
}
fn evidence_presence_gate(
sbom: &Sbom,
framework: &Framework,
) -> Result<ComplianceResult, ComplianceError> {
let mut result = ComplianceResult::new(framework);
let author = has_author(sbom);
result.record("author_accountability", author);
let timestamp = has_timestamp(sbom);
result.record("timestamped_record", timestamp);
let total = sbom.components.len();
let hashed = sbom
.components
.iter()
.filter(|c| !c.hashes.is_empty())
.count();
let integrity_ratio = coverage(sbom, |c| !c.hashes.is_empty());
let integrity_ok = integrity_ratio >= INTEGRITY_THRESHOLD;
result.record("integrity_hash_evidence", integrity_ok);
result.note(ratio_note("integrity-hash coverage", hashed, total));
result.note(format!(
"integrity threshold: {:.0}%",
INTEGRITY_THRESHOLD * 100.0
));
result.note("structural proxy: evidence-presence gate, not a full controls audit");
result.passed = author && timestamp && integrity_ok;
Ok(result)
}
const VEX_THRESHOLD: f64 = 0.80;
pub fn vex_readiness(sbom: &Sbom) -> Result<ComplianceResult, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
let mut result = ComplianceResult::new(&Framework::Vex);
let total = sbom.components.len();
let purl_hits = sbom
.components
.iter()
.filter(|c| c.purl.as_ref().is_some_and(|p| !p.trim().is_empty()))
.count();
let cpe_hits = sbom
.components
.iter()
.filter(|c| c.cpe.as_ref().is_some_and(|p| !p.trim().is_empty()))
.count();
let anchor_hits = sbom.components.iter().filter(|c| has_stable_id(c)).count();
let anchor_ratio = coverage(sbom, has_stable_id);
let anchor_ok = anchor_ratio >= VEX_THRESHOLD;
result.record("stable_identifier_coverage", anchor_ok);
result.note(ratio_note("purl coverage", purl_hits, total));
result.note(ratio_note("cpe coverage", cpe_hits, total));
result.note(ratio_note("vex-anchorable coverage", anchor_hits, total));
result.note(format!(
"readiness threshold: {:.0}%",
VEX_THRESHOLD * 100.0
));
result.passed = anchor_ok;
Ok(result)
}
pub fn assess_all(sbom: &Sbom) -> Result<Vec<ComplianceResult>, ComplianceError> {
if sbom.components.is_empty() {
return Err(ComplianceError::EmptySbom);
}
Ok(vec![
check_ntia(sbom)?,
check_eo_14028(sbom)?,
check_slsa(sbom)?,
check_in_toto(sbom)?,
check_cisa(sbom)?,
check_cscrm(sbom)?,
check_iso_27001(sbom)?,
check_soc2(sbom)?,
vex_readiness(sbom)?,
])
}
pub fn supported_frameworks() -> &'static [&'static str] {
&[
"ntia",
"eo-14028",
"slsa",
"in-toto",
"cisa",
"cscrm-800-161",
"iso-27001",
"soc-2",
"vex",
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sbom::{Component, Dependency, License, Sbom, SbomFormat, Supplier, Tool};
fn component(bom_ref: &str, name: &str, version: &str) -> Component {
Component::library(bom_ref, name, version)
}
fn with_supplier(mut c: Component, name: &str) -> Component {
c.supplier = Some(Supplier {
name: name.to_string(),
url: None,
contact: None,
});
c
}
fn with_purl(mut c: Component, purl: &str) -> Component {
c.purl = Some(purl.to_string());
c
}
fn with_hash(mut c: Component, value: &str) -> Component {
c.hashes.push(crate::sbom::Hash::new("SHA-256", value));
c
}
fn with_license(mut c: Component, id: &str) -> Component {
c.licenses.push(License::id(id));
c
}
fn full_sbom() -> Sbom {
let mut sbom = Sbom::new(SbomFormat::CycloneDx16, "1.6");
sbom.serial_number = Some("urn:uuid:abcd".to_string());
sbom.metadata.author = Some("Build Bot".to_string());
sbom.metadata.timestamp = 1_705_314_600;
sbom.metadata.tools.push(Tool {
vendor: Some("acme".to_string()),
name: "cdxgen".to_string(),
version: Some("9.0".to_string()),
});
sbom.metadata.primary_component = Some("app@1.0".to_string());
sbom.metadata.supplier = Some(Supplier {
name: "Acme".to_string(),
url: None,
contact: None,
});
let a = with_license(
with_hash(
with_purl(
with_supplier(component("a", "alpha", "1.0"), "Acme"),
"pkg:cargo/alpha@1.0",
),
"aaaa",
),
"MIT",
);
let b = with_license(
with_hash(
with_purl(
with_supplier(component("b", "beta", "2.0"), "Beta Inc"),
"pkg:cargo/beta@2.0",
),
"bbbb",
),
"Apache-2.0",
);
sbom.components.push(a);
sbom.components.push(b);
sbom.dependencies.push(Dependency {
dependent: "a".to_string(),
depends_on: vec!["b".to_string()],
});
sbom
}
fn slsa_l2_sbom() -> Sbom {
let mut sbom = Sbom::new(SbomFormat::CycloneDx16, "1.6");
sbom.serial_number = Some("urn:uuid:l2".to_string());
sbom.metadata.tools.push(Tool {
vendor: None,
name: "gen".to_string(),
version: None,
});
let a = with_hash(with_supplier(component("a", "a", "1"), "Acme"), "aa");
let b = with_hash(component("b", "b", "1"), "bb"); sbom.components.push(a);
sbom.components.push(b);
sbom
}
#[test]
fn ntia_passes_on_full_sbom() {
let sbom = full_sbom();
let result = check_ntia(&sbom).unwrap();
assert!(result.passed, "failed: {:?}", result.failed);
assert_eq!(result.requirement_count(), 7);
assert!(result.failed.is_empty());
}
#[test]
fn ntia_fails_when_versions_missing() {
let mut sbom = full_sbom();
sbom.components[0].version = String::new();
let result = check_ntia(&sbom).unwrap();
assert!(!result.passed);
assert!(result.failed.contains(&"version".to_string()));
}
#[test]
fn ntia_errors_on_empty_sbom() {
let sbom = Sbom::new(SbomFormat::CycloneDx16, "1.6");
assert_eq!(check_ntia(&sbom), Err(ComplianceError::EmptySbom));
}
#[test]
fn eo_14028_passes_on_full_sbom() {
let sbom = full_sbom();
let result = check_eo_14028(&sbom).unwrap();
assert!(result.passed, "failed: {:?}", result.failed);
assert!(result
.satisfied
.contains(&"sbom_tooling_provenance".to_string()));
}
#[test]
fn eo_14028_fails_without_tools() {
let mut sbom = full_sbom();
sbom.metadata.tools.clear();
let result = check_eo_14028(&sbom).unwrap();
assert!(!result.passed);
assert!(result
.failed
.contains(&"sbom_tooling_provenance".to_string()));
}
#[test]
fn slsa_reaches_l4_on_full_sbom() {
let sbom = full_sbom();
let (level, result) = assess_slsa(&sbom).unwrap();
assert_eq!(level, SlsaLevel::L4);
assert_eq!(result.level.as_deref(), Some("L4"));
assert!(result.passed);
}
#[test]
fn slsa_l2_sbom_is_exactly_l2_not_l3() {
let sbom = slsa_l2_sbom();
let (level, _) = assess_slsa(&sbom).unwrap();
assert_eq!(level, SlsaLevel::L2);
assert!(level.rank() < SlsaLevel::L3.rank());
}
#[test]
fn slsa_drops_to_l1_without_serial() {
let mut sbom = slsa_l2_sbom();
sbom.serial_number = None; let (level, _) = assess_slsa(&sbom).unwrap();
assert_eq!(level, SlsaLevel::L1);
}
#[test]
fn slsa_is_l0_without_tools() {
let mut sbom = slsa_l2_sbom();
sbom.metadata.tools.clear();
let (level, result) = assess_slsa(&sbom).unwrap();
assert_eq!(level, SlsaLevel::L0);
assert!(!result.passed, "L0 must not pass the SLSA gate");
}
#[test]
fn slsa_l3_without_deps_is_not_l4() {
let mut sbom = full_sbom();
sbom.dependencies.clear();
let (level, _) = assess_slsa(&sbom).unwrap();
assert_eq!(level, SlsaLevel::L3);
}
#[test]
fn in_toto_passes_with_tools_primary_and_deps() {
let sbom = full_sbom();
let result = check_in_toto(&sbom).unwrap();
assert!(result.passed, "failed: {:?}", result.failed);
}
#[test]
fn in_toto_fails_without_primary_component() {
let mut sbom = full_sbom();
sbom.metadata.primary_component = None;
let result = check_in_toto(&sbom).unwrap();
assert!(!result.passed);
assert!(result
.failed
.contains(&"products_primary_component".to_string()));
}
#[test]
fn cisa_reports_coverage_ratios() {
let mut sbom = full_sbom();
sbom.components[1].purl = None;
sbom.components[1].cpe = None;
let result = check_cisa(&sbom).unwrap();
assert!(result
.notes
.iter()
.any(|n| n.contains("identifier (purl/cpe) coverage: 1/2")));
assert!(result
.failed
.contains(&"recommended_identifier_coverage".to_string()));
}
#[test]
fn cisa_recommended_passes_at_full_coverage() {
let sbom = full_sbom();
let result = check_cisa(&sbom).unwrap();
assert!(result
.satisfied
.contains(&"recommended_identifier_coverage".to_string()));
assert!(result
.satisfied
.contains(&"recommended_license_coverage".to_string()));
assert!(result.passed);
}
#[test]
fn cscrm_passes_with_full_supplier_coverage() {
let sbom = full_sbom();
let result = check_cscrm(&sbom).unwrap();
assert!(result.passed, "failed: {:?}", result.failed);
assert!(result.satisfied.contains(&"supplier_coverage".to_string()));
}
#[test]
fn cscrm_fails_below_supplier_threshold() {
let mut sbom = full_sbom();
sbom.components[1].supplier = None;
let result = check_cscrm(&sbom).unwrap();
assert!(!result.passed);
assert!(result.failed.contains(&"supplier_coverage".to_string()));
}
#[test]
fn iso_and_soc2_pass_on_full_sbom() {
let sbom = full_sbom();
let iso = check_iso_27001(&sbom).unwrap();
let soc = check_soc2(&sbom).unwrap();
assert!(iso.passed, "iso failed: {:?}", iso.failed);
assert!(soc.passed, "soc failed: {:?}", soc.failed);
assert_eq!(iso.framework, "ISO/IEC 27001");
assert_eq!(soc.framework, "SOC 2");
}
#[test]
fn iso_fails_without_author() {
let mut sbom = full_sbom();
sbom.metadata.author = None;
let result = check_iso_27001(&sbom).unwrap();
assert!(!result.passed);
assert!(result.failed.contains(&"author_accountability".to_string()));
}
#[test]
fn vex_readiness_passes_at_full_identifier_coverage() {
let sbom = full_sbom();
let result = vex_readiness(&sbom).unwrap();
assert!(result.passed);
assert!(result
.satisfied
.contains(&"stable_identifier_coverage".to_string()));
}
#[test]
fn vex_readiness_fails_below_threshold() {
let mut sbom = full_sbom();
sbom.components[1].purl = None;
sbom.components[1].cpe = None;
let result = vex_readiness(&sbom).unwrap();
assert!(!result.passed);
assert!(result
.notes
.iter()
.any(|n| n.contains("vex-anchorable coverage: 1/2")));
}
#[test]
fn score_is_satisfied_fraction() {
let mut r = ComplianceResult::new(&Framework::Ntia);
r.record("a", true);
r.record("b", true);
r.record("c", false);
r.record("d", false);
assert!((r.score() - 0.5).abs() < 1e-9);
}
#[test]
fn score_is_zero_for_no_requirements() {
let r = ComplianceResult::new(&Framework::Ntia);
assert_eq!(r.score(), 0.0);
}
#[test]
fn assess_all_runs_every_framework_in_order() {
let sbom = full_sbom();
let results = assess_all(&sbom).unwrap();
assert_eq!(results.len(), supported_frameworks().len());
assert_eq!(results.len(), 9);
let names: Vec<&str> = results.iter().map(|r| r.framework.as_str()).collect();
assert_eq!(names[0], Framework::Ntia.display_name());
assert_eq!(names[1], Framework::ExecutiveOrder14028.display_name());
assert_eq!(names[2], Framework::Slsa.display_name());
assert_eq!(names[8], Framework::Vex.display_name());
}
#[test]
fn assess_all_errors_on_empty_sbom() {
let sbom = Sbom::new(SbomFormat::CycloneDx16, "1.6");
assert_eq!(assess_all(&sbom), Err(ComplianceError::EmptySbom));
}
#[test]
fn assess_all_is_deterministic() {
let sbom = full_sbom();
let a = assess_all(&sbom).unwrap();
let b = assess_all(&sbom).unwrap();
assert_eq!(a, b);
}
#[test]
fn supported_frameworks_match_enum_tags() {
let tags = supported_frameworks();
assert_eq!(tags[0], Framework::Ntia.tag());
assert_eq!(tags[2], Framework::Slsa.tag());
assert_eq!(tags[8], Framework::Vex.tag());
assert_eq!(tags.len(), 9);
}
#[test]
fn framework_tags_are_stable_and_distinct() {
let all = [
Framework::Ntia,
Framework::ExecutiveOrder14028,
Framework::Slsa,
Framework::InToto,
Framework::Cisa,
Framework::Cscrm,
Framework::Iso27001,
Framework::Soc2,
Framework::Vex,
];
let mut tags: Vec<&str> = all.iter().map(|f| f.tag()).collect();
let count = tags.len();
tags.sort_unstable();
tags.dedup();
assert_eq!(tags.len(), count, "framework tags must be distinct");
}
}