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