1use serde::{Deserialize, Serialize};
2
3use crate::control::{Control, ControlFinding, ControlStatus, evaluate_all};
4use crate::evidence::EvidenceBundle;
5use crate::profile::{ControlProfile, ProfileOutcome, SeverityLabels, apply_profile};
6use crate::registry::ControlRegistry;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct AssessmentReport {
11 pub profile_name: String,
12 pub findings: Vec<ControlFinding>,
13 pub outcomes: Vec<ProfileOutcome>,
14 pub severity_labels: SeverityLabels,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct VerificationResult {
20 #[serde(flatten)]
21 pub report: AssessmentReport,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub evidence: Option<EvidenceBundle>,
24}
25
26impl VerificationResult {
27 pub fn new(report: AssessmentReport, evidence: Option<EvidenceBundle>) -> Self {
28 Self { report, evidence }
29 }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct BatchReport {
35 pub reports: Vec<BatchEntry>,
36 pub total_pass: usize,
37 pub total_review: usize,
38 pub total_fail: usize,
39 pub skipped: Vec<SkippedEntry>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct BatchEntry {
45 pub subject_id: String,
46 #[serde(flatten)]
47 pub result: VerificationResult,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct SkippedEntry {
53 pub subject_id: String,
54 pub reason: String,
55}
56
57pub fn assess(
59 evidence: &EvidenceBundle,
60 controls: &[Box<dyn Control>],
61 profile: &dyn ControlProfile,
62) -> AssessmentReport {
63 let findings: Vec<ControlFinding> = evaluate_all(controls, evidence)
64 .into_iter()
65 .filter(|f| f.status != ControlStatus::NotApplicable)
66 .collect();
67 let outcomes = apply_profile(profile, &findings);
68
69 AssessmentReport {
70 profile_name: profile.name().to_string(),
71 findings,
72 outcomes,
73 severity_labels: profile.severity_labels(),
74 }
75}
76
77pub fn assess_with_registry(
79 evidence: &EvidenceBundle,
80 registry: &ControlRegistry,
81 profile: &dyn ControlProfile,
82) -> AssessmentReport {
83 assess(evidence, registry.controls(), profile)
84}
85
86use crate::profile::GateDecision;
91use std::collections::HashMap;
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct FleetReport {
96 pub repos: Vec<RepoSummary>,
98 pub control_stats: Vec<ControlFleetStat>,
100 pub total_pass: usize,
102 pub total_review: usize,
103 pub total_fail: usize,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct RepoSummary {
109 pub repo_id: String,
110 pub pass: usize,
111 pub review: usize,
112 pub fail: usize,
113 pub failing_controls: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct ControlFleetStat {
120 pub control_id: String,
121 pub fail_count: usize,
123 pub review_count: usize,
125 pub pass_count: usize,
127 pub tsc_criteria: Vec<String>,
129}
130
131impl FleetReport {
132 pub fn from_assessments(entries: Vec<(String, &AssessmentReport)>) -> Self {
134 let mut repos = Vec::new();
135 let mut control_map: HashMap<String, (usize, usize, usize)> = HashMap::new();
136 let mut total_pass = 0;
137 let mut total_review = 0;
138 let mut total_fail = 0;
139
140 for (repo_id, report) in &entries {
141 let mut pass = 0;
142 let mut review = 0;
143 let mut fail = 0;
144 let mut failing_controls = Vec::new();
145
146 for outcome in &report.outcomes {
147 let key = outcome.control_id.as_str().to_string();
148 let entry = control_map.entry(key.clone()).or_insert((0, 0, 0));
149
150 match outcome.decision {
151 GateDecision::Pass => {
152 pass += 1;
153 entry.2 += 1;
154 }
155 GateDecision::Review => {
156 review += 1;
157 entry.1 += 1;
158 }
159 GateDecision::Fail => {
160 fail += 1;
161 entry.0 += 1;
162 failing_controls.push(key);
163 }
164 }
165 }
166
167 total_pass += pass;
168 total_review += review;
169 total_fail += fail;
170
171 repos.push(RepoSummary {
172 repo_id: repo_id.clone(),
173 pass,
174 review,
175 fail,
176 failing_controls,
177 });
178 }
179
180 repos.sort_by(|a, b| b.fail.cmp(&a.fail));
182
183 let mut control_stats: Vec<ControlFleetStat> = control_map
185 .into_iter()
186 .map(|(id, (fail, review, pass))| ControlFleetStat {
187 tsc_criteria: crate::control::builtin_tsc_mapping(&id)
188 .iter()
189 .map(|s| s.to_string())
190 .collect(),
191 control_id: id,
192 fail_count: fail,
193 review_count: review,
194 pass_count: pass,
195 })
196 .collect();
197 control_stats.sort_by(|a, b| b.fail_count.cmp(&a.fail_count));
198
199 FleetReport {
200 repos,
201 control_stats,
202 total_pass,
203 total_review,
204 total_fail,
205 }
206 }
207}