whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! `AppArmor` mandatory access control check layer.
//!
//! Reads pre-gathered `AppArmor` state from `state.mac_state.apparmor`. No live
//! procfs or securityfs calls are made here; gathering happens in
//! `whyno-gather`. Requires `--features apparmor`.
//!
//! Limitation: without libapparmor linkage, enforce-mode profiles cannot have
//! their access decision queried — the check reports `Degraded` in that case.

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

/// `AppArmor` profile enforcement mode parsed from the kernel label.
pub(crate) enum ProfileMode {
    Enforce,
    Complain,
    Unknown,
}

/// Checks `AppArmor` status for the given state.
///
/// Returns `Degraded` when the probe is unknown/inaccessible or when the
/// profile is in enforce mode without libapparmor support for policy query.
/// Metadata ops in enforce mode return `Degraded` with a capability hint.
/// Returns `Pass` with warnings for complain mode and unconfined subjects.
pub fn check_apparmor(state: &SystemState) -> LayerResult {
    match &state.mac_state.apparmor {
        Probe::Unknown => LayerResult::Degraded {
            reason: "AppArmor state not gathered".into(),
        },
        Probe::Inaccessible => LayerResult::Degraded {
            reason: "AppArmor state inaccessible".into(),
        },
        Probe::Known(aa) => evaluate_apparmor_state(aa, state.operation),
    }
}

fn evaluate_apparmor_state(aa: &AppArmorState, operation: Operation) -> LayerResult {
    let label = &aa.profile_label;

    if label.starts_with("unconfined") {
        return LayerResult::Pass {
            detail: format!("AppArmor: subject is unconfined ({label})"),
            warnings: vec![],
        };
    }

    let (profile_name, mode) = parse_profile_label(label);

    match mode {
        ProfileMode::Enforce => build_enforce_result(profile_name, operation),
        ProfileMode::Complain => LayerResult::Pass {
            detail: format!("AppArmor: profile '{profile_name}' in complain mode"),
            warnings: vec![
                "AppArmor is in complain mode — violations logged but not blocked".into(),
                "deny rules in this profile are still enforced even in complain mode".into(),
            ],
        },
        ProfileMode::Unknown => LayerResult::Degraded {
            reason: format!("AppArmor: cannot determine mode for profile '{profile_name}'"),
        },
    }
}

// metadata ops get a capability hint; others get a libapparmor rebuild hint
fn build_enforce_result(profile_name: &str, operation: Operation) -> LayerResult {
    if operation.is_metadata() {
        return LayerResult::Degraded {
            reason: format!(
                "AppArmor: profile '{profile_name}' in enforce mode — metadata ops require \
                 capability rules (chown/fowner/sys_admin) which cannot be queried \
                 without libapparmor",
            ),
        };
    }
    // enforcement confirmed but without libapparmor we cannot query the
    // actual access decision against policy rules. report degraded to
    // avoid false-green. rebuild with --features apparmor (libapparmor)
    // for full policy query via aa_query_file_path().
    LayerResult::Degraded {
        reason: format!(
            "AppArmor: profile '{profile_name}' in enforce mode — \
             rebuild with libapparmor support for full policy query"
        ),
    }
}

/// Parses an `AppArmor` label into profile name and mode.
///
/// Label format: `"nginx (enforce)"`, `"nginx (complain)"`, or `"nginx"`.
/// Returns the name slice and the parsed mode.
pub(crate) fn parse_profile_label(label: &str) -> (&str, ProfileMode) {
    if let Some(pos) = label.rfind(" (") {
        let name = &label[..pos];
        let suffix = &label[pos + 2..];
        let mode = if suffix.starts_with("enforce") {
            ProfileMode::Enforce
        } else if suffix.starts_with("complain") {
            ProfileMode::Complain
        } else {
            ProfileMode::Unknown
        };
        (name, mode)
    } else {
        (label, ProfileMode::Unknown)
    }
}

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