Skip to main content

sh_layer4/compliance_checker/
report.rs

1//! # Compliance Report
2//!
3//! 合规检查报告生成。
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use super::rules::{ComplianceRule, ComplianceStandard, RuleCategory, RuleSeverity};
10
11/// 合规状态
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub enum ComplianceStatus {
14    /// 合规
15    Compliant,
16    /// 不合规
17    NonCompliant,
18    /// 部分合规
19    PartiallyCompliant,
20    /// 未检查
21    #[default]
22    NotChecked,
23    /// 不适用
24    NotApplicable,
25}
26
27/// 报告格式
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ReportFormat {
30    /// JSON 格式
31    Json,
32    /// CSV 格式
33    Csv,
34    /// HTML 格式
35    Html,
36    /// Markdown 格式
37    Markdown,
38    /// PDF 格式
39    Pdf,
40}
41
42/// 违规项
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Violation {
45    /// 违规 ID
46    pub id: String,
47    /// 规则 ID
48    pub rule_id: String,
49    /// 规则名称
50    pub rule_name: String,
51    /// 严重性
52    pub severity: RuleSeverity,
53    /// 类别
54    pub category: RuleCategory,
55    /// 违规描述
56    pub description: String,
57    /// 发现时间
58    pub detected_at: DateTime<Utc>,
59    /// 资源类型
60    pub resource_type: Option<String>,
61    /// 资源 ID
62    pub resource_id: Option<String>,
63    /// 建议修复措施
64    pub remediation: Option<String>,
65    /// 相关证据
66    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/// 检查结果
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct CheckResult {
110    /// 规则 ID
111    pub rule_id: String,
112    /// 规则名称
113    pub rule_name: String,
114    /// 检查状态
115    pub status: ComplianceStatus,
116    /// 检查时间
117    pub checked_at: DateTime<Utc>,
118    /// 违规项(如果有)
119    pub violations: Vec<Violation>,
120    /// 检查详情
121    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/// 合规报告
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ComplianceReport {
171    /// 报告 ID
172    pub id: String,
173    /// 报告名称
174    pub name: String,
175    /// 生成时间
176    pub generated_at: DateTime<Utc>,
177    /// 检查的标准
178    pub standards: Vec<ComplianceStandard>,
179    /// 总体状态
180    pub overall_status: ComplianceStatus,
181    /// 合规分数 (0-100)
182    pub compliance_score: f32,
183    /// 检查结果
184    pub results: Vec<CheckResult>,
185    /// 所有违规
186    pub violations: Vec<Violation>,
187    /// 摘要统计
188    pub summary: ReportSummary,
189    /// 元数据
190    pub metadata: HashMap<String, serde_json::Value>,
191}
192
193/// 报告摘要
194#[derive(Debug, Clone, Serialize, Deserialize, Default)]
195pub struct ReportSummary {
196    /// 检查的规则总数
197    pub total_rules: usize,
198    /// 合规规则数
199    pub compliant_rules: usize,
200    /// 不合规规则数
201    pub non_compliant_rules: usize,
202    /// 不适用规则数
203    pub not_applicable_rules: usize,
204    /// 总违规数
205    pub total_violations: usize,
206    /// 严重违规数
207    pub critical_violations: usize,
208    /// 高危违规数
209    pub high_violations: usize,
210    /// 中危违规数
211    pub medium_violations: usize,
212    /// 低危违规数
213    pub low_violations: usize,
214}
215
216impl ComplianceReport {
217    /// 创建新的合规报告
218    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    /// 添加检查结果
234    pub fn add_result(&mut self, result: CheckResult) {
235        // 更新摘要统计
236        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        // 统计违规
247        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    /// 计算合规分数
262    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        // 基础分数 = 合规规则数 / 适用规则数 * 100
277        let base_score = (self.summary.compliant_rules as f32 / applicable_rules as f32) * 100.0;
278
279        // 根据违规严重性扣分
280        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        // 确定总体状态
288        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    /// 导出报告
298    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                // PDF 需要额外的依赖库,这里返回 HTML 作为占位
308                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}