use crate::core::{AuditResult, Finding, ForgeGuardError, RiskLevel, Severity};
pub fn generate_report(result: &AuditResult) -> Result<String, ForgeGuardError> {
let mut md = String::new();
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
));
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
));
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));
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
));
if !result.findings.is_empty() {
md.push_str("\n---\n\n");
md.push_str("## 🐛 Detailed Findings\n\n");
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");
}
}
}
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"));
}
}