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 timestamp: chrono::Utc::now().to_rfc3339(),
181 duration_seconds: 0.0,
182 findings,
183 scores,
184 overall_score,
185 risk_level,
186 production_ready,
187 deployment_approved,
188 summary,
189 }
190 }
191
192 pub fn verify_contract(
197 &self,
198 address: &str,
199 contract_name: &str,
200 constructor_args: Option<&str>,
201 ) -> Result<VerificationResult, ForgeGuardError> {
202 let verifier = self.verifier.as_ref().ok_or_else(|| {
203 ForgeGuardError::Config(
204 "Verification not configured. Set auto_verify = true in [deployment] config."
205 .into(),
206 )
207 })?;
208
209 Ok(verifier.forge_verify(address, contract_name, &self.config.chain, constructor_args))
210 }
211
212 pub fn verify_bytecode(
214 &self,
215 address: &str,
216 contract_name: &str,
217 rpc_url: &str,
218 ) -> VerificationResult {
219 match &self.verifier {
220 Some(v) => v.verify_bytecode_match(address, contract_name, rpc_url),
221 None => VerificationResult {
222 verified: false,
223 method: VerificationMethod::BytecodeMatch,
224 details: "Verifier not configured.".into(),
225 duration: std::time::Duration::from_secs(0),
226 },
227 }
228 }
229
230 fn discover_sources(
231 &self,
232 config: &ProjectConfig,
233 ) -> Result<Vec<std::path::PathBuf>, ForgeGuardError> {
234 let mut files = Vec::new();
235 for dir in &config.src_dirs {
236 let dir_path = if dir.is_absolute() {
237 dir.clone()
238 } else {
239 config.project_root.join(dir)
240 };
241 if !dir_path.exists() {
242 continue;
243 }
244 for entry in walkdir::WalkDir::new(&dir_path)
245 .into_iter()
246 .filter_entry(|e| {
247 !config
248 .exclude
249 .iter()
250 .any(|p| e.file_name().to_string_lossy().contains(p))
251 })
252 .filter_map(|e| e.ok())
253 {
254 let path = entry.path();
255 if path.extension().is_some_and(|ext| ext == "sol") {
256 files.push(path.to_path_buf());
257 }
258 }
259 }
260 Ok(files)
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::core::{Finding, SecurityScores, Severity};
268
269 #[test]
270 fn test_can_deploy_clean() {
271 let config = ProjectConfig::default();
272 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
273 let guard = DeploymentGuard::new(&config, &engine).unwrap();
274
275 let result = guard.can_deploy(&[], 100, RiskLevel::Minimal);
276 assert!(result);
277 }
278
279 #[test]
280 fn test_can_deploy_blocked_by_critical() {
281 let mut config = ProjectConfig::default();
282 config.deployment.block_on_critical = true;
283 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
284 let guard = DeploymentGuard::new(&config, &engine).unwrap();
285
286 let findings = vec![Finding::builder()
287 .title("Critical Bug")
288 .description("A critical vulnerability")
289 .severity(Severity::Critical)
290 .build()];
291
292 assert!(!guard.can_deploy(&findings, 100, RiskLevel::Minimal));
293 }
294
295 #[test]
296 fn test_can_deploy_blocked_by_high() {
297 let config = ProjectConfig::default();
298 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
299 let guard = DeploymentGuard::new(&config, &engine).unwrap();
300
301 let findings = vec![Finding::builder()
302 .title("High Bug")
303 .description("A high vulnerability")
304 .severity(Severity::High)
305 .build()];
306
307 assert!(!guard.can_deploy(&findings, 100, RiskLevel::Minimal));
308 }
309
310 #[test]
311 fn test_can_deploy_medium_not_blocked_by_default() {
312 let config = ProjectConfig::default();
313 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
314 let guard = DeploymentGuard::new(&config, &engine).unwrap();
315
316 let findings = vec![Finding::builder()
317 .title("Medium Issue")
318 .description("A medium issue")
319 .severity(Severity::Medium)
320 .build()];
321
322 assert!(guard.can_deploy(&findings, 100, RiskLevel::Minimal));
323 }
324
325 #[test]
326 fn test_can_deploy_blocked_by_high_risk() {
327 let config = ProjectConfig::default();
328 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
329 let guard = DeploymentGuard::new(&config, &engine).unwrap();
330
331 assert!(!guard.can_deploy(&[], 100, RiskLevel::Critical));
332 }
333
334 #[test]
335 fn test_calculate_overall_score() {
336 let config = ProjectConfig::default();
337 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
338 let guard = DeploymentGuard::new(&config, &engine).unwrap();
339
340 let scores = SecurityScores::perfect();
341 assert_eq!(guard.calculate_overall_score(&scores), 100);
342 }
343
344 #[test]
345 fn test_determine_risk_level() {
346 let config = ProjectConfig::default();
347 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
348 let guard = DeploymentGuard::new(&config, &engine).unwrap();
349
350 assert_eq!(guard.determine_risk_level(100, &[]), RiskLevel::Minimal);
351 assert_eq!(guard.determine_risk_level(50, &[]), RiskLevel::Medium);
352 assert_eq!(guard.determine_risk_level(25, &[]), RiskLevel::Critical);
353 }
354
355 #[test]
356 fn test_determine_risk_level_with_critical_finding() {
357 let config = ProjectConfig::default();
358 let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
359 let guard = DeploymentGuard::new(&config, &engine).unwrap();
360
361 let findings = vec![Finding::builder()
362 .title("Critical")
363 .description("desc")
364 .severity(Severity::Critical)
365 .build()];
366 assert_eq!(
367 guard.determine_risk_level(100, &findings),
368 RiskLevel::Critical
369 );
370 }
371}