1pub mod html;
4pub mod json;
5pub mod markdown;
6
7use crate::core::{AuditResult, Finding, Severity};
8
9pub trait ReportGenerator {
11 fn generate(&self, result: &AuditResult) -> Result<String, crate::core::ForgeGuardError>;
13
14 fn extension(&self) -> &'static str;
16}
17
18pub fn generate_executive_summary(result: &AuditResult) -> String {
22 let mut s = String::new();
23
24 s.push_str("═══════════════════════════════════════════════════\n");
26 s.push_str(" FORGE AUDIT — EXECUTIVE SUMMARY\n");
27 s.push_str("═══════════════════════════════════════════════════\n\n");
28
29 s.push_str(&format!(" Project: {}\n", result.project_name));
30 s.push_str(&format!(" Chain: {}\n", result.chain));
31 s.push_str(&format!(
32 " Files Analyzed: {}\n",
33 result.summary.files_analyzed
34 ));
35 s.push_str(&format!(
36 " Duration: {:.2}s\n",
37 result.duration_seconds
38 ));
39
40 s.push_str("\n── VERDICT ──\n");
42 let verdict = if result.production_ready && result.deployment_approved {
43 "✅ PASS — Ready for deployment"
44 } else if result.production_ready {
45 "⚠️ PASS WITH WARNINGS — Review findings before deploying"
46 } else {
47 "❌ FAIL — Must fix issues before deployment"
48 };
49 s.push_str(&format!(" {}\n\n", verdict));
50
51 s.push_str("── KEY METRICS ──\n");
53 s.push_str(&format!(
54 " Overall Score: {:>3}/100\n",
55 result.overall_score
56 ));
57 s.push_str(&format!(" Risk Level: {}\n", result.risk_level));
58 s.push_str(&format!(
59 " Deployment: {}\n",
60 if result.deployment_approved {
61 "✅ APPROVED"
62 } else {
63 "❌ BLOCKED"
64 }
65 ));
66
67 s.push_str("\n── FINDING SUMMARY ──\n");
69 s.push_str(&format!(
70 " 🛑 Critical: {}\n",
71 result.summary.critical_count
72 ));
73 s.push_str(&format!(" 🔴 High: {}\n", result.summary.high_count));
74 s.push_str(&format!(
75 " 🟡 Medium: {}\n",
76 result.summary.medium_count
77 ));
78 s.push_str(&format!(" 🔵 Low: {}\n", result.summary.low_count));
79 s.push_str(&format!(" ⚪ Info: {}\n", result.summary.info_count));
80
81 let critical_high: Vec<&Finding> = result
83 .findings
84 .iter()
85 .filter(|f| f.severity == Severity::Critical || f.severity == Severity::High)
86 .collect();
87
88 if !critical_high.is_empty() {
89 s.push_str("\n── TOP ACTION ITEMS ──\n");
90 for (i, f) in critical_high.iter().take(5).enumerate() {
91 let location = match (&f.file, f.line) {
92 (Some(file), Some(line)) => format!("{}:{}", file, line),
93 (Some(file), None) => file.clone(),
94 (None, _) => "unknown".into(),
95 };
96 s.push_str(&format!(
97 " {}. [{}] {} ({})\n",
98 i + 1,
99 f.severity,
100 f.title,
101 location
102 ));
103 s.push_str(&format!(" 💡 {}\n", f.recommendation));
104 }
105 if critical_high.len() > 5 {
106 s.push_str(&format!(
107 " ... and {} more critical/high findings\n",
108 critical_high.len() - 5
109 ));
110 }
111 }
112
113 let categories = [
115 ("Access Control", result.scores.access_control),
116 ("Security", result.scores.security),
117 ("Architecture", result.scores.architecture),
118 ("Production Ready", result.scores.production_readiness),
119 ("Gas", result.scores.gas),
120 ];
121 let worst: Vec<&(&str, u8)> = categories.iter().filter(|(_, score)| *score < 70).collect();
122 if !worst.is_empty() {
123 s.push_str("\n── AREAS FOR IMPROVEMENT ──\n");
124 for (name, score) in worst {
125 s.push_str(&format!(
126 " 🔸 {}: {:>3}/100 — needs attention\n",
127 name, score
128 ));
129 }
130 }
131
132 if result.duration_seconds < 1.0 {
134 s.push_str("\n── QUICK MODE ──\n");
135 s.push_str(" ⚡ Quick mode enabled — parser-heavy checks were skipped.\n");
136 s.push_str(" 🔍 Run without --quick for a comprehensive audit.\n");
137 }
138
139 s.push_str("\n═══════════════════════════════════════════════════\n");
140 s
141}
142
143pub fn count_recommendations(findings: &[Finding]) -> std::collections::HashMap<String, usize> {
145 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
146 for f in findings {
147 let cat = if f.category.is_empty() {
148 "General"
149 } else {
150 &f.category
151 };
152 *counts.entry(cat.to_string()).or_insert(0) += 1;
153 }
154 counts
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::core::*;
161
162 fn sample_result() -> AuditResult {
163 let findings = vec![
164 Finding::builder()
165 .id("FA-H-001-1")
166 .title("Reentrancy")
167 .description("CEI violation")
168 .severity(Severity::High)
169 .file("Vuln.sol")
170 .location(15, 0)
171 .code("externalCall(); balance -= amount;")
172 .recommendation("Use ReentrancyGuard")
173 .category("Logic")
174 .blocks_deployment(true)
175 .build(),
176 Finding::builder()
177 .id("FA-M-001-1")
178 .title("Gas Problem")
179 .description("Loop gas issue")
180 .severity(Severity::Medium)
181 .file("Vuln.sol")
182 .location(30, 0)
183 .code("for(uint i;i<arr.length;i++)")
184 .recommendation("Cache array length")
185 .category("Gas")
186 .blocks_deployment(false)
187 .build(),
188 ];
189
190 AuditResult {
191 project_name: "test".into(),
192 chain: "ethereum".into(),
193 chains: vec!["ethereum".into()],
194 timestamp: "2026-01-01T00:00:00Z".into(),
195 duration_seconds: 0.5,
196 findings,
197 scores: SecurityScores {
198 access_control: 100,
199 security: 70,
200 fuzzing: 100,
201 gas: 85,
202 architecture: 65,
203 upgradeability: 100,
204 dependencies: 100,
205 deployment: 100,
206 proxy_safety: 100,
207 chain_compatibility: 100,
208 production_readiness: 55,
209 exploit_resistance: 100,
210 },
211 overall_score: 76,
212 risk_level: RiskLevel::High,
213 production_ready: false,
214 deployment_approved: false,
215 summary: AuditSummary {
216 total_findings: 2,
217 critical_count: 0,
218 high_count: 1,
219 medium_count: 1,
220 low_count: 0,
221 info_count: 0,
222 files_analyzed: 5,
223 lines_analyzed: 500,
224 contracts_analyzed: 3,
225 },
226 }
227 }
228
229 #[test]
230 fn test_executive_summary_contains_verdict() {
231 let result = sample_result();
232 let summary = generate_executive_summary(&result);
233 assert!(summary.contains("EXECUTIVE SUMMARY"));
234 assert!(summary.contains("FAIL"));
235 assert!(summary.contains("BLOCKED"));
236 assert!(summary.contains("ACTION ITEMS"));
237 assert!(summary.contains("Reentrancy"));
238 }
239
240 #[test]
241 fn test_executive_summary_top_findings() {
242 let result = sample_result();
243 let summary = generate_executive_summary(&result);
244 assert!(summary.contains("1."));
245 assert!(summary.contains("ReentrancyGuard"));
246 }
247
248 #[test]
249 fn test_executive_summary_improvement_areas() {
250 let result = sample_result();
251 let summary = generate_executive_summary(&result);
252 assert!(summary.contains("IMPROVEMENT"));
253 assert!(summary.contains("Production Ready"));
255 }
256
257 #[test]
258 fn test_executive_summary_quick_mode_indicator() {
259 let result = sample_result();
260 let summary = generate_executive_summary(&result);
261 assert!(summary.contains("QUICK MODE"));
262 assert!(summary.contains("--quick"));
263 }
264
265 #[test]
266 fn test_count_recommendations() {
267 let result = sample_result();
268 let counts = count_recommendations(&result.findings);
269 assert_eq!(counts.get("Logic").copied().unwrap_or(0), 1);
270 assert_eq!(counts.get("Gas").copied().unwrap_or(0), 1);
271 }
272
273 #[test]
274 fn test_executive_summary_passed() {
275 let mut result = sample_result();
276 result.production_ready = true;
277 result.deployment_approved = true;
278 result.overall_score = 92;
279 result.risk_level = RiskLevel::Low;
280 result.duration_seconds = 3.5;
281 let summary = generate_executive_summary(&result);
282 assert!(summary.contains("PASS"));
283 assert!(summary.contains("APPROVED"));
284 assert!(!summary.contains("QUICK MODE"));
285 }
286}