whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Unit tests for `check_selinux`.
//!
//! RED phase: tests compile but fail because `check_selinux` calls the kernel
//! directly instead of reading from `state.mac_state`. Phase 6 will refactor
//! the function to consume `mac_state`, making these tests pass.

use super::*;
use crate::operation::Operation;
use crate::state::mac::{SeLinuxMode, SeLinuxState};
use crate::state::Probe;
use crate::test_helpers::StateBuilder;

// ── operation_to_selinux_perm mapping tests ──────────────────────────────────

// regression: existing mappings must remain unchanged
#[test]
fn read_still_maps_to_file_read() {
    assert_eq!(operation_to_selinux_perm(Operation::Read), ("file", "read"));
}

#[test]
fn write_still_maps_to_file_write() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Write),
        ("file", "write")
    );
}

#[test]
fn execute_still_maps_to_file_execute() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Execute),
        ("file", "execute")
    );
}

#[test]
fn stat_still_maps_to_file_read() {
    assert_eq!(operation_to_selinux_perm(Operation::Stat), ("file", "read"));
}

// new metadata variant mappings
#[test]
fn chmod_maps_to_file_setattr() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Chmod),
        ("file", "setattr")
    );
}

#[test]
fn chown_uid_maps_to_file_setattr() {
    assert_eq!(
        operation_to_selinux_perm(Operation::ChownUid),
        ("file", "setattr")
    );
}

#[test]
fn chown_gid_maps_to_file_setattr() {
    assert_eq!(
        operation_to_selinux_perm(Operation::ChownGid),
        ("file", "setattr")
    );
}

#[test]
fn setxattr_user_maps_to_file_setattr() {
    use crate::operation::XattrNamespace;
    assert_eq!(
        operation_to_selinux_perm(Operation::SetXattr {
            namespace: XattrNamespace::User
        }),
        ("file", "setattr")
    );
}

#[test]
fn setxattr_trusted_maps_to_file_setattr() {
    use crate::operation::XattrNamespace;
    assert_eq!(
        operation_to_selinux_perm(Operation::SetXattr {
            namespace: XattrNamespace::Trusted
        }),
        ("file", "setattr")
    );
}

#[test]
fn setxattr_security_maps_to_file_setattr() {
    use crate::operation::XattrNamespace;
    assert_eq!(
        operation_to_selinux_perm(Operation::SetXattr {
            namespace: XattrNamespace::Security
        }),
        ("file", "setattr")
    );
}

#[test]
fn setxattr_posix_acl_maps_to_file_setattr() {
    use crate::operation::XattrNamespace;
    assert_eq!(
        operation_to_selinux_perm(Operation::SetXattr {
            namespace: XattrNamespace::SystemPosixAcl
        }),
        ("file", "setattr")
    );
}

// ── existing unit tests (preserved from inline mod) ─────────────────────────

#[test]
fn operation_to_selinux_perm_read() {
    assert_eq!(operation_to_selinux_perm(Operation::Read), ("file", "read"));
}

#[test]
fn operation_to_selinux_perm_stat() {
    assert_eq!(operation_to_selinux_perm(Operation::Stat), ("file", "read"));
}

#[test]
fn operation_to_selinux_perm_write() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Write),
        ("file", "write")
    );
}

#[test]
fn operation_to_selinux_perm_create() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Create),
        ("file", "write")
    );
}

#[test]
fn operation_to_selinux_perm_delete() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Delete),
        ("file", "write")
    );
}

#[test]
fn operation_to_selinux_perm_execute() {
    assert_eq!(
        operation_to_selinux_perm(Operation::Execute),
        ("file", "execute")
    );
}

// ── RED: check_selinux state-driven tests ───────────────────────────────────
//
// Each test injects synthetic SeLinuxState via StateBuilder::with_selinux_state.
// All fail at runtime because check_selinux ignores state.mac_state and calls
// the kernel directly, returning Degraded("SELinux not supported…") on this host.

/// Enforcing mode with access granted must pass without warnings.
// RED: check_selinux ignores state.mac_state and calls the kernel directly.
#[test]
fn selinux_enforcing_access_allowed_returns_pass() {
    let state = StateBuilder::new()
        .operation(Operation::Read)
        .with_selinux_state(SeLinuxState {
            mode: SeLinuxMode::Enforcing,
            subject_ctx: "unconfined_u:unconfined_r:unconfined_t:s0".into(),
            target_ctx: "system_u:object_r:httpd_sys_content_t:s0".into(),
            access_allowed: true,
        })
        .component("/", 0, 0, 0o755)
        .component_file("/etc/hosts", 0, 0, 0o644)
        .build();

    let result = check_selinux(&state);
    assert!(
        result.is_pass(),
        "expected Pass for enforcing+allowed, got {result:?}"
    );
    if let LayerResult::Pass { warnings, .. } = &result {
        assert!(
            warnings.is_empty(),
            "no warnings expected when enforcing and allowed"
        );
    }
}

/// Enforcing mode with access denied must produce Fail.
// RED: check_selinux ignores state.mac_state and calls the kernel directly.
#[test]
fn selinux_enforcing_access_denied_returns_fail() {
    let state = StateBuilder::new()
        .operation(Operation::Write)
        .with_selinux_state(SeLinuxState {
            mode: SeLinuxMode::Enforcing,
            subject_ctx: "unconfined_u:unconfined_r:unconfined_t:s0".into(),
            target_ctx: "system_u:object_r:httpd_sys_content_t:s0".into(),
            access_allowed: false,
        })
        .component("/", 0, 0, 0o755)
        .component_file("/var/www/html/index.html", 0, 0, 0o644)
        .build();

    let result = check_selinux(&state);
    assert!(
        result.is_fail(),
        "expected Fail for enforcing+denied, got {result:?}"
    );
}

/// Permissive mode with would-be denial must pass but carry a warning.
// RED: check_selinux ignores state.mac_state and calls the kernel directly.
#[test]
fn selinux_permissive_would_deny_returns_pass_with_warning() {
    let state = StateBuilder::new()
        .operation(Operation::Write)
        .with_selinux_state(SeLinuxState {
            mode: SeLinuxMode::Permissive,
            subject_ctx: "unconfined_u:unconfined_r:unconfined_t:s0".into(),
            target_ctx: "system_u:object_r:httpd_sys_content_t:s0".into(),
            access_allowed: false,
        })
        .component("/", 0, 0, 0o755)
        .component_file("/var/www/html/index.html", 0, 0, 0o644)
        .build();

    let result = check_selinux(&state);
    assert!(
        result.is_pass(),
        "expected Pass (permissive) for permissive+denied, got {result:?}"
    );
    if let LayerResult::Pass { warnings, .. } = &result {
        assert!(
            !warnings.is_empty(),
            "expected at least one warning in permissive mode"
        );
    }
}

/// Disabled mode means `SELinux` is not evaluating policy — report Degraded.
// RED: check_selinux ignores state.mac_state and calls the kernel directly.
#[test]
fn selinux_disabled_returns_degraded() {
    let state = StateBuilder::new()
        .operation(Operation::Read)
        .with_selinux_state(SeLinuxState {
            mode: SeLinuxMode::Disabled,
            subject_ctx: String::new(),
            target_ctx: String::new(),
            access_allowed: false,
        })
        .component("/", 0, 0, 0o755)
        .component_file("/etc/passwd", 0, 0, 0o644)
        .build();

    let result = check_selinux(&state);
    assert!(
        result.is_degraded(),
        "expected Degraded for disabled SELinux, got {result:?}"
    );
}

/// Unknown probe means gathering failed — report Degraded, never false-green.
// RED: check_selinux ignores state.mac_state and calls the kernel directly.
#[test]
fn selinux_probe_unknown_returns_degraded() {
    // StateBuilder defaults mac_state.selinux to Probe::Unknown — no with_selinux_state call.
    let state = StateBuilder::new()
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_file("/etc/shadow", 0, 0, 0o640)
        .build();

    assert_eq!(state.mac_state.selinux, Probe::Unknown);

    let result = check_selinux(&state);
    assert!(
        result.is_degraded(),
        "expected Degraded for unknown probe, got {result:?}"
    );
}

/// Inaccessible probe means EACCES on the gathering syscall — report Degraded.
// RED: check_selinux ignores state.mac_state and calls the kernel directly.
#[test]
fn selinux_probe_inaccessible_returns_degraded() {
    let mut state = StateBuilder::new()
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_file("/etc/shadow", 0, 0, 0o640)
        .build();

    // Directly set inaccessible — no builder method exists for this variant.
    state.mac_state.selinux = Probe::Inaccessible;

    let result = check_selinux(&state);
    assert!(
        result.is_degraded(),
        "expected Degraded for inaccessible probe, got {result:?}"
    );
}