Skip to content

Security dogfood hardening + vxd watch matcher fix + preflight scanner check#111

Merged
tzone85 merged 4 commits into
mainfrom
chore/security-hardening-dogfood
Jul 2, 2026
Merged

Security dogfood hardening + vxd watch matcher fix + preflight scanner check#111
tzone85 merged 4 commits into
mainfrom
chore/security-hardening-dogfood

Conversation

@tzone85

@tzone85 tzone85 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Improvement pass driven by running vxd security scan on vxd itself (dogfooding the new security agent from #109) plus a coverage/bug sweep of what that PR left untested.

1. Dogfood scan hardening (chore)

  • Pinned all 14 GitHub Actions references in ci.yml to full commit SHAs (mutable-tag supply-chain class, CWE-1357), with version comments. All six pins verified against the GitHub API, including the annotated-tag dereference for golangci-lint-action.
  • Annotated all 30 accepted-by-design findings with #nosec / nosemgrep and a one-line rationale each (sampler seed, graceful-shutdown contexts, path-taint sites inside the operator trust boundary, and the 15 exec sites that are the orchestrator's core function — each naming its upstream validation).
  • Result: vxd security scan . reports 0 high+ findings on its own tree, so --min high is usable as a self-gate once CI billing is restored.

2. Bug fix: vxd watch silently dropped every event (fix)

eventMatchesReq compared evt.StoryID[:8] to reqID[:8], but story IDs are namespaced with storyIDPrefix(reqID) — sha256-hashed for every real ULID reqID — so no story event ever matched in production. REQ_* payload routing was promised in a comment but never implemented. Now matched via the newly exported engine.StoryIDPrefix plus payload req_id/id keys (both spellings real emitters use). The old test pinned the broken behavior and was replaced; 8 new tests including an end-to-end tail against real file+sqlite stores.

3. New preflight check: security_scanners (feat)

The security gate degrades gracefully when a scanner binary is missing (skipped, never fatal) — which meant zero operator signal that coverage shrank. CheckSecurityScanners (WARNING tier, check 16) lists missing binaries from the security.KnownScanners registry with install hints. Injectable lookPath matching the package's testability pattern; includes a dangling-wire guard test.

4. Coverage regression restore (test)

internal/cli had regressed 72.9% → 68.0% (PR #109's security commands, dashboard daemon commands, and watch landed untested). Restored to 72.9% with 16 new tests: security scan/kb (including the empty-PATH graceful-degradation path pinning skipped-scanner reporting), dashboard status/stop idempotency + stale/malformed pidfiles, watch matcher + tail.

Review

everything-claude-code:go-reviewer pass: verdict WARN — 1 MEDIUM + 2 LOW, all applied in the final commit (completed the G124 half of the cookie annotation, removed a dead branch, fixed inert test assertions). All SHA pins and suppressions independently verified by the reviewer.

Test plan

  • go build ./... clean, binary at ~/.local/bin/vxd
  • go test ./... -count=1 — all 30 packages pass
  • go vet ./... clean; golangci-lint run — 0 issues
  • vxd security scan . --json — 0 high+ findings
  • vxd preflight live — ✓ Security scanners installed: gitleaks, semgrep, gosec, govulncheck, npm
  • Doc-coverage wiring tests pass (CLAUDE.md/README check counts updated 15 → 16)

tzone85 added 4 commits July 2, 2026 02:29
…notate accepted findings

Ran vxd security scan on vxd itself (346 findings) and closed the high-severity set:

- Pin all 14 GitHub Actions references in ci.yml to full commit SHAs
  (mutable-tag supply-chain class, CWE-1357) with version comments.
- Annotate the 29 accepted-by-design findings with #nosec / nosemgrep and a
  one-line rationale each: sampler seed conversion + math/rand (statistical
  sampling, crypto seed), server shutdown contexts (fresh ctx after parent
  cancel is the graceful-shutdown idiom), G703 path-taint sites (paths derive
  from $HOME/worktrees inside the operator trust boundary), and the 15
  dangerous-exec-command sites that ARE the orchestrator's core function
  (each with its upstream validation named).

vxd security scan . now reports 0 high+ findings on its own tree, so the
scan is usable as a self-gate (--min high) once CI billing is restored.
…t tools

The per-story security gate degrades gracefully when a scanner binary is
absent (skipped, never fatal), which left operators with no signal that scan
coverage was reduced. New CheckSecurityScanners (WARNING tier) lists missing
binaries from the security.KnownScanners registry with install hints
(security.InstallHint) and joins AllChecks — vxd preflight now runs 16 checks.

lookPath is injected for testability, matching CheckBinaryPath's pattern.
4 new tests including a dangling-wire guard (AllChecks must include the check).
Docs updated (CLAUDE.md + README check counts, security-agent section).
…nts + test coverage restore

Two matcher bugs made `vxd watch` a silent no-op tail in production:

1. Story events: eventMatchesReq compared evt.StoryID[:8] against reqID[:8],
   but story IDs are namespaced with storyIDPrefix(reqID) — sha256(reqID)[:8]
   for any reqID longer than 8 chars, which every real ULID reqID is. The
   prefixes never matched, so no story event ever printed. The matcher now
   uses the exported engine.StoryIDPrefix (single source of truth).
2. Requirement events: the code commented 'REQ_* events get routed via payload
   below' but no payload routing existed — REQ_SUBMITTED/PLANNED/COMPLETED/
   BLOCKED never printed. Now matched via the payload req_id/id keys (the two
   keys real emitters use: planner uses 'id', the planning heartbeat 'req_id').

The old TestEventMatchesReq_PrefixMatch pinned the broken raw-prefix behavior
and was replaced (test was wrong, not the spec). New tests: hashed-prefix
match, short-reqID verbatim match, payload routing for both key spellings,
cross-requirement rejection, and an end-to-end tailRequirementEvents run
against real file+sqlite stores that pins print-and-exit-on-terminal.

Also restores the internal/cli coverage regression from PR #109 (68.0% →
72.9%): the security scan/kb commands, dashboard status/stop daemon commands,
and watch were all untested. New: security_test.go (9 tests — pure helpers,
kb text/json, scan with empty PATH pinning graceful degradation + skipped-
scanner reporting), dashboard_daemon_test.go (7 tests — not-running status,
idempotent stop, malformed/stale pidfiles, watch unknown-req error).
…kill dead branch + inert assertions

- auth.go: the cookie site had the nosemgrep half of the annotation but gosec
  G124 still fired; add the #nosec with the same rationale.
- watch.go: drop the unreachable evt.StoryID == prefix branch — the planner
  always emits <prefix>-<suffix> IDs, so HasPrefix covers every real case.
- checks_security_test.go: the installed-scanner negative assertions matched
  a substring ('missing: gitleaks') that could never occur in the real message
  format; assert on the bare scanner names so a regression can actually fire.
@tzone85 tzone85 merged commit 444f9c6 into main Jul 2, 2026
5 checks passed
@tzone85 tzone85 deleted the chore/security-hardening-dogfood branch July 2, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant