whyno-core 0.4.0

Permission check pipeline, fix engine, and state types
Documentation
**Permission check pipeline, fix engine, and state types for Linux filesystem permissions.**

`whyno-core` is the pure-logic engine behind the [whyno](https://gnu.foo/projects/whyno) permission debugger. It takes a pre-gathered snapshot of filesystem state and deterministically evaluates whether an operation is allowed — with zero I/O at check time. When a check fails, it generates least-privilege fix suggestions ranked by impact.

## Overview

`whyno-core` answers one question: **"Why can't this user do this to that file?"**

Given a `SystemState` (subject identity, path walk, mount table, MAC state, and the requested operation), the check pipeline evaluates eight layers of Linux permission enforcement:

1. **Mount** — read-only, `noexec`, `nosuid` filesystem options
2. **FsFlags** — inode flags (`immutable`, `append-only`)
3. **Traversal**`+x` on every ancestor directory
4. **DAC** — discretionary access control (uid/gid/mode bits + capability overrides)
5. **ACL** — POSIX.1e access control lists with mask evaluation
6. **Metadata** — ownership and capability checks for `chmod`, `chown`, `setxattr`
7. **SELinux** — mandatory access control (feature-gated)
8. **AppArmor** — mandatory access control (feature-gated)

Each layer returns `Pass`, `Fail`, or `Degraded` (state unavailable). The fix engine then generates repair suggestions for every failed layer, simulates their cascading effects, and prunes redundant fixes.

## Features

- **Pure logic, zero I/O** — all filesystem state is gathered externally and passed in via `SystemState`
- **Eight-layer check pipeline** — mount options, fs flags, traversal, DAC, ACL, metadata, SELinux, AppArmor
- **Metadata operations**`chmod`, `chown` (UID/GID), `setxattr` with per-namespace capability rules
- **Capability-aware DAC** — models `CAP_DAC_OVERRIDE`, `CAP_DAC_READ_SEARCH`, `CAP_FOWNER`, and more
- **POSIX.1e ACL evaluation** — full mask interaction, named user/group entries, effective permission computation
- **Fix engine** — generates `chmod`, `chown`, `setfacl`, `mount -o remount`, and `chattr` suggestions
- **Impact scoring** — fixes ranked 1 (least privilege) to 6 (broadest impact)
- **Cascade simulation** — applies fixes to cloned state, prunes cross-layer redundancies
- **Serializable** — all types derive `Serialize` for JSON output
- **JSON Schema** — all public types derive `JsonSchema` via `schemars`, so consumers can generate OpenAPI-compatible schemas from Rust types

## Installation

Add `whyno-core` to your project:

```bash
cargo add whyno-core
```

Or add it manually to your `Cargo.toml`:

```toml
[dependencies]
whyno-core = "0.4"
```

## Quick Start

### Build a SystemState

The check pipeline operates on a `SystemState` struct that you build from gathered OS data. This is the only input — the pipeline performs no I/O.

```rust
use whyno_core::state::{
    SystemState, Probe,
    subject::ResolvedSubject,
    path::{PathComponent, StatResult, FileType},
    mount::{MountTable, MountEntry, MountOptions},
    mac::MacState,
};
use whyno_core::operation::Operation;

// 1. Identify who is performing the operation
let subject = ResolvedSubject {
    uid: 1000,
    gid: 1000,
    groups: vec![33, 44],             // supplementary groups
    capabilities: Probe::Unknown,      // no process context available
};

// 2. Walk the path from / to the target, gathering stat + ACL + flags
let walk = vec![
    PathComponent {
        path: "/".into(),
        stat: Probe::Known(StatResult {
            mode: 0o755, uid: 0, gid: 0,
            dev: 1, nlink: 2, file_type: FileType::Directory,
        }),
        acl: Probe::Unknown,
        flags: Probe::Unknown,
        mount: Some(0),
    },
    PathComponent {
        path: "/var/log/app.log".into(),
        stat: Probe::Known(StatResult {
            mode: 0o640, uid: 0, gid: 44,
            dev: 1, nlink: 1, file_type: FileType::Regular,
        }),
        acl: Probe::Unknown,
        flags: Probe::Unknown,
        mount: Some(0),
    },
];

// 3. Mount table from /proc/self/mountinfo + statvfs()
let mounts = MountTable(vec![MountEntry {
    mount_id: 1,
    device: 1,
    mountpoint: "/".into(),
    fs_type: "ext4".into(),
    options: MountOptions {
        read_only: false,
        noexec: false,
        nosuid: false,
    },
}]);

// 4. Assemble the full state
let state = SystemState {
    subject,
    walk,
    mounts,
    operation: Operation::Read,
    mac_state: MacState::default(), // SELinux + AppArmor unknown
};
```

### Run Checks

Pass the assembled state into the check pipeline:

```rust
use whyno_core::checks::{run_checks, MetadataParams};

// For non-metadata operations, use MetadataParams::default()
let params = MetadataParams::default();
let report = run_checks(&state, &params);

if report.is_allowed() {
    println!("Access allowed");
} else {
    for layer in report.failed_layers() {
        println!("Blocked by: {layer:?}");
    }
}
```

The `CheckReport` contains per-layer results (`Pass`, `Fail`, or `Degraded`) for all eight layers. Use `is_allowed()` for a quick verdict or inspect individual layers for detailed diagnostics.

### Generate Fixes

When checks fail, generate fix suggestions ranked by impact:

```rust
use whyno_core::fix::generate_fixes;

let plan = generate_fixes(&report, &state, &params);

for fix in &plan.fixes {
    println!(
        "[impact {}] {:?} — {}",
        fix.impact, fix.action, fix.description
    );
}

for warning in &plan.warnings {
    println!("⚠ {warning}");
}
```

The fix engine produces structured `FixAction` variants (`Chmod`, `Chown`, `SetAcl`, `Remount`, `Chattr`, `GrantCap`) that can be rendered to shell commands at output time. The cascade simulator prunes fixes that become redundant after an earlier fix resolves a later layer's failure.

### Metadata Operations

For metadata-change operations (`chmod`, `chown`, `setxattr`), use the dedicated `Operation` variants and supply a `MetadataParams`:

```rust
use whyno_core::operation::{Operation, MetadataParams, XattrNamespace};
use whyno_core::checks::run_checks;

// Check whether the subject can chmod the target file
let state = SystemState {
    operation: Operation::Chmod,
    ..state // reuse subject, walk, mounts, mac_state from above
};
let params = MetadataParams::default();
let report = run_checks(&state, &params);

// For chown with a specific target GID, populate MetadataParams
let state = SystemState {
    operation: Operation::ChownGid,
    ..state
};
let params = MetadataParams {
    new_gid: Some(33), // target group
    ..MetadataParams::default()
};
let report = run_checks(&state, &params);
```

Metadata operations bypass the DAC and ACL layers (which return `Pass` immediately) and are evaluated by the dedicated Metadata layer using ownership and capability rules.

## Key Types

### State Types

| **Type** | **Module** | **Purpose** |
| --- | --- | --- |
| `SystemState` | `state` | Complete gathered state for a permission query — subject, path walk, mounts, operation, MAC state |
| `Probe<T>` | `state` | Tri-state wrapper: `Known(T)`, `Unknown`, or `Inaccessible` — never produces false-green results |
| `ResolvedSubject` | `state::subject` | UID, GID, supplementary groups, and effective capability bitmask |
| `PathComponent` | `state::path` | Single path segment with stat, ACL, fs-flags, and mount reference |
| `MountTable` | `state::mount` | Collection of mount entries with device-based lookup |
| `PosixAcl` | `state::acl` | POSIX.1e ACL with mask application and effective permission helpers |
| `MacState` | `state::mac` | SELinux and AppArmor probe results |
| `Operation` | `operation` | Read, Write, Execute, Delete, Create, Stat, Chmod, ChownUid, ChownGid, or SetXattr — determines which checks to run |
| `MetadataParams` | `operation` | Caller-supplied intent for metadata ops — target mode, UID, GID. Use `Default::default()` for non-metadata operations |
| `XattrNamespace` | `operation` | User, Trusted, Security, or SystemPosixAcl — determines capability requirements for `SetXattr` |

### Check Types

| **Type** | **Purpose** |
| --- | --- |
| `CheckReport` | Aggregated results from all check layers — `is_allowed()` for quick verdict, `failed_layers()` for diagnostics |
| `LayerResult` | `Pass` (with optional warnings), `Fail` (with detail and component index), or `Degraded` (state unavailable) |
| `CoreLayer` | Enum identifying core layers: Mount, FsFlags, Traversal, Dac, Acl, Metadata |
| `MacLayer` | Enum identifying MAC layers: SeLinux, AppArmor |

### Fix Types

| **Type** | **Purpose** |
| --- | --- |
| `FixPlan` | Ordered fix list with warnings for high-impact changes |
| `Fix` | Single suggestion: target layer, structured action, impact score (1–6), and description |
| `FixAction` | `Chmod`, `Chown`, `SetAcl`, `Remount`, `Chattr`, or `GrantCap` — renderable to shell commands |

## Check Pipeline

The `run_checks()` function evaluates layers in a fixed order. Each layer is a pure function that takes `&SystemState` (and `&MetadataParams` for the metadata layer) and returns a `LayerResult`.

| **Order** | **Layer** | **What it checks** | **Blocks on** |
| --- | --- | --- | --- |
| 1 | Mount | Filesystem mount options | `ro` for writes, `noexec` for execute, `nosuid` advisory |
| 2 | FsFlags | Inode flags via `FS_IOC_GETFLAGS` | `immutable` blocks all writes, `append-only` blocks non-append writes |
| 3 | Traversal | `+x` on every ancestor directory | Missing execute on any component in the path walk |
| 4 | DAC | uid/gid/mode bits + capabilities | Missing permission bits (after `CAP_DAC_OVERRIDE` etc.) |
| 5 | ACL | POSIX.1e access control list | ACL denies access after mask application |
| 6 | Metadata | Ownership + capability rules | `chmod` without ownership/`CAP_FOWNER`, `chown` without `CAP_CHOWN`, `setxattr` without required cap |
| 7 | SELinux | Mandatory access control | AVC denial (requires `selinux` feature) |
| 8 | AppArmor | Mandatory access control | Profile denial (requires `apparmor` feature) |

The DAC layer automatically applies capability overrides. If the subject has `CAP_DAC_OVERRIDE` (or `uid == 0` when capabilities are unknown), read/write checks on regular files pass regardless of mode bits. `CAP_DAC_READ_SEARCH` grants read access to files and search access to directories.

Metadata operations (`Chmod`, `ChownUid`, `ChownGid`, `SetXattr`) bypass the DAC and ACL layers entirely — they are governed by ownership and capability rules in the dedicated Metadata layer. The metadata check implements kernel `setattr_prepare` semantics:

- **chmod** — file owner or `CAP_FOWNER`
- **chown UID** — requires `CAP_CHOWN`
- **chown GID**`CAP_CHOWN`, or file owner who is a member of the target group
- **setxattr user/posix_acl** — file owner or `CAP_FOWNER`
- **setxattr trusted/security** — requires `CAP_SYS_ADMIN`

## Feature Flags

| **Feature** | **Default** | **Effect** |
| --- | --- | --- |
| `selinux` | off | Enables SELinux check layer — without it, SELinux results are always `Degraded` |
| `apparmor` | off | Enables AppArmor check layer — without it, AppArmor results are always `Degraded` |
| `test-helpers` | off | Exposes `test_helpers` module with builders for constructing `SystemState` in tests |

Enable MAC layers when building for systems that use them:

```bash
cargo add whyno-core --features selinux,apparmor
```

## Dependencies

`whyno-core` is intentionally lightweight — no system dependencies, no I/O crates:

- `serde` + `serde_json` — serialization for all public types
- `schemars``JsonSchema` derive for schema generation
- `thiserror` — structured error types
- `enum-map` — fixed-size enum-keyed maps (used for layer results)

## Minimum Supported Rust Version

The MSRV is **Rust 1.78** (enforced in CI). `whyno-core` is a pure Rust crate with no platform-specific code — it compiles on any target `rustc` supports.