1pub mod verifier;
4
5use crate::chains::ChainRegistry;
6use crate::core::{
7 AuditResult, AuditSummary, Finding, ForgeGuardError, ProjectConfig, RiskLevel, SecurityScores,
8 Severity,
9};
10use crate::security::SecurityEngine;
11use verifier::{ContractVerifier, VerificationMethod, VerificationResult};
12
13#[cfg(test)]
14use crate::plugins::PluginRegistry;
15
16pub struct DeploymentGuard {
18 config: ProjectConfig,
19 security_engine: SecurityEngine,
20 verifier: Option<ContractVerifier>,
21}
22
23impl DeploymentGuard {
24 pub fn new(
26 config: &ProjectConfig,
27 security_engine: &SecurityEngine,
28 ) -> Result<Self, ForgeGuardError> {
29 let verifier = if config.deployment.auto_verify || config.deployment.require_verification {
31 Some(ContractVerifier::new(
32 &config.chain,
33 config.deployment.explorer_api_key.clone(),
34 ))
35 } else {
36 None
37 };
38
39 Ok(Self {
40 config: config.clone(),
41 security_engine: security_engine.clone(),
42 verifier,
43 })
44 }
45
46 pub fn run_pre_deployment_checks(
48 &self,
49 config: &ProjectConfig,
50 chain_registry: &ChainRegistry,
51 ) -> Result<Vec<Finding>, ForgeGuardError> {
52 let source_files = self.discover_sources(config)?;
54
55 let findings = self
57 .security_engine
58 .analyze_files(&source_files, chain_registry)?;
59
60 Ok(findings)
61 }
62
63 pub fn can_deploy(
65 &self,
66 findings: &[Finding],
67 overall_score: u8,
68 risk_level: RiskLevel,
69 ) -> bool {
70 let deploy_cfg = &self.config.deployment;
71
72 if deploy_cfg.block_on_critical && findings.iter().any(|f| f.severity == Severity::Critical)
74 {
75 return false;
76 }
77
78 if deploy_cfg.block_on_high && findings.iter().any(|f| f.severity == Severity::High) {
80 return false;
81 }
82
83 if deploy_cfg.block_on_medium && findings.iter().any(|f| f.severity == Severity::Medium) {
85 return false;
86 }
87
88 if overall_score < deploy_cfg.min_score {
90 return false;
91 }
92
93 if matches!(risk_level, RiskLevel::Critical | RiskLevel::High) {
95 return false;
96 }
97
98 if findings.iter().any(|f| f.blocks_deployment) {
100 return false;
101 }
102
103 true
104 }
105
106 pub fn calculate_overall_score(&self, scores: &SecurityScores) -> u8 {
108 let vals = [
109 scores.access_control,
110 scores.security,
111 scores.fuzzing,
112 scores.gas,
113 scores.architecture,
114 scores.upgradeability,
115 scores.dependencies,
116 scores.deployment,
117 scores.proxy_safety,
118 scores.chain_compatibility,
119 scores.production_readiness,
120 scores.exploit_resistance,
121 ];
122 (vals.iter().copied().map(u16::from).sum::<u16>() / vals.len() as u16) as u8
123 }
124
125 pub fn determine_risk_level(&self, score: u8, findings: &[Finding]) -> RiskLevel {
127 let has_critical = findings.iter().any(|f| f.severity == Severity::Critical);
128 let has_high = findings.iter().any(|f| f.severity == Severity::High);
129
130 if has_critical || score < 30 {
131 RiskLevel::Critical
132 } else if has_high || score < 50 {
133 RiskLevel::High
134 } else if score < 70 {
135 RiskLevel::Medium
136 } else if score < 85 {
137 RiskLevel::Low
138 } else {
139 RiskLevel::Minimal
140 }
141 }
142
143 pub fn build_result(
145 &self,
146 findings: Vec<Finding>,
147 scores: SecurityScores,
148 overall_score: u8,
149 risk_level: RiskLevel,
150 source_count: usize,
151 ) -> AuditResult {
152 let production_ready = overall_score >= self.config.min_deployment_score;
153 let deployment_approved = self.can_deploy(&findings, overall_score, risk_level);
154
155 let mut summary = AuditSummary {
156 total_findings: findings.len(),
157 critical_count: 0,
158 high_count: 0,
159 medium_count: 0,
160 low_count: 0,
161 info_count: 0,
162 files_analyzed: source_count,
163 lines_analyzed: 0,
164 contracts_analyzed: source_count,
165 };
166
167 for f in &findings {
168 match f.severity {
169 Severity::Critical => summary.critical_count += 1,
170 Severity::High => summary.high_count += 1,
171 Severity::Medium => summary.medium_count += 1,
172 Severity::Low => summary.low_count += 1,
173 Severity::Informational => summary.info_count += 1,
174 }
175 }
176
177 AuditResult {
178 project_name: self.config.project_root.to_string_lossy().to_string(),
179 chain: self.config.chain.clone(),
180 chains: vec![self.config.chain.clone()],
181 timestamp: chrono::Utc::now().to_rfc3339(),
182 duration_seconds: 0.0,
183 findings,
184 scores,
185 overall_score,
186 risk_level,
187 production_ready,
188 deployment_approved,
189 summary,
190 }
191 }
192
193 pub fn verify_contract(
198 &self,
199 address: &str,
200 contract_name: &str,
201 constructor_args: Option<&str>,
202 ) -> Result<VerificationResult, ForgeGuardError> {
203 let verifier = self.verifier.as_ref().ok_or_else(|| {
204 ForgeGuardError::Config(
205 "Verification not configured. Set auto_verify = true in [deployment] config."
206 .into(),
207 )
208 })?;
209
210 Ok(verifier.forge_verify(address, contract_name, &self.config.chain, constructor_args))
211 }
212
213 pub fn verify_bytecode(
215 &self,
216 address: &str,
217 contract_name: &str,
218 rpc_url: &str,
219 ) -> VerificationResult {
220 match &self.verifier {
221 Some(v) => v.verify_bytecode_match(address, contract_name, rpc_url),
222 None => VerificationResult {
223 verified: false,
224 method: VerificationMethod::BytecodeMatch,
225 details: "Verifier not configured.".into(),
226 duration: std::time::Duration::from_secs(0),
227 },
228 }
229 }
230
231 fn discover_sources(
232 &self,
233 config: &ProjectConfig,
234 ) -> Result<Vec<std::path::PathBuf>, ForgeGuardError> {
235 let mut files = Vec::new();
236 for dir in &config.src_dirs {
237 let dir_path = if dir.is_absolute() {
238 dir.clone()
239 } else {
240 config.project_root.join(dir)
241 };
242 if !dir_path.exists() {
243 continue;
244 }
245 for entry in walkdir::WalkDir::new(&dir_path)
246 .into_iter()
247 .filter_entry(|e| {
248 !config
249 .exclude
250 .iter()
251 .any(|p| e.file_name().to_string_lossy().contains(p))
252 })
253 .filter_map(|e| e.ok())
254 {
255 let path = entry.path();
256 if path.extension().is_some_and(|ext| ext == "sol") {
257 files.push(path.to_path_buf());
258 }
259 }
260 }
261 Ok(files)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::core::{Finding, SecurityScores, Severity};
269
270 #[test]
271 fn test_can_deploy_clean() {
272 let config = ProjectConfig::default();
273 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
274 let guard = DeploymentGuard::new(&config, &engine).unwrap();
275
276 let result = guard.can_deploy(&[], 100, RiskLevel::Minimal);
277 assert!(result);
278 }
279
280 #[test]
281 fn test_can_deploy_blocked_by_critical() {
282 let mut config = ProjectConfig::default();
283 config.deployment.block_on_critical = true;
284 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
285 let guard = DeploymentGuard::new(&config, &engine).unwrap();
286
287 let findings = vec![Finding::builder()
288 .title("Critical Bug")
289 .description("A critical vulnerability")
290 .severity(Severity::Critical)
291 .build()];
292
293 assert!(!guard.can_deploy(&findings, 100, RiskLevel::Minimal));
294 }
295
296 #[test]
297 fn test_can_deploy_blocked_by_high() {
298 let config = ProjectConfig::default();
299 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
300 let guard = DeploymentGuard::new(&config, &engine).unwrap();
301
302 let findings = vec![Finding::builder()
303 .title("High Bug")
304 .description("A high vulnerability")
305 .severity(Severity::High)
306 .build()];
307
308 assert!(!guard.can_deploy(&findings, 100, RiskLevel::Minimal));
309 }
310
311 #[test]
312 fn test_can_deploy_medium_not_blocked_by_default() {
313 let config = ProjectConfig::default();
314 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
315 let guard = DeploymentGuard::new(&config, &engine).unwrap();
316
317 let findings = vec![Finding::builder()
318 .title("Medium Issue")
319 .description("A medium issue")
320 .severity(Severity::Medium)
321 .build()];
322
323 assert!(guard.can_deploy(&findings, 100, RiskLevel::Minimal));
324 }
325
326 #[test]
327 fn test_can_deploy_blocked_by_high_risk() {
328 let config = ProjectConfig::default();
329 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
330 let guard = DeploymentGuard::new(&config, &engine).unwrap();
331
332 assert!(!guard.can_deploy(&[], 100, RiskLevel::Critical));
333 }
334
335 #[test]
336 fn test_calculate_overall_score() {
337 let config = ProjectConfig::default();
338 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
339 let guard = DeploymentGuard::new(&config, &engine).unwrap();
340
341 let scores = SecurityScores::perfect();
342 assert_eq!(guard.calculate_overall_score(&scores), 100);
343 }
344
345 #[test]
346 fn test_determine_risk_level() {
347 let config = ProjectConfig::default();
348 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
349 let guard = DeploymentGuard::new(&config, &engine).unwrap();
350
351 assert_eq!(guard.determine_risk_level(100, &[]), RiskLevel::Minimal);
352 assert_eq!(guard.determine_risk_level(50, &[]), RiskLevel::Medium);
353 assert_eq!(guard.determine_risk_level(25, &[]), RiskLevel::Critical);
354 }
355
356 #[test]
357 fn test_determine_risk_level_with_critical_finding() {
358 let config = ProjectConfig::default();
359 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
360 let guard = DeploymentGuard::new(&config, &engine).unwrap();
361
362 let findings = vec![Finding::builder()
363 .title("Critical")
364 .description("desc")
365 .severity(Severity::Critical)
366 .build()];
367 assert_eq!(
368 guard.determine_risk_level(100, &findings),
369 RiskLevel::Critical
370 );
371 }
372}