feat: protected-binary security primitives (repocfg schema + hook deny) #56

Merged
coilysiren merged 2 commits from feat/security-config-schema into main 2026-06-03 22:10:25 +00:00
Owner

What

The cli-guard half of the ward security-doctor / protected-binary work (coilyco-flight-deck/ward#4). Two generic primitives a consumer assembles into a doctor security surface and a hook deny.

Commits

  • feat(repocfg): add optional security: config section (closes #54) — declarative security: block: protected_binaries, sudo.forbid_passwordless, hooks.{deny_bare_binaries,route_hints}. Parsed and validated, enforces nothing. Old commands-only configs unchanged.
  • feat(hook): basename-aware protected-binary deny (closes #55) — hook.Protected denies a protected binary on the bare token, any absolute-path spelling, and after env/sudo strip. Closes the absolute-path bypass a token-keyed Route leaves open (the 2026-05-08 deny-uv finding). Trailing variadic param, so existing PreToolUse callers are unchanged. Exports Basename.

Why this shape

Per the repo boundary rule, both packages stay importable without consumer-specific defaults: the schema names fields but bakes in no policy, and the hook takes protected entries as data. The downstream consumer (ward, then kap) supplies the actual protected set.

Validation

go build ./..., go test ./repocfg/ ./hook/, go vet, and godoc-current.txt regen all green. New tests cover the schema (full/empty/reject-path/reject-mode/reject-dup) and the hook deny (bare, absolute path, env/sudo prefix, piped, substring non-match).

Not in this PR

ward-side wiring (ward doctor security, hook denies, WARD_CONFIG) and the generic doctor host-checks (PATH-shim, sudo posture, credential-env) land separately. ward also needs its cli-guard dependency migrated off the old github.com/coilysiren/cli-guard module path first.

## What The cli-guard half of the ward security-doctor / protected-binary work ([coilyco-flight-deck/ward#4](https://forgejo.coilysiren.me/coilyco-flight-deck/ward/issues/4)). Two generic primitives a consumer assembles into a `doctor security` surface and a hook deny. ## Commits - **`feat(repocfg): add optional security: config section`** (closes #54) — declarative `security:` block: `protected_binaries`, `sudo.forbid_passwordless`, `hooks.{deny_bare_binaries,route_hints}`. Parsed and validated, enforces nothing. Old commands-only configs unchanged. - **`feat(hook): basename-aware protected-binary deny`** (closes #55) — `hook.Protected` denies a protected binary on the bare token, **any absolute-path spelling**, and after `env`/`sudo` strip. Closes the absolute-path bypass a token-keyed `Route` leaves open (the 2026-05-08 deny-uv finding). Trailing variadic param, so existing `PreToolUse` callers are unchanged. Exports `Basename`. ## Why this shape Per the repo boundary rule, both packages stay importable without consumer-specific defaults: the schema names fields but bakes in no policy, and the hook takes protected entries as data. The downstream consumer (ward, then kap) supplies the actual protected set. ## Validation `go build ./...`, `go test ./repocfg/ ./hook/`, `go vet`, and `godoc-current.txt` regen all green. New tests cover the schema (full/empty/reject-path/reject-mode/reject-dup) and the hook deny (bare, absolute path, env/sudo prefix, piped, substring non-match). ## Not in this PR ward-side wiring (`ward doctor security`, hook denies, `WARD_CONFIG`) and the generic doctor host-checks (PATH-shim, sudo posture, credential-env) land separately. ward also needs its cli-guard dependency migrated off the old `github.com/coilysiren/cli-guard` module path first.
Adds a declarative security: block to the per-repo config: protected_binaries
(name, mode, allowed_wrappers, expected_real_paths, credential_env),
sudo.forbid_passwordless, and hooks.{deny_bare_binaries,route_hints}. repocfg
parses and validates it (basename-only names, deny-direct mode, no dupes) and
enforces nothing on its own. Existing commands: behavior unchanged; a config
with no security: yields a zero Security.

Dependency root for the ward security-doctor / protected-binary work
(coilyco-flight-deck/ward#4).

closes #54

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(hook): basename-aware protected-binary deny
Some checks failed
ci / test (push) Has been cancelled
ci / lint (push) Has been cancelled
ci / govulncheck (push) Has been cancelled
ci / docs (push) Has been cancelled
codeql / analyze (go) (push) Has been cancelled
pages / build (push) Has been cancelled
dependabot-auto-merge / automerge (pull_request_target) Has been cancelled
ci / test (pull_request) Has been cancelled
ci / lint (pull_request) Has been cancelled
ci / govulncheck (pull_request) Has been cancelled
ci / docs (pull_request) Has been cancelled
codeql / analyze (go) (pull_request) Has been cancelled
pages / deploy (push) Has been cancelled
e102d72a01
Adds hook.Protected and a basename-aware deny in the PreToolUse engine: a
configured protected binary (gcloud, clusterctl) is denied on the bare token,
any absolute-path spelling (/opt/homebrew/bin/gcloud), and after env/sudo
strip. Closes the absolute-path bypass a token-keyed Route leaves open (the
2026-05-08 deny-uv finding). protected is a trailing variadic param on
PreToolUse so existing callers are unchanged. Exports Basename; interpreterName
now reuses it.

Part of the ward security-doctor / protected-binary work
(coilyco-flight-deck/ward#4).

closes #55

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.