1use crate::Finding;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum OZVulnerabilityType {
12 AccessControl,
14 Reentrancy,
16 ArithmeticOverflow,
18 TokenTransfer,
20 OracleManipulation,
22 FlashLoan,
24 SignatureReplay,
26 StateManagement,
28 Initialization,
30 Custom(String),
32}
33
34#[derive(Debug, Clone)]
36pub struct OZRecommendation {
37 pub id: String,
39 pub title: String,
41 pub severity: String,
43 pub best_practice: String,
45 pub reference: String,
47}
48
49pub struct OZMappingRegistry {
51 detector_to_oz: HashMap<String, OZVulnerabilityType>,
52 oz_to_recommendations: HashMap<OZVulnerabilityType, Vec<OZRecommendation>>,
53}
54
55impl OZMappingRegistry {
56 #[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 fn initialize_mappings(&mut self) {
70 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 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 self.add_oz_recommendations();
128 }
129
130 fn add_oz_recommendations(&mut self) {
132 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 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 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 pub fn get_oz_type(&self, detector_name: &str) -> Option<OZVulnerabilityType> {
179 self.detector_to_oz.get(detector_name).cloned()
180 }
181
182 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 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#[derive(Debug, Clone)]
213pub struct EnrichedFinding {
214 pub original_finding: Finding,
216 pub oz_type: Option<OZVulnerabilityType>,
218 pub oz_recommendations: Vec<OZRecommendation>,
220}
221
222impl EnrichedFinding {
223 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}