use super::{CanonicalId, Contact, ExternalReference, Organization};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum EvidenceLevel {
#[default]
SelfDeclared,
Structural,
SignaturePresent,
SignatureVerified,
}
impl std::fmt::Display for EvidenceLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SelfDeclared => write!(f, "self-declared"),
Self::Structural => write!(f, "structural"),
Self::SignaturePresent => write!(f, "signature-present"),
Self::SignatureVerified => write!(f, "signature-verified"),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignaturePresence {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub algorithm: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_id: Option<String>,
pub signer_count: usize,
}
impl SignaturePresence {
#[must_use]
pub fn from_jsf(value: &serde_json::Value) -> Option<Self> {
let obj = value.as_object()?;
for multi_key in ["signers", "chain"] {
if let Some(entries) = obj.get(multi_key).and_then(|v| v.as_array()) {
let first = entries.first().and_then(|v| v.as_object());
return Some(Self {
algorithm: first
.and_then(|o| o.get("algorithm"))
.and_then(|v| v.as_str())
.map(String::from),
key_id: first
.and_then(|o| o.get("keyId"))
.and_then(|v| v.as_str())
.map(String::from),
signer_count: entries.len(),
});
}
}
Some(Self {
algorithm: obj
.get("algorithm")
.and_then(|v| v.as_str())
.map(String::from),
key_id: obj.get("keyId").and_then(|v| v.as_str()).map(String::from),
signer_count: 1,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum CdxaResolution {
Claim,
Evidence,
Assessor,
Requirement,
Target,
Inventory(CanonicalId),
Dangling,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CdxaRef {
pub raw: String,
pub resolution: CdxaResolution,
}
impl CdxaRef {
#[must_use]
pub fn is_resolved(&self) -> bool {
!matches!(self.resolution, CdxaResolution::Dangling)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeclaredAssessor {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bom_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub third_party: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub organization: Option<Organization>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AttestationAssertion {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub assessor: Option<CdxaRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub map: Vec<AttestationMapEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AttestationMapEntry {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requirement: Option<CdxaRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub claims: Vec<CdxaRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub counter_claims: Vec<CdxaRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conformance_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub conformance_rationale: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conformance_mitigation_strategies: Vec<CdxaRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence_score: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confidence_rationale: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeclaredClaim {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bom_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<CdxaRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub predicate: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mitigation_strategies: Vec<CdxaRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<CdxaRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub counter_evidence: Vec<CdxaRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub external_refs: Vec<ExternalReference>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EvidenceDataItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub has_attachment: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classification: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sensitive_data: Vec<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub has_governance: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeclaredEvidence {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bom_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub property_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub data: Vec<EvidenceDataItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub author: Option<Contact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reviewer: Option<Contact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
}
impl DeclaredEvidence {
#[must_use]
pub fn is_fresh(&self, as_of: DateTime<Utc>) -> bool {
if let Some(expires) = self.expires
&& expires <= as_of
{
return false;
}
if let Some(created) = self.created
&& created > as_of
{
return false;
}
true
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeclarationTarget {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bom_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeclarationTargets {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub organizations: Vec<DeclarationTarget>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub components: Vec<DeclarationTarget>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub services: Vec<DeclarationTarget>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AffirmationSignatory {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub organization: Option<Organization>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external_reference: Option<ExternalReference>,
}
impl AffirmationSignatory {
#[must_use]
pub fn has_complete_identity(&self) -> bool {
self.signature.is_some()
|| (self.external_reference.is_some() && self.organization.is_some())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeclaredAffirmation {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub statement: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub signatories: Vec<AffirmationSignatory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DefinedRequirement {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bom_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub descriptions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub open_cre: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub external_refs: Vec<ExternalReference>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DefinedStandard {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bom_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requirements: Vec<DefinedRequirement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AttestationDeclarations {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assessors: Vec<DeclaredAssessor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attestations: Vec<AttestationAssertion>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub claims: Vec<DeclaredClaim>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<DeclaredEvidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub targets: Option<DeclarationTargets>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub affirmation: Option<DeclaredAffirmation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<SignaturePresence>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub standards: Vec<DefinedStandard>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AttestationRuleFamily {
Ssdf,
Eo14028,
Cra,
}
impl AttestationRuleFamily {
#[must_use]
pub fn classify(standard: &DefinedStandard, requirement: &DefinedRequirement) -> Option<Self> {
let hay = format!(
"{} {} {}",
standard.name.as_deref().unwrap_or(""),
standard.description.as_deref().unwrap_or(""),
standard.owner.as_deref().unwrap_or("")
)
.to_lowercase();
if hay.contains("ssdf") || hay.contains("secure software development framework") {
return Some(Self::Ssdf);
}
if hay.contains("14028") || hay.contains("executive order") {
return Some(Self::Eo14028);
}
if hay.contains("cyber resilience")
|| hay
.split(|c: char| !c.is_ascii_alphanumeric())
.any(|token| token == "cra")
{
return Some(Self::Cra);
}
if Self::is_ssdf_practice_id(requirement.identifier.as_deref()) {
return Some(Self::Ssdf);
}
None
}
fn is_ssdf_practice_id(identifier: Option<&str>) -> bool {
let Some(identifier) = identifier else {
return false;
};
let id = identifier.trim().to_ascii_uppercase();
["PO.", "PS.", "PW.", "RV."].iter().any(|prefix| {
id.strip_prefix(prefix)
.is_some_and(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_digit()))
})
}
}
#[derive(Debug, Clone)]
pub struct SupportedRequirement<'a> {
pub standard: &'a DefinedStandard,
pub requirement: &'a DefinedRequirement,
pub attestation: &'a AttestationAssertion,
pub map_entry: &'a AttestationMapEntry,
pub supporting_claims: Vec<&'a DeclaredClaim>,
pub evidence_level: EvidenceLevel,
pub third_party_assessed: bool,
}
impl AttestationDeclarations {
#[must_use]
pub fn claim_by_ref(&self, bom_ref: &str) -> Option<&DeclaredClaim> {
self.claims
.iter()
.find(|c| c.bom_ref.as_deref() == Some(bom_ref))
}
#[must_use]
pub fn evidence_by_ref(&self, bom_ref: &str) -> Option<&DeclaredEvidence> {
self.evidence
.iter()
.find(|e| e.bom_ref.as_deref() == Some(bom_ref))
}
#[must_use]
pub fn assessor_by_ref(&self, bom_ref: &str) -> Option<&DeclaredAssessor> {
self.assessors
.iter()
.find(|a| a.bom_ref.as_deref() == Some(bom_ref))
}
#[must_use]
pub fn requirement_by_ref(
&self,
bom_ref: &str,
) -> Option<(&DefinedStandard, &DefinedRequirement)> {
self.standards.iter().find_map(|standard| {
standard
.requirements
.iter()
.find(|req| req.bom_ref.as_deref() == Some(bom_ref))
.map(|req| (standard, req))
})
}
#[must_use]
pub fn document_evidence_level(&self) -> EvidenceLevel {
let affirmation_signed = self.affirmation.as_ref().is_some_and(|a| {
a.signature.is_some() || a.signatories.iter().any(|s| s.signature.is_some())
});
if self.signature.is_some() || affirmation_signed {
EvidenceLevel::SignaturePresent
} else {
EvidenceLevel::Structural
}
}
#[must_use]
pub fn supported_requirements(&self, as_of: DateTime<Utc>) -> Vec<SupportedRequirement<'_>> {
let mut supported = Vec::new();
for attestation in &self.attestations {
for entry in &attestation.map {
let Some(requirement_ref) = &entry.requirement else {
continue;
};
if !matches!(requirement_ref.resolution, CdxaResolution::Requirement) {
continue;
}
let Some((standard, requirement)) = self.requirement_by_ref(&requirement_ref.raw)
else {
continue;
};
if !entry.conformance_score.is_some_and(|score| score >= 1.0) {
continue;
}
if !entry.counter_claims.is_empty() {
continue;
}
let mut supporting_claims = Vec::new();
let mut supporting_evidence_signed = false;
for claim_ref in &entry.claims {
if !matches!(claim_ref.resolution, CdxaResolution::Claim) {
continue;
}
let Some(claim) = self.claim_by_ref(&claim_ref.raw) else {
continue;
};
if !claim.target.as_ref().is_some_and(CdxaRef::is_resolved) {
continue;
}
if !claim.counter_evidence.is_empty() {
continue;
}
let fresh_evidence: Vec<&DeclaredEvidence> = claim
.evidence
.iter()
.filter(|e| matches!(e.resolution, CdxaResolution::Evidence))
.filter_map(|e| self.evidence_by_ref(&e.raw))
.filter(|evidence| evidence.is_fresh(as_of))
.collect();
if fresh_evidence.is_empty() {
continue;
}
if claim.signature.is_some()
|| fresh_evidence.iter().any(|e| e.signature.is_some())
{
supporting_evidence_signed = true;
}
supporting_claims.push(claim);
}
if supporting_claims.is_empty() {
continue;
}
let signature_present = self.signature.is_some()
|| attestation.signature.is_some()
|| supporting_evidence_signed;
let evidence_level = if signature_present {
EvidenceLevel::SignaturePresent
} else {
EvidenceLevel::Structural
};
let third_party_assessed = attestation
.assessor
.as_ref()
.filter(|a| matches!(a.resolution, CdxaResolution::Assessor))
.and_then(|a| self.assessor_by_ref(&a.raw))
.and_then(|a| a.third_party)
.unwrap_or(false);
supported.push(SupportedRequirement {
standard,
requirement,
attestation,
map_entry: entry,
supporting_claims,
evidence_level,
third_party_assessed,
});
}
}
supported
}
#[must_use]
pub fn evidence_for_family(
&self,
family: AttestationRuleFamily,
as_of: DateTime<Utc>,
) -> Vec<SupportedRequirement<'_>> {
self.supported_requirements(as_of)
.into_iter()
.filter(|s| AttestationRuleFamily::classify(s.standard, s.requirement) == Some(family))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ts(s: &str) -> DateTime<Utc> {
DateTime::parse_from_rfc3339(s)
.expect("test timestamp must parse")
.with_timezone(&Utc)
}
#[test]
fn evidence_level_ordering_reflects_strength() {
assert!(EvidenceLevel::SelfDeclared < EvidenceLevel::Structural);
assert!(EvidenceLevel::Structural < EvidenceLevel::SignaturePresent);
assert!(EvidenceLevel::SignaturePresent < EvidenceLevel::SignatureVerified);
}
#[test]
fn signature_presence_handles_all_jsf_forms() {
let single = serde_json::json!({"algorithm": "ES256", "keyId": "k1", "value": "sig"});
let presence = SignaturePresence::from_jsf(&single).expect("object form");
assert_eq!(presence.algorithm.as_deref(), Some("ES256"));
assert_eq!(presence.key_id.as_deref(), Some("k1"));
assert_eq!(presence.signer_count, 1);
let signers = serde_json::json!({"signers": [
{"algorithm": "Ed25519", "value": "a"},
{"algorithm": "ES256", "value": "b"}
]});
let presence = SignaturePresence::from_jsf(&signers).expect("signers form");
assert_eq!(presence.algorithm.as_deref(), Some("Ed25519"));
assert_eq!(presence.signer_count, 2);
let chain = serde_json::json!({"chain": [{"algorithm": "RS256", "value": "a"}]});
let presence = SignaturePresence::from_jsf(&chain).expect("chain form");
assert_eq!(presence.signer_count, 1);
assert_eq!(presence.algorithm.as_deref(), Some("RS256"));
assert!(SignaturePresence::from_jsf(&serde_json::json!("not-an-object")).is_none());
}
#[test]
fn evidence_freshness_fails_closed_on_expiry_and_future_creation() {
let evidence = DeclaredEvidence {
created: Some(ts("2026-01-10T00:00:00Z")),
expires: Some(ts("2027-01-10T00:00:00Z")),
..DeclaredEvidence::default()
};
assert!(evidence.is_fresh(ts("2026-06-01T00:00:00Z")));
assert!(!evidence.is_fresh(ts("2027-01-10T00:00:00Z")));
assert!(!evidence.is_fresh(ts("2027-06-01T00:00:00Z")));
assert!(!evidence.is_fresh(ts("2025-12-01T00:00:00Z")));
assert!(DeclaredEvidence::default().is_fresh(ts("2026-06-01T00:00:00Z")));
}
#[test]
fn rule_family_classification() {
let ssdf = DefinedStandard {
name: Some("NIST Secure Software Development Framework".to_string()),
..DefinedStandard::default()
};
let ps1 = DefinedRequirement {
identifier: Some("PS.1".to_string()),
..DefinedRequirement::default()
};
assert_eq!(
AttestationRuleFamily::classify(&ssdf, &ps1),
Some(AttestationRuleFamily::Ssdf)
);
let eo = DefinedStandard {
name: Some("Executive Order 14028 4(e) attestation".to_string()),
..DefinedStandard::default()
};
assert_eq!(
AttestationRuleFamily::classify(&eo, &ps1),
Some(AttestationRuleFamily::Eo14028)
);
let unlabeled = DefinedStandard::default();
assert_eq!(
AttestationRuleFamily::classify(&unlabeled, &ps1),
Some(AttestationRuleFamily::Ssdf)
);
let scrappy = DefinedStandard {
name: Some("Scrappy Custom Framework".to_string()),
..DefinedStandard::default()
};
let generic_req = DefinedRequirement {
identifier: Some("REQ-1".to_string()),
..DefinedRequirement::default()
};
assert_eq!(
AttestationRuleFamily::classify(&scrappy, &generic_req),
None
);
let cra = DefinedStandard {
name: Some("EU CRA Annex I encoding".to_string()),
..DefinedStandard::default()
};
assert_eq!(
AttestationRuleFamily::classify(&cra, &generic_req),
Some(AttestationRuleFamily::Cra)
);
}
}