1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum Framework {
17 NistAiRmf,
19 EuAiAct,
21}
22
23impl Framework {
24 pub fn all() -> &'static [Self] {
26 &[Self::NistAiRmf, Self::EuAiAct]
27 }
28
29 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62pub enum ComplianceStatus {
63 Compliant,
65 Partial,
67 NeedsAttention,
69 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#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ControlMapping {
87 pub control: SecurityControl,
89 pub framework: Framework,
91 pub requirements: Vec<String>,
93 pub rationale: String,
95 pub status: ComplianceStatus,
97 pub coverage_percent: u8,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ComplianceReport {
104 pub title: String,
106 pub system_name: String,
108 pub generated_at: DateTime<Utc>,
110 pub oxideshield_version: String,
112 pub frameworks: Vec<Framework>,
114 pub mappings: Vec<ControlMapping>,
116 pub active_guards: Vec<String>,
118 pub summary: ComplianceSummary,
120 pub notes: Vec<String>,
122}
123
124#[derive(Debug, Clone, Default, Serialize, Deserialize)]
126pub struct ComplianceSummary {
127 pub total_controls: usize,
129 pub compliant_controls: usize,
131 pub partial_controls: usize,
133 pub needs_attention: usize,
135 pub overall_percentage: u8,
137 pub by_framework: HashMap<String, FrameworkSummary>,
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143pub struct FrameworkSummary {
144 pub name: String,
146 pub total_requirements: usize,
148 pub addressed_requirements: usize,
150 pub coverage_percent: u8,
152}
153
154pub struct ComplianceReportBuilder {
156 system_name: String,
157 frameworks: Vec<Framework>,
158 active_guards: Vec<String>,
159 notes: Vec<String>,
160}
161
162impl ComplianceReportBuilder {
163 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 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 pub fn with_all_frameworks(mut self) -> Self {
183 self.frameworks = Framework::all().to_vec();
184 self
185 }
186
187 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 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 pub fn with_note(mut self, note: impl Into<String>) -> Self {
203 self.notes.push(note.into());
204 self
205 }
206
207 pub fn generate(self) -> Result<ComplianceReport, LicenseError> {
214 require_feature_sync(Feature::ComplianceReports)?;
215 Ok(self.generate_unchecked())
216 }
217
218 pub(crate) fn generate_unchecked(self) -> ComplianceReport {
222 let mut mappings = Vec::new();
223 let controls = available_controls();
224
225 let frameworks = if self.frameworks.is_empty() {
227 Framework::all().to_vec()
228 } else {
229 self.frameworks
230 };
231
232 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 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 pub fn builder(system_name: impl Into<String>) -> ComplianceReportBuilder {
413 ComplianceReportBuilder::new(system_name)
414 }
415
416 pub fn to_json(&self) -> Result<String, serde_json::Error> {
418 serde_json::to_string_pretty(self)
419 }
420
421 pub fn to_markdown(&self) -> String {
423 let mut md = String::new();
424
425 md.push_str(&format!("# {}\n\n", self.title));
427
428 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 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 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 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 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 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 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 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 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 html.push_str(&format!(" <h1>{}</h1>\n", self.title));
630
631 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 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 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 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 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}