Skip to main content

shieldcontract/reporter/
mod.rs

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        // Register helpers
54        handlebars.register_helper("lowercase", Box::new(lowercase_helper));
55        handlebars.register_helper("eq", Box::new(eq_helper));
56
57        // Register default templates
58        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        // Filter findings by severity threshold
86        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(), // TODO: Track actual duration
100        };
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        // Convert benchmark results to findings
168        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        // Convert suggestions to findings
232        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        // Calculate scores
343        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        // Critical security recommendations
378        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        // AI validation recommendations
389        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        // Performance recommendations
405        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        // TODO: Implement PDF generation
434        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        // Simple table format for terminal output
441        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        // Basic XML format
475        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        // CSV format for findings
483        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        // SARIF (Static Analysis Results Interchange Format) for CI/CD integration
501        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(&param.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    // For eq helper, we just need to return Ok(()) since handlebars uses the return value for conditionals
603    Ok(())
604}