use std::time::{Duration, Instant};
use colored::Colorize as _;
use tabled::builder::Builder;
use tabled::settings::{Alignment, Modify, Style, object::Columns};
use crate::engine::analyzer::Severity;
pub(crate) fn apply_no_color(no_color: bool) {
if no_color {
colored::control::set_override(false);
}
}
pub(crate) fn status_info(msg: &str) -> String {
format!("{} {msg}", "[*]".bold().blue())
}
pub(crate) fn status_ok(msg: &str) -> String {
format!("{} {msg}", "[+]".bold().green())
}
pub(crate) fn status_warn(msg: &str) -> String {
format!("{} {msg}", "[!]".bold().yellow())
}
pub(crate) fn status_err(msg: &str) -> String {
format!("{} {msg}", "[-]".bold().red())
}
pub(crate) fn section_header(title: &str) {
let line = format!(" {} {}", title, "-".repeat(60usize.saturating_sub(title.len() + 4)));
println!("{}", line.bold().white());
}
pub(crate) fn banner(title: &str) {
let width = title.len() + 4;
let bar = "=".repeat(width);
println!("{}", format!("+{bar}+").bold().white());
println!("{}", format!("| {title} |").bold().white());
println!("{}", format!("+{bar}+").bold().white());
}
#[must_use]
pub(crate) fn sanitize_terminal(s: &str) -> String {
s.chars().map(|c| if c.is_control() { '.' } else { c }).collect()
}
pub(crate) fn print_handle(label: &str, hex: &str) {
println!(" {}: {}", sanitize_terminal(label).bold(), hex.cyan());
}
pub(crate) fn print_handle_next_steps(hex: &str, host: &str) {
println!();
println!(" {} Copy the handle above and use it with:", "Next steps:".bold().yellow());
println!(" {} shell {} --handle {}", "nfswolf".dimmed(), host, hex.cyan());
println!(" {} mount {} /mnt/escaped --handle {}", "nfswolf".dimmed(), host, hex.cyan());
}
pub(crate) fn elapsed(start: Instant) -> String {
let d = start.elapsed();
if d < Duration::from_secs(1) { format!("{}ms", d.as_millis()) } else { format!("{:.2}s", d.as_secs_f64()) }
}
pub(crate) fn severity_badge(sev: Severity) -> String {
match sev {
Severity::Critical => "[CRITICAL]".red().bold().to_string(),
Severity::High => "[HIGH] ".yellow().bold().to_string(),
Severity::Medium => "[MEDIUM] ".cyan().to_string(),
Severity::Low => "[LOW] ".white().to_string(),
Severity::Info => "[INFO] ".dimmed().to_string(),
}
}
pub(crate) struct ExportRow {
pub path: String,
pub clients: String,
pub auth: String,
pub flags: String,
pub handle_hex: String,
}
pub(crate) fn print_export_table(rows: &[ExportRow]) {
if rows.is_empty() {
println!(" {}", "(no exports found)".dimmed());
return;
}
let mut builder = Builder::default();
builder.push_record(["Path", "Allowed clients", "Auth", "Flags", "Handle (partial)"]);
for r in rows {
let path = sanitize_terminal(&r.path);
let clients_clean = sanitize_terminal(&r.clients);
let clients = if clients_clean == "*" || clients_clean.is_empty() { clients_clean.red().to_string() } else { clients_clean.normal().to_string() };
let flags = if r.flags.contains("WILDCARD") || r.flags.contains("NO_ROOT_SQUASH") {
r.flags.red().to_string()
} else if !r.flags.is_empty() {
r.flags.yellow().to_string()
} else {
r.flags.dimmed().to_string()
};
let handle_short = if r.handle_hex.len() > 16 { format!("{}...", &r.handle_hex[..16]) } else { r.handle_hex.clone() };
builder.push_record([&path, &clients, &r.auth, &flags, &handle_short.dimmed().to_string()]);
}
let mut table = builder.build();
_ = table.with(Style::rounded());
_ = table.with(Modify::new(Columns::first()).with(Alignment::left()));
println!("{table}");
}
pub(crate) fn print_findings(findings: &[crate::engine::analyzer::Finding]) {
if findings.is_empty() {
println!(" {}", status_ok("No findings -- server appears well-configured"));
return;
}
for f in findings {
let badge = severity_badge(f.severity);
let export_tag = f.export.as_deref().map_or_else(String::new, |e| format!(" {}", sanitize_terminal(e).dimmed()));
println!(" {badge}{export_tag} {} {}", f.id.bold(), f.title);
if !f.evidence.is_empty() {
println!(" {}: {}", "Evidence".dimmed(), sanitize_terminal(&f.evidence));
}
}
}
pub(crate) fn print_findings_summary(findings: &[crate::engine::analyzer::Finding]) {
let critical = findings.iter().filter(|f| matches!(f.severity, Severity::Critical)).count();
let high = findings.iter().filter(|f| matches!(f.severity, Severity::High)).count();
let medium = findings.iter().filter(|f| matches!(f.severity, Severity::Medium)).count();
let low = findings.iter().filter(|f| matches!(f.severity, Severity::Low) | matches!(f.severity, Severity::Info)).count();
let mut parts = Vec::new();
if critical > 0 {
parts.push(format!("{critical} critical").red().bold().to_string());
}
if high > 0 {
parts.push(format!("{high} high").yellow().to_string());
}
if medium > 0 {
parts.push(format!("{medium} medium").cyan().to_string());
}
if low > 0 {
parts.push(format!("{low} low/info").dimmed().to_string());
}
if parts.is_empty() {
println!(" {} No findings", "[+]".bold().green());
} else {
println!(" Findings: {}", parts.join(", "));
}
}