whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Fix engine for generating, scoring, and simulating repair suggestions.
//!
//! Takes a [`CheckReport`] and [`SystemState`], produces an ordered
//! [`FixPlan`] with least-privilege suggestions for each failed layer.

pub mod cascade;
pub mod commands;
mod generators;
pub mod scoring;

use std::path::PathBuf;

use serde::Serialize;

use crate::checks::{CheckReport, CoreLayer, LayerResult};
use crate::operation::MetadataParams;
use crate::state::SystemState;

/// Single fix suggestion with impact score and explanation.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct Fix {
    /// Check layer this fix addresses.
    pub layer: CoreLayer,
    /// Structured action to perform.
    pub action: FixAction,
    /// Impact score: 1 (least privilege) to 6 (broadest impact).
    pub impact: u8,
    /// Human-readable description of what this fix does.
    pub description: String,
}

/// Structured fix action, rendered to shell command at output time.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[non_exhaustive]
pub enum FixAction {
    /// Change file mode bits.
    Chmod {
        /// Target path.
        path: PathBuf,
        /// Mode change string (e.g., "o+x", "g+r").
        mode_change: String,
    },
    /// Change file ownership.
    Chown {
        /// Target path.
        path: PathBuf,
        /// New owner UID, if changing.
        owner: Option<u32>,
        /// New group GID, if changing.
        group: Option<u32>,
    },
    /// Set a POSIX ACL entry.
    SetAcl {
        /// Target path.
        path: PathBuf,
        /// ACL entry string (e.g., "u:33:r").
        entry: String,
    },
    /// Remount filesystem with different options.
    Remount {
        /// Mountpoint path.
        mountpoint: PathBuf,
        /// New mount options (e.g., "rw", "exec").
        options: String,
    },
    /// Change filesystem inode flags.
    Chattr {
        /// Target path.
        path: PathBuf,
        /// Flag change string (e.g., "-i", "-a").
        flags: String,
    },
    /// Grant a Linux capability to a process via file capabilities.
    ///
    /// When `path` is `None`, renders as a descriptive recommendation rather
    /// than a concrete `setcap` command. Pass `--executable` to make it actionable.
    /// Impact 5 (`cap_fowner`, `cap_chown`) or 6 (`cap_sys_admin`).
    GrantCap {
        /// Executable path. `None` when the process binary is unknown.
        path: Option<PathBuf>,
        /// Capability name, e.g. `"cap_fowner"`, `"cap_chown"`, `"cap_sys_admin"`.
        capability: String,
    },
}

/// Ordered fix plan with optional warnings for high-impact changes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FixPlan {
    /// Fixes ordered by layer position, then impact ascending.
    pub fixes: Vec<Fix>,
    /// Warnings for high-impact or cascading fixes.
    pub warnings: Vec<String>,
}

/// Generates fix plan from check failures.
///
/// Collects fixes for each failed layer, orders by layer position
/// then impact ascending, runs cascade simulation to prune redundant
/// fixes, returns final plan.
///
/// `params` carries caller-supplied intent for metadata operations. Only
/// the metadata layer generator reads it to tailor `Chmod`/`Chown` fix
/// targets; all other generators ignore it. Pass `MetadataParams::default()`
/// for non-metadata operations.
#[must_use]
pub fn generate_fixes(
    report: &CheckReport,
    state: &SystemState,
    params: &MetadataParams,
) -> FixPlan {
    let mut fixes = Vec::new();
    let mut warnings = Vec::new();

    collect_mount_fixes(report, state, &mut fixes, &mut warnings);
    collect_fsflags_fixes(report, state, &mut fixes, &mut warnings);
    collect_traversal_fixes(report, state, &mut fixes, &mut warnings);
    collect_dac_fixes(report, state, &mut fixes, &mut warnings);
    collect_acl_fixes(report, state, &mut fixes, &mut warnings);
    collect_metadata_fixes(report, state, params, &mut fixes, &mut warnings);

    cascade::simulate_cascade(fixes, warnings, state)
}

fn collect_mount_fixes(
    report: &CheckReport,
    state: &SystemState,
    fixes: &mut Vec<Fix>,
    warnings: &mut Vec<String>,
) {
    if let LayerResult::Fail { .. } = &report.core_results[CoreLayer::Mount] {
        let new_fixes = generators::mount_fixes(state);
        for fix in &new_fixes {
            if scoring::needs_warning(fix.impact) {
                warnings.push(format!("{}: affects entire filesystem", fix.description));
            }
        }
        fixes.extend(new_fixes);
    }
}

fn collect_fsflags_fixes(
    report: &CheckReport,
    state: &SystemState,
    fixes: &mut Vec<Fix>,
    warnings: &mut Vec<String>,
) {
    if let LayerResult::Fail { .. } = &report.core_results[CoreLayer::FsFlags] {
        let new_fixes = generators::fsflags_fixes(state);
        for fix in &new_fixes {
            if scoring::needs_warning(fix.impact) {
                warnings.push(format!(
                    "{}: removes filesystem protection",
                    fix.description
                ));
            }
        }
        fixes.extend(new_fixes);
    }
}

fn collect_traversal_fixes(
    report: &CheckReport,
    state: &SystemState,
    fixes: &mut Vec<Fix>,
    _warnings: &mut [String],
) {
    if let LayerResult::Fail {
        component_index: Some(idx),
        ..
    } = &report.core_results[CoreLayer::Traversal]
    {
        fixes.extend(generators::traversal_fixes(state, *idx));
    }
}

fn collect_dac_fixes(
    report: &CheckReport,
    state: &SystemState,
    fixes: &mut Vec<Fix>,
    _warnings: &mut [String],
) {
    if let LayerResult::Fail { .. } = &report.core_results[CoreLayer::Dac] {
        fixes.extend(generators::dac_fixes(state));
    }
}

fn collect_acl_fixes(
    report: &CheckReport,
    state: &SystemState,
    fixes: &mut Vec<Fix>,
    _warnings: &mut [String],
) {
    if let LayerResult::Fail { .. } = &report.core_results[CoreLayer::Acl] {
        fixes.extend(generators::acl_fixes(state));
    }
}

// emits high-impact warning for cap_sys_admin (impact 6) suggestions
fn collect_metadata_fixes(
    report: &CheckReport,
    state: &SystemState,
    params: &MetadataParams,
    fixes: &mut Vec<Fix>,
    warnings: &mut Vec<String>,
) {
    if let LayerResult::Fail { .. } = &report.core_results[CoreLayer::Metadata] {
        let new_fixes = generators::metadata_fixes(state, params);
        for fix in &new_fixes {
            if scoring::needs_warning(fix.impact) {
                warnings.push(format!(
                    "{}: high-privilege capability grant",
                    fix.description
                ));
            }
        }
        fixes.extend(new_fixes);
    }
}

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

#[cfg(test)]
#[path = "mod_scenario_tests.rs"]
mod scenario_tests;

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

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

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