whyno-cli 0.3.0

Linux permission debugger
**whyno** is a Linux permission debugger. It answers the question: *"Why can't this user do this to this file?"*

Given a subject (user, UID, PID, or service), an operation (read, write, execute, delete, create, stat), and a filesystem path, whyno checks every permission layer from mount options down to POSIX ACLs — then tells you exactly what's blocking and how to fix it with the least-privilege change.

## Installation

whyno ships as a **single static binary** (`x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl`, `armv7-unknown-linux-musleabihf`, `riscv64gc-unknown-linux-musl`). No runtime dependencies.

```bash
# Build from source (x86_64)
cargo build --release --target x86_64-unknown-linux-musl

# Build for aarch64 (requires cross)
cross build --release --target aarch64-unknown-linux-musl

# Build all release targets via justfile
just release

# Optional: install CAP_DAC_READ_SEARCH for full coverage without sudo
sudo whyno caps install
```

## Privilege tiers

| Tier | Setup | Coverage |
| --- | --- | --- |
| **Unprivileged** | None | Partial — limited to paths the running user can traverse |
| **Self-install caps** | `sudo whyno caps install` (once) | Full — `CAP_DAC_READ_SEARCH` granted via raw `setxattr()`, zero external deps |
| **sudo** | `sudo whyno ...` per invocation | Full |

In unprivileged mode, inaccessible checks are marked `[SKIP]` (never false-green). A one-time hint suggests elevated options.

## Usage

```bash
whyno <subject> <operation> <path> [flags]
```

### Subject formats

| Format | Example | Resolution |
| --- | --- | --- |
| Bare username | `whyno nginx read /path` | `/etc/passwd``/etc/group` |
| Bare number | `whyno 33 read /path` | UID lookup in `/etc/passwd` |
| `user:` prefix | `whyno user:nginx read /path` | Explicit username |
| `uid:` prefix | `whyno uid:33 read /path` | Explicit UID |
| `pid:` prefix | `whyno pid:1234 read /path` | `/proc/<pid>/status` |
| `svc:` prefix | `whyno svc:postgres read /path` | systemd → MainPID → `/proc` |

### Operations

| Operation | Checks | Notes |
| --- | --- | --- |
| `read` | `r` on target | File contents or directory listing |
| `write` | `w` on target | Modify, truncate |
| `execute` | `x` on target | Run binary or traverse directory |
| `delete` | `w+x` on **parent** | Redirects check to parent directory |
| `create` | `w+x` on **parent** | Redirects check to parent directory |
| `stat` | Traverse only | "Can I see this exists?" — no file perm needed |

### Flags

- `--json` — structured JSON output (versioned schema, CI-friendly)
- `--explain` — verbose resolution chain with per-component raw data
- `--no-color` — disable ANSI color (also respects `NO_COLOR` env var)
- `--with-cap <CAP>` — inject a capability for hypothetical queries; repeatable. Accepted names: `CAP_CHOWN`, `CAP_DAC_OVERRIDE`, `CAP_DAC_READ_SEARCH`, `CAP_FOWNER`, `CAP_LINUX_IMMUTABLE`
- `--self-test` — cross-check whyno's result against kernel `faccessat2(AT_EACCESS)`; valid only when subject is the calling user and kernel >= 5.8; mismatches reported to stderr; runs automatically in debug builds
- `--json` and `--explain` are **mutually exclusive**

## Example Output

```bash
whyno nginx read /var/log/app/current.log

  Subject: uid=33, gid=33, groups=[33]
  Operation: read
  Target: /var/log/app/current.log

  [PASS] Mount options      — rw on /var (ext4)
  [PASS] Filesystem flags   — no immutable/append-only
  [FAIL] Path traversal     — /var/log/app: o-x (other has no execute)
         Fix: chmod o+x /var/log/app  [impact: 3/6]
  [FAIL] DAC permissions    — mode 0640 owner=root group=root, nginx is other
         Fix: setfacl -m u:nginx:r /var/log/app/current.log  [impact: 1/6]
  [SKIP] POSIX ACLs         — no ACL on target (would pass after DAC fix)
  [SKIP] SELinux            — SELinux state not gathered
  [SKIP] AppArmor           — AppArmor state not gathered
```

For `pid:N` and `svc:` subjects with readable capabilities, the subject line includes `caps=0x...` (e.g. `caps=0x0000000000000000`).

## Permission Layers Checked (v0.3)

All layers run unconditionally — no short-circuiting. This ensures the fix engine sees the full picture.

| Order | Layer | What it checks |
| --- | --- | --- |
| 1 | **Mount options** | `ro`, `noexec`, `nosuid` via `statvfs()` |
| 2 | **Filesystem flags** | `immutable`, `append-only` via `ioctl(FS_IOC_GETFLAGS)` |
| 3 | **Path traversal** | `+x` on every ancestor directory from `/` to target |
| 4 | **DAC permissions** | Owner/group/other `rwx` mode bits + supplementary groups |
| 5 | **POSIX ACLs** | Named user/group entries with mask application per POSIX.1e |
| 6 | **SELinux** | Pure function over `state.mac_state`; when state is not gathered, reports `[SKIP] SELinux state not gathered` |
| 7 | **AppArmor** | Pure function over `state.mac_state`; when state is not gathered, reports `[SKIP] AppArmor state not gathered` |

## Not checked in v0.3

systemd sandboxing directives, seccomp BPF filters, full user namespace UID/GID mapping, per-domain SELinux permissive state (system-wide only). MAC layers (SELinux, AppArmor) run as pure functions over gathered state — when the gather step cannot read MAC state (e.g. securityfs not mounted, or SELinux disabled), the layer degrades to `[SKIP]` with a descriptive message.

## Fix Suggestions

Fixes are ranked by **security impact score** (1 = least privilege, 6 = highest risk):

| Score | Fix class | Example |
| --- | --- | --- |
| 1 | ACL grant (specific user) | `setfacl -m u:nginx:r file` |
| 2 | Group change / ACL group grant | `chown :www-data file` |
| 3 | Permission bit (group) | `chmod g+r file` |
| 4 | Permission bit (other) | `chmod o+r file` |
| 5 | Remove filesystem flag | `chattr -i file` |
| 6 | Remount filesystem | `mount -o remount,rw /var` |

Fixes with score ≥ 5 include a `⚠` warning. `chmod 777` and `o+rwx` are **never** suggested.

When multiple layers block, an ordered **fix plan** is generated (outermost layer first). Cascade simulation re-runs checks after each hypothetical fix to prune redundant suggestions.

## Exit Codes

| Code | Meaning |
| --- | --- |
| `0` | All layers pass — operation allowed |
| `1` | At least one layer blocks — operation denied |
| `2` | Internal error — couldn't complete checks |

Same codes apply to `--json` mode. Degraded layers (unprivileged mode) do not force a non-zero exit.

## JSON Schema

`whyno schema` prints the auto-generated JSON Schema for `--json` output (doc comments are lowercased per schemars convention). Use to validate consumer tooling against the current schema contract.

## Capability Management

```bash
sudo whyno caps install    # Set CAP_DAC_READ_SEARCH on the binary
sudo whyno caps uninstall  # Remove the capability
whyno caps check           # Verify current state
```

Uses raw `setxattr()` / `getxattr()` / `removexattr()` syscalls with VFS cap v2 format (20 bytes). No `libcap` or `setcap` dependency. Filesystem must support extended attributes (ext4, xfs, btrfs — yes; NFS, FAT — no).