Commit Graph

1 Commits

Author SHA1 Message Date
Dragan Spiridonov 76453115a8 fix(security): fail closed on unvalidated security scan --depth/--type/--target (#2866)
`security scan` validated none of its three input flags, so a typo was
indistinguishable from a clean result:

- An unknown --type matched none of the three phase guards, so no phase ran at
  all and the command still printed "No security issues found!" and exited 0.
  The guards are case-sensitive, so `--type CODE` hit this too.
- An unknown --depth fell through the chained ternaries to the shallowest
  traversal, so `--depth full` scanned *less* than the default `standard`
  (3 levels vs 5). On a tree whose only HIGH finding sat below that budget it
  also flipped the critical/high exit-code gate from 1 to 0 — the gate silently
  stopped gating.
- A non-existent or non-directory --target read nothing, and the swallowed
  dir-read catches turned that into zero findings, the clean banner, exit 0, and
  a PERSISTED clean report that getSecurityStatus reports as CLEAN. mkdirSync
  with recursive:true even created the tree to hold it. --target names what gets
  scanned and is the likelier typo of the three.

All three now fail closed before anything is printed, scanned or written.

`--depth full` is deprecated, NOT rejected. It was never a supported depth, but
the CLI itself emitted that exact string in five places — the statusline insight,
the announcement, the release-notes blurb, the CLAUDE.md that `init` generates,
and two shipped agent definitions — across 21 tracked files. Hard-rejecting it
would break the invocations we told users to run, and would strand the statusline
insight in a loop: it fires on scan status PENDING, tells the user to run
`--depth full`, which would exit 1 and never write a report, leaving status
PENDING forever. It is normalised to `deep` (the depth the word promised) with a
warning, and the emitters we control now say `--depth deep`.

Also in this change:

- Chained ternaries replaced with exhaustive Record<ScanDepth, number> maps
  behind real type predicates rather than `as` casts. The casts mattered: an
  invalid key yields undefined, and `undefined <= 0` is false while
  `undefined - 1` is NaN, so the recursion guard would never fire and the depth
  limiter would silently disable itself. The guard now positive-tests its budget
  (`!(depthLimit > 0)`) so a bad value stops traversal instead of unbounding it.
- `container` was advertised in --type's help since this command was written but
  no phase implements it, so it validated fine, scanned nothing and reported
  clean. Rejected explicitly, case-insensitively so `--type Container` gets the
  accurate "not implemented" message rather than being called a misspelling.
  Breaking for pipelines passing it; noted in CHANGELOG.
- The report path is built as scan-${scanType}-${depth}.json, so an unvalidated
  --type could shape it: `--type ../../../escaped` wrote
  <target>/.claude/escaped-deep.json. Validating the enums closes it.

Tests: 20 cases, black-box against the built CLI. Beyond the rejection paths they
pin the parts a previous revision of this suite left unconstrained — a level-6
fixture so `deep` is distinguishable from `standard` (a mutant cutting deep from
10 to 4 passed the earlier suite and now fails three tests), stdout/stderr kept
separate so a stdout-redirecting caller is provably not left with an empty
document and no diagnostic, and the --target and deprecation paths.
security-scan-persistence, security-audit, security-verification,
issue-2670-security-defend, fs-secure, funnel and funnel-messages-seed-pool all
pass. tsc error count is 17 with and without the patch, none in security.ts.

Note for anyone re-verifying: bin/cli.js loads dist/, tsconfig has
composite: true, and the build cache lives at dist/tsconfig.tsbuildinfo — an
incremental `npx tsc` will not re-emit after an out-of-band dist edit.

Reported privately via SECURITY.md by an external reporter, with a reproduction
and a proposed patch.

Co-Authored-By: Ruflo & AQE
2026-07-30 08:39:44 -04:00