Skip to content

feat(audit): PATH shadowing + system package audit - #37

Merged
DanFlannel merged 4 commits into
mainfrom
feat/phase4
Aug 18, 2026
Merged

feat(audit): PATH shadowing + system package audit#37
DanFlannel merged 4 commits into
mainfrom
feat/phase4

Conversation

@DanFlannel

@DanFlannel DanFlannel commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Phases A, B, and C of the system-audit roadmap (#32, #33, #34). Together they answer: is the binary that executes the one that was verified, and what else is installed on this machine that nobody is auditing?

PATH shadowing detection (#32)

A verified install means nothing if a different binary earlier on PATH is what actually executes. doctor --audit now resolves every managed tool on PATH and reports:

  • shadowed-binary (critical): PATH resolves the tool name to a binary other than the one scuta verified. Message names both paths.
  • bin-dir-not-in-path (warning, machine-level): scuta's bin directory is not on PATH at all, so installed tools are not runnable by name.

system package audit framework + go adapter (#33)

Opt-in doctor --audit --system extends the audit beyond scuta-managed tools to system package managers, via a small Manager interface (lib/managers). Undetected managers are listed explicitly rather than omitted.

  • go install: reads debug/buildinfo from each binary in the go bin dir (zero exec). Flags source-checkout builds (unpinned-build), missing module sums (no-integrity-data), and dirty VCS trees (dirty-build).

brew, mise, and dpkg adapters (#34)

  • brew: reads Cellar INSTALL_RECEIPT.json directly, never shells out. Flags third-party taps (third-party-source) and source builds (unpinned-build). Homebrew keeps no per-file manifest, so integrity is honestly reported as not-verifiable.
  • mise: inventories $MISE_DATA_DIR/installs, skipping symlinked partial-version aliases. Checksums live in per-project mise.lock, so integrity is not-verifiable machine-wide.
  • dpkg: counts packages from /var/lib/dpkg/status (summarized, not listed), runs a single dpkg --verify pass. Non-conffile checksum mismatches are binary-drift (critical): the first --system findings that can gate CI, deliberately, since a modified package binary is exactly what this audit exists to catch. Conffile changes are config-drift (info). Findings capped at 25 with an overflow summary.

Design

  • PathEnv injects PATH/PATHEXT/GOOS/Stat/EvalSymlinks, so windows and linux semantics are both tested from any host. Lookup mirrors exec.LookPath: first match wins, exec-bit check on POSIX, PATHEXT on windows (bare names don't match), symlink-resolved and case-insensitive-on-windows comparison
  • Every manager adapter is exec-free except dpkg's single --verify call (exit code tolerated as data, not failure); all OS touchpoints are injectable for tests
  • Integrity is reported in three honest states: recorded, not-verifiable, unknown. Managers with no integrity data are never passed silently
  • Report schema stays v1: all new fields (Tool.effective_path/shadowed, Report.system, Summary.system_packages) are additive omitempty

Testing

  • 35 new tests across lib/audit and lib/managers: shadowing semantics on both platforms, gobin buildinfo findings, brew receipt parsing (verified against a real Cellar receipt), mise symlink skipping, dpkg verify parsing (drift vs conffile severity, cap + overflow)
  • go test -race ./... green (29 pkgs), darwin + windows/amd64 builds, golangci-lint 0 issues, all 8 CI checks green
  • Verified live: full --audit --system run (go/brew/mise sections, dpkg not-detected on macOS) and an end-to-end PATH shadowing exercise (critical fires with exit 1; symlink to verified binary correctly not flagged)

Also updates README (commands, Security section, roadmap) and docs/SECURITY.md.

Closes #32
Closes #33
Closes #34

A verified install means nothing if a different binary earlier on PATH is
what actually executes. doctor --audit now resolves every managed tool on
PATH (exec.LookPath semantics, PATHEXT-aware on windows, symlink-resolved,
case-insensitive compare on windows) and reports:

- shadowed-binary (critical): PATH resolves the tool name to a binary other
  than the one scuta verified
- bin-dir-not-in-path (warning): scuta's bin directory is not on PATH, so
  installed tools are not runnable by name

Report schema stays v1: new Tool fields effective_path and shadowed are
additive and omitempty. PathEnv injects PATH/PATHEXT/GOOS/Stat/EvalSymlinks
so both platforms are tested from any host.

Closes #32
…apter

doctor --audit --system extends the audit beyond scuta-managed tools: one
adapter per package manager, each answering where a package came from and
whether its integrity can be verified. Adapters report only what the
manager can prove — no integrity data means IntegrityNotVerifiable, never
a silent pass. Undetected managers are still listed so the report is
explicit about what was and was not checked.

First adapter: go install. Reads toolchain-embedded build metadata
(debug/buildinfo, no commands executed) and reports module origin,
version, and sum, flagging source-checkout builds (unpinned-build, info),
missing module sums (no-integrity-data, info), and dirty VCS working
trees (dirty-build, warning). No criticals: --system never gates CI yet.

Report schema stays v1: system section and summary count are additive
omitempty fields.

Closes #33
Extends --system audit with three new manager adapters:

- brew: reads Cellar INSTALL_RECEIPT.json directly (no shelling out).
  Flags third-party taps (third-party-source) and source builds
  (unpinned-build). Homebrew keeps no per-file manifest, so integrity
  is honestly reported as not-verifiable.
- mise: inventories $MISE_DATA_DIR/installs, skipping symlinked
  partial-version aliases. Checksums live in per-project mise.lock,
  so integrity is not-verifiable machine-wide.
- dpkg: counts packages from /var/lib/dpkg/status (summarized, not
  listed), runs a single 'dpkg --verify' pass. Non-conffile checksum
  mismatches are reported as binary-drift at critical severity: these
  are the first --system findings that can gate CI, deliberately, since
  a modified package binary is exactly what this audit exists to catch.
  Conffile changes are config-drift (info). Findings capped at 25 with
  an overflow summary.

New finding codes: third-party-source, config-drift,
inventory-summarized. Report schema stays v1 (all fields omitempty).

Closes #34
lookPath joined candidate paths with filepath.Join, which uses the host
separator. On a Windows runner that produced backslash paths, so the
injected-GOOS test environments (keyed with forward slashes) never
matched and all five cross-platform shadowing tests failed there.

Join with a forward slash instead, trimming trailing separators per the
injected GOOS. Every supported platform accepts forward slashes, so the
real check is unaffected; lookups now depend only on the injected GOOS,
not the machine running them.
@DanFlannel DanFlannel changed the title feat(audit): PATH shadowing detection (Phase A) feat(audit): PATH shadowing + system package audit (Phases A-C) Aug 17, 2026
@DanFlannel DanFlannel changed the title feat(audit): PATH shadowing + system package audit (Phases A-C) feat(audit): PATH shadowing + system package audit Aug 18, 2026
@DanFlannel
DanFlannel merged commit ca8b4b2 into main Aug 18, 2026
8 checks passed
@DanFlannel
DanFlannel deleted the feat/phase4 branch August 18, 2026 14:47
@DanFlannel
DanFlannel restored the feat/phase4 branch August 18, 2026 15:07
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.

Phase C: package manager adapters (brew, mise, dpkg) Phase B: system audit framework + Go binaries adapter Phase A: PATH shadowing detection

1 participant