forge-guard 0.1.3

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! Report engine — generates JSON, Markdown, and executive summary audit reports.

pub mod json;
pub mod markdown;

use crate::core::{AuditResult, Finding, Severity};

/// Trait for report generators.
pub trait ReportGenerator {
    /// Generate a report from audit results.
    fn generate(&self, result: &AuditResult) -> Result<String, crate::core::ForgeGuardError>;

    /// Get the file extension for this report format.
    fn extension(&self) -> &'static str;
}

/// Generate an executive summary report with actionable findings.
/// This is a concise, decision-focused report showing the key risks,
/// action items, and deployment readiness at a glance.
pub fn generate_executive_summary(result: &AuditResult) -> String {
    let mut s = String::new();

    // ── Header ──
    s.push_str("═══════════════════════════════════════════════════\n");
    s.push_str("  FORGE AUDIT — EXECUTIVE SUMMARY\n");
    s.push_str("═══════════════════════════════════════════════════\n\n");

    s.push_str(&format!("  Project:          {}\n", result.project_name));
    s.push_str(&format!("  Chain:            {}\n", result.chain));
    s.push_str(&format!(
        "  Files Analyzed:   {}\n",
        result.summary.files_analyzed
    ));
    s.push_str(&format!(
        "  Duration:         {:.2}s\n",
        result.duration_seconds
    ));

    // ── Verdict ──
    s.push_str("\n── VERDICT ──\n");
    let verdict = if result.production_ready && result.deployment_approved {
        "✅ PASS — Ready for deployment"
    } else if result.production_ready {
        "⚠️  PASS WITH WARNINGS — Review findings before deploying"
    } else {
        "❌ FAIL — Must fix issues before deployment"
    };
    s.push_str(&format!("  {}\n\n", verdict));

    // ── Key Metrics ──
    s.push_str("── KEY METRICS ──\n");
    s.push_str(&format!(
        "  Overall Score:    {:>3}/100\n",
        result.overall_score
    ));
    s.push_str(&format!("  Risk Level:       {}\n", result.risk_level));
    s.push_str(&format!(
        "  Deployment:      {}\n",
        if result.deployment_approved {
            "✅ APPROVED"
        } else {
            "❌ BLOCKED"
        }
    ));

    // ── Finding Summary ──
    s.push_str("\n── FINDING SUMMARY ──\n");
    s.push_str(&format!(
        "  🛑 Critical:  {}\n",
        result.summary.critical_count
    ));
    s.push_str(&format!("  🔴 High:      {}\n", result.summary.high_count));
    s.push_str(&format!(
        "  🟡 Medium:    {}\n",
        result.summary.medium_count
    ));
    s.push_str(&format!("  🔵 Low:       {}\n", result.summary.low_count));
    s.push_str(&format!("  ⚪ Info:      {}\n", result.summary.info_count));

    // ── Top Action Items ──
    let critical_high: Vec<&Finding> = result
        .findings
        .iter()
        .filter(|f| f.severity == Severity::Critical || f.severity == Severity::High)
        .collect();

    if !critical_high.is_empty() {
        s.push_str("\n── TOP ACTION ITEMS ──\n");
        for (i, f) in critical_high.iter().take(5).enumerate() {
            let location = match (&f.file, f.line) {
                (Some(file), Some(line)) => format!("{}:{}", file, line),
                (Some(file), None) => file.clone(),
                (None, _) => "unknown".into(),
            };
            s.push_str(&format!(
                "  {}. [{}] {} ({})\n",
                i + 1,
                f.severity,
                f.title,
                location
            ));
            s.push_str(&format!("     💡 {}\n", f.recommendation));
        }
        if critical_high.len() > 5 {
            s.push_str(&format!(
                "     ... and {} more critical/high findings\n",
                critical_high.len() - 5
            ));
        }
    }

    // ── Worst Categories ──
    let categories = [
        ("Access Control", result.scores.access_control),
        ("Security", result.scores.security),
        ("Architecture", result.scores.architecture),
        ("Production Ready", result.scores.production_readiness),
        ("Gas", result.scores.gas),
    ];
    let worst: Vec<&(&str, u8)> = categories.iter().filter(|(_, score)| *score < 70).collect();
    if !worst.is_empty() {
        s.push_str("\n── AREAS FOR IMPROVEMENT ──\n");
        for (name, score) in worst {
            s.push_str(&format!(
                "  🔸 {}: {:>3}/100 — needs attention\n",
                name, score
            ));
        }
    }

    // ── Quick Mode Indicator ──
    if result.duration_seconds < 1.0 {
        s.push_str("\n── QUICK MODE ──\n");
        s.push_str("  ⚡ Quick mode enabled — parser-heavy checks were skipped.\n");
        s.push_str("  🔍 Run without --quick for a comprehensive audit.\n");
    }

    s.push_str("\n═══════════════════════════════════════════════════\n");
    s
}

/// Generate a human-readable recommendation count breakdown.
pub fn count_recommendations(findings: &[Finding]) -> std::collections::HashMap<String, usize> {
    let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    for f in findings {
        let cat = if f.category.is_empty() {
            "General"
        } else {
            &f.category
        };
        *counts.entry(cat.to_string()).or_insert(0) += 1;
    }
    counts
}

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

    fn sample_result() -> AuditResult {
        let findings = vec![
            Finding::builder()
                .id("FA-H-001-1")
                .title("Reentrancy")
                .description("CEI violation")
                .severity(Severity::High)
                .file("Vuln.sol")
                .location(15, 0)
                .code("externalCall(); balance -= amount;")
                .recommendation("Use ReentrancyGuard")
                .category("Logic")
                .blocks_deployment(true)
                .build(),
            Finding::builder()
                .id("FA-M-001-1")
                .title("Gas Problem")
                .description("Loop gas issue")
                .severity(Severity::Medium)
                .file("Vuln.sol")
                .location(30, 0)
                .code("for(uint i;i<arr.length;i++)")
                .recommendation("Cache array length")
                .category("Gas")
                .blocks_deployment(false)
                .build(),
        ];

        AuditResult {
            project_name: "test".into(),
            chain: "ethereum".into(),
            timestamp: "2026-01-01T00:00:00Z".into(),
            duration_seconds: 0.5,
            findings,
            scores: SecurityScores {
                access_control: 100,
                security: 70,
                fuzzing: 100,
                gas: 85,
                architecture: 65,
                upgradeability: 100,
                dependencies: 100,
                deployment: 100,
                proxy_safety: 100,
                chain_compatibility: 100,
                production_readiness: 55,
                exploit_resistance: 100,
            },
            overall_score: 76,
            risk_level: RiskLevel::High,
            production_ready: false,
            deployment_approved: false,
            summary: AuditSummary {
                total_findings: 2,
                critical_count: 0,
                high_count: 1,
                medium_count: 1,
                low_count: 0,
                info_count: 0,
                files_analyzed: 5,
                lines_analyzed: 500,
                contracts_analyzed: 3,
            },
        }
    }

    #[test]
    fn test_executive_summary_contains_verdict() {
        let result = sample_result();
        let summary = generate_executive_summary(&result);
        assert!(summary.contains("EXECUTIVE SUMMARY"));
        assert!(summary.contains("FAIL"));
        assert!(summary.contains("BLOCKED"));
        assert!(summary.contains("ACTION ITEMS"));
        assert!(summary.contains("Reentrancy"));
    }

    #[test]
    fn test_executive_summary_top_findings() {
        let result = sample_result();
        let summary = generate_executive_summary(&result);
        assert!(summary.contains("1."));
        assert!(summary.contains("ReentrancyGuard"));
    }

    #[test]
    fn test_executive_summary_improvement_areas() {
        let result = sample_result();
        let summary = generate_executive_summary(&result);
        assert!(summary.contains("IMPROVEMENT"));
        // Production Ready is 55 — below 70
        assert!(summary.contains("Production Ready"));
    }

    #[test]
    fn test_executive_summary_quick_mode_indicator() {
        let result = sample_result();
        let summary = generate_executive_summary(&result);
        assert!(summary.contains("QUICK MODE"));
        assert!(summary.contains("--quick"));
    }

    #[test]
    fn test_count_recommendations() {
        let result = sample_result();
        let counts = count_recommendations(&result.findings);
        assert_eq!(counts.get("Logic").copied().unwrap_or(0), 1);
        assert_eq!(counts.get("Gas").copied().unwrap_or(0), 1);
    }

    #[test]
    fn test_executive_summary_passed() {
        let mut result = sample_result();
        result.production_ready = true;
        result.deployment_approved = true;
        result.overall_score = 92;
        result.risk_level = RiskLevel::Low;
        result.duration_seconds = 3.5;
        let summary = generate_executive_summary(&result);
        assert!(summary.contains("PASS"));
        assert!(summary.contains("APPROVED"));
        assert!(!summary.contains("QUICK MODE"));
    }
}