1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::rules::{ComplianceRule, ComplianceStandard, RuleCategory, RuleSeverity};
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ComplianceStatus {
14 Compliant,
16 NonCompliant,
18 PartiallyCompliant,
20 #[default]
22 NotChecked,
23 NotApplicable,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ReportFormat {
30 Json,
32 Csv,
34 Html,
36 Markdown,
38 Pdf,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Violation {
45 pub id: String,
47 pub rule_id: String,
49 pub rule_name: String,
51 pub severity: RuleSeverity,
53 pub category: RuleCategory,
55 pub description: String,
57 pub detected_at: DateTime<Utc>,
59 pub resource_type: Option<String>,
61 pub resource_id: Option<String>,
63 pub remediation: Option<String>,
65 pub evidence: Option<serde_json::Value>,
67}
68
69impl Violation {
70 pub fn new(rule: &ComplianceRule, description: impl Into<String>) -> Self {
71 Self {
72 id: format!("VIO-{}", uuid::Uuid::new_v4()),
73 rule_id: rule.id.clone(),
74 rule_name: rule.name.clone(),
75 severity: rule.severity,
76 category: rule.category,
77 description: description.into(),
78 detected_at: Utc::now(),
79 resource_type: None,
80 resource_id: None,
81 remediation: rule.description.clone(),
82 evidence: None,
83 }
84 }
85
86 pub fn with_resource(
87 mut self,
88 resource_type: impl Into<String>,
89 resource_id: impl Into<String>,
90 ) -> Self {
91 self.resource_type = Some(resource_type.into());
92 self.resource_id = Some(resource_id.into());
93 self
94 }
95
96 pub fn with_remediation(mut self, remediation: impl Into<String>) -> Self {
97 self.remediation = Some(remediation.into());
98 self
99 }
100
101 pub fn with_evidence(mut self, evidence: serde_json::Value) -> Self {
102 self.evidence = Some(evidence);
103 self
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct CheckResult {
110 pub rule_id: String,
112 pub rule_name: String,
114 pub status: ComplianceStatus,
116 pub checked_at: DateTime<Utc>,
118 pub violations: Vec<Violation>,
120 pub details: Option<String>,
122}
123
124impl CheckResult {
125 pub fn compliant(rule: &ComplianceRule) -> Self {
126 Self {
127 rule_id: rule.id.clone(),
128 rule_name: rule.name.clone(),
129 status: ComplianceStatus::Compliant,
130 checked_at: Utc::now(),
131 violations: Vec::new(),
132 details: None,
133 }
134 }
135
136 pub fn non_compliant(rule: &ComplianceRule, violations: Vec<Violation>) -> Self {
137 Self {
138 rule_id: rule.id.clone(),
139 rule_name: rule.name.clone(),
140 status: if violations.is_empty() {
141 ComplianceStatus::Compliant
142 } else {
143 ComplianceStatus::NonCompliant
144 },
145 checked_at: Utc::now(),
146 violations,
147 details: None,
148 }
149 }
150
151 pub fn not_applicable(rule: &ComplianceRule) -> Self {
152 Self {
153 rule_id: rule.id.clone(),
154 rule_name: rule.name.clone(),
155 status: ComplianceStatus::NotApplicable,
156 checked_at: Utc::now(),
157 violations: Vec::new(),
158 details: Some("Rule not applicable to this system".to_string()),
159 }
160 }
161
162 pub fn with_details(mut self, details: impl Into<String>) -> Self {
163 self.details = Some(details.into());
164 self
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ComplianceReport {
171 pub id: String,
173 pub name: String,
175 pub generated_at: DateTime<Utc>,
177 pub standards: Vec<ComplianceStandard>,
179 pub overall_status: ComplianceStatus,
181 pub compliance_score: f32,
183 pub results: Vec<CheckResult>,
185 pub violations: Vec<Violation>,
187 pub summary: ReportSummary,
189 pub metadata: HashMap<String, serde_json::Value>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, Default)]
195pub struct ReportSummary {
196 pub total_rules: usize,
198 pub compliant_rules: usize,
200 pub non_compliant_rules: usize,
202 pub not_applicable_rules: usize,
204 pub total_violations: usize,
206 pub critical_violations: usize,
208 pub high_violations: usize,
210 pub medium_violations: usize,
212 pub low_violations: usize,
214}
215
216impl ComplianceReport {
217 pub fn new(name: impl Into<String>, standards: Vec<ComplianceStandard>) -> Self {
219 Self {
220 id: format!("RPT-{}", uuid::Uuid::new_v4()),
221 name: name.into(),
222 generated_at: Utc::now(),
223 standards,
224 overall_status: ComplianceStatus::NotChecked,
225 compliance_score: 0.0,
226 results: Vec::new(),
227 violations: Vec::new(),
228 summary: ReportSummary::default(),
229 metadata: HashMap::new(),
230 }
231 }
232
233 pub fn add_result(&mut self, result: CheckResult) {
235 self.summary.total_rules += 1;
237
238 match result.status {
239 ComplianceStatus::Compliant => self.summary.compliant_rules += 1,
240 ComplianceStatus::NonCompliant => self.summary.non_compliant_rules += 1,
241 ComplianceStatus::PartiallyCompliant => self.summary.non_compliant_rules += 1,
242 ComplianceStatus::NotApplicable => self.summary.not_applicable_rules += 1,
243 ComplianceStatus::NotChecked => {}
244 }
245
246 for violation in &result.violations {
248 self.summary.total_violations += 1;
249 match violation.severity {
250 RuleSeverity::Critical => self.summary.critical_violations += 1,
251 RuleSeverity::High => self.summary.high_violations += 1,
252 RuleSeverity::Medium => self.summary.medium_violations += 1,
253 RuleSeverity::Low => self.summary.low_violations += 1,
254 }
255 }
256
257 self.violations.extend(result.violations.clone());
258 self.results.push(result);
259 }
260
261 pub fn calculate_score(&mut self) {
263 if self.summary.total_rules == 0 {
264 self.compliance_score = 100.0;
265 self.overall_status = ComplianceStatus::NotChecked;
266 return;
267 }
268
269 let applicable_rules = self.summary.total_rules - self.summary.not_applicable_rules;
270 if applicable_rules == 0 {
271 self.compliance_score = 100.0;
272 self.overall_status = ComplianceStatus::NotApplicable;
273 return;
274 }
275
276 let base_score = (self.summary.compliant_rules as f32 / applicable_rules as f32) * 100.0;
278
279 let penalty = (self.summary.critical_violations as f32 * 10.0)
281 + (self.summary.high_violations as f32 * 5.0)
282 + (self.summary.medium_violations as f32 * 2.0)
283 + (self.summary.low_violations as f32 * 0.5);
284
285 self.compliance_score = (base_score - penalty).clamp(0.0, 100.0);
286
287 self.overall_status = if self.compliance_score >= 90.0 {
289 ComplianceStatus::Compliant
290 } else if self.compliance_score >= 60.0 {
291 ComplianceStatus::PartiallyCompliant
292 } else {
293 ComplianceStatus::NonCompliant
294 };
295 }
296
297 pub fn export(&self, format: ReportFormat) -> Vec<u8> {
299 match format {
300 ReportFormat::Json => serde_json::to_string_pretty(self)
301 .unwrap_or_default()
302 .into_bytes(),
303 ReportFormat::Csv => self.export_csv(),
304 ReportFormat::Html => self.export_html().into_bytes(),
305 ReportFormat::Markdown => self.export_markdown().into_bytes(),
306 ReportFormat::Pdf => {
307 self.export_html().into_bytes()
309 }
310 }
311 }
312
313 fn export_csv(&self) -> Vec<u8> {
314 let mut csv = String::from("Rule ID,Rule Name,Status,Violations Count,Severity\n");
315 for result in &self.results {
316 let status = format!("{:?}", result.status);
317 let violation_count = result.violations.len();
318 let severity = result
319 .violations
320 .first()
321 .map(|v| format!("{:?}", v.severity))
322 .unwrap_or_else(|| "N/A".to_string());
323 csv.push_str(&format!(
324 "{},{},{},{},{}\n",
325 result.rule_id, result.rule_name, status, violation_count, severity
326 ));
327 }
328 csv.into_bytes()
329 }
330
331 fn export_html(&self) -> String {
332 format!(
333 r#"<!DOCTYPE html>
334<html>
335<head>
336 <title>Compliance Report - {}</title>
337 <style>
338 body {{ font-family: Arial, sans-serif; margin: 20px; }}
339 h1 {{ color: #333; }}
340 .score {{ font-size: 24px; font-weight: bold; margin: 20px 0; }}
341 .compliant {{ color: green; }}
342 .non-compliant {{ color: red; }}
343 .partial {{ color: orange; }}
344 table {{ border-collapse: collapse; width: 100%; }}
345 th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
346 th {{ background-color: #f2f2f2; }}
347 </style>
348</head>
349<body>
350 <h1>Compliance Report</h1>
351 <p>Generated: {}</p>
352 <p>Standards: {}</p>
353 <div class="score">Compliance Score: {:.1}%</div>
354 <h2>Summary</h2>
355 <ul>
356 <li>Total Rules: {}</li>
357 <li>Compliant: {}</li>
358 <li>Non-Compliant: {}</li>
359 <li>Total Violations: {}</li>
360 </ul>
361 <h2>Results</h2>
362 <table>
363 <tr><th>Rule ID</th><th>Rule Name</th><th>Status</th><th>Violations</th></tr>
364 {}
365 </table>
366</body>
367</html>"#,
368 self.name,
369 self.generated_at.format("%Y-%m-%d %H:%M:%S UTC"),
370 self.standards
371 .iter()
372 .map(|s| s.name())
373 .collect::<Vec<_>>()
374 .join(", "),
375 self.compliance_score,
376 self.summary.total_rules,
377 self.summary.compliant_rules,
378 self.summary.non_compliant_rules,
379 self.summary.total_violations,
380 self.results
381 .iter()
382 .map(|r| format!(
383 "<tr><td>{}</td><td>{}</td><td>{:?}</td><td>{}</td></tr>",
384 r.rule_id,
385 r.rule_name,
386 r.status,
387 r.violations.len()
388 ))
389 .collect::<Vec<_>>()
390 .join("\n ")
391 )
392 }
393
394 fn export_markdown(&self) -> String {
395 format!(
396 r#"# Compliance Report: {}
397
398**Generated:** {}
399**Standards:** {}
400**Score:** {:.1}%
401
402## Summary
403
404| Metric | Count |
405|--------|-------|
406| Total Rules | {} |
407| Compliant | {} |
408| Non-Compliant | {} |
409| Total Violations | {} |
410| Critical Violations | {} |
411
412## Results
413
414| Rule ID | Rule Name | Status | Violations |
415|---------|-----------|--------|------------|
416{}
417
418## Violations
419
420{}
421"#,
422 self.name,
423 self.generated_at.format("%Y-%m-%d %H:%M:%S UTC"),
424 self.standards
425 .iter()
426 .map(|s| s.name())
427 .collect::<Vec<_>>()
428 .join(", "),
429 self.compliance_score,
430 self.summary.total_rules,
431 self.summary.compliant_rules,
432 self.summary.non_compliant_rules,
433 self.summary.total_violations,
434 self.summary.critical_violations,
435 self.results
436 .iter()
437 .map(|r| format!(
438 "| {} | {} | {:?} | {} |",
439 r.rule_id,
440 r.rule_name,
441 r.status,
442 r.violations.len()
443 ))
444 .collect::<Vec<_>>()
445 .join("\n"),
446 self.violations
447 .iter()
448 .map(|v| format!(
449 "- **{}**: {} (Severity: {:?})",
450 v.rule_id, v.description, v.severity
451 ))
452 .collect::<Vec<_>>()
453 .join("\n")
454 )
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn test_violation_creation() {
464 let rule = ComplianceRule::new("TEST-1", "Test", RuleCategory::Security);
465 let violation = Violation::new(&rule, "Test violation");
466
467 assert_eq!(violation.rule_id, "TEST-1");
468 assert_eq!(violation.description, "Test violation");
469 }
470
471 #[test]
472 fn test_check_result_compliant() {
473 let rule = ComplianceRule::new("TEST-1", "Test", RuleCategory::Security);
474 let result = CheckResult::compliant(&rule);
475
476 assert_eq!(result.status, ComplianceStatus::Compliant);
477 assert!(result.violations.is_empty());
478 }
479
480 #[test]
481 fn test_compliance_report() {
482 let mut report = ComplianceReport::new("Test Report", vec![ComplianceStandard::SOC2]);
483
484 let rule = ComplianceRule::new("TEST-1", "Test", RuleCategory::Security);
485 let result = CheckResult::compliant(&rule);
486
487 report.add_result(result);
488 report.calculate_score();
489
490 assert_eq!(report.summary.total_rules, 1);
491 assert_eq!(report.summary.compliant_rules, 1);
492 assert!(report.compliance_score > 0.0);
493 }
494
495 #[test]
496 fn test_export_json() {
497 let report = ComplianceReport::new("Test", vec![ComplianceStandard::SOC2]);
498 let json = report.export(ReportFormat::Json);
499 assert!(!json.is_empty());
500 }
501
502 #[test]
503 fn test_export_markdown() {
504 let mut report = ComplianceReport::new("Test Report", vec![ComplianceStandard::SOC2]);
505 let rule = ComplianceRule::new("TEST-1", "Test Rule", RuleCategory::Security);
506 report.add_result(CheckResult::compliant(&rule));
507 report.calculate_score();
508
509 let md = report.export(ReportFormat::Markdown);
510 let md_str = String::from_utf8(md).unwrap();
511 assert!(md_str.contains("# Compliance Report"));
512 assert!(md_str.contains("TEST-1"));
513 }
514}