whyno-cli 0.1.4

cli for whyno
**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`). No runtime dependencies.

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

# 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)
- `--json` and `--explain` are **mutually exclusive**

---

## Example Output

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

  Subject: nginx (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)
```

---

## Permission Layers Checked (v0.1)

All 5 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` from `/proc/self/mountinfo`        |
| 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 |

### Not checked in v0.1

SELinux, AppArmor, Linux capabilities, systemd sandboxing, user namespaces, seccomp. When SELinux or AppArmor is detected, a stderr warning is printed.

---

## Fix Suggestions

Fixes are ranked by **security impact score** (1 = least privilege, 6 = broadest blast radius):

| 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.

---

## 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).