Skip to main content

panic_attacker/report/
formatter.rs

1// SPDX-License-Identifier: PMPL-1.0-or-later
2
3//! Report formatting and output
4
5use crate::types::*;
6use anyhow::Result;
7use colored::*;
8use std::fs;
9use std::path::Path;
10
11pub struct ReportFormatter;
12
13impl ReportFormatter {
14    pub fn new() -> Self {
15        Self
16    }
17
18    pub fn print(&self, report: &AssaultReport) {
19        println!(
20            "\n{}",
21            "=== PANIC-ATTACKER ASSAULT REPORT ===".bold().cyan()
22        );
23        println!();
24
25        self.print_xray_summary(&report.xray_report);
26        println!();
27
28        self.print_attack_summary(&report.attack_results);
29        println!();
30
31        self.print_signatures(&report.attack_results);
32        println!();
33
34        self.print_overall_assessment(&report.overall_assessment);
35        println!();
36    }
37
38    fn print_xray_summary(&self, xray: &XRayReport) {
39        println!("{}", "X-RAY ANALYSIS".bold().yellow());
40        println!("  Program: {}", xray.program_path.display());
41        println!("  Language: {:?}", xray.language);
42        println!("  Frameworks: {:?}", xray.frameworks);
43        println!();
44
45        println!("  Statistics:");
46        println!("    Total lines: {}", xray.statistics.total_lines);
47        println!("    Unsafe blocks: {}", xray.statistics.unsafe_blocks);
48        println!("    Panic sites: {}", xray.statistics.panic_sites);
49        println!("    Unwrap calls: {}", xray.statistics.unwrap_calls);
50        println!("    Allocation sites: {}", xray.statistics.allocation_sites);
51        println!("    I/O operations: {}", xray.statistics.io_operations);
52        println!(
53            "    Threading constructs: {}",
54            xray.statistics.threading_constructs
55        );
56        println!();
57
58        if !xray.weak_points.is_empty() {
59            println!("  Weak Points Detected: {}", xray.weak_points.len());
60            for (i, wp) in xray.weak_points.iter().enumerate() {
61                let severity_color = match wp.severity {
62                    Severity::Critical => "red",
63                    Severity::High => "yellow",
64                    Severity::Medium => "blue",
65                    Severity::Low => "green",
66                };
67                println!(
68                    "    {}. [{:?}] {} - {}",
69                    i + 1,
70                    wp.severity.to_string().color(severity_color),
71                    format!("{:?}", wp.category).bold(),
72                    wp.description
73                );
74            }
75        }
76
77        // Per-file breakdown sorted by risk score
78        if !xray.file_statistics.is_empty() {
79            println!();
80            println!("  Per-file Breakdown (top 10 by risk):");
81
82            let mut scored: Vec<_> = xray
83                .file_statistics
84                .iter()
85                .map(|fs| {
86                    let risk = fs.unsafe_blocks * 3
87                        + fs.panic_sites * 2
88                        + fs.unwrap_calls
89                        + fs.threading_constructs * 2;
90                    (risk, fs)
91                })
92                .collect();
93            scored.sort_by(|a, b| b.0.cmp(&a.0));
94
95            for (rank, (risk, fs)) in scored.iter().take(10).enumerate() {
96                println!(
97                    "    {}. {} (risk: {}, unsafe: {}, panics: {}, unwraps: {}, threads: {})",
98                    rank + 1,
99                    fs.file_path.bold(),
100                    risk,
101                    fs.unsafe_blocks,
102                    fs.panic_sites,
103                    fs.unwrap_calls,
104                    fs.threading_constructs,
105                );
106            }
107
108            if scored.len() > 10 {
109                println!("    ... and {} more files", scored.len() - 10);
110            }
111        }
112    }
113
114    fn print_attack_summary(&self, results: &[AttackResult]) {
115        println!("{}", "ATTACK RESULTS".bold().yellow());
116
117        for result in results {
118            let status = if result.success {
119                "PASSED".green()
120            } else {
121                "FAILED".red()
122            };
123
124            println!(
125                "  {:?} attack: {} (exit code: {:?}, duration: {:.2}s)",
126                result.axis,
127                status,
128                result.exit_code,
129                result.duration.as_secs_f64()
130            );
131
132            if !result.crashes.is_empty() {
133                println!(
134                    "    Crashes: {}",
135                    result.crashes.len().to_string().red().bold()
136                );
137                for (i, crash) in result.crashes.iter().enumerate() {
138                    println!("      {}. Signal: {:?}", i + 1, crash.signal);
139                    if let Some(bt) = &crash.backtrace {
140                        println!("         Backtrace available: {} bytes", bt.len());
141                    }
142                }
143            }
144
145            if result.peak_memory > 0 {
146                println!("    Peak memory: {} MB", result.peak_memory / (1024 * 1024));
147            }
148        }
149    }
150
151    fn print_signatures(&self, results: &[AttackResult]) {
152        let total_sigs: usize = results.iter().map(|r| r.signatures_detected.len()).sum();
153
154        if total_sigs > 0 {
155            println!("{}", "BUG SIGNATURES DETECTED".bold().red());
156            println!("  Total: {}", total_sigs);
157            println!();
158
159            for result in results {
160                if !result.signatures_detected.is_empty() {
161                    println!("  During {:?} attack:", result.axis);
162                    for sig in &result.signatures_detected {
163                        println!(
164                            "    - {:?} (confidence: {:.2})",
165                            sig.signature_type, sig.confidence
166                        );
167                        for evidence in &sig.evidence {
168                            println!("      Evidence: {}", evidence.dimmed());
169                        }
170                        if let Some(loc) = &sig.location {
171                            println!("      Location: {}", loc.dimmed());
172                        }
173                    }
174                    println!();
175                }
176            }
177        } else {
178            println!("{}", "No bug signatures detected".green());
179        }
180    }
181
182    fn print_overall_assessment(&self, assessment: &OverallAssessment) {
183        println!("{}", "OVERALL ASSESSMENT".bold().yellow());
184
185        let score_color = if assessment.robustness_score >= 80.0 {
186            "green"
187        } else if assessment.robustness_score >= 50.0 {
188            "yellow"
189        } else {
190            "red"
191        };
192
193        println!(
194            "  Robustness Score: {}/100",
195            format!("{:.1}", assessment.robustness_score)
196                .color(score_color)
197                .bold()
198        );
199        println!();
200
201        if !assessment.critical_issues.is_empty() {
202            println!("  Critical Issues:");
203            for issue in &assessment.critical_issues {
204                println!("    - {}", issue.red());
205            }
206            println!();
207        }
208
209        if !assessment.recommendations.is_empty() {
210            println!("  Recommendations:");
211            for rec in &assessment.recommendations {
212                println!("    - {}", rec);
213            }
214        }
215    }
216
217    pub fn save<P: AsRef<Path>>(&self, report: &AssaultReport, path: P) -> Result<()> {
218        let json = serde_json::to_string_pretty(report)?;
219        fs::write(path.as_ref(), json)?;
220        println!("Report saved to: {}", path.as_ref().display());
221        Ok(())
222    }
223}
224
225impl Default for ReportFormatter {
226    fn default() -> Self {
227        Self::new()
228    }
229}