use crate::Finding;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OZVulnerabilityType {
AccessControl,
Reentrancy,
ArithmeticOverflow,
TokenTransfer,
OracleManipulation,
FlashLoan,
SignatureReplay,
StateManagement,
Initialization,
Custom(String),
}
#[derive(Debug, Clone)]
pub struct OZRecommendation {
pub id: String,
pub title: String,
pub severity: String,
pub best_practice: String,
pub reference: String,
}
pub struct OZMappingRegistry {
detector_to_oz: HashMap<String, OZVulnerabilityType>,
oz_to_recommendations: HashMap<OZVulnerabilityType, Vec<OZRecommendation>>,
}
impl OZMappingRegistry {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let mut registry = Self {
detector_to_oz: HashMap::new(),
oz_to_recommendations: HashMap::new(),
};
registry.initialize_mappings();
registry
}
fn initialize_mappings(&mut self) {
self.detector_to_oz.insert(
"health_check".to_string(),
OZVulnerabilityType::StateManagement,
);
self.detector_to_oz.insert(
"merkle_root".to_string(),
OZVulnerabilityType::StateManagement,
);
self.detector_to_oz.insert(
"dvn_single_point".to_string(),
OZVulnerabilityType::AccessControl,
);
self.detector_to_oz.insert(
"synthetic_mint".to_string(),
OZVulnerabilityType::StateManagement,
);
self.detector_to_oz.insert(
"lst_depeg".to_string(),
OZVulnerabilityType::OracleManipulation,
);
self.detector_to_oz.insert(
"oracle_self_trade".to_string(),
OZVulnerabilityType::OracleManipulation,
);
self.detector_to_oz.insert(
"solana_durable_nonce".to_string(),
OZVulnerabilityType::SignatureReplay,
);
self.detector_to_oz.insert(
"synthetic_collateral_oracle".to_string(),
OZVulnerabilityType::OracleManipulation,
);
self.detector_to_oz.insert(
"erc4626_inflation".to_string(),
OZVulnerabilityType::ArithmeticOverflow,
);
self.detector_to_oz.insert(
"arbitrary_call_msg_value".to_string(),
OZVulnerabilityType::StateManagement,
);
self.detector_to_oz.insert(
"reentrancy_whitelisted".to_string(),
OZVulnerabilityType::Reentrancy,
);
self.detector_to_oz.insert(
"proxy_storage_collision".to_string(),
OZVulnerabilityType::StateManagement,
);
self.detector_to_oz.insert(
"bridge_address_crypto".to_string(),
OZVulnerabilityType::SignatureReplay,
);
self.add_oz_recommendations();
}
fn add_oz_recommendations(&mut self) {
let access_control_recs = vec![
OZRecommendation {
id: "OZ-AC-001".to_string(),
title: "Use onlyOwner modifier for privileged functions".to_string(),
severity: "High".to_string(),
best_practice: "Use OpenZeppelin's Ownable contract".to_string(),
reference: "https://docs.openzeppelin.com/contracts/4.x/access-control".to_string(),
},
OZRecommendation {
id: "OZ-AC-002".to_string(),
title: "Implement role-based access control".to_string(),
severity: "High".to_string(),
best_practice: "Use AccessControl for complex permissions".to_string(),
reference: "https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl"
.to_string(),
},
];
self.oz_to_recommendations
.insert(OZVulnerabilityType::AccessControl, access_control_recs);
let reentrancy_recs = vec![OZRecommendation {
id: "OZ-RE-001".to_string(),
title: "Apply Checks-Effects-Interactions pattern".to_string(),
severity: "Critical".to_string(),
best_practice: "Update state before external calls".to_string(),
reference: "https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard"
.to_string(),
}];
self.oz_to_recommendations
.insert(OZVulnerabilityType::Reentrancy, reentrancy_recs);
let oracle_recs = vec![OZRecommendation {
id: "OZ-OR-001".to_string(),
title: "Use multiple oracle sources".to_string(),
severity: "High".to_string(),
best_practice: "Implement oracle aggregation pattern".to_string(),
reference: "https://docs.openzeppelin.com/contracts/4.x/".to_string(),
}];
self.oz_to_recommendations
.insert(OZVulnerabilityType::OracleManipulation, oracle_recs);
}
pub fn get_oz_type(&self, detector_name: &str) -> Option<OZVulnerabilityType> {
self.detector_to_oz.get(detector_name).cloned()
}
pub fn get_recommendations(
&self,
vuln_type: &OZVulnerabilityType,
) -> Option<&[OZRecommendation]> {
self.oz_to_recommendations
.get(vuln_type)
.map(|v| v.as_slice())
}
pub fn enrich_finding(&self, finding: &Finding) -> EnrichedFinding {
let detector_name = finding.invariant_id.to_lowercase();
let oz_type = self.get_oz_type(&detector_name);
let recommendations = oz_type
.as_ref()
.and_then(|t| self.get_recommendations(t))
.unwrap_or(&[])
.to_vec();
EnrichedFinding {
original_finding: finding.clone(),
oz_type,
oz_recommendations: recommendations,
}
}
}
#[derive(Debug, Clone)]
pub struct EnrichedFinding {
pub original_finding: Finding,
pub oz_type: Option<OZVulnerabilityType>,
pub oz_recommendations: Vec<OZRecommendation>,
}
impl EnrichedFinding {
pub fn to_audit_report(&self) -> String {
let mut report = format!("## Finding: {}\n\n", self.original_finding.message);
report.push_str(&format!(
"**Severity:** {:?}\n",
self.original_finding.severity
));
if let Some(ref oz_type) = self.oz_type {
report.push_str(&format!("**OZ Classification:** {:?}\n\n", oz_type));
}
if !self.oz_recommendations.is_empty() {
report.push_str("### OpenZeppelin Recommendations\n\n");
for rec in &self.oz_recommendations {
report.push_str(&format!(
"- **{}** ({}): {}\n",
rec.id, rec.severity, rec.title
));
report.push_str(&format!(" - Best Practice: {}\n", rec.best_practice));
report.push_str(&format!(" - Reference: {}\n\n", rec.reference));
}
}
report.push_str(&format!(
"**Location:** {}:{}\n",
self.original_finding.file, self.original_finding.line
));
report
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn oz_registry_maps_detectors() {
let registry = OZMappingRegistry::new();
assert_eq!(
registry.get_oz_type("health_check"),
Some(OZVulnerabilityType::StateManagement)
);
assert_eq!(
registry.get_oz_type("oracle_self_trade"),
Some(OZVulnerabilityType::OracleManipulation)
);
}
#[test]
fn oz_registry_provides_recommendations() {
let registry = OZMappingRegistry::new();
let recs = registry.get_recommendations(&OZVulnerabilityType::AccessControl);
assert!(recs.is_some());
assert!(!recs.unwrap().is_empty());
}
#[test]
fn enriched_finding_generates_audit_report() {
let finding = Finding::new(
"test_detector".to_string(),
crate::Severity::High,
"test.sol".to_string(),
42,
0,
"Test vulnerability".to_string(),
"test code".to_string(),
);
let registry = OZMappingRegistry::new();
let enriched = registry.enrich_finding(&finding);
let report = enriched.to_audit_report();
assert!(report.contains("test.sol:42"));
}
#[test]
fn oz_type_equality() {
let t1 = OZVulnerabilityType::Reentrancy;
let t2 = OZVulnerabilityType::Reentrancy;
assert_eq!(t1, t2);
}
}