whyno-core 0.3.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 seven 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. **SELinux** — mandatory access control (feature-gated)
7. **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`
- **Seven-layer check pipeline** — mount options, fs flags, traversal, DAC, ACL, SELinux, AppArmor
- **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`

## Installation

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

## Quick Start

### Build a SystemState

The check pipeline operates on a `SystemState` struct built from gathered OS data. 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;

let subject = ResolvedSubject {
    uid: 1000,
    gid: 1000,
    groups: vec![33, 44],
    capabilities: Probe::Unknown,
};

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),
    },
];

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 },
}]);

let state = SystemState {
    subject,
    walk,
    mounts,
    operation: Operation::Read,
    mac_state: MacState::default(),
};
```

### Run Checks

```rust
use whyno_core::checks::run_checks;

let report = run_checks(&state);

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

### Generate Fixes

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

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

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

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

## Key Types

### State Types

| Type | Module | Purpose |
| --- | --- | --- |
| `SystemState` | `state` | Complete gathered state — subject, path walk, mounts, operation, MAC state |
| `Probe<T>` | `state` | Tri-state: `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, or Stat |

### Check Types

| Type | Purpose |
| --- | --- |
| `CheckReport` | Aggregated results — `is_allowed()` for quick verdict, `failed_layers()` for diagnostics |
| `LayerResult` | `Pass` (with optional warnings), `Fail` (with detail), or `Degraded` (state unavailable) |
| `CoreLayer` | Mount, FsFlags, Traversal, Dac, Acl |
| `MacLayer` | SeLinux, AppArmor |

### Fix Types

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

## Check Pipeline

`run_checks()` evaluates layers in a fixed order. All layers always run — no short-circuiting.

| Order | Layer | What it checks |
| --- | --- | --- |
| 1 | Mount | `ro` for writes, `noexec` for execute, `nosuid` advisory |
| 2 | FsFlags | `immutable` blocks all writes, `append-only` blocks non-append writes |
| 3 | Traversal | `+x` on every ancestor directory |
| 4 | DAC | uid/gid/mode bits + capability overrides |
| 5 | ACL | POSIX.1e named entries with mask application |
| 6 | SELinux | AVC decision over pre-gathered `mac_state` (`selinux` feature) |
| 7 | AppArmor | Profile mode over pre-gathered `mac_state` (`apparmor` feature) |

## 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 fluent builders for constructing `SystemState` in tests |

## Dependencies

- `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 for layer results

## Minimum Supported Rust Version

**Rust 1.78.** Pure Rust, no platform-specific code — compiles on any target `rustc` supports.