1use crate::{analyzer::AnalysisResult, AnalysisConfig, Finding, OutputFormat, Result, Severity};
2use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::Path;
6
7pub struct Reporter {
8 handlebars: Handlebars<'static>,
9}
10
11#[derive(Debug, Serialize, Deserialize)]
12pub struct Report {
13 pub metadata: ReportMetadata,
14 pub summary: ReportSummary,
15 pub findings: Vec<Finding>,
16 pub recommendations: Vec<Recommendation>,
17}
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct ReportMetadata {
21 pub tool_version: String,
22 pub timestamp: String,
23 pub files_analyzed: usize,
24 pub total_lines: usize,
25 pub analysis_duration: String,
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29pub struct ReportSummary {
30 pub total_findings: usize,
31 pub critical_findings: usize,
32 pub high_findings: usize,
33 pub medium_findings: usize,
34 pub low_findings: usize,
35 pub info_findings: usize,
36 pub security_score: f64,
37 pub ai_validation_score: f64,
38 pub complexity_score: f64,
39}
40
41#[derive(Debug, Serialize, Deserialize)]
42pub struct Recommendation {
43 pub priority: String,
44 pub category: String,
45 pub description: String,
46 pub impact: String,
47}
48
49impl Reporter {
50 pub fn new() -> Self {
51 let mut handlebars = Handlebars::new();
52
53 handlebars.register_helper("lowercase", Box::new(lowercase_helper));
55 handlebars.register_helper("eq", Box::new(eq_helper));
56
57 let html_template = include_str!("templates/report.html");
59 let md_template = include_str!("templates/report.md");
60
61 handlebars
62 .register_template_string("html", html_template)
63 .expect("Failed to register HTML template");
64
65 handlebars
66 .register_template_string("markdown", md_template)
67 .expect("Failed to register Markdown template");
68
69 Self { handlebars }
70 }
71
72 pub fn generate_report(
73 &self,
74 results: &[AnalysisResult],
75 config: &AnalysisConfig,
76 ) -> Result<Report> {
77 let mut all_findings = Vec::new();
78 let mut total_lines = 0;
79
80 for result in results {
81 all_findings.extend(result.findings.clone());
82 total_lines += result.metrics.total_lines;
83 }
84
85 let filtered_findings: Vec<Finding> = all_findings
87 .into_iter()
88 .filter(|f| self.meets_severity_threshold(&f.severity, &config.severity_threshold))
89 .collect();
90
91 let summary = self.calculate_summary(&filtered_findings, results);
92 let recommendations = self.generate_recommendations(&filtered_findings, results);
93
94 let metadata = ReportMetadata {
95 tool_version: env!("CARGO_PKG_VERSION").to_string(),
96 timestamp: chrono::Utc::now().to_rfc3339(),
97 files_analyzed: results.len(),
98 total_lines,
99 analysis_duration: "0s".to_string(), };
101
102 Ok(Report {
103 metadata,
104 summary,
105 findings: filtered_findings,
106 recommendations,
107 })
108 }
109
110 pub fn generate_scan_report(&self, results: &[AnalysisResult]) -> Result<Report> {
111 let config = AnalysisConfig::default();
112 self.generate_report(results, &config)
113 }
114
115 pub fn generate_audit_report(&self, results: &crate::auditor::AuditResult) -> Result<Report> {
116 let findings = results.findings.clone();
117 let summary = ReportSummary {
118 total_findings: findings.len(),
119 critical_findings: findings
120 .iter()
121 .filter(|f| f.severity == Severity::Critical)
122 .count(),
123 high_findings: findings
124 .iter()
125 .filter(|f| f.severity == Severity::High)
126 .count(),
127 medium_findings: findings
128 .iter()
129 .filter(|f| f.severity == Severity::Medium)
130 .count(),
131 low_findings: findings
132 .iter()
133 .filter(|f| f.severity == Severity::Low)
134 .count(),
135 info_findings: findings
136 .iter()
137 .filter(|f| f.severity == Severity::Info)
138 .count(),
139 security_score: results.compliance_score as f64,
140 ai_validation_score: 100.0,
141 complexity_score: 100.0,
142 };
143
144 let metadata = ReportMetadata {
145 tool_version: env!("CARGO_PKG_VERSION").to_string(),
146 timestamp: chrono::Utc::now().to_rfc3339(),
147 files_analyzed: 1,
148 total_lines: 0,
149 analysis_duration: "0s".to_string(),
150 };
151
152 Ok(Report {
153 metadata,
154 summary,
155 findings,
156 recommendations: vec![],
157 })
158 }
159
160 pub fn generate_benchmark_report(
161 &self,
162 results: &crate::benchmark::BenchmarkResults,
163 ) -> Result<Report> {
164 let mut findings = Vec::new();
165 let mut recommendations = Vec::new();
166
167 if let Some(ref throughput) = results.throughput {
169 for bottleneck in &throughput.bottlenecks {
170 findings.push(Finding {
171 id: "BENCH-PERF-001".to_string(),
172 severity: Severity::Medium,
173 category: "Performance".to_string(),
174 title: "Performance bottleneck".to_string(),
175 description: bottleneck.clone(),
176 file: "".to_string(),
177 line: 0,
178 column: 0,
179 code_snippet: None,
180 remediation: None,
181 references: vec![],
182 ai_consensus: None,
183 });
184 }
185
186 if throughput.tps < 100.0 {
187 recommendations.push(Recommendation {
188 priority: "HIGH".to_string(),
189 category: "Performance".to_string(),
190 description: "Transaction throughput is below acceptable levels".to_string(),
191 impact: format!("Current: {:.1} TPS, Target: 100+ TPS", throughput.tps),
192 });
193 }
194 }
195
196 let summary = ReportSummary {
197 total_findings: findings.len(),
198 critical_findings: 0,
199 high_findings: 0,
200 medium_findings: findings.len(),
201 low_findings: 0,
202 info_findings: 0,
203 security_score: 100.0,
204 ai_validation_score: 100.0,
205 complexity_score: results.overall_score as f64,
206 };
207
208 let metadata = ReportMetadata {
209 tool_version: env!("CARGO_PKG_VERSION").to_string(),
210 timestamp: chrono::Utc::now().to_rfc3339(),
211 files_analyzed: 1,
212 total_lines: 0,
213 analysis_duration: "0s".to_string(),
214 };
215
216 Ok(Report {
217 metadata,
218 summary,
219 findings,
220 recommendations,
221 })
222 }
223
224 pub fn generate_optimization_report(
225 &self,
226 suggestions: &[crate::optimizer::OptimizationSuggestion],
227 ) -> Result<Report> {
228 let mut findings = Vec::new();
229 let mut recommendations = Vec::new();
230
231 for suggestion in suggestions {
233 findings.push(Finding {
234 id: suggestion.id.clone(),
235 severity: Severity::Info,
236 category: format!("Optimization/{}", suggestion.category),
237 title: suggestion.title.clone(),
238 description: suggestion.description.clone(),
239 file: "".to_string(),
240 line: 0,
241 column: 0,
242 code_snippet: Some(format!(
243 "Before:\n{}\n\nAfter:\n{}",
244 suggestion.code_before, suggestion.code_after
245 )),
246 remediation: Some(suggestion.explanation.clone()),
247 references: vec![],
248 ai_consensus: None,
249 });
250
251 recommendations.push(Recommendation {
252 priority: match suggestion.implementation_difficulty {
253 crate::optimizer::Difficulty::Easy => "LOW",
254 crate::optimizer::Difficulty::Medium => "MEDIUM",
255 crate::optimizer::Difficulty::Hard => "HIGH",
256 }
257 .to_string(),
258 category: suggestion.category.clone(),
259 description: suggestion.title.clone(),
260 impact: format!(
261 "Expected performance gain: {:.1}%",
262 suggestion.performance_gain
263 ),
264 });
265 }
266
267 let total_gain: f32 = suggestions.iter().map(|s| s.performance_gain).sum();
268
269 let summary = ReportSummary {
270 total_findings: findings.len(),
271 critical_findings: 0,
272 high_findings: 0,
273 medium_findings: 0,
274 low_findings: 0,
275 info_findings: findings.len(),
276 security_score: 100.0,
277 ai_validation_score: 100.0,
278 complexity_score: (100.0 + total_gain as f64).min(100.0),
279 };
280
281 let metadata = ReportMetadata {
282 tool_version: env!("CARGO_PKG_VERSION").to_string(),
283 timestamp: chrono::Utc::now().to_rfc3339(),
284 files_analyzed: 1,
285 total_lines: 0,
286 analysis_duration: "0s".to_string(),
287 };
288
289 Ok(Report {
290 metadata,
291 summary,
292 findings,
293 recommendations,
294 })
295 }
296
297 pub async fn save_report(
298 &self,
299 report: &Report,
300 path: &Path,
301 format: OutputFormat,
302 ) -> Result<()> {
303 let content = match format {
304 OutputFormat::Json => serde_json::to_string_pretty(report)
305 .map_err(|e| crate::ShieldContractError::Report(e.to_string()))?,
306 OutputFormat::Html => self.render_html(report)?,
307 OutputFormat::Markdown => self.render_markdown(report)?,
308 OutputFormat::Pdf => self.render_pdf(report)?,
309 OutputFormat::Table => self.render_table(report)?,
310 OutputFormat::Xml => self.render_xml(report)?,
311 OutputFormat::Csv => self.render_csv(report)?,
312 OutputFormat::Sarif => self.render_sarif(report)?,
313 };
314
315 tokio::fs::write(path, content).await?;
316 Ok(())
317 }
318
319 fn calculate_summary(&self, findings: &[Finding], results: &[AnalysisResult]) -> ReportSummary {
320 let mut summary = ReportSummary {
321 total_findings: findings.len(),
322 critical_findings: 0,
323 high_findings: 0,
324 medium_findings: 0,
325 low_findings: 0,
326 info_findings: 0,
327 security_score: 0.0,
328 ai_validation_score: 0.0,
329 complexity_score: 0.0,
330 };
331
332 for finding in findings {
333 match finding.severity {
334 Severity::Critical => summary.critical_findings += 1,
335 Severity::High => summary.high_findings += 1,
336 Severity::Medium => summary.medium_findings += 1,
337 Severity::Low => summary.low_findings += 1,
338 Severity::Info => summary.info_findings += 1,
339 }
340 }
341
342 if !results.is_empty() {
344 summary.security_score = results
345 .iter()
346 .map(|r| r.metrics.security_score)
347 .sum::<f64>()
348 / results.len() as f64;
349
350 summary.ai_validation_score = results
351 .iter()
352 .map(|r| r.metrics.ai_validation_score)
353 .sum::<f64>()
354 / results.len() as f64;
355
356 summary.complexity_score = 100.0
357 - (results
358 .iter()
359 .map(|r| r.metrics.cyclomatic_complexity)
360 .sum::<f64>()
361 / results.len() as f64
362 * 5.0)
363 .min(100.0)
364 .max(0.0);
365 }
366
367 summary
368 }
369
370 fn generate_recommendations(
371 &self,
372 findings: &[Finding],
373 results: &[AnalysisResult],
374 ) -> Vec<Recommendation> {
375 let mut recommendations = Vec::new();
376
377 if findings.iter().any(|f| f.severity == Severity::Critical) {
379 recommendations.push(Recommendation {
380 priority: "URGENT".to_string(),
381 category: "Security".to_string(),
382 description: "Address all critical security vulnerabilities immediately"
383 .to_string(),
384 impact: "Prevents potential security breaches and consensus failures".to_string(),
385 });
386 }
387
388 let ai_findings: Vec<_> = findings
390 .iter()
391 .filter(|f| f.category.starts_with("ai-validation"))
392 .collect();
393
394 if !ai_findings.is_empty() {
395 recommendations.push(Recommendation {
396 priority: "HIGH".to_string(),
397 category: "AI Validation".to_string(),
398 description: format!("Review {} AI-generated code issues", ai_findings.len()),
399 impact: "Ensures code reliability and prevents hallucinated dependencies"
400 .to_string(),
401 });
402 }
403
404 if let Some(result) = results.first() {
406 if result.metrics.cyclomatic_complexity > 15.0 {
407 recommendations.push(Recommendation {
408 priority: "MEDIUM".to_string(),
409 category: "Code Quality".to_string(),
410 description: "Refactor complex functions to improve maintainability"
411 .to_string(),
412 impact: "Reduces bugs and improves code readability".to_string(),
413 });
414 }
415 }
416
417 recommendations
418 }
419
420 fn render_html(&self, report: &Report) -> Result<String> {
421 self.handlebars
422 .render("html", report)
423 .map_err(|e| crate::ShieldContractError::Report(e.to_string()))
424 }
425
426 fn render_markdown(&self, report: &Report) -> Result<String> {
427 self.handlebars
428 .render("markdown", report)
429 .map_err(|e| crate::ShieldContractError::Report(e.to_string()))
430 }
431
432 fn render_pdf(&self, _report: &Report) -> Result<String> {
433 Err(crate::ShieldContractError::Report(
435 "PDF generation not yet implemented".to_string(),
436 ))
437 }
438
439 fn render_table(&self, report: &Report) -> Result<String> {
440 let mut output = String::new();
442 output.push_str(&format!(
443 "ShieldContract Analysis Report - {}\n",
444 report.metadata.timestamp
445 ));
446 output.push_str(&format!("{}\n", "=".repeat(80)));
447 output.push_str(&format!(
448 "Total Findings: {}\n",
449 report.summary.total_findings
450 ));
451 output.push_str(&format!(
452 "Critical: {} | High: {} | Medium: {} | Low: {} | Info: {}\n",
453 report.summary.critical_findings,
454 report.summary.high_findings,
455 report.summary.medium_findings,
456 report.summary.low_findings,
457 report.summary.info_findings
458 ));
459 output.push_str(&format!("{}\n", "-".repeat(80)));
460
461 for finding in &report.findings {
462 output.push_str(&format!(
463 "[{}] {} - {}\n",
464 finding.severity, finding.id, finding.title
465 ));
466 output.push_str(&format!(" File: {}:{}\n", finding.file, finding.line));
467 output.push_str(&format!(" {}\n\n", finding.description));
468 }
469
470 Ok(output)
471 }
472
473 fn render_xml(&self, report: &Report) -> Result<String> {
474 let xml = quick_xml::se::to_string(report).map_err(|e| {
476 crate::ShieldContractError::Report(format!("XML serialization failed: {}", e))
477 })?;
478 Ok(xml)
479 }
480
481 fn render_csv(&self, report: &Report) -> Result<String> {
482 let mut csv = String::from("ID,Severity,Category,Title,File,Line,Description\n");
484 for finding in &report.findings {
485 csv.push_str(&format!(
486 "{},{},{},{},{},{},{}\n",
487 finding.id,
488 finding.severity,
489 finding.category,
490 finding.title,
491 finding.file,
492 finding.line,
493 finding.description.replace(',', ";")
494 ));
495 }
496 Ok(csv)
497 }
498
499 fn render_sarif(&self, report: &Report) -> Result<String> {
500 let sarif = serde_json::json!({
502 "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
503 "version": "2.1.0",
504 "runs": [{
505 "tool": {
506 "driver": {
507 "name": "ShieldContract",
508 "version": report.metadata.tool_version,
509 "informationUri": "https://github.com/KoushikGavini/ShieldContract"
510 }
511 },
512 "results": report.findings.iter().map(|f| {
513 serde_json::json!({
514 "ruleId": f.id,
515 "level": match f.severity {
516 Severity::Critical | Severity::High => "error",
517 Severity::Medium => "warning",
518 _ => "note"
519 },
520 "message": {
521 "text": f.description
522 },
523 "locations": [{
524 "physicalLocation": {
525 "artifactLocation": {
526 "uri": f.file
527 },
528 "region": {
529 "startLine": f.line,
530 "startColumn": f.column
531 }
532 }
533 }]
534 })
535 }).collect::<Vec<_>>()
536 }]
537 });
538
539 serde_json::to_string_pretty(&sarif).map_err(|e| {
540 crate::ShieldContractError::Report(format!("SARIF generation failed: {}", e))
541 })
542 }
543
544 fn meets_severity_threshold(&self, severity: &Severity, threshold: &Severity) -> bool {
545 match threshold {
546 Severity::Critical => matches!(severity, Severity::Critical),
547 Severity::High => matches!(severity, Severity::Critical | Severity::High),
548 Severity::Medium => matches!(
549 severity,
550 Severity::Critical | Severity::High | Severity::Medium
551 ),
552 Severity::Low => !matches!(severity, Severity::Info),
553 Severity::Info => true,
554 }
555 }
556}
557
558impl Report {
559 pub fn summary(&self) -> String {
560 format!(
561 "ShieldContract Analysis Report\n\
562 ==========================\n\
563 Total Findings: {}\n\
564 Critical: {} | High: {} | Medium: {} | Low: {} | Info: {}\n\
565 Security Score: {:.1}/100\n\
566 AI Validation Score: {:.1}/100\n\
567 Complexity Score: {:.1}/100",
568 self.summary.total_findings,
569 self.summary.critical_findings,
570 self.summary.high_findings,
571 self.summary.medium_findings,
572 self.summary.low_findings,
573 self.summary.info_findings,
574 self.summary.security_score,
575 self.summary.ai_validation_score,
576 self.summary.complexity_score
577 )
578 }
579}
580
581fn lowercase_helper(
582 h: &Helper,
583 _: &Handlebars,
584 _: &Context,
585 _: &mut RenderContext,
586 out: &mut dyn Output,
587) -> HelperResult {
588 let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
589 out.write(¶m.to_lowercase())?;
590 Ok(())
591}
592
593fn eq_helper(
594 h: &Helper,
595 _: &Handlebars,
596 _: &Context,
597 _: &mut RenderContext,
598 _: &mut dyn Output,
599) -> HelperResult {
600 let param1 = h.param(0).and_then(|v| v.value().as_str()).unwrap_or("");
601 let param2 = h.param(1).and_then(|v| v.value().as_str()).unwrap_or("");
602 Ok(())
604}