forge-guard 0.1.8

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! Markdown report generation with rich formatting.

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

/// Generate a formatted Markdown audit report.
pub fn generate_report(result: &AuditResult) -> Result<String, ForgeGuardError> {
    let mut md = String::new();

    // Header
    md.push_str("# 🔒 Forge Guard Report\n\n");
    md.push_str(&format!("**Project:** {}  \n", result.project_name));
    md.push_str(&format!("**Chain:** {}  \n", result.chain));
    md.push_str(&format!("**Date:** {}  \n", result.timestamp));
    md.push_str(&format!(
        "**Duration:** {:.2}s  \n",
        result.duration_seconds
    ));
    md.push_str(&format!(
        "**Files Analyzed:** {}  \n\n",
        result.summary.files_analyzed
    ));

    // Summary
    md.push_str("---\n\n");
    md.push_str("## 📊 Summary\n\n");
    md.push_str("| Metric | Value |\n");
    md.push_str("|--------|-------|\n");
    md.push_str(&format!(
        "| **Overall Score** | **{}/100** |\n",
        result.overall_score
    ));
    md.push_str(&format!("| **Risk Level** | **{}** |\n", result.risk_level));
    md.push_str(&format!(
        "| **Production Ready** | {} |\n",
        if result.production_ready {
            "✅ YES"
        } else {
            "❌ NO"
        }
    ));
    md.push_str(&format!(
        "| **Deployment** | {} |\n",
        if result.deployment_approved {
            "✅ APPROVED"
        } else {
            "❌ BLOCKED"
        }
    ));
    md.push_str(&format!(
        "| **Total Findings** | {} |\n",
        result.summary.total_findings
    ));

    // Finding counts
    md.push_str("\n### Finding Breakdown\n\n");
    md.push_str("| Severity | Count |\n");
    md.push_str("|----------|-------|\n");
    md.push_str(&format!(
        "| 🛑 **Critical** | **{}** |\n",
        result.summary.critical_count
    ));
    md.push_str(&format!("| 🔴 High | {} |\n", result.summary.high_count));
    md.push_str(&format!(
        "| 🟡 Medium | {} |\n",
        result.summary.medium_count
    ));
    md.push_str(&format!("| 🔵 Low | {} |\n", result.summary.low_count));
    md.push_str(&format!("| ⚪ Info | {} |\n", result.summary.info_count));

    // Scores
    md.push_str("\n## 📈 Security Scores\n\n");
    md.push_str("| Category | Score |\n");
    md.push_str("|----------|-------|\n");
    md.push_str(&format!(
        "| 🔐 Access Control | {}/100 |\n",
        result.scores.access_control
    ));
    md.push_str(&format!(
        "| 🛡️ Security | {}/100 |\n",
        result.scores.security
    ));
    md.push_str(&format!("| 🎯 Fuzzing | {}/100 |\n", result.scores.fuzzing));
    md.push_str(&format!("| ⛽ Gas | {}/100 |\n", result.scores.gas));
    md.push_str(&format!(
        "| 🏗️ Architecture | {}/100 |\n",
        result.scores.architecture
    ));
    md.push_str(&format!(
        "| ⬆️ Upgradeability | {}/100 |\n",
        result.scores.upgradeability
    ));
    md.push_str(&format!(
        "| 📦 Dependencies | {}/100 |\n",
        result.scores.dependencies
    ));
    md.push_str(&format!(
        "| 🚀 Deployment | {}/100 |\n",
        result.scores.deployment
    ));
    md.push_str(&format!(
        "| 🔗 Proxy Safety | {}/100 |\n",
        result.scores.proxy_safety
    ));
    md.push_str(&format!(
        "| ⛓️ Chain Compat | {}/100 |\n",
        result.scores.chain_compatibility
    ));
    md.push_str(&format!(
        "| ✅ Production Ready | {}/100 |\n",
        result.scores.production_readiness
    ));
    md.push_str(&format!(
        "| 💥 Exploit Resistance | {}/100 |\n",
        result.scores.exploit_resistance
    ));

    // Findings details
    if !result.findings.is_empty() {
        md.push_str("\n---\n\n");
        md.push_str("## 🐛 Detailed Findings\n\n");

        // Group by severity
        let severities = [
            Severity::Critical,
            Severity::High,
            Severity::Medium,
            Severity::Low,
            Severity::Informational,
        ];
        for severity in severities {
            let findings: Vec<&Finding> = result
                .findings
                .iter()
                .filter(|f| f.severity == severity)
                .collect();
            if findings.is_empty() {
                continue;
            }

            let emoji = match severity {
                Severity::Critical => "🛑",
                Severity::High => "🔴",
                Severity::Medium => "🟡",
                Severity::Low => "🔵",
                Severity::Informational => "",
            };

            md.push_str(&format!(
                "### {} {} ({} found)\n\n",
                emoji,
                severity,
                findings.len()
            ));

            for (i, finding) in findings.iter().enumerate() {
                md.push_str(&format!(
                    "#### {}. {} `{}`\n\n",
                    i + 1,
                    finding.title,
                    finding.id
                ));
                md.push_str(&format!("**Severity:** {}  \n", finding.severity));
                md.push_str(&format!("**Category:** {}  \n", finding.category));

                if let Some(file) = &finding.file {
                    md.push_str(&format!("**File:** `{}`", file));
                    if let Some(line) = finding.line {
                        md.push_str(&format!(":{}", line));
                    }
                    md.push_str("  \n");
                }

                md.push_str(&format!(
                    "\n**Description:**  \n{}  \n\n",
                    finding.description
                ));

                if let Some(snippet) = &finding.code_snippet {
                    md.push_str("**Code:**  \n```solidity\n");
                    md.push_str(snippet);
                    md.push_str("\n```\n\n");
                }

                if let Some(exploit_path) = &finding.exploit_path {
                    md.push_str("**Exploit Path:**  \n");
                    for (j, step) in exploit_path.iter().enumerate() {
                        md.push_str(&format!("{}. {}\n", j + 1, step));
                    }
                    md.push('\n');
                }

                md.push_str(&format!(
                    "**Recommendation:**  \n{}  \n\n",
                    finding.recommendation
                ));

                if !finding.references.is_empty() {
                    md.push_str("**References:**  \n");
                    for ref_ in &finding.references {
                        md.push_str(&format!("- {}\n", ref_));
                    }
                    md.push('\n');
                }

                md.push_str("---\n\n");
            }
        }
    }

    // Footer
    md.push_str("## 📝 Recommendations\n\n");
    if result.deployment_approved {
        md.push_str("✅ Deployment approved. All security checks passed.\n\n");
    } else {
        md.push_str("❌ **Deployment blocked.** Address the issues above before deploying.\n\n");
        if result.risk_level == RiskLevel::Critical || result.risk_level == RiskLevel::High {
            md.push_str(
                "⚠️ **High risk detected.** Manual review recommended before any deployment.\n\n",
            );
        }
    }

    md.push_str("---\n\n");
    md.push_str(&format!(
        "*Report generated by [Forge Guard](https://github.com/codetibo/forge-guard) on {}*\n",
        chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
    ));

    Ok(md)
}

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

    #[test]
    fn test_generate_markdown() {
        let result = AuditResult {
            project_name: "test".into(),
            chain: "ethereum".into(),
            timestamp: "2026-01-01T00:00:00Z".into(),
            duration_seconds: 1.5,
            findings: vec![],
            scores: SecurityScores::perfect(),
            overall_score: 100,
            risk_level: RiskLevel::Minimal,
            production_ready: true,
            deployment_approved: true,
            summary: AuditSummary {
                total_findings: 0,
                critical_count: 0,
                high_count: 0,
                medium_count: 0,
                low_count: 0,
                info_count: 0,
                files_analyzed: 5,
                lines_analyzed: 500,
                contracts_analyzed: 3,
            },
        };

        let md = generate_report(&result).unwrap();
        assert!(md.contains("Forge Guard Report"));
        assert!(md.contains("100/100"));
    }
}