pub mod verifier;
use crate::chains::ChainRegistry;
use crate::core::{
AuditResult, AuditSummary, Finding, ForgeGuardError, ProjectConfig, RiskLevel, SecurityScores,
Severity,
};
use crate::security::SecurityEngine;
use verifier::{ContractVerifier, VerificationMethod, VerificationResult};
#[cfg(test)]
use crate::plugins::PluginRegistry;
pub struct DeploymentGuard {
config: ProjectConfig,
security_engine: SecurityEngine,
verifier: Option<ContractVerifier>,
}
impl DeploymentGuard {
pub fn new(
config: &ProjectConfig,
security_engine: &SecurityEngine,
) -> Result<Self, ForgeGuardError> {
let verifier = if config.deployment.auto_verify || config.deployment.require_verification {
Some(ContractVerifier::new(
&config.chain,
config.deployment.explorer_api_key.clone(),
))
} else {
None
};
Ok(Self {
config: config.clone(),
security_engine: security_engine.clone(),
verifier,
})
}
pub fn run_pre_deployment_checks(
&self,
config: &ProjectConfig,
chain_registry: &ChainRegistry,
) -> Result<Vec<Finding>, ForgeGuardError> {
let source_files = self.discover_sources(config)?;
let findings = self
.security_engine
.analyze_files(&source_files, chain_registry)?;
Ok(findings)
}
pub fn can_deploy(
&self,
findings: &[Finding],
overall_score: u8,
risk_level: RiskLevel,
) -> bool {
let deploy_cfg = &self.config.deployment;
if deploy_cfg.block_on_critical && findings.iter().any(|f| f.severity == Severity::Critical)
{
return false;
}
if deploy_cfg.block_on_high && findings.iter().any(|f| f.severity == Severity::High) {
return false;
}
if deploy_cfg.block_on_medium && findings.iter().any(|f| f.severity == Severity::Medium) {
return false;
}
if overall_score < deploy_cfg.min_score {
return false;
}
if matches!(risk_level, RiskLevel::Critical | RiskLevel::High) {
return false;
}
if findings.iter().any(|f| f.blocks_deployment) {
return false;
}
true
}
pub fn calculate_overall_score(&self, scores: &SecurityScores) -> u8 {
let vals = [
scores.access_control,
scores.security,
scores.fuzzing,
scores.gas,
scores.architecture,
scores.upgradeability,
scores.dependencies,
scores.deployment,
scores.proxy_safety,
scores.chain_compatibility,
scores.production_readiness,
scores.exploit_resistance,
];
(vals.iter().copied().map(u16::from).sum::<u16>() / vals.len() as u16) as u8
}
pub fn determine_risk_level(&self, score: u8, findings: &[Finding]) -> RiskLevel {
let has_critical = findings.iter().any(|f| f.severity == Severity::Critical);
let has_high = findings.iter().any(|f| f.severity == Severity::High);
if has_critical || score < 30 {
RiskLevel::Critical
} else if has_high || score < 50 {
RiskLevel::High
} else if score < 70 {
RiskLevel::Medium
} else if score < 85 {
RiskLevel::Low
} else {
RiskLevel::Minimal
}
}
pub fn build_result(
&self,
findings: Vec<Finding>,
scores: SecurityScores,
overall_score: u8,
risk_level: RiskLevel,
source_count: usize,
) -> AuditResult {
let production_ready = overall_score >= self.config.min_deployment_score;
let deployment_approved = self.can_deploy(&findings, overall_score, risk_level);
let mut summary = AuditSummary {
total_findings: findings.len(),
critical_count: 0,
high_count: 0,
medium_count: 0,
low_count: 0,
info_count: 0,
files_analyzed: source_count,
lines_analyzed: 0,
contracts_analyzed: source_count,
};
for f in &findings {
match f.severity {
Severity::Critical => summary.critical_count += 1,
Severity::High => summary.high_count += 1,
Severity::Medium => summary.medium_count += 1,
Severity::Low => summary.low_count += 1,
Severity::Informational => summary.info_count += 1,
}
}
AuditResult {
project_name: self.config.project_root.to_string_lossy().to_string(),
chain: self.config.chain.clone(),
chains: vec![self.config.chain.clone()],
timestamp: chrono::Utc::now().to_rfc3339(),
duration_seconds: 0.0,
findings,
scores,
overall_score,
risk_level,
production_ready,
deployment_approved,
summary,
}
}
pub fn verify_contract(
&self,
address: &str,
contract_name: &str,
constructor_args: Option<&str>,
) -> Result<VerificationResult, ForgeGuardError> {
let verifier = self.verifier.as_ref().ok_or_else(|| {
ForgeGuardError::Config(
"Verification not configured. Set auto_verify = true in [deployment] config."
.into(),
)
})?;
Ok(verifier.forge_verify(address, contract_name, &self.config.chain, constructor_args))
}
pub fn verify_bytecode(
&self,
address: &str,
contract_name: &str,
rpc_url: &str,
) -> VerificationResult {
match &self.verifier {
Some(v) => v.verify_bytecode_match(address, contract_name, rpc_url),
None => VerificationResult {
verified: false,
method: VerificationMethod::BytecodeMatch,
details: "Verifier not configured.".into(),
duration: std::time::Duration::from_secs(0),
},
}
}
fn discover_sources(
&self,
config: &ProjectConfig,
) -> Result<Vec<std::path::PathBuf>, ForgeGuardError> {
let mut files = Vec::new();
for dir in &config.src_dirs {
let dir_path = if dir.is_absolute() {
dir.clone()
} else {
config.project_root.join(dir)
};
if !dir_path.exists() {
continue;
}
for entry in walkdir::WalkDir::new(&dir_path)
.into_iter()
.filter_entry(|e| {
!config
.exclude
.iter()
.any(|p| e.file_name().to_string_lossy().contains(p))
})
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "sol") {
files.push(path.to_path_buf());
}
}
}
Ok(files)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Finding, SecurityScores, Severity};
#[test]
fn test_can_deploy_clean() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
let result = guard.can_deploy(&[], 100, RiskLevel::Minimal);
assert!(result);
}
#[test]
fn test_can_deploy_blocked_by_critical() {
let mut config = ProjectConfig::default();
config.deployment.block_on_critical = true;
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
let findings = vec![Finding::builder()
.title("Critical Bug")
.description("A critical vulnerability")
.severity(Severity::Critical)
.build()];
assert!(!guard.can_deploy(&findings, 100, RiskLevel::Minimal));
}
#[test]
fn test_can_deploy_blocked_by_high() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
let findings = vec![Finding::builder()
.title("High Bug")
.description("A high vulnerability")
.severity(Severity::High)
.build()];
assert!(!guard.can_deploy(&findings, 100, RiskLevel::Minimal));
}
#[test]
fn test_can_deploy_medium_not_blocked_by_default() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
let findings = vec![Finding::builder()
.title("Medium Issue")
.description("A medium issue")
.severity(Severity::Medium)
.build()];
assert!(guard.can_deploy(&findings, 100, RiskLevel::Minimal));
}
#[test]
fn test_can_deploy_blocked_by_high_risk() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
assert!(!guard.can_deploy(&[], 100, RiskLevel::Critical));
}
#[test]
fn test_calculate_overall_score() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
let scores = SecurityScores::perfect();
assert_eq!(guard.calculate_overall_score(&scores), 100);
}
#[test]
fn test_determine_risk_level() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
assert_eq!(guard.determine_risk_level(100, &[]), RiskLevel::Minimal);
assert_eq!(guard.determine_risk_level(50, &[]), RiskLevel::Medium);
assert_eq!(guard.determine_risk_level(25, &[]), RiskLevel::Critical);
}
#[test]
fn test_determine_risk_level_with_critical_finding() {
let config = ProjectConfig::default();
let engine = SecurityEngine::new(&config, &PluginRegistry::new(&config).unwrap()).unwrap();
let guard = DeploymentGuard::new(&config, &engine).unwrap();
let findings = vec![Finding::builder()
.title("Critical")
.description("desc")
.severity(Severity::Critical)
.build()];
assert_eq!(
guard.determine_risk_level(100, &findings),
RiskLevel::Critical
);
}
}