whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! `SELinux` mandatory access control check layer.
//!
//! Reads pre-gathered `SELinux` state from `state.mac_state.selinux`. No live
//! kernel calls are made here; gathering happens in `whyno-gather`. Requires
//! `--features selinux`.

use crate::checks::LayerResult;
use crate::operation::Operation;
use crate::state::mac::{SeLinuxMode, SeLinuxState};
use crate::state::{Probe, SystemState};

/// Checks `SELinux` access for the given state.
///
/// Returns `Degraded` when the probe is unknown/inaccessible or `SELinux` is
/// disabled. Returns `Pass` with a warning in permissive mode. Returns `Fail`
/// when enforcing mode denies access.
#[must_use]
pub fn check_selinux(state: &SystemState) -> LayerResult {
    match &state.mac_state.selinux {
        Probe::Unknown => LayerResult::Degraded {
            reason: "SELinux state not gathered".into(),
        },
        Probe::Inaccessible => LayerResult::Degraded {
            reason: "SELinux state inaccessible".into(),
        },
        Probe::Known(sel) => evaluate_selinux_state(sel),
    }
}

fn evaluate_selinux_state(sel: &SeLinuxState) -> LayerResult {
    match sel.mode {
        SeLinuxMode::Disabled => LayerResult::Degraded {
            reason: "SELinux disabled on this system".into(),
        },
        SeLinuxMode::Permissive => build_permissive_result(sel),
        SeLinuxMode::Enforcing => build_enforcing_result(sel),
    }
}

fn build_permissive_result(sel: &SeLinuxState) -> LayerResult {
    let warning = "SELinux is in permissive mode — violations logged but not blocked".to_owned();
    if sel.access_allowed {
        LayerResult::Pass {
            detail: format!(
                "SELinux: permissive mode — access allowed (subject: {}, target: {})",
                sel.subject_ctx, sel.target_ctx
            ),
            warnings: vec![warning],
        }
    } else {
        LayerResult::Pass {
            detail: format!(
                "SELinux: permissive mode — would deny (subject: {}, target: {})",
                sel.subject_ctx, sel.target_ctx
            ),
            warnings: vec![warning],
        }
    }
}

fn build_enforcing_result(sel: &SeLinuxState) -> LayerResult {
    if sel.access_allowed {
        LayerResult::Pass {
            detail: format!(
                "SELinux: enforcing — access allowed (subject: {}, target: {})",
                sel.subject_ctx, sel.target_ctx
            ),
            warnings: vec![],
        }
    } else {
        LayerResult::Fail {
            detail: format!(
                "SELinux: enforcing — access denied (subject: {}, target: {})",
                sel.subject_ctx, sel.target_ctx
            ),
            component_index: None,
        }
    }
}

/// Maps an operation to a `SELinux` object class and permission string.
///
/// Returns `(tclass, perm)` for use with `check_access()`. Canonical mapping
/// used by both the check layer and `whyno-gather`. All operations use the
/// `file` object class; metadata ops map to `setattr`.
// TODO(v0.5): differentiate security.selinux key → relabelfrom/relabelto
#[must_use]
#[allow(clippy::match_same_arms)]
pub fn operation_to_selinux_perm(op: Operation) -> (&'static str, &'static str) {
    match op {
        Operation::Read | Operation::Stat => ("file", "read"),
        Operation::Write | Operation::Create | Operation::Delete => ("file", "write"),
        Operation::Execute => ("file", "execute"),
        Operation::Chmod | Operation::ChownUid | Operation::ChownGid => ("file", "setattr"),
        Operation::SetXattr { .. } => ("file", "setattr"),
    }
}

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