use crate::model::{NormalizedSbom, SbomFormat};
use serde::{Deserialize, Serialize};
mod bsi;
mod bsi_sbom_for_ai;
mod context;
mod cra;
mod crypto;
mod eo14028;
mod eu_ai_act;
mod eucc;
mod generic;
mod registry;
mod shared;
mod ssdf;
use context::{ComplianceContext, checker_for};
use registry::REMEDIATION_GENERIC;
pub use registry::{RuleMeta, rule_meta};
use shared::{is_valid_email_format, truncate_list};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CraPhase {
Phase1,
Phase2,
}
impl CraPhase {
pub const fn name(self) -> &'static str {
match self {
Self::Phase1 => "Phase 1 (2027)",
Self::Phase2 => "Phase 2 (2029)",
}
}
pub const fn deadline(self) -> &'static str {
match self {
Self::Phase1 => "11 December 2027",
Self::Phase2 => "11 December 2029",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ComplianceLevel {
Minimum,
Standard,
NtiaMinimum,
CraPhase1,
CraPhase2,
FdaMedicalDevice,
NistSsdf,
Eo14028,
Cnsa2,
NistPqc,
BsiTr03183_2,
CraOssSteward,
EuccSubstantial,
EuAiAct,
BsiSbomForAi,
Comprehensive,
}
impl ComplianceLevel {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Minimum => "Minimum",
Self::Standard => "Standard",
Self::NtiaMinimum => "NTIA Minimum Elements",
Self::CraPhase1 => "EU CRA Phase 1 (2027)",
Self::CraPhase2 => "EU CRA Phase 2 (2029)",
Self::FdaMedicalDevice => "FDA Medical Device",
Self::NistSsdf => "NIST SSDF (SP 800-218)",
Self::Eo14028 => "EO 14028 Section 4",
Self::Cnsa2 => "CNSA 2.0",
Self::NistPqc => "NIST PQC Readiness",
Self::BsiTr03183_2 => "BSI TR-03183-2",
Self::CraOssSteward => "CRA OSS Steward (Art. 24)",
Self::EuccSubstantial => "EUCC Substantial (Reg. 2024/482)",
Self::EuAiAct => "EU AI Act Annex IV Readiness",
Self::BsiSbomForAi => "BSI/G7 SBOM-for-AI Minimum Elements Readiness",
Self::Comprehensive => "Comprehensive",
}
}
#[must_use]
pub const fn short_name(&self) -> &'static str {
match self {
Self::Minimum => "Min",
Self::Standard => "Std",
Self::NtiaMinimum => "NTIA",
Self::CraPhase1 => "CRA-1",
Self::CraPhase2 => "CRA-2",
Self::FdaMedicalDevice => "FDA",
Self::NistSsdf => "SSDF",
Self::Eo14028 => "EO14028",
Self::Cnsa2 => "CNSA2",
Self::NistPqc => "PQC",
Self::BsiTr03183_2 => "BSI",
Self::CraOssSteward => "OSS",
Self::EuccSubstantial => "EUCC",
Self::EuAiAct => "AI-Act",
Self::BsiSbomForAi => "BSI-AI",
Self::Comprehensive => "Full",
}
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self {
Self::Minimum => "Basic component identification only",
Self::Standard => "Recommended fields for general use",
Self::NtiaMinimum => "NTIA minimum elements for software transparency",
Self::CraPhase1 => {
"CRA reporting obligations โ product ID, SBOM format, manufacturer (deadline: 11 Dec 2027)"
}
Self::CraPhase2 => {
"Full CRA compliance โ adds vulnerability metadata, lifecycle, disclosure (deadline: 11 Dec 2029)"
}
Self::FdaMedicalDevice => "FDA premarket submission requirements for medical devices",
Self::NistSsdf => {
"Secure Software Development Framework โ provenance, build integrity, VCS references"
}
Self::Eo14028 => {
"Executive Order 14028 โ machine-readable SBOM, auto-generation, supply chain security"
}
Self::Cnsa2 => {
"CNSA 2.0 โ AES-256, SHA-384+, ML-KEM-1024, ML-DSA-87, quantum security level 5"
}
Self::NistPqc => {
"NIST PQC โ quantum-vulnerable algorithm detection, FIPS 203/204/205, SP 800-131A"
}
Self::BsiTr03183_2 => {
"BSI TR-03183-2 โ German national SBOM guideline (free, ENISA-cited): mandatory hashes, identifiers, ISO-8601 timestamps"
}
Self::CraOssSteward => {
"CRA Article 24 โ Open-source software steward (lighter than full manufacturer obligations): SBOM + CVD policy + vuln-handling required, no DoC/module/manufacturer-email enforcement"
}
Self::EuccSubstantial => {
"EUCC Substantial (Reg. (EU) 2024/482) โ reference-only check for Common-Criteria Protection Profile, Target of Evaluation, ITSEF, and certificate valid-until date"
}
Self::EuAiAct => {
"EU AI Act (Reg. (EU) 2024/1689) Annex IV technical-documentation READINESS โ model description, training-data characteristics, validation/testing metrics, limitations (readiness only, not a legal-conformity guarantee; N/A for non-AI SBOMs)"
}
Self::BsiSbomForAi => {
"BSI/G7 SBOM-for-AI Minimum Elements (Feb 2026) READINESS โ scores an AI-BOM element-by-element across the Metadata, System-Level, Models, Datasets, Infrastructure, and Security clusters (readiness only, not a legal-conformity guarantee; N/A for non-AI SBOMs)"
}
Self::Comprehensive => "All recommended fields and best practices",
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::Minimum,
Self::Standard,
Self::NtiaMinimum,
Self::CraPhase1,
Self::CraPhase2,
Self::FdaMedicalDevice,
Self::NistSsdf,
Self::Eo14028,
Self::Cnsa2,
Self::NistPqc,
Self::BsiTr03183_2,
Self::CraOssSteward,
Self::EuccSubstantial,
Self::EuAiAct,
Self::BsiSbomForAi,
Self::Comprehensive,
]
}
#[must_use]
pub const fn is_cra(&self) -> bool {
matches!(
self,
Self::CraPhase1 | Self::CraPhase2 | Self::CraOssSteward
)
}
#[must_use]
pub const fn cra_phase(&self) -> Option<CraPhase> {
match self {
Self::CraPhase1 => Some(CraPhase::Phase1),
Self::CraPhase2 => Some(CraPhase::Phase2),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StandardKind {
CraArticle,
CraAnnex,
Pren40000_1_3,
BsiTr03183_2,
NistSsdf,
Eo14028,
FdaPremarket,
NtiaMinimum,
Csaf2,
Cnsa2,
NistPqc,
EuAiAct,
BsiSbomForAi,
Other,
}
impl StandardKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::CraArticle => "CRA Article",
Self::CraAnnex => "CRA Annex",
Self::Pren40000_1_3 => "prEN 40000-1-3",
Self::BsiTr03183_2 => "BSI TR-03183-2",
Self::NistSsdf => "NIST SSDF",
Self::Eo14028 => "EO 14028",
Self::FdaPremarket => "FDA",
Self::NtiaMinimum => "NTIA",
Self::Csaf2 => "CSAF v2.0",
Self::Cnsa2 => "CNSA 2.0",
Self::NistPqc => "NIST PQC",
Self::EuAiAct => "EU AI Act",
Self::BsiSbomForAi => "BSI/G7 AI-SBOM",
Self::Other => "Other",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StandardRef {
pub standard: StandardKind,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub help_uri: Option<String>,
}
impl StandardRef {
#[must_use]
pub fn new(standard: StandardKind, id: impl Into<String>) -> Self {
let id = id.into();
let help_uri = standard.canonical_help_uri(&id);
Self {
standard,
id,
help_uri,
}
}
#[must_use]
pub fn with_uri(mut self, uri: impl Into<String>) -> Self {
self.help_uri = Some(uri.into());
self
}
}
impl StandardKind {
#[must_use]
pub fn canonical_help_uri(self, _id: &str) -> Option<String> {
let url = match self {
Self::CraArticle | Self::CraAnnex => {
"https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng"
}
Self::Pren40000_1_3 => return None,
Self::BsiTr03183_2 => {
"https://www.bsi.bund.de/EN/Themen/Unternehmen-und-Organisationen/Standards-und-Zertifizierung/Technische-Richtlinien/TR-nach-Thema-sortiert/tr03183/TR-03183_node.html"
}
Self::NistSsdf => "https://doi.org/10.6028/NIST.SP.800-218",
Self::Eo14028 => "https://www.federalregister.gov/d/2021-10460",
Self::FdaPremarket => {
"https://www.fda.gov/regulatory-information/search-fda-guidance-documents/cybersecurity-medical-devices-quality-system-considerations-and-content-premarket-submissions"
}
Self::NtiaMinimum => {
"https://www.ntia.doc.gov/files/ntia/publications/sbom_minimum_elements_report.pdf"
}
Self::Csaf2 => "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html",
Self::Cnsa2 => {
"https://media.defense.gov/2022/Sep/07/2003071834/-1/-1/0/CSA_CNSA_2.0_ALGORITHMS_.PDF"
}
Self::NistPqc => "https://csrc.nist.gov/projects/post-quantum-cryptography",
Self::EuAiAct => "https://eur-lex.europa.eu/eli/reg/2024/1689/oj/eng",
Self::BsiSbomForAi => "https://www.bsi.bund.de",
Self::Other => return None,
};
Some(url.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub severity: ViolationSeverity,
pub category: ViolationCategory,
pub message: String,
pub element: Option<String>,
pub requirement: String,
#[serde(skip, default = "default_rule_id")]
pub rule_id: &'static str,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub standard_refs: Vec<StandardRef>,
}
fn default_rule_id() -> &'static str {
"SBOM-CRA-GENERAL"
}
impl Violation {
#[must_use]
pub fn registry_standard_refs(&self) -> Vec<StandardRef> {
rule_meta(self.rule_id)
.map(|m| {
m.refs
.iter()
.map(|(kind, id)| StandardRef::new(*kind, *id))
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn remediation_guidance(&self) -> &'static str {
rule_meta(self.rule_id).map_or(REMEDIATION_GENERIC, |m| m.remediation)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViolationSeverity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ViolationCategory {
DocumentMetadata,
ComponentIdentification,
DependencyInfo,
LicenseInfo,
SupplierInfo,
IntegrityInfo,
SecurityInfo,
FormatSpecific,
CryptographyInfo,
}
impl ViolationCategory {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::DocumentMetadata => "Document Metadata",
Self::ComponentIdentification => "Component Identification",
Self::DependencyInfo => "Dependency Information",
Self::LicenseInfo => "License Information",
Self::SupplierInfo => "Supplier Information",
Self::IntegrityInfo => "Integrity Information",
Self::SecurityInfo => "Security Information",
Self::FormatSpecific => "Format-Specific",
Self::CryptographyInfo => "Cryptography",
}
}
#[must_use]
pub const fn short_name(&self) -> &'static str {
match self {
Self::DocumentMetadata => "Doc Meta",
Self::ComponentIdentification => "Comp IDs",
Self::DependencyInfo => "Deps",
Self::LicenseInfo => "License",
Self::SupplierInfo => "Supplier",
Self::IntegrityInfo => "Integrity",
Self::SecurityInfo => "Security",
Self::FormatSpecific => "Format",
Self::CryptographyInfo => "Crypto",
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::SupplierInfo,
Self::ComponentIdentification,
Self::DocumentMetadata,
Self::IntegrityInfo,
Self::LicenseInfo,
Self::DependencyInfo,
Self::SecurityInfo,
Self::FormatSpecific,
Self::CryptographyInfo,
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceResult {
pub is_compliant: bool,
pub level: ComplianceLevel,
pub violations: Vec<Violation>,
pub error_count: usize,
pub warning_count: usize,
pub info_count: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conformity_summary: Option<ConformityAssessmentSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformityAssessmentSummary {
pub product_class: crate::model::CraProductClass,
pub route: crate::model::ConformityRoute,
pub evidence: Vec<ConformityEvidence>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformityEvidence {
pub label: String,
pub detail: String,
pub satisfied: bool,
}
impl ComplianceResult {
#[must_use]
pub fn new(level: ComplianceLevel, violations: Vec<Violation>) -> Self {
let error_count = violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Error)
.count();
let warning_count = violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Warning)
.count();
let info_count = violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Info)
.count();
Self {
is_compliant: error_count == 0,
level,
violations,
conformity_summary: None,
error_count,
warning_count,
info_count,
}
}
#[must_use]
pub fn violations_by_severity(&self, severity: ViolationSeverity) -> Vec<&Violation> {
self.violations
.iter()
.filter(|v| v.severity == severity)
.collect()
}
#[must_use]
pub fn violations_by_category(&self, category: ViolationCategory) -> Vec<&Violation> {
self.violations
.iter()
.filter(|v| v.category == category)
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClassCheck {
VendorHashCoverage,
EolComponents,
Cycles,
DocReference,
EuccReference,
Psirt,
ModuleAttestation,
}
#[derive(Debug, Clone)]
pub struct ComplianceChecker {
level: ComplianceLevel,
sidecar: Option<crate::model::CraSidecarMetadata>,
product_class: Option<crate::model::CraProductClass>,
}
impl ComplianceChecker {
#[must_use]
pub const fn new(level: ComplianceLevel) -> Self {
Self {
level,
sidecar: None,
product_class: None,
}
}
#[must_use]
pub fn with_sidecar(mut self, sidecar: crate::model::CraSidecarMetadata) -> Self {
self.sidecar = Some(sidecar);
self
}
#[must_use]
pub const fn with_product_class(mut self, class: crate::model::CraProductClass) -> Self {
self.product_class = Some(class);
self
}
#[must_use]
pub fn effective_product_class(&self) -> crate::model::CraProductClass {
self.sidecar
.as_ref()
.and_then(|s| s.product_class)
.or(self.product_class)
.unwrap_or(crate::model::CraProductClass::Default)
}
#[must_use]
pub fn effective_route(&self) -> crate::model::ConformityRoute {
self.sidecar
.as_ref()
.and_then(|s| s.conformity_assessment_route)
.unwrap_or_else(|| self.effective_product_class().default_route())
}
#[must_use]
pub fn class_severity(&self, check: ClassCheck) -> Option<ViolationSeverity> {
use crate::model::CraProductClass as C;
let class = self.effective_product_class();
match (check, class) {
(ClassCheck::VendorHashCoverage, C::Default | C::ImportantClass1) => {
Some(ViolationSeverity::Warning)
}
(ClassCheck::VendorHashCoverage, C::ImportantClass2 | C::Critical) => {
Some(ViolationSeverity::Error)
}
(ClassCheck::EolComponents, C::Default | C::ImportantClass1) => {
Some(ViolationSeverity::Warning)
}
(ClassCheck::EolComponents, C::ImportantClass2 | C::Critical) => {
Some(ViolationSeverity::Error)
}
(ClassCheck::Cycles, C::Default | C::ImportantClass1) => {
Some(ViolationSeverity::Warning)
}
(ClassCheck::Cycles, C::ImportantClass2 | C::Critical) => {
Some(ViolationSeverity::Error)
}
(ClassCheck::DocReference, C::Default) => Some(ViolationSeverity::Info),
(ClassCheck::DocReference, C::ImportantClass1) => Some(ViolationSeverity::Warning),
(ClassCheck::DocReference, C::ImportantClass2 | C::Critical) => {
Some(ViolationSeverity::Error)
}
(ClassCheck::EuccReference, C::Default | C::ImportantClass1) => None,
(ClassCheck::EuccReference, C::ImportantClass2) => Some(ViolationSeverity::Info),
(ClassCheck::EuccReference, C::Critical) => Some(ViolationSeverity::Error),
(ClassCheck::Psirt, C::Default | C::ImportantClass1) => {
Some(ViolationSeverity::Warning)
}
(ClassCheck::Psirt, C::ImportantClass2 | C::Critical) => Some(ViolationSeverity::Error),
(ClassCheck::ModuleAttestation, C::Default) => None,
(ClassCheck::ModuleAttestation, C::ImportantClass1) => Some(ViolationSeverity::Warning),
(ClassCheck::ModuleAttestation, C::ImportantClass2 | C::Critical) => {
Some(ViolationSeverity::Error)
}
}
}
#[must_use]
pub fn vendor_hash_threshold(&self) -> f64 {
use crate::model::CraProductClass as C;
match self.effective_product_class() {
C::Default => 0.50,
C::ImportantClass1 | C::ImportantClass2 => 0.80,
C::Critical => 1.00,
}
}
#[must_use]
pub fn has_explicit_product_class(&self) -> bool {
self.product_class.is_some()
|| self
.sidecar
.as_ref()
.and_then(|s| s.product_class)
.is_some()
}
#[must_use]
pub fn check(&self, sbom: &NormalizedSbom) -> ComplianceResult {
let ctx = ComplianceContext::new(self, sbom);
let checker = checker_for(self.level);
debug_assert_eq!(
checker.level(),
self.level,
"dispatched checker must match the configured level"
);
let mut violations = checker.check(&ctx);
for v in &mut violations {
if v.standard_refs.is_empty() {
v.standard_refs = v.registry_standard_refs();
}
}
let mut result = ComplianceResult::new(self.level, violations);
if self.level.is_cra() && self.has_explicit_product_class() {
result.conformity_summary = Some(self.build_conformity_summary(sbom));
}
result
}
}
impl Default for ComplianceChecker {
fn default() -> Self {
Self::new(ComplianceLevel::Standard)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compliance_level_names() {
assert_eq!(ComplianceLevel::Minimum.name(), "Minimum");
assert_eq!(ComplianceLevel::NtiaMinimum.name(), "NTIA Minimum Elements");
assert_eq!(ComplianceLevel::CraPhase1.name(), "EU CRA Phase 1 (2027)");
assert_eq!(ComplianceLevel::CraPhase2.name(), "EU CRA Phase 2 (2029)");
assert_eq!(ComplianceLevel::NistSsdf.name(), "NIST SSDF (SP 800-218)");
assert_eq!(ComplianceLevel::Eo14028.name(), "EO 14028 Section 4");
}
#[test]
fn test_nist_ssdf_empty_sbom() {
let sbom = NormalizedSbom::default();
let checker = ComplianceChecker::new(ComplianceLevel::NistSsdf);
let result = checker.check(&sbom);
assert!(
result
.violations
.iter()
.any(|v| v.requirement.contains("PS.1"))
);
}
#[test]
fn test_eo14028_empty_sbom() {
let sbom = NormalizedSbom::default();
let checker = ComplianceChecker::new(ComplianceLevel::Eo14028);
let result = checker.check(&sbom);
assert!(
result
.violations
.iter()
.any(|v| v.requirement.contains("EO 14028"))
);
}
#[test]
fn test_compliance_result_counts() {
let violations = vec![
Violation {
severity: ViolationSeverity::Error,
category: ViolationCategory::ComponentIdentification,
message: "Error 1".to_string(),
element: None,
requirement: "Test".to_string(),
rule_id: "SBOM-CRA-GENERAL",
standard_refs: Vec::new(),
},
Violation {
severity: ViolationSeverity::Warning,
category: ViolationCategory::LicenseInfo,
message: "Warning 1".to_string(),
element: None,
requirement: "Test".to_string(),
rule_id: "SBOM-CRA-GENERAL",
standard_refs: Vec::new(),
},
Violation {
severity: ViolationSeverity::Info,
category: ViolationCategory::FormatSpecific,
message: "Info 1".to_string(),
element: None,
requirement: "Test".to_string(),
rule_id: "SBOM-CRA-GENERAL",
standard_refs: Vec::new(),
},
];
let result = ComplianceResult::new(ComplianceLevel::Standard, violations);
assert!(!result.is_compliant);
assert_eq!(result.error_count, 1);
assert_eq!(result.warning_count, 1);
assert_eq!(result.info_count, 1);
}
fn make_crypto_sbom(algos: &[(&str, &str, Option<&str>, Option<u8>)]) -> NormalizedSbom {
use crate::model::{
AlgorithmProperties, ComponentType, CryptoAssetType, CryptoPrimitive, CryptoProperties,
};
let mut sbom = NormalizedSbom::default();
for (name, family, param, ql) in algos {
let mut c = crate::model::Component::new(name.to_string(), format!("{name}@1.0"));
c.component_type = ComponentType::Cryptographic;
let mut algo = AlgorithmProperties::new(CryptoPrimitive::Ae)
.with_algorithm_family(family.to_string());
if let Some(p) = param {
algo = algo.with_parameter_set_identifier(p.to_string());
}
if let Some(level) = ql {
algo = algo.with_nist_quantum_security_level(*level);
}
c.crypto_properties = Some(
CryptoProperties::new(CryptoAssetType::Algorithm).with_algorithm_properties(algo),
);
sbom.add_component(c);
}
sbom
}
#[test]
fn test_cnsa2_aes128_violation() {
let sbom = make_crypto_sbom(&[("AES-128-GCM", "AES", Some("128"), Some(1))]);
let checker = ComplianceChecker::new(ComplianceLevel::Cnsa2);
let result = checker.check(&sbom);
assert!(
result
.violations
.iter()
.any(|v| v.severity == ViolationSeverity::Error && v.message.contains("AES-128")),
"CNSA 2.0 should flag AES-128"
);
}
#[test]
fn test_cnsa2_mlkem1024_passes() {
let sbom = make_crypto_sbom(&[("ML-KEM-1024", "ML-KEM", Some("1024"), Some(5))]);
let checker = ComplianceChecker::new(ComplianceLevel::Cnsa2);
let result = checker.check(&sbom);
let algo_errors: Vec<_> = result
.violations
.iter()
.filter(|v| {
v.severity == ViolationSeverity::Error
&& v.element.as_deref() == Some("ML-KEM-1024")
})
.collect();
assert!(algo_errors.is_empty(), "ML-KEM-1024 should pass CNSA 2.0");
}
#[test]
fn test_pqc_quantum_vulnerable() {
let sbom = make_crypto_sbom(&[("RSA-2048", "RSA", None, Some(0))]);
let checker = ComplianceChecker::new(ComplianceLevel::NistPqc);
let result = checker.check(&sbom);
assert!(
result
.violations
.iter()
.any(|v| v.severity == ViolationSeverity::Error
&& v.message.contains("quantum-vulnerable")),
"PQC should flag RSA-2048 as quantum-vulnerable"
);
}
#[test]
fn test_pqc_approved_algorithm_info() {
let sbom = make_crypto_sbom(&[("ML-DSA-65", "ML-DSA", Some("65"), Some(3))]);
let checker = ComplianceChecker::new(ComplianceLevel::NistPqc);
let result = checker.check(&sbom);
assert!(
result
.violations
.iter()
.any(|v| v.severity == ViolationSeverity::Info && v.message.contains("approved")),
"PQC should report ML-DSA-65 as approved"
);
}
fn refs_for(rule_id: &'static str) -> Vec<StandardRef> {
let v = Violation {
severity: ViolationSeverity::Warning,
category: ViolationCategory::DocumentMetadata,
message: String::new(),
element: None,
requirement: String::new(),
rule_id,
standard_refs: Vec::new(),
};
v.registry_standard_refs()
}
#[test]
fn registry_refs_for_art_13_4_include_article_and_pren() {
let refs = refs_for("SBOM-CRA-ART-13-4");
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::CraArticle && r.id == "Art. 13(4)"),
"expected CRA Art. 13(4); got {refs:?}"
);
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::Pren40000_1_3 && r.id == "PRE-7-RQ-04"),
"expected prEN PRE-7-RQ-04; got {refs:?}"
);
}
#[test]
fn registry_refs_for_annex_i_identifier_include_pren_07() {
let refs = refs_for("SBOM-CRA-ANNEX-I-IDENTIFIER");
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::Pren40000_1_3 && r.id == "PRE-7-RQ-07"),
"expected PRE-7-RQ-07; got {refs:?}"
);
let pren_count = refs
.iter()
.filter(|r| r.standard == StandardKind::Pren40000_1_3 && r.id == "PRE-7-RQ-07")
.count();
assert_eq!(pren_count, 1, "PRE-7-RQ-07 should appear exactly once");
}
#[test]
fn registry_refs_for_supply_chain_include_annex_and_pren() {
let refs = refs_for("SBOM-CRA-ANNEX-I-SUPPLY-CHAIN");
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::CraAnnex && r.id == "Annex I Part III"),
"expected Annex I Part III; got {refs:?}"
);
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::Pren40000_1_3 && r.id == "PRE-7-RQ-01"),
"expected PRE-7-RQ-01; got {refs:?}"
);
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::Pren40000_1_3 && r.id == "PRE-7-RQ-03"),
"expected PRE-7-RQ-03; got {refs:?}"
);
}
#[test]
fn registry_refs_for_art_13_7_include_pren_rls() {
let refs = refs_for("SBOM-CRA-ART-13-7");
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::Pren40000_1_3 && r.id == "RLS-2-RQ-03-RE"),
"expected RLS-2-RQ-03-RE; got {refs:?}"
);
}
#[test]
fn registry_refs_for_ssdf_ps2() {
let refs = refs_for("SBOM-SSDF-PS2");
assert!(
refs.iter()
.any(|r| r.standard == StandardKind::NistSsdf && r.id == "PS.2"),
"expected NIST SSDF PS.2; got {refs:?}"
);
}
#[test]
fn every_emitted_violation_has_a_registered_rule_id() {
let sbom = NormalizedSbom::default();
for level in ComplianceLevel::all() {
let result = ComplianceChecker::new(*level).check(&sbom);
for v in &result.violations {
assert!(
rule_meta(v.rule_id).is_some(),
"level {level:?}: violation {:?} has unregistered rule_id {:?}",
v.requirement,
v.rule_id
);
}
}
}
#[test]
fn check_populates_standard_refs_for_cra_violations() {
let sbom = NormalizedSbom::default();
let checker = ComplianceChecker::new(ComplianceLevel::CraPhase2);
let result = checker.check(&sbom);
let cra_violations: Vec<_> = result
.violations
.iter()
.filter(|v| v.requirement.to_lowercase().contains("cra"))
.collect();
assert!(
!cra_violations.is_empty(),
"empty SBOM should produce some CRA violations"
);
for v in &cra_violations {
assert!(
!v.standard_refs.is_empty(),
"CRA violation {:?} should have standard_refs populated",
v.requirement
);
}
}
#[test]
fn sidecar_supplies_security_contact_downgrades_art_13_6() {
use crate::model::CraSidecarMetadata;
let sbom = NormalizedSbom::default();
let bare = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let art_13_6_warning = bare.violations.iter().find(|v| {
v.requirement.contains("Art. 13(6)") && v.severity == ViolationSeverity::Warning
});
assert!(
art_13_6_warning.is_some(),
"Without sidecar, Art. 13(6) should be a Warning"
);
let sidecar = CraSidecarMetadata {
security_contact: Some("security@example.com".to_string()),
..Default::default()
};
let withsc = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
let art_13_6_info = withsc.violations.iter().find(|v| {
v.requirement.contains("Art. 13(6)") && v.severity == ViolationSeverity::Info
});
assert!(
art_13_6_info.is_some(),
"With sidecar, Art. 13(6) should be downgraded to Info"
);
assert!(
!withsc
.violations
.iter()
.any(|v| v.requirement.contains("Art. 13(6)")
&& v.severity == ViolationSeverity::Warning),
"With sidecar, no Warning-level Art. 13(6) violation should remain"
);
}
#[test]
fn sidecar_supplies_product_name_downgrades_art_13_12() {
use crate::model::CraSidecarMetadata;
let sbom = NormalizedSbom::default();
let sidecar = CraSidecarMetadata {
product_name: Some("Demo Product".to_string()),
..Default::default()
};
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
let downgraded = result.violations.iter().find(|v| {
v.requirement.contains("Art. 13(12)") && v.severity == ViolationSeverity::Info
});
assert!(
downgraded.is_some(),
"Sidecar product_name should downgrade Art. 13(12) to Info"
);
}
#[test]
fn sidecar_supplies_manufacturer_downgrades_art_13_15() {
use crate::model::CraSidecarMetadata;
let sbom = NormalizedSbom::default();
let sidecar = CraSidecarMetadata {
manufacturer_name: Some("Demo Corp".to_string()),
..Default::default()
};
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
let downgraded = result.violations.iter().find(|v| {
v.requirement.contains("Art. 13(15)") && v.severity == ViolationSeverity::Info
});
assert!(
downgraded.is_some(),
"Sidecar manufacturer_name should downgrade Art. 13(15) to Info"
);
}
#[test]
fn sidecar_supplies_cvd_url_downgrades_art_13_7() {
use crate::model::CraSidecarMetadata;
let sbom = NormalizedSbom::default();
let sidecar = CraSidecarMetadata {
vulnerability_disclosure_url: Some("https://example.com/security".to_string()),
..Default::default()
};
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
let downgraded = result.violations.iter().find(|v| {
v.requirement.contains("Art. 13(7)") && v.severity == ViolationSeverity::Info
});
assert!(
downgraded.is_some(),
"Sidecar CVD URL should downgrade Art. 13(7) to Info"
);
}
fn vendor_component(name: &str, with_hash: bool) -> crate::model::Component {
use crate::model::{Component, Hash, HashAlgorithm, Organization};
let mut c = Component::new(name.to_string(), name.to_string())
.with_purl(format!("pkg:cargo/{name}@1.0.0"));
c.supplier = Some(Organization::new("VendorCorp".to_string()));
if with_hash {
c.hashes.push(Hash::new(
HashAlgorithm::Sha256,
"0000000000000000000000000000000000000000000000000000000000000000".to_string(),
));
}
c
}
fn hw_component(
name: &str,
kind: crate::model::ComponentType,
with_purl: bool,
with_supplier: bool,
version: Option<&str>,
) -> crate::model::Component {
use crate::model::{Component, Organization};
let mut c = Component::new(name.to_string(), name.to_string());
c.component_type = kind;
if with_purl {
c = c.with_purl(format!("pkg:generic/{name}"));
}
if with_supplier {
c.supplier = Some(Organization::new("HardwareCorp".to_string()));
}
if let Some(v) = version {
c = c.with_version(v.to_string());
}
c
}
#[test]
fn hardware_check_skipped_for_software_only_sbom() {
let mut sbom = NormalizedSbom::default();
let c = vendor_component("software", true);
sbom.components.insert(c.canonical_id.clone(), c);
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
!result
.violations
.iter()
.any(|v| v.requirement.contains("PRE-8-RQ-02")),
"Software-only SBOM should produce no PRE-8-RQ-02 violations"
);
}
#[test]
fn hardware_check_passes_for_complete_firmware() {
use crate::model::ComponentType;
let mut sbom = NormalizedSbom::default();
let c = hw_component(
"router-fw",
ComponentType::Firmware,
true,
true,
Some("1.2.3"),
);
sbom.components.insert(c.canonical_id.clone(), c);
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
!result
.violations
.iter()
.any(|v| v.requirement.contains("PRE-8-RQ-02")),
"Complete firmware component should pass [PRE-8-RQ-02]"
);
}
#[test]
fn hardware_check_flags_firmware_without_version() {
use crate::model::ComponentType;
let mut sbom = NormalizedSbom::default();
let c = hw_component("router-fw", ComponentType::Firmware, true, true, None);
sbom.components.insert(c.canonical_id.clone(), c);
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
result.violations.iter().any(|v| {
v.requirement.contains("Firmware version") && v.severity == ViolationSeverity::Error
}),
"Firmware without version should produce an Error"
);
}
#[test]
fn hardware_check_flags_missing_producer() {
use crate::model::ComponentType;
let mut sbom = NormalizedSbom::default();
let c = hw_component("router", ComponentType::Device, true, false, Some("1.0"));
sbom.components.insert(c.canonical_id.clone(), c);
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
result.violations.iter().any(|v| {
v.requirement.contains("Hardware producer")
&& v.severity == ViolationSeverity::Error
}),
"Hardware without producer should produce an Error"
);
}
#[test]
fn hardware_check_flags_synthetic_identifier() {
use crate::model::{Component, ComponentType, Organization};
let mut sbom = NormalizedSbom::default();
let mut c = Component::new("router".to_string(), "router".to_string())
.with_version("1.0".to_string());
c.component_type = ComponentType::Device;
c.supplier = Some(Organization::new("HardwareCorp".to_string()));
sbom.components.insert(c.canonical_id.clone(), c);
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
result.violations.iter().any(|v| {
v.requirement.contains("Hardware identifier")
&& v.severity == ViolationSeverity::Error
}),
"Hardware with synthetic ID should produce an Error"
);
}
#[test]
fn hardware_check_device_with_firmware_dep_passes() {
use crate::model::{ComponentType, DependencyEdge, DependencyType};
let mut sbom = NormalizedSbom::default();
let device = hw_component("router", ComponentType::Device, true, true, None);
let firmware = hw_component(
"router-fw",
ComponentType::Firmware,
true,
true,
Some("1.2.3"),
);
let device_id = device.canonical_id.clone();
let firmware_id = firmware.canonical_id.clone();
sbom.components.insert(device_id.clone(), device);
sbom.components.insert(firmware_id.clone(), firmware);
sbom.edges.push(DependencyEdge::new(
device_id,
firmware_id,
DependencyType::DependsOn,
));
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
!result
.violations
.iter()
.any(|v| { v.requirement.contains("Device firmware association") }),
"Device with firmware dependency should not trigger version warning"
);
}
#[test]
fn vendor_hash_coverage_full() {
use crate::quality::HashQualityMetrics;
let mut sbom = NormalizedSbom::default();
for n in ["a", "b", "c", "d", "e"] {
let c = vendor_component(n, true);
sbom.components.insert(c.canonical_id.clone(), c);
}
let m = HashQualityMetrics::from_sbom(&sbom);
assert_eq!(m.vendor_components_total, 5);
assert_eq!(m.vendor_components_with_hash, 5);
assert_eq!(m.vendor_hash_coverage(), Some(1.0));
}
#[test]
fn vendor_hash_coverage_partial_triggers_warning() {
let mut sbom = NormalizedSbom::default();
for n in ["a", "b", "c", "d", "e", "f", "g"] {
let c = vendor_component(n, true);
sbom.components.insert(c.canonical_id.clone(), c);
}
for n in ["h", "i", "j"] {
let c = vendor_component(n, false);
sbom.components.insert(c.canonical_id.clone(), c);
}
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let v = result.violations.iter().find(|v| {
v.requirement.contains("PRE-7-RQ-07-RE") && v.severity == ViolationSeverity::Warning
});
assert!(
v.is_some(),
"70% vendor-hash coverage should produce a Warning under CraPhase2"
);
}
#[test]
fn vendor_hash_coverage_below_50_triggers_error() {
let mut sbom = NormalizedSbom::default();
for n in ["a", "b", "c", "d"] {
let c = vendor_component(n, true);
sbom.components.insert(c.canonical_id.clone(), c);
}
for n in ["e", "f", "g", "h", "i", "j"] {
let c = vendor_component(n, false);
sbom.components.insert(c.canonical_id.clone(), c);
}
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let v = result.violations.iter().find(|v| {
v.requirement.contains("PRE-7-RQ-07-RE") && v.severity == ViolationSeverity::Error
});
assert!(
v.is_some(),
"40% vendor-hash coverage should produce an Error under CraPhase2"
);
}
#[test]
fn vendor_hash_coverage_no_vendor_components_no_violation() {
let mut sbom = NormalizedSbom::default();
use crate::model::Component;
for n in ["a", "b", "c"] {
let c = Component::new(n.to_string(), n.to_string());
sbom.components.insert(c.canonical_id.clone(), c);
}
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
assert!(
!result
.violations
.iter()
.any(|v| v.requirement.contains("PRE-7-RQ-07-RE")),
"No vendor components โ no [PRE-7-RQ-07-RE] violation"
);
}
#[test]
fn art_13_2_warns_when_no_risk_assessment_referenced() {
let sbom = NormalizedSbom::default();
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let v = result.violations.iter().find(|v| {
v.requirement.contains("Art. 13(2)") && v.severity == ViolationSeverity::Warning
});
assert!(v.is_some(), "Empty SBOM should produce Art. 13(2) Warning");
}
#[test]
fn art_13_2_silenced_by_sidecar_risk_assessment_url() {
use crate::model::CraSidecarMetadata;
let sbom = NormalizedSbom::default();
let sidecar = CraSidecarMetadata {
risk_assessment_url: Some("https://example.com/ra.pdf".to_string()),
..Default::default()
};
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
assert!(
!result
.violations
.iter()
.any(|v| v.requirement.contains("Art. 13(2)")),
"Sidecar risk_assessment_url should suppress Art. 13(2) violation"
);
}
#[test]
fn article_14_pre_deadline_emits_info_only() {
let sbom = NormalizedSbom::default();
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let art14_count = result
.violations
.iter()
.filter(|v| v.requirement.contains("Art. 14"))
.count();
assert!(
art14_count >= 4,
"Art. 14 readiness should produce โฅ4 violations (PSIRT, 14(1), 14(2), 14(7)); got {art14_count}"
);
}
#[test]
fn article_14_pre_deadline_mocked_clock_emits_4_infos() {
let checker = ComplianceChecker::new(ComplianceLevel::CraPhase2);
let mut violations = Vec::new();
let now = chrono::DateTime::parse_from_rfc3339("2026-04-26T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
checker.check_article_14_readiness_at(now, &mut violations);
let infos = violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Info && v.requirement.contains("Art. 14"))
.count();
let warnings = violations
.iter()
.filter(|v| {
v.severity == ViolationSeverity::Warning && v.requirement.contains("Art. 14")
})
.count();
assert_eq!(
infos, 4,
"Pre-deadline expects 4 Info-level Art. 14 findings; got {infos} (full list: {violations:?})"
);
assert_eq!(
warnings, 0,
"Pre-deadline expects 0 Warning-level Art. 14 findings"
);
}
#[test]
fn article_14_post_deadline_mocked_clock_emits_3_warnings_1_info() {
let checker = ComplianceChecker::new(ComplianceLevel::CraPhase2);
let mut violations = Vec::new();
let now = chrono::DateTime::parse_from_rfc3339("2026-12-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
checker.check_article_14_readiness_at(now, &mut violations);
let infos = violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Info && v.requirement.contains("Art. 14"))
.count();
let warnings = violations
.iter()
.filter(|v| {
v.severity == ViolationSeverity::Warning && v.requirement.contains("Art. 14")
})
.count();
assert_eq!(
warnings, 3,
"Post-deadline expects 3 Warning-level Art. 14 findings (PSIRT/14(1)/14(2)); got {warnings} (full: {violations:?})"
);
assert_eq!(
infos, 1,
"Post-deadline expects 1 Info-level Art. 14 finding (Art. 14(7) ENISA platform stays Info regardless of date)"
);
}
#[test]
fn article_14_sidecar_suppresses_psirt_warning() {
use crate::model::CraSidecarMetadata;
let sbom = NormalizedSbom::default();
let sidecar = CraSidecarMetadata {
psirt_url: Some("https://example.com/psirt".to_string()),
early_warning_contact: Some("psirt@example.com".to_string()),
incident_report_contact: Some("ir@example.com".to_string()),
..Default::default()
};
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
let art_14_psirt = result
.violations
.iter()
.any(|v| v.requirement.contains("Art. 14: PSIRT"));
let art_14_1 = result
.violations
.iter()
.any(|v| v.requirement.contains("Art. 14(1)"));
let art_14_2 = result
.violations
.iter()
.any(|v| v.requirement.contains("Art. 14(2)"));
assert!(
!art_14_psirt,
"Sidecar psirt_url should suppress PSIRT check"
);
assert!(
!art_14_1,
"Sidecar early_warning_contact should suppress 14(1)"
);
assert!(
!art_14_2,
"Sidecar incident_report_contact should suppress 14(2)"
);
}
#[test]
fn direct_dep_missing_supplier_is_error_under_cra_phase2() {
use crate::model::{Component, DependencyEdge, DependencyType};
let mut sbom = NormalizedSbom::default();
let app = Component::new("app".to_string(), "app".to_string())
.with_purl("pkg:cargo/app@1.0".to_string());
let lib = Component::new("lib".to_string(), "lib".to_string())
.with_purl("pkg:cargo/lib@1.0".to_string());
let app_id = app.canonical_id.clone();
let lib_id = lib.canonical_id.clone();
sbom.primary_component_id = Some(app_id.clone());
sbom.components.insert(app_id.clone(), app);
sbom.components.insert(lib_id.clone(), lib);
sbom.edges.push(DependencyEdge::new(
app_id,
lib_id,
DependencyType::DependsOn,
));
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let v = result.violations.iter().find(|v| {
v.requirement.contains("Direct dependency supplier")
&& v.severity == ViolationSeverity::Error
});
assert!(
v.is_some(),
"Direct dep without supplier should produce an Error under CraPhase2"
);
}
#[test]
fn transitive_dep_missing_supplier_is_softer_than_direct() {
use crate::model::{Component, DependencyEdge, DependencyType, Organization};
let mut sbom = NormalizedSbom::default();
let mut app = Component::new("app".to_string(), "app".to_string())
.with_purl("pkg:cargo/app@1.0".to_string());
app.supplier = Some(Organization::new("AppCorp".to_string()));
let mut lib = Component::new("lib".to_string(), "lib".to_string())
.with_purl("pkg:cargo/lib@1.0".to_string());
lib.supplier = Some(Organization::new("LibCorp".to_string()));
let deep = Component::new("deep".to_string(), "deep".to_string())
.with_purl("pkg:cargo/deep@1.0".to_string());
let app_id = app.canonical_id.clone();
let lib_id = lib.canonical_id.clone();
let deep_id = deep.canonical_id.clone();
sbom.primary_component_id = Some(app_id.clone());
sbom.components.insert(app_id.clone(), app);
sbom.components.insert(lib_id.clone(), lib);
sbom.components.insert(deep_id.clone(), deep);
sbom.edges.push(DependencyEdge::new(
app_id,
lib_id.clone(),
DependencyType::DependsOn,
));
sbom.edges.push(DependencyEdge::new(
lib_id,
deep_id,
DependencyType::DependsOn,
));
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&sbom);
let direct_err = result.violations.iter().any(|v| {
v.requirement.contains("Direct dependency supplier")
&& v.severity == ViolationSeverity::Error
});
let transitive = result
.violations
.iter()
.find(|v| v.requirement.contains("Transitive dependency supplier"));
assert!(
!direct_err,
"No direct deps lack a supplier; should not error"
);
assert!(transitive.is_some(), "Transitive dep should be reported");
assert_ne!(
transitive.unwrap().severity,
ViolationSeverity::Error,
"Transitive supplier missing should never be Error (it's recommended, not mandatory)"
);
}
#[test]
fn bsi_tr_03183_2_empty_sbom_emits_errors() {
let sbom = NormalizedSbom::default();
let result = ComplianceChecker::new(ComplianceLevel::BsiTr03183_2).check(&sbom);
assert!(
result
.violations
.iter()
.any(|v| v.requirement.contains("BSI TR-03183-2 ยง5.1")
&& v.severity == ViolationSeverity::Error),
"Empty SBOM should fail BSI ยง5.1"
);
}
#[test]
fn bsi_tr_03183_2_flags_missing_strong_hash() {
use crate::model::{Component, Hash, HashAlgorithm};
let mut sbom = NormalizedSbom::default();
let mut c = Component::new("lib".to_string(), "lib".to_string())
.with_purl("pkg:cargo/lib@1.0".to_string());
c.hashes.push(Hash::new(HashAlgorithm::Md5, "0".repeat(32)));
sbom.add_component(c);
let result = ComplianceChecker::new(ComplianceLevel::BsiTr03183_2).check(&sbom);
assert!(
result.violations.iter().any(|v| {
v.requirement.contains("BSI TR-03183-2 ยง5.4")
&& v.severity == ViolationSeverity::Error
}),
"Component without SHA-256+ hash should fail BSI ยง5.4"
);
}
#[test]
fn bsi_tr_03183_2_passes_for_complete_component() {
use crate::model::{
Component, Creator, CreatorType, DependencyEdge, DependencyType, Hash, HashAlgorithm,
LicenseExpression, Organization,
};
let mut sbom = NormalizedSbom::default();
sbom.document.creators.push(Creator {
creator_type: CreatorType::Tool,
name: "sbom-tools".to_string(),
email: None,
});
let mut a = Component::new("a".to_string(), "a".to_string())
.with_purl("pkg:cargo/a@1.0".to_string())
.with_version("1.0".to_string());
a.hashes
.push(Hash::new(HashAlgorithm::Sha256, "f".repeat(64)));
a.supplier = Some(Organization::new("SupplierA".to_string()));
a.licenses
.add_declared(LicenseExpression::new("MIT".to_string()));
let mut b = Component::new("b".to_string(), "b".to_string())
.with_purl("pkg:cargo/b@1.0".to_string())
.with_version("1.0".to_string());
b.hashes
.push(Hash::new(HashAlgorithm::Sha256, "0".repeat(64)));
b.supplier = Some(Organization::new("SupplierB".to_string()));
b.licenses
.add_declared(LicenseExpression::new("MIT".to_string()));
let a_id = a.canonical_id.clone();
let b_id = b.canonical_id.clone();
sbom.components.insert(a_id.clone(), a);
sbom.components.insert(b_id.clone(), b);
sbom.edges
.push(DependencyEdge::new(a_id, b_id, DependencyType::DependsOn));
let result = ComplianceChecker::new(ComplianceLevel::BsiTr03183_2).check(&sbom);
let errors: Vec<_> = result
.violations
.iter()
.filter(|v| v.severity == ViolationSeverity::Error)
.collect();
assert!(
errors.is_empty(),
"Complete BSI-compliant SBOM should produce no Errors; got: {errors:?}"
);
}
#[test]
fn bsi_tr_03183_2_in_compliance_level_all() {
assert_eq!(ComplianceLevel::all().len(), 16);
assert!(ComplianceLevel::all().contains(&ComplianceLevel::BsiTr03183_2));
assert!(ComplianceLevel::all().contains(&ComplianceLevel::CraOssSteward));
assert!(ComplianceLevel::all().contains(&ComplianceLevel::EuccSubstantial));
assert!(ComplianceLevel::all().contains(&ComplianceLevel::EuAiAct));
assert!(ComplianceLevel::all().contains(&ComplianceLevel::BsiSbomForAi));
}
#[test]
fn sidecar_does_not_override_present_sbom_field() {
use crate::model::{CraSidecarMetadata, Creator, CreatorType};
let mut sbom = NormalizedSbom::default();
sbom.document.creators.push(Creator {
creator_type: CreatorType::Organization,
name: "SbomDeclaredCorp".to_string(),
email: None,
});
let sidecar = CraSidecarMetadata {
manufacturer_name: Some("SidecarCorp".to_string()),
..Default::default()
};
let result = ComplianceChecker::new(ComplianceLevel::CraPhase2)
.with_sidecar(sidecar)
.check(&sbom);
assert!(
!result.violations.iter().any(|v| v
.requirement
.contains("Art. 13(15): Manufacturer identification")),
"When SBOM provides manufacturer, no Art. 13(15) violation should be emitted"
);
}
}