baseid_pctf/
report.rs

1//! PCTF compliance self-assessment reporting.
2//!
3//! Generates a structured report covering all 5 PCTF components,
4//! with bilingual EN/FR output support.
5
6use baseid_core::types::AssuranceLevel;
7use serde::{Deserialize, Serialize};
8
9/// Conformance status for a PCTF component.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ComponentStatus {
12    /// Fully conformant with PCTF requirements.
13    Conformant,
14    /// Partially conformant (some requirements met).
15    PartiallyConformant,
16    /// Not conformant.
17    NonConformant,
18    /// Not applicable to this deployment.
19    NotApplicable,
20}
21
22impl ComponentStatus {
23    /// Bilingual display name.
24    pub fn display(&self, french: bool) -> &'static str {
25        match (self, french) {
26            (Self::Conformant, false) => "Conformant",
27            (Self::Conformant, true) => "Conforme",
28            (Self::PartiallyConformant, false) => "Partially Conformant",
29            (Self::PartiallyConformant, true) => "Partiellement conforme",
30            (Self::NonConformant, false) => "Non-Conformant",
31            (Self::NonConformant, true) => "Non conforme",
32            (Self::NotApplicable, false) => "Not Applicable",
33            (Self::NotApplicable, true) => "Sans objet",
34        }
35    }
36}
37
38/// Assessment of a single PCTF component.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ComponentAssessment {
41    /// PCTF component name.
42    pub component: String,
43    /// PCTF component name in French.
44    pub component_fr: String,
45    /// Conformance status.
46    pub status: ComponentStatus,
47    /// Evidence/justification for the status.
48    pub evidence: Vec<String>,
49    /// Recommendations for improvement.
50    pub recommendations: Vec<String>,
51}
52
53/// Full PCTF compliance self-assessment report.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct PctfComplianceReport {
56    /// Report title.
57    pub title: String,
58    /// Report title in French.
59    pub title_fr: String,
60    /// Organization / system name.
61    pub system_name: String,
62    /// Report generation timestamp (RFC 3339).
63    pub generated_at: String,
64    /// Target assurance level.
65    pub target_level: AssuranceLevel,
66    /// Component assessments.
67    pub components: Vec<ComponentAssessment>,
68    /// Overall conformance status.
69    pub overall_status: ComponentStatus,
70    /// Summary observations.
71    pub summary: String,
72    /// Summary in French.
73    pub summary_fr: String,
74}
75
76/// Builder for PCTF compliance reports.
77pub struct ReportBuilder {
78    system_name: String,
79    target_level: AssuranceLevel,
80    has_assurance_eval: bool,
81    has_consent_mgmt: bool,
82    has_audit_logging: bool,
83    has_crypto_integrity: bool,
84    has_revocation: bool,
85    evidence_types_supported: usize,
86    generated_at: String,
87}
88
89impl ReportBuilder {
90    /// Create a new report builder.
91    pub fn new(
92        system_name: impl Into<String>,
93        target_level: AssuranceLevel,
94        generated_at: impl Into<String>,
95    ) -> Self {
96        Self {
97            system_name: system_name.into(),
98            target_level,
99            has_assurance_eval: false,
100            has_consent_mgmt: false,
101            has_audit_logging: false,
102            has_crypto_integrity: false,
103            has_revocation: false,
104            evidence_types_supported: 0,
105            generated_at: generated_at.into(),
106        }
107    }
108
109    /// Set whether assurance level evaluation is implemented.
110    pub fn with_assurance_evaluation(mut self, enabled: bool, evidence_types: usize) -> Self {
111        self.has_assurance_eval = enabled;
112        self.evidence_types_supported = evidence_types;
113        self
114    }
115
116    /// Set whether consent management is implemented.
117    pub fn with_consent_management(mut self, enabled: bool) -> Self {
118        self.has_consent_mgmt = enabled;
119        self
120    }
121
122    /// Set whether audit logging is implemented.
123    pub fn with_audit_logging(mut self, enabled: bool) -> Self {
124        self.has_audit_logging = enabled;
125        self
126    }
127
128    /// Set whether cryptographic integrity (signing, verification) is implemented.
129    pub fn with_crypto_integrity(mut self, enabled: bool) -> Self {
130        self.has_crypto_integrity = enabled;
131        self
132    }
133
134    /// Set whether credential revocation is implemented.
135    pub fn with_revocation(mut self, enabled: bool) -> Self {
136        self.has_revocation = enabled;
137        self
138    }
139
140    /// Generate the compliance report.
141    pub fn build(self) -> PctfComplianceReport {
142        let mut components = Vec::new();
143
144        // Component 1: Verified Person
145        let vp_status = if self.has_assurance_eval && self.evidence_types_supported >= 5 {
146            ComponentStatus::Conformant
147        } else if self.has_assurance_eval {
148            ComponentStatus::PartiallyConformant
149        } else {
150            ComponentStatus::NonConformant
151        };
152        components.push(ComponentAssessment {
153            component: "Verified Person".to_string(),
154            component_fr: "Personne vérifiée".to_string(),
155            status: vp_status,
156            evidence: if self.has_assurance_eval {
157                vec![
158                    format!(
159                        "IAL evaluation implemented with {} evidence types",
160                        self.evidence_types_supported
161                    ),
162                    format!(
163                        "Supports Levels 1-3 (target: {})",
164                        self.target_level.pctf_name()
165                    ),
166                ]
167            } else {
168                vec!["IAL evaluation not implemented".to_string()]
169            },
170            recommendations: if vp_status == ComponentStatus::Conformant {
171                vec![]
172            } else {
173                vec!["Implement full evidence taxonomy for all PCTF evidence types".to_string()]
174            },
175        });
176
177        // Component 2: Verified Organization (assessed as N/A for individual wallets)
178        components.push(ComponentAssessment {
179            component: "Verified Organization".to_string(),
180            component_fr: "Organisation vérifiée".to_string(),
181            status: ComponentStatus::NotApplicable,
182            evidence: vec!["Individual wallet deployment — organizational verification is handled by issuer infrastructure".to_string()],
183            recommendations: vec![],
184        });
185
186        // Component 3: Credential Management
187        let cm_status = if self.has_crypto_integrity && self.has_revocation {
188            ComponentStatus::Conformant
189        } else if self.has_crypto_integrity {
190            ComponentStatus::PartiallyConformant
191        } else {
192            ComponentStatus::NonConformant
193        };
194        components.push(ComponentAssessment {
195            component: "Credential Management".to_string(),
196            component_fr: "Gestion des justificatifs".to_string(),
197            status: cm_status,
198            evidence: {
199                let mut ev = Vec::new();
200                if self.has_crypto_integrity {
201                    ev.push("Cryptographic signing and verification implemented".to_string());
202                }
203                if self.has_revocation {
204                    ev.push("Credential revocation support implemented".to_string());
205                }
206                if ev.is_empty() {
207                    ev.push("Credential management not fully implemented".to_string());
208                }
209                ev
210            },
211            recommendations: if cm_status == ComponentStatus::Conformant {
212                vec![]
213            } else {
214                vec!["Implement credential revocation checking".to_string()]
215            },
216        });
217
218        // Component 4: Notice & Consent
219        let nc_status = if self.has_consent_mgmt {
220            ComponentStatus::Conformant
221        } else {
222            ComponentStatus::NonConformant
223        };
224        components.push(ComponentAssessment {
225            component: "Notice & Consent".to_string(),
226            component_fr: "Avis et consentement".to_string(),
227            status: nc_status,
228            evidence: if self.has_consent_mgmt {
229                vec![
230                    "Consent lifecycle management implemented".to_string(),
231                    "Purpose limitation enforcement active".to_string(),
232                    "Consent expiry and revocation supported".to_string(),
233                ]
234            } else {
235                vec!["Consent management not implemented".to_string()]
236            },
237            recommendations: if nc_status == ComponentStatus::Conformant {
238                vec![]
239            } else {
240                vec!["Implement ConsentManager with purpose limitation".to_string()]
241            },
242        });
243
244        // Component 5: Digital Integrity / Privacy
245        let di_status = if self.has_audit_logging && self.has_crypto_integrity {
246            ComponentStatus::Conformant
247        } else if self.has_audit_logging || self.has_crypto_integrity {
248            ComponentStatus::PartiallyConformant
249        } else {
250            ComponentStatus::NonConformant
251        };
252        components.push(ComponentAssessment {
253            component: "Digital Integrity".to_string(),
254            component_fr: "Intégrité numérique".to_string(),
255            status: di_status,
256            evidence: {
257                let mut ev = Vec::new();
258                if self.has_audit_logging {
259                    ev.push("Hash-chained audit log with tamper detection".to_string());
260                    ev.push("Privacy redaction policy for audit exports".to_string());
261                }
262                if self.has_crypto_integrity {
263                    ev.push("Ed25519 / P-256 cryptographic signatures".to_string());
264                }
265                if ev.is_empty() {
266                    ev.push("Digital integrity controls not implemented".to_string());
267                }
268                ev
269            },
270            recommendations: if di_status == ComponentStatus::Conformant {
271                vec![]
272            } else {
273                let mut recs = Vec::new();
274                if !self.has_audit_logging {
275                    recs.push("Implement audit logging with hash chaining".to_string());
276                }
277                if !self.has_crypto_integrity {
278                    recs.push("Implement cryptographic signing".to_string());
279                }
280                recs
281            },
282        });
283
284        // Overall status
285        let conformant_count = components
286            .iter()
287            .filter(|c| {
288                c.status == ComponentStatus::Conformant
289                    || c.status == ComponentStatus::NotApplicable
290            })
291            .count();
292        let overall_status = if conformant_count == components.len() {
293            ComponentStatus::Conformant
294        } else if conformant_count > 0 {
295            ComponentStatus::PartiallyConformant
296        } else {
297            ComponentStatus::NonConformant
298        };
299
300        let summary = format!(
301            "{} of {} PCTF components are conformant (target: {}).",
302            conformant_count,
303            components.len(),
304            self.target_level.pctf_name()
305        );
306        let summary_fr = format!(
307            "{} des {} composantes du CCNIP sont conformes (cible : {}).",
308            conformant_count,
309            components.len(),
310            self.target_level.pctf_name()
311        );
312
313        PctfComplianceReport {
314            title: "PCTF Compliance Self-Assessment Report".to_string(),
315            title_fr: "Rapport d'auto-évaluation de conformité au CCNIP".to_string(),
316            system_name: self.system_name,
317            generated_at: self.generated_at,
318            target_level: self.target_level,
319            components,
320            overall_status,
321            summary,
322            summary_fr,
323        }
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn fully_conformant_report() {
333        let report = ReportBuilder::new(
334            "BaseID Wallet",
335            AssuranceLevel::Substantial,
336            "2026-03-22T00:00:00Z",
337        )
338        .with_assurance_evaluation(true, 11)
339        .with_consent_management(true)
340        .with_audit_logging(true)
341        .with_crypto_integrity(true)
342        .with_revocation(true)
343        .build();
344
345        assert_eq!(report.overall_status, ComponentStatus::Conformant);
346        assert_eq!(report.components.len(), 5);
347        assert!(report.summary.contains("5 of 5"));
348    }
349
350    #[test]
351    fn partially_conformant_report() {
352        let report = ReportBuilder::new("TestSystem", AssuranceLevel::Low, "2026-03-22T00:00:00Z")
353            .with_assurance_evaluation(true, 3)
354            .with_consent_management(false)
355            .with_audit_logging(false)
356            .with_crypto_integrity(true)
357            .with_revocation(false)
358            .build();
359
360        assert_eq!(report.overall_status, ComponentStatus::PartiallyConformant);
361    }
362
363    #[test]
364    fn bilingual_component_status() {
365        assert_eq!(ComponentStatus::Conformant.display(false), "Conformant");
366        assert_eq!(ComponentStatus::Conformant.display(true), "Conforme");
367        assert_eq!(
368            ComponentStatus::PartiallyConformant.display(false),
369            "Partially Conformant"
370        );
371        assert_eq!(
372            ComponentStatus::PartiallyConformant.display(true),
373            "Partiellement conforme"
374        );
375        assert_eq!(
376            ComponentStatus::NonConformant.display(false),
377            "Non-Conformant"
378        );
379        assert_eq!(ComponentStatus::NonConformant.display(true), "Non conforme");
380        assert_eq!(
381            ComponentStatus::NotApplicable.display(false),
382            "Not Applicable"
383        );
384        assert_eq!(ComponentStatus::NotApplicable.display(true), "Sans objet");
385    }
386
387    #[test]
388    fn report_serde_roundtrip() {
389        let report = ReportBuilder::new("BaseID", AssuranceLevel::High, "2026-03-22T00:00:00Z")
390            .with_assurance_evaluation(true, 11)
391            .with_consent_management(true)
392            .with_audit_logging(true)
393            .with_crypto_integrity(true)
394            .with_revocation(true)
395            .build();
396
397        let json = serde_json::to_string_pretty(&report).unwrap();
398        let back: PctfComplianceReport = serde_json::from_str(&json).unwrap();
399        assert_eq!(back.system_name, "BaseID");
400        assert_eq!(back.overall_status, ComponentStatus::Conformant);
401        assert_eq!(back.components.len(), 5);
402    }
403
404    #[test]
405    fn report_has_french_titles() {
406        let report =
407            ReportBuilder::new("BaseID", AssuranceLevel::Low, "2026-03-22T00:00:00Z").build();
408
409        assert!(report.title_fr.contains("CCNIP"));
410        assert!(report.summary_fr.contains("CCNIP"));
411        for component in &report.components {
412            assert!(!component.component_fr.is_empty());
413        }
414    }
415}