pub mod html;
pub mod json;
pub mod markdown;
use crate::core::{AuditResult, Finding, Severity};
pub trait ReportGenerator {
fn generate(&self, result: &AuditResult) -> Result<String, crate::core::ForgeGuardError>;
fn extension(&self) -> &'static str;
}
pub fn generate_executive_summary(result: &AuditResult) -> String {
let mut s = String::new();
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
));
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));
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"
}
));
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));
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
));
}
}
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
));
}
}
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
}
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"));
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"));
}
}