Skip to content

security: AST09 governance / audit mitigations (Issue #3)#15

Merged
kellenff merged 1 commit into
mainfrom
issue-3
Jul 6, 2026
Merged

security: AST09 governance / audit mitigations (Issue #3)#15
kellenff merged 1 commit into
mainfrom
issue-3

Conversation

@kellenff

@kellenff kellenff commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #3 — adds application-level audit trail for the MCP server so a downstream host can spot-check what yactt reads, where it rooted, and whether the running binary matches the install hook's TOFU record.

What's in this PR

Four AST09 mitigations, mirroring the layout of the prior AST02 (#1) and AST05 (#2) close-outs:

  1. Structured startup line on stderr (event=startup). Always emitted. Carries the resolved absolute root, MaxFiles cap, loaded file count, grammar set, per-language LSP presence + tool + version, and the running binary's SHA-256. The binary_sha256 field is the runtime anchor for the install-hook trust chain — a downstream host can cross-check it against the published SHA256SUMS for the running release.

  2. Opt-in per-tool audit via --audit-log=<path>. Writes one JSON line per tools/call to the file (mode 0600, append). Each line carries the tool name, every path discovered in the input JSON (via a narrow heuristic that recognises repo/file/scope/file_filter field names plus POSIX/Windows absolute paths), output byte count, wall-clock duration, and is_error. internal/mcp/server.go::dispatch times the handler; the server depends only on a tiny unexported interface so internal/audit stays free of MCP imports.

  3. Install-hook runtime TOFU assertion. cmd/yactt/main.go::warnInstallTrustChain reads ${XDG_DATA_HOME:-~/.local/share}/yactt/known-good (the file install.sh already writes) and compares the recorded (version, sha256) against the running binary's hash. A mismatch writes a WARNING: line on stderr identifying recorded, actual, and version, then continues (a compromised install is surfaced, not silently run). Silent on dev builds, missing TOFU, and version mismatches (legitimate upgrades).

  4. Audit story in docs/security.md — new §6 walks the threat, the four mitigations, what they don't defend against, and the test-coverage anchors. README.md Security section + plugins/yactt/README.md updated to reflect AST09 as mitigated and link the audit-log quickstart.

Files

  • New: internal/audit/audit.go, internal/audit/audit_test.go, cmd/yactt/main_test.go.
  • Modified: cmd/yactt/main.go, internal/mcp/server.go, internal/mcp/server_test.go, docs/security.md, README.md, plugins/yactt/README.md.

Test coverage

  • internal/audit/audit_test.go — path-extraction heuristic (relative paths via field-name allowlist, absolute paths via POSIX/Windows shapes, empty-field filtering, sort-stable order), all four CheckKnownGood status branches (missing / unknown_version / match / mismatch), concurrent emission (Logger is mutex-guarded), file-backed append (NewFileLogger survives repeated opens).
  • internal/mcp/server_test.go — three new tests pinning the dispatch-side contract: one line per tools/call, is_error set on handler error, clean no-op when no auditor is attached.
  • cmd/yactt/main_test.go — startup snapshot shape, runtime TOFU warning branches, binary path resolution.

go vet ./... clean. Full package test suite passes.

Smoke test

Live run on tests/fixtures/sample-go with gopls on PATH:

yactt mcp serve --audit-log=/tmp/audit.log /path/to/sample-go

Stderr (startup line):

{"event":"startup","timestamp":"2026-07-06T06:32:49Z","version":"dev","repo_root":"/…/sample-go","max_files":50000,"loaded_files":4,"grammars":["go","typescript","javascript"],"lsp":[{"language":"go","tool":"gopls","version":"v0.22.0"}, ],"binary_sha256":"6ddda703…"}

Audit log (per-tool lines):

{"event":"tool_call","tool":"get_symbols_overview","input_paths":["auth/login.go"], }
{"event":"tool_call","tool":"find_code","input_paths":["/Users/…/proj","src/auth/**/*.go"], }

Out of scope (deliberate)

  • CLI-supplied WithMaxFiles override is not surfaced in the startup line's max_files field. loadOptions is unexported; lifting it would need either a public Probe() interface on LoadOption or to expose loadOptions. Both are heavier than the value today (no caller sets a non-default cap in production). Marked with a // ponytail: comment in buildStartupInfo naming the upgrade path.
  • The TOFU has no signing key. A compromised release endpoint that ships a matching (binary, sha256) pair is still installable. The runtime check catches same-version replay; the SLSA L3 attestation (gh attestation verify) documented in §2 is the back-line defense for full supply-chain integrity.

Adds application-level audit trail for the MCP server so a downstream
host can spot-check what yactt reads, where it rooted, and whether
the running binary matches the install hook's TOFU record.

- structured startup line on stderr (event=startup): resolved root,
  MaxFiles cap, loaded file count, grammar set, per-language LSP
  presence + version, binary SHA-256.
- opt-in per-tool audit via --audit-log=<path>: one JSON line per
  tools/call with tool name, input paths (extracted from common
  field names + absolute-path heuristic), output bytes, duration,
  is_error.
- runtime TOFU assertion: reads install hook's known-good file at
  XDG_DATA_HOME/yactt/known-good and warns on stderr when the
  running binary's SHA-256 diverges from the recorded hash at the
  same version (silent on version mismatch / missing TOFU / dev
  builds).
- documents the audit story in docs/security.md §6 + README.md
  Security section + plugins/yactt/README.md trust section.
@kellenff
kellenff merged commit b53b4d2 into main Jul 6, 2026
5 checks passed
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.

security: AST09 — Surface resolved root, capabilities, and per-tool access to host for audit

2 participants