Skip to main content

sentri_core/
integration_testing.rs

1/// Integration Testing Module
2///
3/// Comprehensive integration tests against real exploit patterns and documented vulnerabilities.
4/// Tests detectors using actual code from documented exploits to validate real-world effectiveness.
5use crate::Finding;
6use std::collections::HashMap;
7
8/// Real exploit test case
9#[derive(Debug, Clone)]
10pub struct ExploitTestCase {
11    /// H-code identifier (e.g., H19, H16)
12    pub h_code: String,
13    /// Project name
14    pub project_name: String,
15    /// Loss amount in millions
16    pub loss_millions: f64,
17    /// Year of exploit
18    pub year: u32,
19    /// Vulnerable code pattern
20    pub vulnerable_code: String,
21    /// Expected detector hits
22    pub expected_detections: Vec<String>,
23    /// Minimum expected findings
24    pub min_findings: usize,
25}
26
27/// Integration test suite
28pub struct IntegrationTestSuite {
29    test_cases: Vec<ExploitTestCase>,
30    results: HashMap<String, IntegrationTestResult>,
31}
32
33/// Result of integration test
34#[derive(Debug, Clone)]
35pub struct IntegrationTestResult {
36    /// H-code
37    pub h_code: String,
38    /// Whether test passed
39    pub passed: bool,
40    /// Number of findings
41    pub findings_count: usize,
42    /// Matching detectors
43    pub matching_detectors: Vec<String>,
44    /// False negatives (expected but not found)
45    pub false_negatives: Vec<String>,
46    /// Execution time in ms
47    pub execution_time_ms: u64,
48}
49
50impl IntegrationTestSuite {
51    /// Create new integration test suite
52    #[allow(clippy::new_without_default)]
53    pub fn new() -> Self {
54        let mut suite = Self {
55            test_cases: Vec::new(),
56            results: HashMap::new(),
57        };
58
59        suite.initialize_real_exploits();
60        suite
61    }
62
63    /// Initialize test cases with real exploits
64    fn initialize_real_exploits(&mut self) {
65        // Phase A exploits
66        self.test_cases.push(ExploitTestCase {
67            h_code: "H19".to_string(),
68            project_name: "Euler".to_string(),
69            loss_millions: 197.0,
70            year: 2023,
71            vulnerable_code: r#"
72function depositCollateral(address asset, uint256 amount) public {
73    collateral[msg.sender][asset] += amount;
74    // Missing: require(isHealthy(msg.sender));
75    emit DepositCollateral(asset, amount);
76}
77            "#
78            .to_string(),
79            expected_detections: vec!["health_check".to_string()],
80            min_findings: 1,
81        });
82
83        self.test_cases.push(ExploitTestCase {
84            h_code: "H16".to_string(),
85            project_name: "Nomad".to_string(),
86            loss_millions: 190.0,
87            year: 2022,
88            vulnerable_code: r#"
89bytes32 public merkleRoot;
90
91function initialize() external {
92    // Zero root initialization
93    merkleRoot = bytes32(0);
94}
95
96function verifyMessage(bytes32[] calldata proof) public {
97    if (verifyProof(proof, merkleRoot)) {
98        executeMessage();
99    }
100}
101            "#
102            .to_string(),
103            expected_detections: vec!["merkle_root".to_string()],
104            min_findings: 1,
105        });
106
107        self.test_cases.push(ExploitTestCase {
108            h_code: "H47".to_string(),
109            project_name: "KelpDAO".to_string(),
110            loss_millions: 292.0,
111            year: 2024,
112            vulnerable_code: r#"
113address[] public dvns;
114
115function setDVNs(address[] calldata _dvns) external {
116    // No minimum count validation
117    dvns = _dvns;
118}
119
120function sendMessage(bytes calldata message) external {
121    require(dvns.length > 0);
122    (bool success,) = dvns[0].call(message);
123    require(success);
124}
125            "#
126            .to_string(),
127            expected_detections: vec!["dvn_single_point".to_string()],
128            min_findings: 1,
129        });
130
131        self.test_cases.push(ExploitTestCase {
132            h_code: "H56".to_string(),
133            project_name: "Echo".to_string(),
134            loss_millions: 73.0,
135            year: 2023,
136            vulnerable_code: r#"
137function mint(uint256 amount) public {
138    // No collateral verification
139    totalMinted += amount;
140    balances[msg.sender] += amount;
141}
142            "#
143            .to_string(),
144            expected_detections: vec!["synthetic_mint".to_string()],
145            min_findings: 1,
146        });
147
148        // Phase B exploits
149        self.test_cases.push(ExploitTestCase {
150            h_code: "H17".to_string(),
151            project_name: "Mango".to_string(),
152            loss_millions: 117.0,
153            year: 2022,
154            vulnerable_code: r#"
155function swapWithOracle() public {
156    uint256 price = oracle.getPrice(token);
157    // Manipulate spot price while using it
158    tokenPrice[msg.sender] = price;
159    executeSwap(price);
160}
161            "#
162            .to_string(),
163            expected_detections: vec!["oracle_self_trade".to_string()],
164            min_findings: 1,
165        });
166    }
167
168    /// Add test case
169    pub fn add_test_case(&mut self, test_case: ExploitTestCase) {
170        self.test_cases.push(test_case);
171    }
172
173    /// Run integration tests
174    pub fn run_tests<F>(&mut self, detector_fn: F) -> IntegrationTestResults
175    where
176        F: Fn(&str) -> Vec<Finding>,
177    {
178        let mut results = Vec::new();
179        let mut passed = 0;
180        let mut failed = 0;
181
182        for test_case in &self.test_cases {
183            let start = std::time::Instant::now();
184            let findings = detector_fn(&test_case.vulnerable_code);
185            let elapsed = start.elapsed().as_millis() as u64;
186
187            let findings_count = findings.len();
188            let test_passed = findings_count >= test_case.min_findings;
189
190            if test_passed {
191                passed += 1;
192            } else {
193                failed += 1;
194            }
195
196            let matching_detectors = findings.iter().map(|f| f.invariant_id.clone()).collect();
197
198            let result = IntegrationTestResult {
199                h_code: test_case.h_code.clone(),
200                passed: test_passed,
201                findings_count,
202                matching_detectors,
203                false_negatives: test_case
204                    .expected_detections
205                    .iter()
206                    .filter(|d| !test_case.expected_detections.iter().any(|m| m == *d))
207                    .cloned()
208                    .collect(),
209                execution_time_ms: elapsed,
210            };
211
212            results.push(result.clone());
213            self.results.insert(test_case.h_code.clone(), result);
214        }
215
216        IntegrationTestResults {
217            total: self.test_cases.len(),
218            passed,
219            failed,
220            results,
221        }
222    }
223
224    /// Get result for H-code
225    pub fn get_result(&self, h_code: &str) -> Option<&IntegrationTestResult> {
226        self.results.get(h_code)
227    }
228}
229
230/// Integration test results summary
231#[derive(Debug, Clone)]
232pub struct IntegrationTestResults {
233    /// Total tests
234    pub total: usize,
235    /// Tests passed
236    pub passed: usize,
237    /// Tests failed
238    pub failed: usize,
239    /// Individual results
240    pub results: Vec<IntegrationTestResult>,
241}
242
243impl IntegrationTestResults {
244    /// Success rate as percentage
245    pub fn success_rate(&self) -> f64 {
246        if self.total == 0 {
247            0.0
248        } else {
249            (self.passed as f64 / self.total as f64) * 100.0
250        }
251    }
252
253    /// Generate report
254    pub fn report(&self) -> String {
255        let mut report = format!(
256            "# Integration Test Results\n\n## Summary\n- **Total Tests:** {}\n",
257            self.total
258        );
259        report.push_str(&format!("- **Passed:** {}\n", self.passed));
260        report.push_str(&format!("- **Failed:** {}\n", self.failed));
261        report.push_str(&format!(
262            "- **Success Rate:** {:.1}%\n\n",
263            self.success_rate()
264        ));
265
266        report.push_str("## Details\n\n");
267        for result in &self.results {
268            let status = if result.passed {
269                "✓ PASS"
270            } else {
271                "✗ FAIL"
272            };
273            report.push_str(&format!(
274                "### {} {}\n- **Findings:** {}\n- **Time:** {} ms\n\n",
275                result.h_code, status, result.findings_count, result.execution_time_ms
276            ));
277        }
278
279        report
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn integration_suite_initializes() {
289        let suite = IntegrationTestSuite::new();
290        assert!(!suite.test_cases.is_empty());
291    }
292
293    #[test]
294    fn exploit_test_case_structure() {
295        let test = ExploitTestCase {
296            h_code: "H99".to_string(),
297            project_name: "Test".to_string(),
298            loss_millions: 10.0,
299            year: 2024,
300            vulnerable_code: "test code".to_string(),
301            expected_detections: vec!["detector1".to_string()],
302            min_findings: 1,
303        };
304
305        assert_eq!(test.h_code, "H99");
306        assert_eq!(test.min_findings, 1);
307    }
308
309    #[test]
310    fn test_results_success_rate() {
311        let results = IntegrationTestResults {
312            total: 10,
313            passed: 8,
314            failed: 2,
315            results: vec![],
316        };
317
318        assert_eq!(results.success_rate(), 80.0);
319    }
320}