1use super::{CanonicalId, Contact, ExternalReference, Organization};
55use chrono::{DateTime, Utc};
56use serde::{Deserialize, Serialize};
57
58#[derive(
66 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
67)]
68pub enum EvidenceLevel {
69 #[default]
71 SelfDeclared,
72 Structural,
74 SignaturePresent,
76 SignatureVerified,
78}
79
80impl std::fmt::Display for EvidenceLevel {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::SelfDeclared => write!(f, "self-declared"),
84 Self::Structural => write!(f, "structural"),
85 Self::SignaturePresent => write!(f, "signature-present"),
86 Self::SignatureVerified => write!(f, "signature-verified"),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
97pub struct SignaturePresence {
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub algorithm: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub key_id: Option<String>,
105 pub signer_count: usize,
108}
109
110impl SignaturePresence {
111 #[must_use]
117 pub fn from_jsf(value: &serde_json::Value) -> Option<Self> {
118 let obj = value.as_object()?;
119 for multi_key in ["signers", "chain"] {
120 if let Some(entries) = obj.get(multi_key).and_then(|v| v.as_array()) {
121 let first = entries.first().and_then(|v| v.as_object());
122 return Some(Self {
123 algorithm: first
124 .and_then(|o| o.get("algorithm"))
125 .and_then(|v| v.as_str())
126 .map(String::from),
127 key_id: first
128 .and_then(|o| o.get("keyId"))
129 .and_then(|v| v.as_str())
130 .map(String::from),
131 signer_count: entries.len(),
132 });
133 }
134 }
135 Some(Self {
136 algorithm: obj
137 .get("algorithm")
138 .and_then(|v| v.as_str())
139 .map(String::from),
140 key_id: obj.get("keyId").and_then(|v| v.as_str()).map(String::from),
141 signer_count: 1,
142 })
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub enum CdxaResolution {
149 Claim,
151 Evidence,
153 Assessor,
155 Requirement,
157 Target,
160 Inventory(CanonicalId),
163 Dangling,
166}
167
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct CdxaRef {
171 pub raw: String,
173 pub resolution: CdxaResolution,
175}
176
177impl CdxaRef {
178 #[must_use]
180 pub fn is_resolved(&self) -> bool {
181 !matches!(self.resolution, CdxaResolution::Dangling)
182 }
183}
184
185#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
187pub struct DeclaredAssessor {
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub bom_ref: Option<String>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub third_party: Option<bool>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub organization: Option<Organization>,
197}
198
199#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
202pub struct AttestationAssertion {
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub summary: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub assessor: Option<CdxaRef>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
209 pub map: Vec<AttestationMapEntry>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub signature: Option<SignaturePresence>,
213}
214
215#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
218pub struct AttestationMapEntry {
219 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub requirement: Option<CdxaRef>,
223 #[serde(default, skip_serializing_if = "Vec::is_empty")]
225 pub claims: Vec<CdxaRef>,
226 #[serde(default, skip_serializing_if = "Vec::is_empty")]
229 pub counter_claims: Vec<CdxaRef>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub conformance_score: Option<f64>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub conformance_rationale: Option<String>,
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub conformance_mitigation_strategies: Vec<CdxaRef>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub confidence_score: Option<f64>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub confidence_rationale: Option<String>,
245}
246
247#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
249pub struct DeclaredClaim {
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub bom_ref: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub target: Option<CdxaRef>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub predicate: Option<String>,
261 #[serde(default, skip_serializing_if = "Vec::is_empty")]
264 pub mitigation_strategies: Vec<CdxaRef>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub reasoning: Option<String>,
268 #[serde(default, skip_serializing_if = "Vec::is_empty")]
270 pub evidence: Vec<CdxaRef>,
271 #[serde(default, skip_serializing_if = "Vec::is_empty")]
274 pub counter_evidence: Vec<CdxaRef>,
275 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 pub external_refs: Vec<ExternalReference>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub signature: Option<SignaturePresence>,
280}
281
282#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
284pub struct EvidenceDataItem {
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub name: Option<String>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub url: Option<String>,
290 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
293 pub has_attachment: bool,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub classification: Option<String>,
297 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub sensitive_data: Vec<String>,
300 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
302 pub has_governance: bool,
303}
304
305#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
307pub struct DeclaredEvidence {
308 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub bom_ref: Option<String>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub property_name: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub description: Option<String>,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 pub data: Vec<EvidenceDataItem>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub created: Option<DateTime<Utc>>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub expires: Option<DateTime<Utc>>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub author: Option<Contact>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub reviewer: Option<Contact>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub signature: Option<SignaturePresence>,
330}
331
332impl DeclaredEvidence {
333 #[must_use]
342 pub fn is_fresh(&self, as_of: DateTime<Utc>) -> bool {
343 if let Some(expires) = self.expires
344 && expires <= as_of
345 {
346 return false;
347 }
348 if let Some(created) = self.created
349 && created > as_of
350 {
351 return false;
352 }
353 true
354 }
355}
356
357#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
361pub struct DeclarationTarget {
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub bom_ref: Option<String>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub name: Option<String>,
366}
367
368#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
370pub struct DeclarationTargets {
371 #[serde(default, skip_serializing_if = "Vec::is_empty")]
372 pub organizations: Vec<DeclarationTarget>,
373 #[serde(default, skip_serializing_if = "Vec::is_empty")]
374 pub components: Vec<DeclarationTarget>,
375 #[serde(default, skip_serializing_if = "Vec::is_empty")]
376 pub services: Vec<DeclarationTarget>,
377}
378
379#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
387pub struct AffirmationSignatory {
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub name: Option<String>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub role: Option<String>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub signature: Option<SignaturePresence>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub organization: Option<Organization>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub external_reference: Option<ExternalReference>,
399}
400
401impl AffirmationSignatory {
402 #[must_use]
405 pub fn has_complete_identity(&self) -> bool {
406 self.signature.is_some()
407 || (self.external_reference.is_some() && self.organization.is_some())
408 }
409}
410
411#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
413pub struct DeclaredAffirmation {
414 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub statement: Option<String>,
417 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub signatories: Vec<AffirmationSignatory>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
421 pub signature: Option<SignaturePresence>,
422}
423
424#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
431pub struct DefinedRequirement {
432 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub bom_ref: Option<String>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub identifier: Option<String>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub title: Option<String>,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub text: Option<String>,
442 #[serde(default, skip_serializing_if = "Vec::is_empty")]
443 pub descriptions: Vec<String>,
444 #[serde(default, skip_serializing_if = "Vec::is_empty")]
445 pub open_cre: Vec<String>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub parent: Option<String>,
449 #[serde(default, skip_serializing_if = "Vec::is_empty")]
450 pub external_refs: Vec<ExternalReference>,
451}
452
453#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
456pub struct DefinedStandard {
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub bom_ref: Option<String>,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub name: Option<String>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub version: Option<String>,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub description: Option<String>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub owner: Option<String>,
467 #[serde(default, skip_serializing_if = "Vec::is_empty")]
468 pub requirements: Vec<DefinedRequirement>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub signature: Option<SignaturePresence>,
472}
473
474#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
484pub struct AttestationDeclarations {
485 #[serde(default, skip_serializing_if = "Vec::is_empty")]
486 pub assessors: Vec<DeclaredAssessor>,
487 #[serde(default, skip_serializing_if = "Vec::is_empty")]
488 pub attestations: Vec<AttestationAssertion>,
489 #[serde(default, skip_serializing_if = "Vec::is_empty")]
490 pub claims: Vec<DeclaredClaim>,
491 #[serde(default, skip_serializing_if = "Vec::is_empty")]
492 pub evidence: Vec<DeclaredEvidence>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub targets: Option<DeclarationTargets>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub affirmation: Option<DeclaredAffirmation>,
497 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub signature: Option<SignaturePresence>,
501 #[serde(default, skip_serializing_if = "Vec::is_empty")]
504 pub standards: Vec<DefinedStandard>,
505}
506
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
512pub enum AttestationRuleFamily {
513 Ssdf,
515 Eo14028,
517 Cra,
519}
520
521impl AttestationRuleFamily {
522 #[must_use]
530 pub fn classify(standard: &DefinedStandard, requirement: &DefinedRequirement) -> Option<Self> {
531 let hay = format!(
532 "{} {} {}",
533 standard.name.as_deref().unwrap_or(""),
534 standard.description.as_deref().unwrap_or(""),
535 standard.owner.as_deref().unwrap_or("")
536 )
537 .to_lowercase();
538 if hay.contains("ssdf") || hay.contains("secure software development framework") {
539 return Some(Self::Ssdf);
540 }
541 if hay.contains("14028") || hay.contains("executive order") {
542 return Some(Self::Eo14028);
543 }
544 if hay.contains("cyber resilience")
545 || hay
546 .split(|c: char| !c.is_ascii_alphanumeric())
547 .any(|token| token == "cra")
548 {
549 return Some(Self::Cra);
550 }
551 if Self::is_ssdf_practice_id(requirement.identifier.as_deref()) {
552 return Some(Self::Ssdf);
553 }
554 None
555 }
556
557 fn is_ssdf_practice_id(identifier: Option<&str>) -> bool {
559 let Some(identifier) = identifier else {
560 return false;
561 };
562 let id = identifier.trim().to_ascii_uppercase();
563 ["PO.", "PS.", "PW.", "RV."].iter().any(|prefix| {
564 id.strip_prefix(prefix)
565 .is_some_and(|rest| rest.chars().next().is_some_and(|c| c.is_ascii_digit()))
566 })
567 }
568}
569
570#[derive(Debug, Clone)]
575pub struct SupportedRequirement<'a> {
576 pub standard: &'a DefinedStandard,
578 pub requirement: &'a DefinedRequirement,
581 pub attestation: &'a AttestationAssertion,
583 pub map_entry: &'a AttestationMapEntry,
585 pub supporting_claims: Vec<&'a DeclaredClaim>,
588 pub evidence_level: EvidenceLevel,
593 pub third_party_assessed: bool,
596}
597
598impl AttestationDeclarations {
599 #[must_use]
601 pub fn claim_by_ref(&self, bom_ref: &str) -> Option<&DeclaredClaim> {
602 self.claims
603 .iter()
604 .find(|c| c.bom_ref.as_deref() == Some(bom_ref))
605 }
606
607 #[must_use]
609 pub fn evidence_by_ref(&self, bom_ref: &str) -> Option<&DeclaredEvidence> {
610 self.evidence
611 .iter()
612 .find(|e| e.bom_ref.as_deref() == Some(bom_ref))
613 }
614
615 #[must_use]
617 pub fn assessor_by_ref(&self, bom_ref: &str) -> Option<&DeclaredAssessor> {
618 self.assessors
619 .iter()
620 .find(|a| a.bom_ref.as_deref() == Some(bom_ref))
621 }
622
623 #[must_use]
625 pub fn requirement_by_ref(
626 &self,
627 bom_ref: &str,
628 ) -> Option<(&DefinedStandard, &DefinedRequirement)> {
629 self.standards.iter().find_map(|standard| {
630 standard
631 .requirements
632 .iter()
633 .find(|req| req.bom_ref.as_deref() == Some(bom_ref))
634 .map(|req| (standard, req))
635 })
636 }
637
638 #[must_use]
644 pub fn document_evidence_level(&self) -> EvidenceLevel {
645 let affirmation_signed = self.affirmation.as_ref().is_some_and(|a| {
646 a.signature.is_some() || a.signatories.iter().any(|s| s.signature.is_some())
647 });
648 if self.signature.is_some() || affirmation_signed {
649 EvidenceLevel::SignaturePresent
650 } else {
651 EvidenceLevel::Structural
652 }
653 }
654
655 #[must_use]
678 pub fn supported_requirements(&self, as_of: DateTime<Utc>) -> Vec<SupportedRequirement<'_>> {
679 let mut supported = Vec::new();
680 for attestation in &self.attestations {
681 for entry in &attestation.map {
682 let Some(requirement_ref) = &entry.requirement else {
683 continue;
684 };
685 if !matches!(requirement_ref.resolution, CdxaResolution::Requirement) {
686 continue;
687 }
688 let Some((standard, requirement)) = self.requirement_by_ref(&requirement_ref.raw)
689 else {
690 continue;
691 };
692 if !entry.conformance_score.is_some_and(|score| score >= 1.0) {
693 continue;
694 }
695 if !entry.counter_claims.is_empty() {
696 continue;
697 }
698
699 let mut supporting_claims = Vec::new();
700 let mut supporting_evidence_signed = false;
701 for claim_ref in &entry.claims {
702 if !matches!(claim_ref.resolution, CdxaResolution::Claim) {
703 continue;
704 }
705 let Some(claim) = self.claim_by_ref(&claim_ref.raw) else {
706 continue;
707 };
708 if !claim.target.as_ref().is_some_and(CdxaRef::is_resolved) {
709 continue;
710 }
711 if !claim.counter_evidence.is_empty() {
712 continue;
713 }
714 let fresh_evidence: Vec<&DeclaredEvidence> = claim
715 .evidence
716 .iter()
717 .filter(|e| matches!(e.resolution, CdxaResolution::Evidence))
718 .filter_map(|e| self.evidence_by_ref(&e.raw))
719 .filter(|evidence| evidence.is_fresh(as_of))
720 .collect();
721 if fresh_evidence.is_empty() {
722 continue;
723 }
724 if claim.signature.is_some()
725 || fresh_evidence.iter().any(|e| e.signature.is_some())
726 {
727 supporting_evidence_signed = true;
728 }
729 supporting_claims.push(claim);
730 }
731 if supporting_claims.is_empty() {
732 continue;
733 }
734
735 let signature_present = self.signature.is_some()
736 || attestation.signature.is_some()
737 || supporting_evidence_signed;
738 let evidence_level = if signature_present {
739 EvidenceLevel::SignaturePresent
740 } else {
741 EvidenceLevel::Structural
742 };
743 let third_party_assessed = attestation
744 .assessor
745 .as_ref()
746 .filter(|a| matches!(a.resolution, CdxaResolution::Assessor))
747 .and_then(|a| self.assessor_by_ref(&a.raw))
748 .and_then(|a| a.third_party)
749 .unwrap_or(false);
750
751 supported.push(SupportedRequirement {
752 standard,
753 requirement,
754 attestation,
755 map_entry: entry,
756 supporting_claims,
757 evidence_level,
758 third_party_assessed,
759 });
760 }
761 }
762 supported
763 }
764
765 #[must_use]
770 pub fn evidence_for_family(
771 &self,
772 family: AttestationRuleFamily,
773 as_of: DateTime<Utc>,
774 ) -> Vec<SupportedRequirement<'_>> {
775 self.supported_requirements(as_of)
776 .into_iter()
777 .filter(|s| AttestationRuleFamily::classify(s.standard, s.requirement) == Some(family))
778 .collect()
779 }
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785
786 fn ts(s: &str) -> DateTime<Utc> {
787 DateTime::parse_from_rfc3339(s)
788 .expect("test timestamp must parse")
789 .with_timezone(&Utc)
790 }
791
792 #[test]
793 fn evidence_level_ordering_reflects_strength() {
794 assert!(EvidenceLevel::SelfDeclared < EvidenceLevel::Structural);
795 assert!(EvidenceLevel::Structural < EvidenceLevel::SignaturePresent);
796 assert!(EvidenceLevel::SignaturePresent < EvidenceLevel::SignatureVerified);
797 }
798
799 #[test]
800 fn signature_presence_handles_all_jsf_forms() {
801 let single = serde_json::json!({"algorithm": "ES256", "keyId": "k1", "value": "sig"});
802 let presence = SignaturePresence::from_jsf(&single).expect("object form");
803 assert_eq!(presence.algorithm.as_deref(), Some("ES256"));
804 assert_eq!(presence.key_id.as_deref(), Some("k1"));
805 assert_eq!(presence.signer_count, 1);
806
807 let signers = serde_json::json!({"signers": [
808 {"algorithm": "Ed25519", "value": "a"},
809 {"algorithm": "ES256", "value": "b"}
810 ]});
811 let presence = SignaturePresence::from_jsf(&signers).expect("signers form");
812 assert_eq!(presence.algorithm.as_deref(), Some("Ed25519"));
813 assert_eq!(presence.signer_count, 2);
814
815 let chain = serde_json::json!({"chain": [{"algorithm": "RS256", "value": "a"}]});
816 let presence = SignaturePresence::from_jsf(&chain).expect("chain form");
817 assert_eq!(presence.signer_count, 1);
818 assert_eq!(presence.algorithm.as_deref(), Some("RS256"));
819
820 assert!(SignaturePresence::from_jsf(&serde_json::json!("not-an-object")).is_none());
821 }
822
823 #[test]
824 fn evidence_freshness_fails_closed_on_expiry_and_future_creation() {
825 let evidence = DeclaredEvidence {
826 created: Some(ts("2026-01-10T00:00:00Z")),
827 expires: Some(ts("2027-01-10T00:00:00Z")),
828 ..DeclaredEvidence::default()
829 };
830 assert!(evidence.is_fresh(ts("2026-06-01T00:00:00Z")));
831 assert!(!evidence.is_fresh(ts("2027-01-10T00:00:00Z")));
833 assert!(!evidence.is_fresh(ts("2027-06-01T00:00:00Z")));
834 assert!(!evidence.is_fresh(ts("2025-12-01T00:00:00Z")));
836 assert!(DeclaredEvidence::default().is_fresh(ts("2026-06-01T00:00:00Z")));
838 }
839
840 #[test]
841 fn rule_family_classification() {
842 let ssdf = DefinedStandard {
843 name: Some("NIST Secure Software Development Framework".to_string()),
844 ..DefinedStandard::default()
845 };
846 let ps1 = DefinedRequirement {
847 identifier: Some("PS.1".to_string()),
848 ..DefinedRequirement::default()
849 };
850 assert_eq!(
851 AttestationRuleFamily::classify(&ssdf, &ps1),
852 Some(AttestationRuleFamily::Ssdf)
853 );
854
855 let eo = DefinedStandard {
857 name: Some("Executive Order 14028 4(e) attestation".to_string()),
858 ..DefinedStandard::default()
859 };
860 assert_eq!(
861 AttestationRuleFamily::classify(&eo, &ps1),
862 Some(AttestationRuleFamily::Eo14028)
863 );
864
865 let unlabeled = DefinedStandard::default();
867 assert_eq!(
868 AttestationRuleFamily::classify(&unlabeled, &ps1),
869 Some(AttestationRuleFamily::Ssdf)
870 );
871
872 let scrappy = DefinedStandard {
874 name: Some("Scrappy Custom Framework".to_string()),
875 ..DefinedStandard::default()
876 };
877 let generic_req = DefinedRequirement {
878 identifier: Some("REQ-1".to_string()),
879 ..DefinedRequirement::default()
880 };
881 assert_eq!(
882 AttestationRuleFamily::classify(&scrappy, &generic_req),
883 None
884 );
885 let cra = DefinedStandard {
886 name: Some("EU CRA Annex I encoding".to_string()),
887 ..DefinedStandard::default()
888 };
889 assert_eq!(
890 AttestationRuleFamily::classify(&cra, &generic_req),
891 Some(AttestationRuleFamily::Cra)
892 );
893 }
894}