1use anyhow::Result;
2use assay_evidence::bundle::writer::{ErrorClass, ErrorCode};
3use serde::Serialize;
4
5#[derive(Debug, Serialize, Clone)]
6pub struct SimReport {
7 pub suite: String,
8 pub seed: u64,
9 pub summary: SimSummary,
10 pub results: Vec<AttackResult>,
11 #[serde(skip_serializing_if = "std::ops::Not::not")]
13 pub time_budget_exceeded: bool,
14 #[serde(skip_serializing_if = "Vec::is_empty")]
16 pub skipped_phases: Vec<String>,
17 #[serde(skip_serializing_if = "Vec::is_empty")]
25 pub phases_not_attempted: Vec<String>,
26}
27
28#[derive(Debug, Serialize, Clone, Default)]
29pub struct SimSummary {
30 pub total: usize,
31 pub passed: usize, pub blocked: usize, pub bypassed: usize, pub failed: usize, pub errors: usize,
36}
37
38#[derive(Debug, Serialize, Clone)]
39pub struct AttackResult {
40 pub name: String,
41 pub status: AttackStatus,
42 pub error_class: Option<String>,
43 pub error_code: Option<String>,
44 pub message: Option<String>,
45 pub duration_ms: u64,
46}
47
48#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
49pub enum AttackStatus {
50 Passed, Failed, Blocked, Bypassed, Error, }
56
57impl SimReport {
58 pub fn new(suite: &str, seed: u64) -> Self {
59 Self {
60 suite: suite.to_string(),
61 seed,
62 summary: SimSummary::default(),
63 results: Vec::new(),
64 time_budget_exceeded: false,
65 skipped_phases: Vec::new(),
66 phases_not_attempted: Vec::new(),
67 }
68 }
69
70 pub fn set_phases_not_attempted(&mut self, not_attempted: Vec<String>) {
75 self.phases_not_attempted = not_attempted;
76 }
77
78 pub fn set_time_budget_exceeded(&mut self, skipped: Vec<String>) {
79 self.time_budget_exceeded = true;
80 self.skipped_phases = skipped;
81 }
82
83 pub fn add_attack(
84 &mut self,
85 name: &str,
86 result: Result<(ErrorClass, ErrorCode, String), anyhow::Error>,
87 duration_ms: u64,
88 ) {
89 self.summary.total += 1;
90 let res = match result {
91 Ok((class, code, message)) => {
92 self.summary.blocked += 1;
93 AttackResult {
94 name: name.to_string(),
95 status: AttackStatus::Blocked,
96 error_class: Some(format!("{:?}", class)),
97 error_code: Some(format!("{:?}", code)),
98 message: Some(message),
99 duration_ms,
100 }
101 }
102 Err(e) => {
103 self.summary.bypassed += 1;
104 AttackResult {
105 name: name.to_string(),
106 status: AttackStatus::Bypassed,
107 error_class: None,
108 error_code: None,
109 message: Some(e.to_string()),
110 duration_ms,
111 }
112 }
113 };
114 self.results.push(res);
115 }
116
117 pub fn add_result(&mut self, result: AttackResult) {
119 self.summary.total += 1;
120 match result.status {
121 AttackStatus::Passed => self.summary.passed += 1,
122 AttackStatus::Failed => self.summary.failed += 1,
123 AttackStatus::Blocked => self.summary.blocked += 1,
124 AttackStatus::Bypassed => self.summary.bypassed += 1,
125 AttackStatus::Error => self.summary.errors += 1,
126 }
127 self.results.push(result);
128 }
129
130 pub fn add_check(&mut self, name: &str, result: Result<()>, duration_ms: u64) {
131 self.summary.total += 1;
132 let res = match result {
133 Ok(_) => {
134 self.summary.passed += 1;
135 AttackResult {
136 name: name.to_string(),
137 status: AttackStatus::Passed,
138 error_class: None,
139 error_code: None,
140 message: None,
141 duration_ms,
142 }
143 }
144 Err(e) => {
145 self.summary.failed += 1;
146 AttackResult {
147 name: name.to_string(),
148 status: AttackStatus::Failed,
149 error_class: None,
150 error_code: None,
151 message: Some(e.to_string()),
152 duration_ms,
153 }
154 }
155 };
156 self.results.push(res);
157 }
158}