use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::rules::{ComplianceRule, ComplianceStandard, RuleCategory, RuleSeverity};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComplianceStatus {
Compliant,
NonCompliant,
PartiallyCompliant,
#[default]
NotChecked,
NotApplicable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportFormat {
Json,
Csv,
Html,
Markdown,
Pdf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub id: String,
pub rule_id: String,
pub rule_name: String,
pub severity: RuleSeverity,
pub category: RuleCategory,
pub description: String,
pub detected_at: DateTime<Utc>,
pub resource_type: Option<String>,
pub resource_id: Option<String>,
pub remediation: Option<String>,
pub evidence: Option<serde_json::Value>,
}
impl Violation {
pub fn new(rule: &ComplianceRule, description: impl Into<String>) -> Self {
Self {
id: format!("VIO-{}", uuid::Uuid::new_v4()),
rule_id: rule.id.clone(),
rule_name: rule.name.clone(),
severity: rule.severity,
category: rule.category,
description: description.into(),
detected_at: Utc::now(),
resource_type: None,
resource_id: None,
remediation: rule.description.clone(),
evidence: None,
}
}
pub fn with_resource(
mut self,
resource_type: impl Into<String>,
resource_id: impl Into<String>,
) -> Self {
self.resource_type = Some(resource_type.into());
self.resource_id = Some(resource_id.into());
self
}
pub fn with_remediation(mut self, remediation: impl Into<String>) -> Self {
self.remediation = Some(remediation.into());
self
}
pub fn with_evidence(mut self, evidence: serde_json::Value) -> Self {
self.evidence = Some(evidence);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub rule_id: String,
pub rule_name: String,
pub status: ComplianceStatus,
pub checked_at: DateTime<Utc>,
pub violations: Vec<Violation>,
pub details: Option<String>,
}
impl CheckResult {
pub fn compliant(rule: &ComplianceRule) -> Self {
Self {
rule_id: rule.id.clone(),
rule_name: rule.name.clone(),
status: ComplianceStatus::Compliant,
checked_at: Utc::now(),
violations: Vec::new(),
details: None,
}
}
pub fn non_compliant(rule: &ComplianceRule, violations: Vec<Violation>) -> Self {
Self {
rule_id: rule.id.clone(),
rule_name: rule.name.clone(),
status: if violations.is_empty() {
ComplianceStatus::Compliant
} else {
ComplianceStatus::NonCompliant
},
checked_at: Utc::now(),
violations,
details: None,
}
}
pub fn not_applicable(rule: &ComplianceRule) -> Self {
Self {
rule_id: rule.id.clone(),
rule_name: rule.name.clone(),
status: ComplianceStatus::NotApplicable,
checked_at: Utc::now(),
violations: Vec::new(),
details: Some("Rule not applicable to this system".to_string()),
}
}
pub fn with_details(mut self, details: impl Into<String>) -> Self {
self.details = Some(details.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplianceReport {
pub id: String,
pub name: String,
pub generated_at: DateTime<Utc>,
pub standards: Vec<ComplianceStandard>,
pub overall_status: ComplianceStatus,
pub compliance_score: f32,
pub results: Vec<CheckResult>,
pub violations: Vec<Violation>,
pub summary: ReportSummary,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ReportSummary {
pub total_rules: usize,
pub compliant_rules: usize,
pub non_compliant_rules: usize,
pub not_applicable_rules: usize,
pub total_violations: usize,
pub critical_violations: usize,
pub high_violations: usize,
pub medium_violations: usize,
pub low_violations: usize,
}
impl ComplianceReport {
pub fn new(name: impl Into<String>, standards: Vec<ComplianceStandard>) -> Self {
Self {
id: format!("RPT-{}", uuid::Uuid::new_v4()),
name: name.into(),
generated_at: Utc::now(),
standards,
overall_status: ComplianceStatus::NotChecked,
compliance_score: 0.0,
results: Vec::new(),
violations: Vec::new(),
summary: ReportSummary::default(),
metadata: HashMap::new(),
}
}
pub fn add_result(&mut self, result: CheckResult) {
self.summary.total_rules += 1;
match result.status {
ComplianceStatus::Compliant => self.summary.compliant_rules += 1,
ComplianceStatus::NonCompliant => self.summary.non_compliant_rules += 1,
ComplianceStatus::PartiallyCompliant => self.summary.non_compliant_rules += 1,
ComplianceStatus::NotApplicable => self.summary.not_applicable_rules += 1,
ComplianceStatus::NotChecked => {}
}
for violation in &result.violations {
self.summary.total_violations += 1;
match violation.severity {
RuleSeverity::Critical => self.summary.critical_violations += 1,
RuleSeverity::High => self.summary.high_violations += 1,
RuleSeverity::Medium => self.summary.medium_violations += 1,
RuleSeverity::Low => self.summary.low_violations += 1,
}
}
self.violations.extend(result.violations.clone());
self.results.push(result);
}
pub fn calculate_score(&mut self) {
if self.summary.total_rules == 0 {
self.compliance_score = 100.0;
self.overall_status = ComplianceStatus::NotChecked;
return;
}
let applicable_rules = self.summary.total_rules - self.summary.not_applicable_rules;
if applicable_rules == 0 {
self.compliance_score = 100.0;
self.overall_status = ComplianceStatus::NotApplicable;
return;
}
let base_score = (self.summary.compliant_rules as f32 / applicable_rules as f32) * 100.0;
let penalty = (self.summary.critical_violations as f32 * 10.0)
+ (self.summary.high_violations as f32 * 5.0)
+ (self.summary.medium_violations as f32 * 2.0)
+ (self.summary.low_violations as f32 * 0.5);
self.compliance_score = (base_score - penalty).clamp(0.0, 100.0);
self.overall_status = if self.compliance_score >= 90.0 {
ComplianceStatus::Compliant
} else if self.compliance_score >= 60.0 {
ComplianceStatus::PartiallyCompliant
} else {
ComplianceStatus::NonCompliant
};
}
pub fn export(&self, format: ReportFormat) -> Vec<u8> {
match format {
ReportFormat::Json => serde_json::to_string_pretty(self)
.unwrap_or_default()
.into_bytes(),
ReportFormat::Csv => self.export_csv(),
ReportFormat::Html => self.export_html().into_bytes(),
ReportFormat::Markdown => self.export_markdown().into_bytes(),
ReportFormat::Pdf => {
self.export_html().into_bytes()
}
}
}
fn export_csv(&self) -> Vec<u8> {
let mut csv = String::from("Rule ID,Rule Name,Status,Violations Count,Severity\n");
for result in &self.results {
let status = format!("{:?}", result.status);
let violation_count = result.violations.len();
let severity = result
.violations
.first()
.map(|v| format!("{:?}", v.severity))
.unwrap_or_else(|| "N/A".to_string());
csv.push_str(&format!(
"{},{},{},{},{}\n",
result.rule_id, result.rule_name, status, violation_count, severity
));
}
csv.into_bytes()
}
fn export_html(&self) -> String {
format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>Compliance Report - {}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
h1 {{ color: #333; }}
.score {{ font-size: 24px; font-weight: bold; margin: 20px 0; }}
.compliant {{ color: green; }}
.non-compliant {{ color: red; }}
.partial {{ color: orange; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
</style>
</head>
<body>
<h1>Compliance Report</h1>
<p>Generated: {}</p>
<p>Standards: {}</p>
<div class="score">Compliance Score: {:.1}%</div>
<h2>Summary</h2>
<ul>
<li>Total Rules: {}</li>
<li>Compliant: {}</li>
<li>Non-Compliant: {}</li>
<li>Total Violations: {}</li>
</ul>
<h2>Results</h2>
<table>
<tr><th>Rule ID</th><th>Rule Name</th><th>Status</th><th>Violations</th></tr>
{}
</table>
</body>
</html>"#,
self.name,
self.generated_at.format("%Y-%m-%d %H:%M:%S UTC"),
self.standards
.iter()
.map(|s| s.name())
.collect::<Vec<_>>()
.join(", "),
self.compliance_score,
self.summary.total_rules,
self.summary.compliant_rules,
self.summary.non_compliant_rules,
self.summary.total_violations,
self.results
.iter()
.map(|r| format!(
"<tr><td>{}</td><td>{}</td><td>{:?}</td><td>{}</td></tr>",
r.rule_id,
r.rule_name,
r.status,
r.violations.len()
))
.collect::<Vec<_>>()
.join("\n ")
)
}
fn export_markdown(&self) -> String {
format!(
r#"# Compliance Report: {}
**Generated:** {}
**Standards:** {}
**Score:** {:.1}%
## Summary
| Metric | Count |
|--------|-------|
| Total Rules | {} |
| Compliant | {} |
| Non-Compliant | {} |
| Total Violations | {} |
| Critical Violations | {} |
## Results
| Rule ID | Rule Name | Status | Violations |
|---------|-----------|--------|------------|
{}
## Violations
{}
"#,
self.name,
self.generated_at.format("%Y-%m-%d %H:%M:%S UTC"),
self.standards
.iter()
.map(|s| s.name())
.collect::<Vec<_>>()
.join(", "),
self.compliance_score,
self.summary.total_rules,
self.summary.compliant_rules,
self.summary.non_compliant_rules,
self.summary.total_violations,
self.summary.critical_violations,
self.results
.iter()
.map(|r| format!(
"| {} | {} | {:?} | {} |",
r.rule_id,
r.rule_name,
r.status,
r.violations.len()
))
.collect::<Vec<_>>()
.join("\n"),
self.violations
.iter()
.map(|v| format!(
"- **{}**: {} (Severity: {:?})",
v.rule_id, v.description, v.severity
))
.collect::<Vec<_>>()
.join("\n")
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_violation_creation() {
let rule = ComplianceRule::new("TEST-1", "Test", RuleCategory::Security);
let violation = Violation::new(&rule, "Test violation");
assert_eq!(violation.rule_id, "TEST-1");
assert_eq!(violation.description, "Test violation");
}
#[test]
fn test_check_result_compliant() {
let rule = ComplianceRule::new("TEST-1", "Test", RuleCategory::Security);
let result = CheckResult::compliant(&rule);
assert_eq!(result.status, ComplianceStatus::Compliant);
assert!(result.violations.is_empty());
}
#[test]
fn test_compliance_report() {
let mut report = ComplianceReport::new("Test Report", vec![ComplianceStandard::SOC2]);
let rule = ComplianceRule::new("TEST-1", "Test", RuleCategory::Security);
let result = CheckResult::compliant(&rule);
report.add_result(result);
report.calculate_score();
assert_eq!(report.summary.total_rules, 1);
assert_eq!(report.summary.compliant_rules, 1);
assert!(report.compliance_score > 0.0);
}
#[test]
fn test_export_json() {
let report = ComplianceReport::new("Test", vec![ComplianceStandard::SOC2]);
let json = report.export(ReportFormat::Json);
assert!(!json.is_empty());
}
#[test]
fn test_export_markdown() {
let mut report = ComplianceReport::new("Test Report", vec![ComplianceStandard::SOC2]);
let rule = ComplianceRule::new("TEST-1", "Test Rule", RuleCategory::Security);
report.add_result(CheckResult::compliant(&rule));
report.calculate_score();
let md = report.export(ReportFormat::Markdown);
let md_str = String::from_utf8(md).unwrap();
assert!(md_str.contains("# Compliance Report"));
assert!(md_str.contains("TEST-1"));
}
}