1use baseid_core::types::AssuranceLevel;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum ComponentStatus {
12 Conformant,
14 PartiallyConformant,
16 NonConformant,
18 NotApplicable,
20}
21
22impl ComponentStatus {
23 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#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ComponentAssessment {
41 pub component: String,
43 pub component_fr: String,
45 pub status: ComponentStatus,
47 pub evidence: Vec<String>,
49 pub recommendations: Vec<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct PctfComplianceReport {
56 pub title: String,
58 pub title_fr: String,
60 pub system_name: String,
62 pub generated_at: String,
64 pub target_level: AssuranceLevel,
66 pub components: Vec<ComponentAssessment>,
68 pub overall_status: ComponentStatus,
70 pub summary: String,
72 pub summary_fr: String,
74}
75
76pub 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 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 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 pub fn with_consent_management(mut self, enabled: bool) -> Self {
118 self.has_consent_mgmt = enabled;
119 self
120 }
121
122 pub fn with_audit_logging(mut self, enabled: bool) -> Self {
124 self.has_audit_logging = enabled;
125 self
126 }
127
128 pub fn with_crypto_integrity(mut self, enabled: bool) -> Self {
130 self.has_crypto_integrity = enabled;
131 self
132 }
133
134 pub fn with_revocation(mut self, enabled: bool) -> Self {
136 self.has_revocation = enabled;
137 self
138 }
139
140 pub fn build(self) -> PctfComplianceReport {
142 let mut components = Vec::new();
143
144 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 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 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 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 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 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}