Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,11 @@ enum Commands {
headless: bool,
},

/// GUI review of a saved report (requires `--features gui` at build time)
#[cfg(feature = "gui")]
/// GUI review of a saved report.
///
/// `--headless` always works (text panel summaries to stdout). The
/// windowed renderer requires `--features gui` at build time because
/// eframe/egui raise MSRV above 1.85.0.
Gui {
/// Assault report JSON file
#[arg(value_name = "REPORT")]
Expand Down Expand Up @@ -1701,14 +1704,22 @@ fn run_main() -> Result<()> {
}
}

#[cfg(feature = "gui")]
Commands::Gui { report, headless } => {
let content = read_report_bounded(&report)?;
let assault_report: AssaultReport = serde_json::from_str(&content)?;
if headless {
report::ReportGui::run_headless(assault_report)?;
report::gui_text::run_headless(assault_report)?;
} else {
report::ReportGui::run(assault_report)?;
#[cfg(feature = "gui")]
{
report::ReportGui::run(assault_report)?;
}
#[cfg(not(feature = "gui"))]
{
anyhow::bail!(
"windowed GUI requires the `gui` feature; rebuild with `cargo build --features gui`, or pass --headless"
);
}
}
}

Expand Down
98 changes: 5 additions & 93 deletions src/report/gui.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// SPDX-License-Identifier: MPL-2.0

//! Minimal GUI for reviewing assault reports, system images, and temporal diffs.
//!
//! This module is compiled only when the `gui` feature is enabled because the
//! `eframe`/`egui` dependency chain raises MSRV above the 1.85.0 baseline.
//! The text-only `gui --headless` path lives in [`super::gui_text`] and is
//! always compiled.

use crate::mass_panic::imaging::SystemImage;
use crate::mass_panic::temporal::TemporalDiff;
Expand Down Expand Up @@ -65,99 +70,6 @@ impl ReportGui {
Ok(())
}

/// Run without a display server: print a structured text summary of all panels.
///
/// Safe to call in CI or headless environments with no Wayland/X11 display.
/// Promotes the gui subcommand from Grade E to Grade D.
pub fn run_headless(report: AssaultReport) -> Result<()> {
let assail = &report.assail_report;
let formatter = ReportFormatter::new();

println!("PANIC-ATTACK GUI REPORT (headless)");
println!();

println!("=== Summary ===");
println!("Language: {:?}", assail.language);
println!(
"Score: {:.1}/100",
report.overall_assessment.robustness_score
);
println!("Crashes: {}", report.total_crashes);
println!("Signatures: {}", report.total_signatures);
println!("Weak points: {}", assail.weak_points.len());
println!("Frameworks: {:?}", assail.frameworks);
println!();

println!("=== Assail ===");
println!("Program: {}", assail.program_path.display());
println!(
"Stats: lines={} unsafe={} panics={} unwraps={}",
assail.statistics.total_lines,
assail.statistics.unsafe_blocks,
assail.statistics.panic_sites,
assail.statistics.unwrap_calls
);
for wp in &assail.weak_points {
let loc = wp
.location
.as_deref()
.or(wp.file.as_deref())
.unwrap_or("<unknown>");
println!(
" [{:?}] {:?} @ {} — {}",
wp.severity, wp.category, loc, wp.description
);
}
println!();

println!("=== File Risk ===");
for detail in formatter.file_risk_details(assail) {
println!(" {}", detail);
}
println!();

println!("=== Matrix ===");
println!(
"Dependency edges: {} Taint rows: {}",
assail.dependency_graph.edges.len(),
assail.taint_matrix.rows.len()
);
for detail in formatter.taint_matrix_details(assail) {
println!(" {}", detail);
}
println!();

println!("=== Attacks ===");
for result in &report.attack_results {
let status = if result.skipped {
"skipped"
} else if result.success {
"passed"
} else {
"failed"
};
println!(
" {:?}: {} crashes={} duration={:.2}s",
result.axis,
status,
result.crashes.len(),
result.duration.as_secs_f64()
);
}
println!();

println!("=== Assessment ===");
for issue in &report.overall_assessment.critical_issues {
println!(" CRITICAL: {}", issue);
}
for rec in &report.overall_assessment.recommendations {
println!(" REC: {}", rec);
}
println!();

Ok(())
}

/// Attempt to load a JSON file as either an `AssaultReport`, `SystemImage`,
/// or `TemporalDiff`. Detection is format-based: we try each in turn and
/// keep the first successful parse.
Expand Down
106 changes: 106 additions & 0 deletions src/report/gui_text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MPL-2.0

//! Headless GUI report renderer — text-only output, no display server.
//!
//! This is the `gui --headless` path. It is always compiled (independent of the
//! `gui` feature flag) so the readiness test `readiness_d_gui_headless_runs`
//! and CI integrations keep working in MSRV-clean default builds. The
//! windowed renderer in [`crate::report::gui`] is feature-gated because eframe
//! raises MSRV above 1.85.0.

use crate::report::formatter::ReportFormatter;
use crate::types::AssaultReport;
use anyhow::Result;

/// Print a structured text summary of every GUI panel.
///
/// Safe to call in CI or headless environments with no Wayland/X11 display.
/// Promotes the `gui` subcommand from Grade E to Grade D.
pub fn run_headless(report: AssaultReport) -> Result<()> {
let assail = &report.assail_report;
let formatter = ReportFormatter::new();

println!("PANIC-ATTACK GUI REPORT (headless)");
println!();

println!("=== Summary ===");
println!("Language: {:?}", assail.language);
println!(
"Score: {:.1}/100",
report.overall_assessment.robustness_score
);
println!("Crashes: {}", report.total_crashes);
println!("Signatures: {}", report.total_signatures);
println!("Weak points: {}", assail.weak_points.len());
println!("Frameworks: {:?}", assail.frameworks);
println!();

println!("=== Assail ===");
println!("Program: {}", assail.program_path.display());
println!(
"Stats: lines={} unsafe={} panics={} unwraps={}",
assail.statistics.total_lines,
assail.statistics.unsafe_blocks,
assail.statistics.panic_sites,
assail.statistics.unwrap_calls
);
for wp in &assail.weak_points {
let loc = wp
.location
.as_deref()
.or(wp.file.as_deref())
.unwrap_or("<unknown>");
println!(
" [{:?}] {:?} @ {} — {}",
wp.severity, wp.category, loc, wp.description
);
}
println!();

println!("=== File Risk ===");
for detail in formatter.file_risk_details(assail) {
println!(" {}", detail);
}
println!();

println!("=== Matrix ===");
println!(
"Dependency edges: {} Taint rows: {}",
assail.dependency_graph.edges.len(),
assail.taint_matrix.rows.len()
);
for detail in formatter.taint_matrix_details(assail) {
println!(" {}", detail);
}
println!();

println!("=== Attacks ===");
for result in &report.attack_results {
let status = if result.skipped {
"skipped"
} else if result.success {
"passed"
} else {
"failed"
};
println!(
" {:?}: {} crashes={} duration={:.2}s",
result.axis,
status,
result.crashes.len(),
result.duration.as_secs_f64()
);
}
println!();

println!("=== Assessment ===");
for issue in &report.overall_assessment.critical_issues {
println!(" CRITICAL: {}", issue);
}
for rec in &report.overall_assessment.recommendations {
println!(" REC: {}", rec);
}
println!();

Ok(())
}
1 change: 1 addition & 0 deletions src/report/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod formatter;
pub mod generator;
#[cfg(feature = "gui")]
pub mod gui;
pub mod gui_text;
pub mod migration;
pub mod output;
pub mod sarif;
Expand Down
Loading