whyno-cli 0.2.0

Linux permission debugger
//! Verbose `--explain` output.
//!
//! Shows subject identity, per-component path walk (mode, ownership,
//! ACLs, flags, mounts), per-layer check reasoning, fix plan with
//! Scoring rationale. verbosity is the point.

#[path = "explain_walk.rs"]
mod explain_walk;

use std::io::Write;

use whyno_core::checks::{CheckReport, LayerResult};
use whyno_core::fix::{commands, scoring, Fix, FixPlan};
use whyno_core::state::{Probe, SystemState};

use super::layer_name;
use crate::error::OutputError;

/// Renders full verbose explanation.
pub fn render(
    report: &CheckReport,
    plan: &FixPlan,
    state: &SystemState,
    writer: &mut dyn Write,
) -> Result<(), OutputError> {
    write_subject_section(state, writer)?;
    writeln!(writer)?;
    explain_walk::write_path_walk(state, writer)?;
    writeln!(writer)?;
    write_layer_details(report, writer)?;
    writeln!(writer)?;
    write_fix_plan(plan, writer)?;
    Ok(())
}

/// Subject identity section with uid, gid, and groups.
fn write_subject_section(state: &SystemState, writer: &mut dyn Write) -> Result<(), OutputError> {
    let s = &state.subject;
    writeln!(writer, "=== Subject ===")?;
    writeln!(writer, "  uid: {}", s.uid)?;
    writeln!(writer, "  gid: {}", s.gid)?;
    writeln!(writer, "  groups: {:?}", s.groups)?;
    if let Probe::Known(bitmask) = s.capabilities {
        writeln!(writer, "  caps: {bitmask:#018x}")?;
    }
    let op = format!("{:?}", state.operation).to_lowercase();
    writeln!(writer, "  operation: {op}")?;
    Ok(())
}

/// Per-layer check detail blocks for all core and MAC layers.
fn write_layer_details(report: &CheckReport, writer: &mut dyn Write) -> Result<(), OutputError> {
    writeln!(writer, "=== Layer Results ===")?;
    for (layer, result) in &report.core_results {
        write_layer_result(layer_name::short(layer), result, writer)?;
    }
    for (layer, result) in &report.mac_results {
        write_layer_result(layer_name::short_mac(*layer), result, writer)?;
    }
    Ok(())
}

/// Single layer result with header, detail string, and component index.
fn write_layer_result(
    name: &str,
    result: &LayerResult,
    writer: &mut dyn Write,
) -> Result<(), OutputError> {
    match result {
        LayerResult::Pass { detail, warnings } => {
            writeln!(writer, "  {name}: PASS -- {detail}")?;
            for warning in warnings {
                writeln!(writer, "    warning: {warning}")?;
            }
        }
        LayerResult::Fail {
            detail,
            component_index,
        } => {
            writeln!(writer, "  {name}: FAIL -- {detail}")?;
            if let Some(idx) = component_index {
                writeln!(writer, "    failing component index: {idx}")?;
            }
        }
        LayerResult::Degraded { reason } => {
            writeln!(writer, "  {name}: DEGRADED -- {reason}")?;
        }
        _ => {
            writeln!(writer, "  {name}: UNKNOWN")?;
        }
    }
    Ok(())
}

/// Fix plan section: scored fixes ordered by layer then impact.
fn write_fix_plan(plan: &FixPlan, writer: &mut dyn Write) -> Result<(), OutputError> {
    writeln!(writer, "=== Fix Plan ===")?;
    if plan.fixes.is_empty() {
        writeln!(writer, "  No fixes needed.")?;
        return Ok(());
    }
    for (i, fix) in plan.fixes.iter().enumerate() {
        write_fix_detail(i, fix, writer)?;
    }
    write_plan_warnings(&plan.warnings, writer)?;
    Ok(())
}

/// Single fix with impact score, command, and cascade note if present.
fn write_fix_detail(index: usize, fix: &Fix, writer: &mut dyn Write) -> Result<(), OutputError> {
    let name = layer_name::short(fix.layer);
    let cmd = commands::render_command(&fix.action);
    let warn = if scoring::needs_warning(fix.impact) {
        " [HIGH IMPACT]"
    } else {
        ""
    };
    writeln!(
        writer,
        "  [{index}] layer={name} impact={}/6{warn}",
        fix.impact
    )?;
    writeln!(writer, "      command: {cmd}")?;
    writeln!(writer, "      description: {}", fix.description)?;
    Ok(())
}

/// Plan-level warnings, one per line.
fn write_plan_warnings(warnings: &[String], writer: &mut dyn Write) -> Result<(), OutputError> {
    if warnings.is_empty() {
        return Ok(());
    }
    writeln!(writer, "  Warnings:")?;
    for warning in warnings {
        writeln!(writer, "    - {warning}")?;
    }
    Ok(())
}

#[cfg(test)]
#[path = "explain_tests.rs"]
mod tests;