Skip to main content

oxideshield_guard/compliance/
report.rs

1//! Compliance Report Generation
2//!
3//! Generates compliance reports mapping OxideShield controls to regulatory frameworks.
4//!
5//! **License**: Requires Professional tier.
6
7use super::{available_controls, eu_ai_act, nist_ai_rmf, SecurityControl};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use oxide_license::{require_feature_sync, Feature, LicenseError};
13
14/// Supported compliance frameworks.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum Framework {
17    /// NIST AI Risk Management Framework
18    NistAiRmf,
19    /// EU Artificial Intelligence Act
20    EuAiAct,
21}
22
23impl Framework {
24    /// Get all available frameworks.
25    pub fn all() -> &'static [Self] {
26        &[Self::NistAiRmf, Self::EuAiAct]
27    }
28
29    /// Get the framework name.
30    pub fn name(&self) -> &'static str {
31        match self {
32            Self::NistAiRmf => "NIST AI Risk Management Framework",
33            Self::EuAiAct => "EU AI Act",
34        }
35    }
36
37    /// Get the framework short code.
38    pub fn code(&self) -> &'static str {
39        match self {
40            Self::NistAiRmf => "NIST-AI-RMF",
41            Self::EuAiAct => "EU-AI-ACT",
42        }
43    }
44
45    /// Get the reference URL.
46    pub fn reference_url(&self) -> &'static str {
47        match self {
48            Self::NistAiRmf => "https://www.nist.gov/itl/ai-risk-management-framework",
49            Self::EuAiAct => "https://eur-lex.europa.eu/eli/reg/2024/1689",
50        }
51    }
52}
53
54impl std::fmt::Display for Framework {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{}", self.name())
57    }
58}
59
60/// Compliance status for a control mapping.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62pub enum ComplianceStatus {
63    /// Control is fully compliant.
64    Compliant,
65    /// Control is partially compliant.
66    Partial,
67    /// Control needs attention.
68    NeedsAttention,
69    /// Control is not applicable.
70    NotApplicable,
71}
72
73impl std::fmt::Display for ComplianceStatus {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Self::Compliant => write!(f, "Compliant"),
77            Self::Partial => write!(f, "Partial"),
78            Self::NeedsAttention => write!(f, "Needs Attention"),
79            Self::NotApplicable => write!(f, "N/A"),
80        }
81    }
82}
83
84/// A mapping between an OxideShield control and a framework requirement.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ControlMapping {
87    /// The OxideShield control.
88    pub control: SecurityControl,
89    /// Framework this mapping applies to.
90    pub framework: Framework,
91    /// Requirement IDs addressed by this control.
92    pub requirements: Vec<String>,
93    /// How the control addresses the requirements.
94    pub rationale: String,
95    /// Current compliance status.
96    pub status: ComplianceStatus,
97    /// Coverage percentage (0-100).
98    pub coverage_percent: u8,
99}
100
101/// A compliance report for one or more frameworks.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ComplianceReport {
104    /// Report title.
105    pub title: String,
106    /// System or deployment being assessed.
107    pub system_name: String,
108    /// Report generation timestamp.
109    pub generated_at: DateTime<Utc>,
110    /// OxideShield version.
111    pub oxideshield_version: String,
112    /// Frameworks included in this report.
113    pub frameworks: Vec<Framework>,
114    /// Control mappings.
115    pub mappings: Vec<ControlMapping>,
116    /// Active guards in the deployment.
117    pub active_guards: Vec<String>,
118    /// Summary statistics.
119    pub summary: ComplianceSummary,
120    /// Additional notes.
121    pub notes: Vec<String>,
122}
123
124/// Summary statistics for a compliance report.
125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
126pub struct ComplianceSummary {
127    /// Total controls evaluated.
128    pub total_controls: usize,
129    /// Controls that are compliant.
130    pub compliant_controls: usize,
131    /// Controls that are partially compliant.
132    pub partial_controls: usize,
133    /// Controls needing attention.
134    pub needs_attention: usize,
135    /// Overall compliance percentage.
136    pub overall_percentage: u8,
137    /// Breakdown by framework.
138    pub by_framework: HashMap<String, FrameworkSummary>,
139}
140
141/// Summary for a single framework.
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143pub struct FrameworkSummary {
144    /// Framework name.
145    pub name: String,
146    /// Total requirements in framework.
147    pub total_requirements: usize,
148    /// Requirements addressed.
149    pub addressed_requirements: usize,
150    /// Coverage percentage.
151    pub coverage_percent: u8,
152}
153
154/// Builder for creating compliance reports.
155pub struct ComplianceReportBuilder {
156    system_name: String,
157    frameworks: Vec<Framework>,
158    active_guards: Vec<String>,
159    notes: Vec<String>,
160}
161
162impl ComplianceReportBuilder {
163    /// Create a new compliance report builder.
164    pub fn new(system_name: impl Into<String>) -> Self {
165        Self {
166            system_name: system_name.into(),
167            frameworks: Vec::new(),
168            active_guards: Vec::new(),
169            notes: Vec::new(),
170        }
171    }
172
173    /// Add a framework to the report.
174    pub fn with_framework(mut self, framework: Framework) -> Self {
175        if !self.frameworks.contains(&framework) {
176            self.frameworks.push(framework);
177        }
178        self
179    }
180
181    /// Add all available frameworks.
182    pub fn with_all_frameworks(mut self) -> Self {
183        self.frameworks = Framework::all().to_vec();
184        self
185    }
186
187    /// Add an active guard.
188    pub fn with_guard(mut self, guard_name: impl Into<String>) -> Self {
189        self.active_guards.push(guard_name.into());
190        self
191    }
192
193    /// Add multiple active guards.
194    pub fn with_guards(mut self, guards: &[&str]) -> Self {
195        for guard in guards {
196            self.active_guards.push((*guard).to_string());
197        }
198        self
199    }
200
201    /// Add a note to the report.
202    pub fn with_note(mut self, note: impl Into<String>) -> Self {
203        self.notes.push(note.into());
204        self
205    }
206
207    /// Generate the compliance report.
208    ///
209    /// # License Requirement
210    ///
211    /// ComplianceReports requires a Professional license.
212    /// Returns an error if the license requirement is not met.
213    pub fn generate(self) -> Result<ComplianceReport, LicenseError> {
214        require_feature_sync(Feature::ComplianceReports)?;
215        Ok(self.generate_unchecked())
216    }
217
218    /// Generate the compliance report without license validation.
219    ///
220    /// Restricted to crate-internal use.
221    pub(crate) fn generate_unchecked(self) -> ComplianceReport {
222        let mut mappings = Vec::new();
223        let controls = available_controls();
224
225        // If no frameworks specified, use all
226        let frameworks = if self.frameworks.is_empty() {
227            Framework::all().to_vec()
228        } else {
229            self.frameworks
230        };
231
232        // Generate mappings for each framework
233        for framework in &frameworks {
234            match framework {
235                Framework::NistAiRmf => {
236                    let nist_mappings = nist_ai_rmf::get_mappings();
237                    for nm in nist_mappings {
238                        if let Some(control) = controls.iter().find(|c| c.id == nm.control_id) {
239                            let is_active = self.active_guards.is_empty()
240                                || self
241                                    .active_guards
242                                    .iter()
243                                    .any(|g| g.to_lowercase() == control.component.to_lowercase());
244
245                            let status = if is_active {
246                                match nm.coverage {
247                                    nist_ai_rmf::CoverageLevel::Full => ComplianceStatus::Compliant,
248                                    nist_ai_rmf::CoverageLevel::Partial => {
249                                        ComplianceStatus::Partial
250                                    }
251                                    nist_ai_rmf::CoverageLevel::Supportive => {
252                                        ComplianceStatus::Partial
253                                    }
254                                }
255                            } else {
256                                ComplianceStatus::NeedsAttention
257                            };
258
259                            let coverage = if is_active {
260                                match nm.coverage {
261                                    nist_ai_rmf::CoverageLevel::Full => 100,
262                                    nist_ai_rmf::CoverageLevel::Partial => 70,
263                                    nist_ai_rmf::CoverageLevel::Supportive => 50,
264                                }
265                            } else {
266                                0
267                            };
268
269                            mappings.push(ControlMapping {
270                                control: control.clone().with_enabled(is_active),
271                                framework: *framework,
272                                requirements: nm.subcategories,
273                                rationale: nm.rationale,
274                                status,
275                                coverage_percent: coverage,
276                            });
277                        }
278                    }
279                }
280                Framework::EuAiAct => {
281                    let eu_mappings = eu_ai_act::get_mappings();
282                    for em in eu_mappings {
283                        if let Some(control) = controls.iter().find(|c| c.id == em.control_id) {
284                            let is_active = self.active_guards.is_empty()
285                                || self
286                                    .active_guards
287                                    .iter()
288                                    .any(|g| g.to_lowercase() == control.component.to_lowercase());
289
290                            let status = if is_active {
291                                match em.contribution {
292                                    eu_ai_act::ContributionLevel::Direct => {
293                                        ComplianceStatus::Compliant
294                                    }
295                                    eu_ai_act::ContributionLevel::Supporting => {
296                                        ComplianceStatus::Partial
297                                    }
298                                    eu_ai_act::ContributionLevel::Enabling => {
299                                        ComplianceStatus::Partial
300                                    }
301                                }
302                            } else {
303                                ComplianceStatus::NeedsAttention
304                            };
305
306                            let coverage = if is_active {
307                                match em.contribution {
308                                    eu_ai_act::ContributionLevel::Direct => 100,
309                                    eu_ai_act::ContributionLevel::Supporting => 70,
310                                    eu_ai_act::ContributionLevel::Enabling => 50,
311                                }
312                            } else {
313                                0
314                            };
315
316                            mappings.push(ControlMapping {
317                                control: control.clone().with_enabled(is_active),
318                                framework: *framework,
319                                requirements: em.requirements,
320                                rationale: em.rationale,
321                                status,
322                                coverage_percent: coverage,
323                            });
324                        }
325                    }
326                }
327            }
328        }
329
330        // Calculate summary
331        let summary = calculate_summary(&mappings, &frameworks);
332
333        ComplianceReport {
334            title: format!("OxideShield Compliance Report - {}", self.system_name),
335            system_name: self.system_name,
336            generated_at: Utc::now(),
337            oxideshield_version: env!("CARGO_PKG_VERSION").to_string(),
338            frameworks,
339            mappings,
340            active_guards: self.active_guards,
341            summary,
342            notes: self.notes,
343        }
344    }
345}
346
347fn calculate_summary(mappings: &[ControlMapping], frameworks: &[Framework]) -> ComplianceSummary {
348    let total_controls = mappings.len();
349    let compliant_controls = mappings
350        .iter()
351        .filter(|m| m.status == ComplianceStatus::Compliant)
352        .count();
353    let partial_controls = mappings
354        .iter()
355        .filter(|m| m.status == ComplianceStatus::Partial)
356        .count();
357    let needs_attention = mappings
358        .iter()
359        .filter(|m| m.status == ComplianceStatus::NeedsAttention)
360        .count();
361
362    let overall_percentage = if total_controls > 0 {
363        let total_coverage: usize = mappings.iter().map(|m| m.coverage_percent as usize).sum();
364        (total_coverage / total_controls) as u8
365    } else {
366        0
367    };
368
369    let mut by_framework = HashMap::new();
370
371    for framework in frameworks {
372        let fw_mappings: Vec<_> = mappings
373            .iter()
374            .filter(|m| m.framework == *framework)
375            .collect();
376        let addressed: usize = fw_mappings.iter().map(|m| m.requirements.len()).sum();
377
378        let total_reqs = match framework {
379            Framework::NistAiRmf => nist_ai_rmf::get_subcategories().len(),
380            Framework::EuAiAct => eu_ai_act::get_requirements().len(),
381        };
382
383        let coverage = if total_reqs > 0 {
384            ((addressed.min(total_reqs) * 100) / total_reqs) as u8
385        } else {
386            0
387        };
388
389        by_framework.insert(
390            framework.code().to_string(),
391            FrameworkSummary {
392                name: framework.name().to_string(),
393                total_requirements: total_reqs,
394                addressed_requirements: addressed,
395                coverage_percent: coverage.min(100),
396            },
397        );
398    }
399
400    ComplianceSummary {
401        total_controls,
402        compliant_controls,
403        partial_controls,
404        needs_attention,
405        overall_percentage,
406        by_framework,
407    }
408}
409
410impl ComplianceReport {
411    /// Create a new report builder.
412    pub fn builder(system_name: impl Into<String>) -> ComplianceReportBuilder {
413        ComplianceReportBuilder::new(system_name)
414    }
415
416    /// Render the report as JSON.
417    pub fn to_json(&self) -> Result<String, serde_json::Error> {
418        serde_json::to_string_pretty(self)
419    }
420
421    /// Render the report as Markdown.
422    pub fn to_markdown(&self) -> String {
423        let mut md = String::new();
424
425        // Title
426        md.push_str(&format!("# {}\n\n", self.title));
427
428        // Metadata
429        md.push_str("## Report Information\n\n");
430        md.push_str(&format!("- **System:** {}\n", self.system_name));
431        md.push_str(&format!(
432            "- **Generated:** {}\n",
433            self.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
434        ));
435        md.push_str(&format!(
436            "- **OxideShield Version:** {}\n",
437            self.oxideshield_version
438        ));
439        md.push_str(&format!(
440            "- **Frameworks:** {}\n\n",
441            self.frameworks
442                .iter()
443                .map(|f| f.name())
444                .collect::<Vec<_>>()
445                .join(", ")
446        ));
447
448        // Summary
449        md.push_str("## Executive Summary\n\n");
450        md.push_str(&format!(
451            "**Overall Compliance: {}%**\n\n",
452            self.summary.overall_percentage
453        ));
454        md.push_str("| Metric | Value |\n");
455        md.push_str("|--------|-------|\n");
456        md.push_str(&format!(
457            "| Total Controls Evaluated | {} |\n",
458            self.summary.total_controls
459        ));
460        md.push_str(&format!(
461            "| Compliant | {} |\n",
462            self.summary.compliant_controls
463        ));
464        md.push_str(&format!(
465            "| Partial | {} |\n",
466            self.summary.partial_controls
467        ));
468        md.push_str(&format!(
469            "| Needs Attention | {} |\n\n",
470            self.summary.needs_attention
471        ));
472
473        // Framework summaries
474        md.push_str("### Framework Coverage\n\n");
475        md.push_str("| Framework | Requirements Addressed | Coverage |\n");
476        md.push_str("|-----------|----------------------|----------|\n");
477        for (code, summary) in &self.summary.by_framework {
478            md.push_str(&format!(
479                "| {} | {} / {} | {}% |\n",
480                code,
481                summary.addressed_requirements,
482                summary.total_requirements,
483                summary.coverage_percent
484            ));
485        }
486        md.push('\n');
487
488        // Active guards
489        if !self.active_guards.is_empty() {
490            md.push_str("## Active Security Controls\n\n");
491            for guard in &self.active_guards {
492                md.push_str(&format!("- {}\n", guard));
493            }
494            md.push('\n');
495        }
496
497        // Detailed mappings by framework
498        for framework in &self.frameworks {
499            md.push_str(&format!("## {} Mappings\n\n", framework.name()));
500            md.push_str(&format!(
501                "Reference: [{}]({})\n\n",
502                framework.code(),
503                framework.reference_url()
504            ));
505
506            let fw_mappings: Vec<_> = self
507                .mappings
508                .iter()
509                .filter(|m| m.framework == *framework)
510                .collect();
511
512            md.push_str("| Control | Requirements | Status | Coverage |\n");
513            md.push_str("|---------|--------------|--------|----------|\n");
514
515            for mapping in fw_mappings {
516                let reqs = mapping.requirements.join(", ");
517                let status_icon = match mapping.status {
518                    ComplianceStatus::Compliant => "✅",
519                    ComplianceStatus::Partial => "⚠️",
520                    ComplianceStatus::NeedsAttention => "❌",
521                    ComplianceStatus::NotApplicable => "➖",
522                };
523                md.push_str(&format!(
524                    "| {} | {} | {} {} | {}% |\n",
525                    mapping.control.name,
526                    reqs,
527                    status_icon,
528                    mapping.status,
529                    mapping.coverage_percent
530                ));
531            }
532            md.push('\n');
533
534            // Detailed rationale
535            md.push_str("### Control Details\n\n");
536            for mapping in self.mappings.iter().filter(|m| m.framework == *framework) {
537                md.push_str(&format!(
538                    "#### {} ({})\n\n",
539                    mapping.control.name, mapping.control.id
540                ));
541                md.push_str(&format!("**Component:** {}\n\n", mapping.control.component));
542                md.push_str(&format!(
543                    "**Description:** {}\n\n",
544                    mapping.control.description
545                ));
546                md.push_str(&format!(
547                    "**Requirements Addressed:** {}\n\n",
548                    mapping.requirements.join(", ")
549                ));
550                md.push_str(&format!("**Rationale:** {}\n\n", mapping.rationale));
551                md.push_str(&format!(
552                    "**Status:** {} ({}% coverage)\n\n",
553                    mapping.status, mapping.coverage_percent
554                ));
555                md.push_str("---\n\n");
556            }
557        }
558
559        // Notes
560        if !self.notes.is_empty() {
561            md.push_str("## Notes\n\n");
562            for note in &self.notes {
563                md.push_str(&format!("- {}\n", note));
564            }
565            md.push('\n');
566        }
567
568        // Footer
569        md.push_str("---\n\n");
570        md.push_str("*Generated by OxideShield. OxideShield is a trading name of Toasteez Limited, a UK registered company.*\n");
571
572        md
573    }
574
575    /// Render the report as HTML.
576    pub fn to_html(&self) -> String {
577        let mut html = String::new();
578
579        html.push_str(r#"<!DOCTYPE html>
580<html lang="en">
581<head>
582    <meta charset="UTF-8">
583    <meta name="viewport" content="width=device-width, initial-scale=1.0">
584    <title>OxideShield Compliance Report</title>
585    <style>
586        :root {
587            --compliant: #16a34a;
588            --partial: #ca8a04;
589            --attention: #dc2626;
590            --na: #6b7280;
591        }
592        body {
593            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
594            line-height: 1.6;
595            max-width: 1200px;
596            margin: 0 auto;
597            padding: 20px;
598            background: #f8fafc;
599            color: #1e293b;
600        }
601        h1 { color: #0f172a; border-bottom: 3px solid #3b82f6; padding-bottom: 10px; }
602        h2 { color: #1e40af; margin-top: 30px; }
603        h3 { color: #374151; }
604        .summary-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
605        .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px; }
606        .stat-box { background: #f1f5f9; padding: 15px; border-radius: 6px; text-align: center; }
607        .stat-box .value { font-size: 2rem; font-weight: 700; color: #1e40af; }
608        .stat-box .label { color: #64748b; font-size: 0.875rem; }
609        .overall { font-size: 3rem; font-weight: 700; }
610        .overall.good { color: var(--compliant); }
611        .overall.medium { color: var(--partial); }
612        .overall.low { color: var(--attention); }
613        table { width: 100%; border-collapse: collapse; margin: 15px 0; background: white; border-radius: 8px; overflow: hidden; }
614        th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }
615        th { background: #f1f5f9; font-weight: 600; }
616        .status-compliant { color: var(--compliant); font-weight: 600; }
617        .status-partial { color: var(--partial); font-weight: 600; }
618        .status-attention { color: var(--attention); font-weight: 600; }
619        .status-na { color: var(--na); }
620        .framework-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
621        .control-detail { background: #f8fafc; padding: 15px; border-radius: 6px; margin: 10px 0; border-left: 4px solid #3b82f6; }
622        footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #e2e8f0; color: #64748b; font-size: 0.875rem; text-align: center; }
623    </style>
624</head>
625<body>
626"#);
627
628        // Title
629        html.push_str(&format!("    <h1>{}</h1>\n", self.title));
630
631        // Metadata
632        html.push_str("    <div class=\"summary-card\">\n");
633        html.push_str("        <h2>Report Information</h2>\n");
634        html.push_str(&format!(
635            "        <p><strong>System:</strong> {}</p>\n",
636            self.system_name
637        ));
638        html.push_str(&format!(
639            "        <p><strong>Generated:</strong> {}</p>\n",
640            self.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
641        ));
642        html.push_str(&format!(
643            "        <p><strong>OxideShield Version:</strong> {}</p>\n",
644            self.oxideshield_version
645        ));
646        html.push_str("    </div>\n\n");
647
648        // Summary
649        html.push_str("    <div class=\"summary-card\">\n");
650        html.push_str("        <h2>Executive Summary</h2>\n");
651        let overall_class = if self.summary.overall_percentage >= 80 {
652            "good"
653        } else if self.summary.overall_percentage >= 50 {
654            "medium"
655        } else {
656            "low"
657        };
658        html.push_str(&format!(
659            "        <p class=\"overall {}\">{}% Compliant</p>\n",
660            overall_class, self.summary.overall_percentage
661        ));
662
663        html.push_str("        <div class=\"summary-grid\">\n");
664        html.push_str(&format!("            <div class=\"stat-box\"><span class=\"value\">{}</span><span class=\"label\">Total Controls</span></div>\n",
665            self.summary.total_controls));
666        html.push_str(&format!("            <div class=\"stat-box\"><span class=\"value\" style=\"color:var(--compliant)\">{}</span><span class=\"label\">Compliant</span></div>\n",
667            self.summary.compliant_controls));
668        html.push_str(&format!("            <div class=\"stat-box\"><span class=\"value\" style=\"color:var(--partial)\">{}</span><span class=\"label\">Partial</span></div>\n",
669            self.summary.partial_controls));
670        html.push_str(&format!("            <div class=\"stat-box\"><span class=\"value\" style=\"color:var(--attention)\">{}</span><span class=\"label\">Needs Attention</span></div>\n",
671            self.summary.needs_attention));
672        html.push_str("        </div>\n");
673        html.push_str("    </div>\n\n");
674
675        // Framework sections
676        for framework in &self.frameworks {
677            html.push_str("    <div class=\"framework-section\">\n");
678            html.push_str(&format!("        <h2>{}</h2>\n", framework.name()));
679            html.push_str(&format!(
680                "        <p>Reference: <a href=\"{}\">{}</a></p>\n",
681                framework.reference_url(),
682                framework.code()
683            ));
684
685            html.push_str("        <table>\n");
686            html.push_str("            <tr><th>Control</th><th>Requirements</th><th>Status</th><th>Coverage</th></tr>\n");
687
688            for mapping in self.mappings.iter().filter(|m| m.framework == *framework) {
689                let status_class = match mapping.status {
690                    ComplianceStatus::Compliant => "status-compliant",
691                    ComplianceStatus::Partial => "status-partial",
692                    ComplianceStatus::NeedsAttention => "status-attention",
693                    ComplianceStatus::NotApplicable => "status-na",
694                };
695                let reqs = mapping.requirements.join(", ");
696                html.push_str(&format!("            <tr><td>{}</td><td>{}</td><td class=\"{}\">{}</td><td>{}%</td></tr>\n",
697                    mapping.control.name, reqs, status_class, mapping.status, mapping.coverage_percent));
698            }
699
700            html.push_str("        </table>\n");
701            html.push_str("    </div>\n\n");
702        }
703
704        // Footer
705        html.push_str(&format!(
706            "    <footer>Generated by OxideShield v{} | {} | OxideShield is a trading name of Toasteez Limited</footer>\n",
707            self.oxideshield_version,
708            Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
709        ));
710
711        html.push_str("</body>\n</html>\n");
712
713        html
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720
721    #[test]
722    fn test_framework_basics() {
723        assert_eq!(Framework::NistAiRmf.code(), "NIST-AI-RMF");
724        assert_eq!(Framework::EuAiAct.code(), "EU-AI-ACT");
725        assert_eq!(Framework::all().len(), 2);
726    }
727
728    #[test]
729    fn test_compliance_status_display() {
730        assert_eq!(format!("{}", ComplianceStatus::Compliant), "Compliant");
731        assert_eq!(format!("{}", ComplianceStatus::Partial), "Partial");
732        assert_eq!(
733            format!("{}", ComplianceStatus::NeedsAttention),
734            "Needs Attention"
735        );
736    }
737
738    #[test]
739    fn test_report_builder_basic() {
740        let report = ComplianceReport::builder("Test System")
741            .with_all_frameworks()
742            .generate_unchecked();
743
744        assert_eq!(report.system_name, "Test System");
745        assert_eq!(report.frameworks.len(), 2);
746        assert!(!report.mappings.is_empty());
747    }
748
749    #[test]
750    fn test_report_builder_with_guards() {
751        let report = ComplianceReport::builder("Test System")
752            .with_framework(Framework::NistAiRmf)
753            .with_guards(&["PatternGuard", "PIIGuard"])
754            .generate_unchecked();
755
756        assert_eq!(report.active_guards.len(), 2);
757
758        // Check that specified guards are marked as enabled
759        let pattern_mappings: Vec<_> = report
760            .mappings
761            .iter()
762            .filter(|m| m.control.component == "PatternGuard")
763            .collect();
764        assert!(pattern_mappings.iter().all(|m| m.control.enabled));
765    }
766
767    #[test]
768    fn test_report_builder_with_note() {
769        let report = ComplianceReport::builder("Test System")
770            .with_framework(Framework::EuAiAct)
771            .with_note("This is a test note")
772            .generate_unchecked();
773
774        assert_eq!(report.notes.len(), 1);
775        assert!(report.notes[0].contains("test note"));
776    }
777
778    #[test]
779    fn test_report_to_json() {
780        let report = ComplianceReport::builder("Test System")
781            .with_framework(Framework::NistAiRmf)
782            .generate_unchecked();
783
784        let json = report.to_json().unwrap();
785        assert!(json.contains("Test System"));
786        assert!(json.contains("NIST-AI-RMF"));
787    }
788
789    #[test]
790    fn test_report_to_markdown() {
791        let report = ComplianceReport::builder("Test System")
792            .with_all_frameworks()
793            .generate_unchecked();
794
795        let md = report.to_markdown();
796        assert!(md.contains("# OxideShield Compliance Report"));
797        assert!(md.contains("NIST AI Risk Management Framework"));
798        assert!(md.contains("EU AI Act"));
799        assert!(md.contains("Executive Summary"));
800    }
801
802    #[test]
803    fn test_report_to_html() {
804        let report = ComplianceReport::builder("Test System")
805            .with_framework(Framework::EuAiAct)
806            .generate_unchecked();
807
808        let html = report.to_html();
809        assert!(html.contains("<!DOCTYPE html>"));
810        assert!(html.contains("EU AI Act"));
811        assert!(html.contains("Test System"));
812    }
813
814    #[test]
815    fn test_summary_calculation() {
816        let report = ComplianceReport::builder("Test System")
817            .with_all_frameworks()
818            .generate_unchecked();
819
820        assert!(report.summary.total_controls > 0);
821        assert!(report.summary.overall_percentage <= 100);
822        assert_eq!(report.summary.by_framework.len(), 2);
823    }
824}