use super::{ComplianceChecker, ComplianceLevel, NormalizedSbom, Violation};
use crate::model::{AttestationDeclarations, AttestationRuleFamily, SupportedRequirement};
pub(crate) struct ComplianceContext<'a> {
pub(crate) checker: &'a ComplianceChecker,
pub(crate) sbom: &'a NormalizedSbom,
}
impl<'a> ComplianceContext<'a> {
pub(crate) const fn new(checker: &'a ComplianceChecker, sbom: &'a NormalizedSbom) -> Self {
Self { checker, sbom }
}
pub(crate) fn attestation_declarations(&self) -> Option<&'a AttestationDeclarations> {
self.sbom.declarations()
}
pub(crate) fn evidence_for(
&self,
family: AttestationRuleFamily,
) -> Vec<SupportedRequirement<'a>> {
self.sbom
.declarations()
.map_or_else(Vec::new, |declarations| {
declarations.evidence_for_family(family, self.checker.now())
})
}
}
pub(crate) trait StandardChecker {
fn level(&self) -> ComplianceLevel;
fn check(&self, ctx: &ComplianceContext) -> Vec<Violation>;
}
pub(crate) struct GenericChecker {
level: ComplianceLevel,
}
impl GenericChecker {
pub(crate) const fn new(level: ComplianceLevel) -> Self {
Self { level }
}
}
impl StandardChecker for GenericChecker {
fn level(&self) -> ComplianceLevel {
self.level
}
fn check(&self, ctx: &ComplianceContext) -> Vec<Violation> {
let checker = ctx.checker;
let sbom = ctx.sbom;
let mut violations = Vec::new();
checker.check_document_metadata(sbom, &mut violations);
checker.check_components(sbom, &mut violations);
checker.check_dependencies(sbom, &mut violations);
checker.check_vulnerability_metadata(sbom, &mut violations);
checker.check_format_specific(sbom, &mut violations);
if checker.level.is_cra() {
checker.check_cra_gaps(sbom, &mut violations);
checker.check_hardware_components(sbom, &mut violations);
}
violations
}
}
macro_rules! dedicated_checker {
($name:ident, $level:expr, $method:ident) => {
pub(crate) struct $name;
impl StandardChecker for $name {
fn level(&self) -> ComplianceLevel {
$level
}
fn check(&self, ctx: &ComplianceContext) -> Vec<Violation> {
let mut violations = Vec::new();
ctx.checker.$method(ctx.sbom, &mut violations);
violations
}
}
};
}
dedicated_checker!(NistSsdfChecker, ComplianceLevel::NistSsdf, check_nist_ssdf);
dedicated_checker!(Eo14028Checker, ComplianceLevel::Eo14028, check_eo14028);
dedicated_checker!(Cnsa2Checker, ComplianceLevel::Cnsa2, check_cnsa2);
dedicated_checker!(NistPqcChecker, ComplianceLevel::NistPqc, check_nist_pqc);
dedicated_checker!(
BsiTr03183Checker,
ComplianceLevel::BsiTr03183_2,
check_bsi_tr_03183_2
);
dedicated_checker!(
CraOssStewardChecker,
ComplianceLevel::CraOssSteward,
check_cra_oss_steward
);
dedicated_checker!(
EuccSubstantialChecker,
ComplianceLevel::EuccSubstantial,
check_eucc_substantial
);
dedicated_checker!(EuAiActChecker, ComplianceLevel::EuAiAct, check_eu_ai_act);
dedicated_checker!(
BsiSbomForAiChecker,
ComplianceLevel::BsiSbomForAi,
check_bsi_sbom_for_ai
);
dedicated_checker!(Cisa2026Checker, ComplianceLevel::Cisa2026, check_cisa2026);
dedicated_checker!(
PciDss632Checker,
ComplianceLevel::PciDss632,
check_pci_dss_6_3_2
);
dedicated_checker!(FsctChecker, ComplianceLevel::Fsct, check_fsct);
pub(crate) fn checker_for(level: ComplianceLevel) -> Box<dyn StandardChecker> {
match level {
ComplianceLevel::NistSsdf => Box::new(NistSsdfChecker),
ComplianceLevel::Eo14028 => Box::new(Eo14028Checker),
ComplianceLevel::Cnsa2 => Box::new(Cnsa2Checker),
ComplianceLevel::NistPqc => Box::new(NistPqcChecker),
ComplianceLevel::BsiTr03183_2 => Box::new(BsiTr03183Checker),
ComplianceLevel::CraOssSteward => Box::new(CraOssStewardChecker),
ComplianceLevel::EuccSubstantial => Box::new(EuccSubstantialChecker),
ComplianceLevel::EuAiAct => Box::new(EuAiActChecker),
ComplianceLevel::BsiSbomForAi => Box::new(BsiSbomForAiChecker),
ComplianceLevel::Cisa2026 => Box::new(Cisa2026Checker),
ComplianceLevel::PciDss632 => Box::new(PciDss632Checker),
ComplianceLevel::Fsct => Box::new(FsctChecker),
other => Box::new(GenericChecker::new(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{
AttestationAssertion, AttestationMapEntry, CdxaRef, CdxaResolution, DeclaredClaim,
DeclaredEvidence, DefinedRequirement, DefinedStandard, EvidenceLevel, SignaturePresence,
};
fn ts(s: &str) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::parse_from_rfc3339(s)
.expect("test timestamp must parse")
.with_timezone(&chrono::Utc)
}
fn sbom_with_ssdf_declarations() -> NormalizedSbom {
let declarations = AttestationDeclarations {
claims: vec![DeclaredClaim {
bom_ref: Some("claim-1".into()),
target: Some(CdxaRef {
raw: "target-1".into(),
resolution: CdxaResolution::Target,
}),
evidence: vec![CdxaRef {
raw: "ev-1".into(),
resolution: CdxaResolution::Evidence,
}],
..DeclaredClaim::default()
}],
evidence: vec![DeclaredEvidence {
bom_ref: Some("ev-1".into()),
created: Some(ts("2026-01-10T00:00:00Z")),
expires: Some(ts("2027-01-10T00:00:00Z")),
signature: Some(SignaturePresence {
algorithm: Some("ES256".into()),
key_id: None,
signer_count: 1,
}),
..DeclaredEvidence::default()
}],
attestations: vec![AttestationAssertion {
map: vec![AttestationMapEntry {
requirement: Some(CdxaRef {
raw: "req-ps1".into(),
resolution: CdxaResolution::Requirement,
}),
claims: vec![CdxaRef {
raw: "claim-1".into(),
resolution: CdxaResolution::Claim,
}],
conformance_score: Some(1.0),
..AttestationMapEntry::default()
}],
..AttestationAssertion::default()
}],
standards: vec![DefinedStandard {
bom_ref: Some("std-ssdf".into()),
name: Some("NIST Secure Software Development Framework".into()),
requirements: vec![DefinedRequirement {
bom_ref: Some("req-ps1".into()),
identifier: Some("PS.1".into()),
..DefinedRequirement::default()
}],
..DefinedStandard::default()
}],
..AttestationDeclarations::default()
};
let mut sbom = NormalizedSbom::default();
sbom.extensions.declarations = Some(declarations);
sbom
}
#[test]
fn context_exposes_attestation_evidence_per_family() {
let sbom = sbom_with_ssdf_declarations();
let checker = ComplianceChecker::new(ComplianceLevel::NistSsdf)
.with_as_of(ts("2026-06-01T00:00:00Z"));
let ctx = ComplianceContext::new(&checker, &sbom);
assert!(ctx.attestation_declarations().is_some());
let ssdf = ctx.evidence_for(AttestationRuleFamily::Ssdf);
assert_eq!(ssdf.len(), 1);
assert_eq!(ssdf[0].requirement.identifier.as_deref(), Some("PS.1"));
assert_eq!(ssdf[0].evidence_level, EvidenceLevel::SignaturePresent);
assert!(ctx.evidence_for(AttestationRuleFamily::Cra).is_empty());
assert!(ctx.evidence_for(AttestationRuleFamily::Eo14028).is_empty());
}
#[test]
fn context_evidence_respects_pinned_clock_and_absence() {
let sbom = sbom_with_ssdf_declarations();
let expired = ComplianceChecker::new(ComplianceLevel::NistSsdf)
.with_as_of(ts("2027-06-01T00:00:00Z"));
let ctx = ComplianceContext::new(&expired, &sbom);
assert!(ctx.evidence_for(AttestationRuleFamily::Ssdf).is_empty());
let bare = NormalizedSbom::default();
let checker = ComplianceChecker::new(ComplianceLevel::NistSsdf);
let ctx = ComplianceContext::new(&checker, &bare);
assert!(ctx.attestation_declarations().is_none());
assert!(ctx.evidence_for(AttestationRuleFamily::Ssdf).is_empty());
}
}