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}
18
19#[derive(Debug, Serialize, Clone, Default)]
20pub struct SimSummary {
21 pub total: usize,
22 pub passed: usize, pub blocked: usize, pub bypassed: usize, pub failed: usize, pub errors: usize,
27}
28
29#[derive(Debug, Serialize, Clone)]
30pub struct AttackResult {
31 pub name: String,
32 pub status: AttackStatus,
33 pub error_class: Option<String>,
34 pub error_code: Option<String>,
35 pub message: Option<String>,
36 pub duration_ms: u64,
37}
38
39#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
40pub enum AttackStatus {
41 Passed, Failed, Blocked, Bypassed, Error, }
47
48impl SimReport {
49 pub fn new(suite: &str, seed: u64) -> Self {
50 Self {
51 suite: suite.to_string(),
52 seed,
53 summary: SimSummary::default(),
54 results: Vec::new(),
55 time_budget_exceeded: false,
56 skipped_phases: Vec::new(),
57 }
58 }
59
60 pub fn set_time_budget_exceeded(&mut self, skipped: Vec<String>) {
61 self.time_budget_exceeded = true;
62 self.skipped_phases = skipped;
63 }
64
65 pub fn add_attack(
66 &mut self,
67 name: &str,
68 result: Result<(ErrorClass, ErrorCode), anyhow::Error>,
69 duration_ms: u64,
70 ) {
71 self.summary.total += 1;
72 let res = match result {
73 Ok((class, code)) => {
74 self.summary.blocked += 1;
75 AttackResult {
76 name: name.to_string(),
77 status: AttackStatus::Blocked,
78 error_class: Some(format!("{:?}", class)),
79 error_code: Some(format!("{:?}", code)),
80 message: None,
81 duration_ms,
82 }
83 }
84 Err(e) => {
85 self.summary.bypassed += 1;
86 AttackResult {
87 name: name.to_string(),
88 status: AttackStatus::Bypassed,
89 error_class: None,
90 error_code: None,
91 message: Some(e.to_string()),
92 duration_ms,
93 }
94 }
95 };
96 self.results.push(res);
97 }
98
99 pub fn add_result(&mut self, result: AttackResult) {
101 self.summary.total += 1;
102 match result.status {
103 AttackStatus::Passed => self.summary.passed += 1,
104 AttackStatus::Failed => self.summary.failed += 1,
105 AttackStatus::Blocked => self.summary.blocked += 1,
106 AttackStatus::Bypassed => self.summary.bypassed += 1,
107 AttackStatus::Error => self.summary.errors += 1,
108 }
109 self.results.push(result);
110 }
111
112 pub fn add_check(&mut self, name: &str, result: Result<()>, duration_ms: u64) {
113 self.summary.total += 1;
114 let res = match result {
115 Ok(_) => {
116 self.summary.passed += 1;
117 AttackResult {
118 name: name.to_string(),
119 status: AttackStatus::Passed,
120 error_class: None,
121 error_code: None,
122 message: None,
123 duration_ms,
124 }
125 }
126 Err(e) => {
127 self.summary.failed += 1;
128 AttackResult {
129 name: name.to_string(),
130 status: AttackStatus::Failed,
131 error_class: None,
132 error_code: None,
133 message: Some(e.to_string()),
134 duration_ms,
135 }
136 }
137 };
138 self.results.push(res);
139 }
140}