forge-guard 0.3.3

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! `forge-guard deploy-safe` — deploy with mandatory security pass.

use crate::chains::ChainRegistry;
use crate::core::{OutputFormat, ProjectConfig};
use crate::deployment::DeploymentGuard;
use crate::plugins::PluginRegistry;
use crate::security::SecurityEngine;
use crate::utils::Cache;

use super::DeploySafeArgs;
use anyhow::{Context, Result};
use colored::*;

/// Deploy with mandatory security pass requirement.
/// Unlike `forge-guard deploy`, this CANNOT be bypassed with --force.
pub fn run(args: &DeploySafeArgs) -> Result<()> {
    let mut config = ProjectConfig::from_default_location();
    config.chain = args.shared.chain.clone();
    config.project_root = args.shared.project.clone();
    config.strict = true; // Always strict for deploy-safe
    config.offline = args.shared.offline;
    config.production = args.shared.production;
    config.output = if args.shared.json {
        OutputFormat::Json
    } else {
        OutputFormat::Terminal
    };

    let _cache = Cache::new(&config)?;
    let chain_registry = ChainRegistry::default();
    let plugin_registry = PluginRegistry::new(&config)?;
    let security_engine = SecurityEngine::new(&config, &plugin_registry)?;
    let deploy_guard = DeploymentGuard::new(&config, &security_engine)?;

    eprintln!("{}", "🔒 Forge Guard — Safe Deployment Mode".bold().green());
    eprintln!(
        "{}",
        "   Deployment requires a MANDATORY security pass.".bold()
    );
    eprintln!("{}", "   This mode cannot be bypassed.".dimmed());
    eprintln!("{}", "   ─────────────────────────────────────".dimmed());

    // Run full pre-deployment checks
    let findings = deploy_guard.run_pre_deployment_checks(&config, &chain_registry)?;

    // Calculate scores
    let scores = security_engine.calculate_scores(&findings);
    let overall = deploy_guard.calculate_overall_score(&scores);
    let risk = deploy_guard.determine_risk_level(overall, &findings);

    // Show detailed report
    println!(
        "\n{}",
        "═══════════════════════════════════════".bright_green()
    );
    println!(
        "{}",
        "      SAFE DEPLOYMENT — AUDIT REPORT    "
            .bright_green()
            .bold()
    );
    println!(
        "{}",
        "═══════════════════════════════════════".bright_green()
    );

    println!(
        "\n  📊 Security Score:  {}/100",
        overall
            .to_string()
            .color(if overall >= 85 {
                "green"
            } else if overall >= 70 {
                "yellow"
            } else {
                "red"
            })
            .bold()
    );
    println!("  ⚠️  Risk Level:      {}", format!("{}", risk).bold());
    println!("  🛡️  Findings:        {} total", findings.len());

    // Check for blocking findings
    let blocking = findings
        .iter()
        .filter(|f| f.blocks_deployment || f.severity >= crate::core::Severity::High)
        .count();

    // Webhook notification (--notify): blocked deployments always notify;
    // approvals respect the configured minimum severity.
    if args.notify {
        let approved = blocking == 0 && overall >= config.min_deployment_score;
        let notification = crate::notify::AuditNotification {
            title: if approved {
                "Safe deployment approved".into()
            } else {
                "Safe deployment blocked".into()
            },
            project: config.project_root.to_string_lossy().to_string(),
            chain: args.shared.chain.clone(),
            overall_score: Some(overall),
            risk: Some(risk.to_string()),
            success: approved,
            findings: findings.clone(),
        };
        crate::notify::notify_from_config(&config, &notification, !approved)?;
    }

    if blocking == 0 && overall >= config.min_deployment_score {
        println!(
            "\n  {}  {}",
            "✅ SAFE DEPLOYMENT: APPROVED".green().bold(),
            "All security checks passed.".green()
        );

        if let Some(contract) = &args.contract {
            println!("  📄 Contract: {}", contract);
        }

        // Execute forge create
        let mut cmd = std::process::Command::new("forge");
        cmd.arg("create");
        if let Some(contract) = &args.contract {
            cmd.arg("--contract").arg(contract);
        }
        let rpc = crate::deployment::verifier::rpc_url_for_chain(&args.shared.chain);
        cmd.arg("--rpc-url").arg(&rpc);

        let output = cmd.output().context("Forge create failed")?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            anyhow::bail!("Deployment failed: {}", stderr);
        }
        let stdout = String::from_utf8_lossy(&output.stdout);
        println!("{}", stdout);

        // Auto-verify if enabled
        if args.verify || config.deployment.auto_verify {
            let deployed_addr = crate::cli::deploy::extract_deployed_address(&stdout);
            if let Some(ref addr) = deployed_addr {
                eprintln!("\n🔍 Running post-deployment verification...");
                let contract_name = args.contract.as_deref().unwrap_or("Contract");
                let result =
                    deploy_guard.verify_contract(addr, contract_name, args.args.as_deref());
                match result {
                    Ok(vr) if vr.verified => {
                        println!("{}", vr.details.green());
                    }
                    Ok(vr) => {
                        eprintln!("  ⚠️  {}", vr.details.yellow());
                        eprintln!("  🔄 Trying bytecode match...");
                        let bm = deploy_guard.verify_bytecode(addr, contract_name, &rpc);
                        if bm.verified {
                            println!("{}", bm.details.green());
                        }
                    }
                    Err(e) => eprintln!("  ⚠️  Verification error: {}", e),
                }
            }
        }
    } else {
        println!(
            "\n  {}  {}",
            "❌ SAFE DEPLOYMENT: BLOCKED".red().bold(),
            "Security requirements not met.".red()
        );

        for f in findings
            .iter()
            .filter(|f| f.blocks_deployment || f.severity >= crate::core::Severity::High)
            .take(10)
        {
            println!(
                "    • [{}] {}",
                f.severity.to_string().red().bold(),
                f.title
            );
            println!(
                "      📄 {}:{}",
                f.file.as_deref().unwrap_or("?"),
                f.line.map_or("?".into(), |l| l.to_string())
            );
            println!("      💡 {}", f.recommendation);
        }

        anyhow::bail!("Safe deployment blocked — security requirements not met");
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_deploy_safe_arg_defaults() {
        let args = DeploySafeArgs {
            shared: super::super::SharedFlags {
                chain: "ethereum".into(),
                project: std::path::PathBuf::from("."),
                json: false,
                markdown: false,
                html: false,
                strict: false,
                offline: false,
                production: false,
                report: false,
                parallelism: 4,
            },
            contract: None,
            args: None,
            verify: false,
            notify: false,
        };
        assert!(args.contract.is_none());
        assert!(args.args.is_none());
        assert!(!args.verify);
    }

    #[test]
    fn test_deploy_safe_with_contract() {
        let args = DeploySafeArgs {
            shared: super::super::SharedFlags::default(),
            contract: Some("Vault".into()),
            args: Some("0xowner".into()),
            verify: true,
            notify: false,
        };
        assert_eq!(args.contract.as_deref(), Some("Vault"));
        assert_eq!(args.args.as_deref(), Some("0xowner"));
        assert!(args.verify);
    }

    #[test]
    fn test_deploy_safe_default_output_format_is_terminal() {
        let config = ProjectConfig::default();
        assert_eq!(config.output, crate::core::OutputFormat::Terminal);
    }
}