sentri_core/
test_infrastructure.rs1use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct DetectorTestResult {
10 pub detector_name: String,
12 pub chain: String,
14 pub vulnerable_count: usize,
16 pub safe_count: usize,
18 pub true_positives: usize,
20 pub false_positives: usize,
22 pub false_negatives: usize,
24 pub execution_time_ms: u64,
26}
27
28impl DetectorTestResult {
29 pub fn precision(&self) -> f64 {
31 let total = self.true_positives + self.false_positives;
32 if total == 0 {
33 1.0
34 } else {
35 self.true_positives as f64 / total as f64
36 }
37 }
38
39 pub fn recall(&self) -> f64 {
41 let total = self.true_positives + self.false_negatives;
42 if total == 0 {
43 1.0
44 } else {
45 self.true_positives as f64 / total as f64
46 }
47 }
48
49 pub fn f1_score(&self) -> f64 {
51 let p = self.precision();
52 let r = self.recall();
53 if p + r == 0.0 {
54 0.0
55 } else {
56 2.0 * p * r / (p + r)
57 }
58 }
59
60 pub fn passes_quality_threshold(&self) -> bool {
62 self.precision() >= 0.90 && self.recall() >= 0.90 && self.f1_score() >= 0.90
63 }
64}
65
66#[derive(Debug, Clone)]
68pub struct DetectorTestCase {
69 pub name: String,
71 pub code: String,
73 pub is_vulnerable: bool,
75 pub expected_findings: usize,
77 pub category: String,
79}
80
81pub struct DetectorTestSuite {
83 test_cases: HashMap<String, Vec<DetectorTestCase>>,
84 results: HashMap<String, DetectorTestResult>,
85}
86
87impl Default for DetectorTestSuite {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl DetectorTestSuite {
94 pub fn new() -> Self {
96 Self {
97 test_cases: HashMap::new(),
98 results: HashMap::new(),
99 }
100 }
101
102 pub fn add_test_case(&mut self, detector_name: String, test_case: DetectorTestCase) {
104 self.test_cases
105 .entry(detector_name)
106 .or_default()
107 .push(test_case);
108 }
109
110 pub fn get_test_cases(&self, detector_name: &str) -> Option<&[DetectorTestCase]> {
112 self.test_cases.get(detector_name).map(|v| v.as_slice())
113 }
114
115 pub fn add_result(&mut self, detector_name: String, result: DetectorTestResult) {
117 self.results.insert(detector_name, result);
118 }
119
120 pub fn get_result(&self, detector_name: &str) -> Option<&DetectorTestResult> {
122 self.results.get(detector_name)
123 }
124
125 pub fn generate_report(&self) -> String {
127 let mut report = "# Detector Test Report\n\n".to_string();
128 report.push_str("## Summary\n\n");
129
130 let mut total_tests = 0;
131 let mut passed_tests = 0;
132 let mut avg_precision = 0.0;
133 let mut avg_recall = 0.0;
134 let mut avg_f1 = 0.0;
135
136 for (detector, result) in &self.results {
137 report.push_str(&format!("### {} ({})\n", detector, result.chain));
138
139 report.push_str(&format!(
140 "- **Vulnerable Patterns Tested:** {}\n",
141 result.vulnerable_count
142 ));
143 report.push_str(&format!(
144 "- **Safe Patterns Tested:** {}\n",
145 result.safe_count
146 ));
147 report.push_str(&format!(
148 "- **True Positives:** {}\n",
149 result.true_positives
150 ));
151 report.push_str(&format!(
152 "- **False Positives:** {}\n",
153 result.false_positives
154 ));
155 report.push_str(&format!(
156 "- **False Negatives:** {}\n",
157 result.false_negatives
158 ));
159 report.push_str(&format!(
160 "- **Precision:** {:.2}%\n",
161 result.precision() * 100.0
162 ));
163 report.push_str(&format!("- **Recall:** {:.2}%\n", result.recall() * 100.0));
164 report.push_str(&format!("- **F1 Score:** {:.4}\n", result.f1_score()));
165 report.push_str(&format!(
166 "- **Execution Time:** {} ms\n",
167 result.execution_time_ms
168 ));
169
170 let status = if result.passes_quality_threshold() {
171 "✓ PASS"
172 } else {
173 "✗ FAIL"
174 };
175 report.push_str(&format!("- **Status:** {}\n\n", status));
176
177 total_tests += 1;
178 if result.passes_quality_threshold() {
179 passed_tests += 1;
180 }
181
182 avg_precision += result.precision();
183 avg_recall += result.recall();
184 avg_f1 += result.f1_score();
185 }
186
187 if !self.results.is_empty() {
188 let count = self.results.len() as f64;
189 report.push_str("\n## Aggregate Metrics\n\n");
190 report.push_str(&format!(
191 "- **Total Detectors Tested:** {}\n",
192 self.results.len()
193 ));
194 report.push_str(&format!(
195 "- **Passed Quality Threshold:** {}/{}\n",
196 passed_tests, total_tests
197 ));
198 report.push_str(&format!(
199 "- **Average Precision:** {:.2}%\n",
200 (avg_precision / count) * 100.0
201 ));
202 report.push_str(&format!(
203 "- **Average Recall:** {:.2}%\n",
204 (avg_recall / count) * 100.0
205 ));
206 report.push_str(&format!("- **Average F1 Score:** {:.4}\n", avg_f1 / count));
207 }
208
209 report
210 }
211}
212
213pub mod utils {
215 pub fn extract_code_context(code: &str, line: usize, context_lines: usize) -> String {
217 let lines: Vec<&str> = code.lines().collect();
218 let start = line.saturating_sub(context_lines);
219 let end = std::cmp::min(line + context_lines, lines.len());
220
221 lines[start..end].join("\n")
222 }
223
224 pub fn findings_match(f1: &crate::Finding, f2: &crate::Finding) -> bool {
226 f1.invariant_id == f2.invariant_id && f1.file == f2.file && f1.line == f2.line
227 }
228
229 pub fn generate_variant(base_code: &str, mutation: &str) -> String {
231 base_code.replace("// MUTATION_POINT", mutation)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_result_metrics() {
241 let result = DetectorTestResult {
242 detector_name: "test".to_string(),
243 chain: "evm".to_string(),
244 vulnerable_count: 50,
245 safe_count: 50,
246 true_positives: 48,
247 false_positives: 1,
248 false_negatives: 2,
249 execution_time_ms: 150,
250 };
251
252 assert!(result.precision() > 0.95);
253 assert!(result.recall() > 0.95);
254 assert!(result.f1_score() > 0.95);
255 }
256
257 #[test]
258 fn test_suite_stores_cases() {
259 let mut suite = DetectorTestSuite::new();
260 let test_case = DetectorTestCase {
261 name: "vulnerable_case".to_string(),
262 code: "malicious code".to_string(),
263 is_vulnerable: true,
264 expected_findings: 1,
265 category: "vulnerable".to_string(),
266 };
267
268 suite.add_test_case("detector1".to_string(), test_case);
269 assert!(suite.get_test_cases("detector1").is_some());
270 }
271
272 #[test]
273 fn test_suite_generates_report() {
274 let mut suite = DetectorTestSuite::new();
275 let result = DetectorTestResult {
276 detector_name: "test_detector".to_string(),
277 chain: "evm".to_string(),
278 vulnerable_count: 50,
279 safe_count: 50,
280 true_positives: 45,
281 false_positives: 2,
282 false_negatives: 5,
283 execution_time_ms: 200,
284 };
285
286 suite.add_result("test_detector".to_string(), result);
287 let report = suite.generate_report();
288
289 assert!(report.contains("test_detector"));
290 assert!(report.contains("Precision"));
291 assert!(report.contains("Recall"));
292 }
293}