Skip to main content

sentri_core/
oz_integration.rs

1/// OpenZeppelin Integration Module
2///
3/// Integrates Sentri findings with OpenZeppelin Contracts library patterns and audit standards.
4/// Provides mapping between Sentri detectors and known OZ vulnerabilities, enabling
5/// cross-reference with OZ audit recommendations and security standards.
6use crate::Finding;
7use std::collections::HashMap;
8
9/// OpenZeppelin vulnerability classification
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum OZVulnerabilityType {
12    /// Access control bypass
13    AccessControl,
14    /// Reentrancy attacks
15    Reentrancy,
16    /// Integer overflow/underflow
17    ArithmeticOverflow,
18    /// Token transfer vulnerabilities
19    TokenTransfer,
20    /// Oracle manipulation
21    OracleManipulation,
22    /// Flash loan attacks
23    FlashLoan,
24    /// Signature replay
25    SignatureReplay,
26    /// State management issues
27    StateManagement,
28    /// Initialization vulnerabilities
29    Initialization,
30    /// Custom vulnerability
31    Custom(String),
32}
33
34/// OpenZeppelin audit recommendation
35#[derive(Debug, Clone)]
36pub struct OZRecommendation {
37    /// Recommendation ID
38    pub id: String,
39    /// Title of recommendation
40    pub title: String,
41    /// Severity level
42    pub severity: String,
43    /// OZ best practice code
44    pub best_practice: String,
45    /// Reference URL or document
46    pub reference: String,
47}
48
49/// Mapping between Sentri detectors and OZ vulnerability types
50pub struct OZMappingRegistry {
51    detector_to_oz: HashMap<String, OZVulnerabilityType>,
52    oz_to_recommendations: HashMap<OZVulnerabilityType, Vec<OZRecommendation>>,
53}
54
55impl OZMappingRegistry {
56    /// Create new OZ mapping registry
57    #[allow(clippy::new_without_default)]
58    pub fn new() -> Self {
59        let mut registry = Self {
60            detector_to_oz: HashMap::new(),
61            oz_to_recommendations: HashMap::new(),
62        };
63
64        registry.initialize_mappings();
65        registry
66    }
67
68    /// Initialize detector to OZ vulnerability mappings
69    fn initialize_mappings(&mut self) {
70        // Phase A detectors
71        self.detector_to_oz.insert(
72            "health_check".to_string(),
73            OZVulnerabilityType::StateManagement,
74        );
75        self.detector_to_oz.insert(
76            "merkle_root".to_string(),
77            OZVulnerabilityType::StateManagement,
78        );
79        self.detector_to_oz.insert(
80            "dvn_single_point".to_string(),
81            OZVulnerabilityType::AccessControl,
82        );
83        self.detector_to_oz.insert(
84            "synthetic_mint".to_string(),
85            OZVulnerabilityType::StateManagement,
86        );
87        self.detector_to_oz.insert(
88            "lst_depeg".to_string(),
89            OZVulnerabilityType::OracleManipulation,
90        );
91
92        // Phase B detectors
93        self.detector_to_oz.insert(
94            "oracle_self_trade".to_string(),
95            OZVulnerabilityType::OracleManipulation,
96        );
97        self.detector_to_oz.insert(
98            "solana_durable_nonce".to_string(),
99            OZVulnerabilityType::SignatureReplay,
100        );
101        self.detector_to_oz.insert(
102            "synthetic_collateral_oracle".to_string(),
103            OZVulnerabilityType::OracleManipulation,
104        );
105        self.detector_to_oz.insert(
106            "erc4626_inflation".to_string(),
107            OZVulnerabilityType::ArithmeticOverflow,
108        );
109        self.detector_to_oz.insert(
110            "arbitrary_call_msg_value".to_string(),
111            OZVulnerabilityType::StateManagement,
112        );
113        self.detector_to_oz.insert(
114            "reentrancy_whitelisted".to_string(),
115            OZVulnerabilityType::Reentrancy,
116        );
117        self.detector_to_oz.insert(
118            "proxy_storage_collision".to_string(),
119            OZVulnerabilityType::StateManagement,
120        );
121        self.detector_to_oz.insert(
122            "bridge_address_crypto".to_string(),
123            OZVulnerabilityType::SignatureReplay,
124        );
125
126        // Initialize OZ recommendations
127        self.add_oz_recommendations();
128    }
129
130    /// Add OpenZeppelin recommendations
131    fn add_oz_recommendations(&mut self) {
132        // Access control recommendations
133        let access_control_recs = vec![
134            OZRecommendation {
135                id: "OZ-AC-001".to_string(),
136                title: "Use onlyOwner modifier for privileged functions".to_string(),
137                severity: "High".to_string(),
138                best_practice: "Use OpenZeppelin's Ownable contract".to_string(),
139                reference: "https://docs.openzeppelin.com/contracts/4.x/access-control".to_string(),
140            },
141            OZRecommendation {
142                id: "OZ-AC-002".to_string(),
143                title: "Implement role-based access control".to_string(),
144                severity: "High".to_string(),
145                best_practice: "Use AccessControl for complex permissions".to_string(),
146                reference: "https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl"
147                    .to_string(),
148            },
149        ];
150        self.oz_to_recommendations
151            .insert(OZVulnerabilityType::AccessControl, access_control_recs);
152
153        // Reentrancy recommendations
154        let reentrancy_recs = vec![OZRecommendation {
155            id: "OZ-RE-001".to_string(),
156            title: "Apply Checks-Effects-Interactions pattern".to_string(),
157            severity: "Critical".to_string(),
158            best_practice: "Update state before external calls".to_string(),
159            reference: "https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard"
160                .to_string(),
161        }];
162        self.oz_to_recommendations
163            .insert(OZVulnerabilityType::Reentrancy, reentrancy_recs);
164
165        // Oracle recommendations
166        let oracle_recs = vec![OZRecommendation {
167            id: "OZ-OR-001".to_string(),
168            title: "Use multiple oracle sources".to_string(),
169            severity: "High".to_string(),
170            best_practice: "Implement oracle aggregation pattern".to_string(),
171            reference: "https://docs.openzeppelin.com/contracts/4.x/".to_string(),
172        }];
173        self.oz_to_recommendations
174            .insert(OZVulnerabilityType::OracleManipulation, oracle_recs);
175    }
176
177    /// Get OZ vulnerability type for a detector
178    pub fn get_oz_type(&self, detector_name: &str) -> Option<OZVulnerabilityType> {
179        self.detector_to_oz.get(detector_name).cloned()
180    }
181
182    /// Get OZ recommendations for a vulnerability type
183    pub fn get_recommendations(
184        &self,
185        vuln_type: &OZVulnerabilityType,
186    ) -> Option<&[OZRecommendation]> {
187        self.oz_to_recommendations
188            .get(vuln_type)
189            .map(|v| v.as_slice())
190    }
191
192    /// Enrich a finding with OZ context
193    pub fn enrich_finding(&self, finding: &Finding) -> EnrichedFinding {
194        let detector_name = finding.invariant_id.to_lowercase();
195        let oz_type = self.get_oz_type(&detector_name);
196
197        let recommendations = oz_type
198            .as_ref()
199            .and_then(|t| self.get_recommendations(t))
200            .unwrap_or(&[])
201            .to_vec();
202
203        EnrichedFinding {
204            original_finding: finding.clone(),
205            oz_type,
206            oz_recommendations: recommendations,
207        }
208    }
209}
210
211/// Finding enriched with OZ context
212#[derive(Debug, Clone)]
213pub struct EnrichedFinding {
214    /// Original Sentri finding
215    pub original_finding: Finding,
216    /// Mapped OZ vulnerability type
217    pub oz_type: Option<OZVulnerabilityType>,
218    /// OZ recommendations
219    pub oz_recommendations: Vec<OZRecommendation>,
220}
221
222impl EnrichedFinding {
223    /// Generate OZ audit report section
224    pub fn to_audit_report(&self) -> String {
225        let mut report = format!("## Finding: {}\n\n", self.original_finding.message);
226
227        report.push_str(&format!(
228            "**Severity:** {:?}\n",
229            self.original_finding.severity
230        ));
231
232        if let Some(ref oz_type) = self.oz_type {
233            report.push_str(&format!("**OZ Classification:** {:?}\n\n", oz_type));
234        }
235
236        if !self.oz_recommendations.is_empty() {
237            report.push_str("### OpenZeppelin Recommendations\n\n");
238            for rec in &self.oz_recommendations {
239                report.push_str(&format!(
240                    "- **{}** ({}): {}\n",
241                    rec.id, rec.severity, rec.title
242                ));
243                report.push_str(&format!("  - Best Practice: {}\n", rec.best_practice));
244                report.push_str(&format!("  - Reference: {}\n\n", rec.reference));
245            }
246        }
247
248        report.push_str(&format!(
249            "**Location:** {}:{}\n",
250            self.original_finding.file, self.original_finding.line
251        ));
252
253        report
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn oz_registry_maps_detectors() {
263        let registry = OZMappingRegistry::new();
264
265        assert_eq!(
266            registry.get_oz_type("health_check"),
267            Some(OZVulnerabilityType::StateManagement)
268        );
269
270        assert_eq!(
271            registry.get_oz_type("oracle_self_trade"),
272            Some(OZVulnerabilityType::OracleManipulation)
273        );
274    }
275
276    #[test]
277    fn oz_registry_provides_recommendations() {
278        let registry = OZMappingRegistry::new();
279
280        let recs = registry.get_recommendations(&OZVulnerabilityType::AccessControl);
281        assert!(recs.is_some());
282        assert!(!recs.unwrap().is_empty());
283    }
284
285    #[test]
286    fn enriched_finding_generates_audit_report() {
287        let finding = Finding::new(
288            "test_detector".to_string(),
289            crate::Severity::High,
290            "test.sol".to_string(),
291            42,
292            0,
293            "Test vulnerability".to_string(),
294            "test code".to_string(),
295        );
296
297        let registry = OZMappingRegistry::new();
298        let enriched = registry.enrich_finding(&finding);
299
300        let report = enriched.to_audit_report();
301        assert!(report.contains("test.sol:42"));
302    }
303
304    #[test]
305    fn oz_type_equality() {
306        let t1 = OZVulnerabilityType::Reentrancy;
307        let t2 = OZVulnerabilityType::Reentrancy;
308        assert_eq!(t1, t2);
309    }
310}