feat(server): wire the audit stream into every MCP request - #25
Merged
Conversation
Replace the macro-generated tool dispatch with hand-written call_tool, list_tools and get_tool so that, when auditing is enabled, every tool call produces exactly one audit record — unknown tools, stripped tools and refusals included — persisted before the response is returned. initialize records a session-start event unconditionally. - An AuditCell in the request extensions lets the guard call sites enrich records with verdict, deciding rule, suppressed bug ids and redacted fields; enrichment observes and never changes what is served (client responses are byte-identical with auditing on, off or failing open — tested). - Fail modes are enforced at the wrapper: a failing sink gates further work pre-dispatch, and refusals reuse each tool's existing uniform failure text, never varying with the guard's verdict. Defaults derive from the transport: stdio fails open, http closed_all. - Request params are recorded through a closed key allowlist; free-text values appear as byte lengths only. The API key remains unrepresentable (I12). - --audit-config / BUGWARDEN_AUDIT_CONFIG loads the audit configuration; the guard policy file is digested into every record's policy_hash; http without auditing warns at startup. - New invariant I15 in docs/DESIGN.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wires the audit module into the MCP request path. With
--audit-config/BUGWARDEN_AUDIT_CONFIGset, every tool call produces exactly one audit record — unknown tools, read-only-stripped tools, refusals and protocol errors included — persisted before the response is returned;initializerecords a session-start event unconditionally.What changed
#[tool_handler]is replaced by hand-writtencall_tool,list_toolsandget_tool(verified textually equivalent to the rmcp 2.2 macro expansion, dispatching through the pruned instance router — I13).initializereplicates the default handler's peer bookkeeping and version negotiation, then records the session event.AuditCellin the request extensions lets the existing guard call sites enrich records with verdict (worst-wins merge), deciding rule, suppressed bug ids and redacted fields. Enrichment observes; it never changes what is served.closed_all: every tool;closed_writes_denials: write tools) so an audit outage cannot accumulate unaudited mutations; post-failure refusals reuse each tool's existing uniform failure text and never vary with the guard's verdict. Defaults derive from the transport: stdio →open, http →closed_all; explicit config wins.main.rs: audit config loading, transport-derived fail-mode resolution,policy_hash(sha256 of the policy file bytes) stamped into every record, a prominent warning when http runs without auditing, and connect-info wiring so records carry the remote address.docs/DESIGN.md;examples/audit.tomlwording updated.Adversarial review
An independent adversarial review ran before this PR: line-by-line pass plus 23 gutting mutations, each expected to be killed by the suite. 20 were killed on the spot; the 3 survivors were all missing tests, not production defects, and are fixed here:
list_tools/get_toolwere not test-pinned to the pruned instance router (the unprunedSelf::tool_router()trap survived) → new listing/lookup tests over the duplex transport against read-only anddisabled_toolsservers.policy_hashwas computed inline inmain.rsand unexecuted by tests → extracted topolicy_hash_of, unit-bound to the FIPS 180-2 sha256("abc") vector.suppressed_ids = falsecounts-only knob was unpinned through the wrapper → new wiremock test asserting empty id array with a positive count.Also from review: bug_history now notes scrubbed link ids like the other read tools, and the
closed_writes_denialsdocs were tightened to match the code (suppressed, denied, or refused). All formerly surviving mutations were re-applied and confirmed killed in two independent passes. Accepted review notes: guard denials record outcome classok(the denial lives inguard.verdict, per the schema docs), and quicksearch window drops are not recorded (the accounting lives insidebugwarden-core, which this change does not touch — documented at the site).Tests
223 → 249, covering: one record per call for every routed tool; persisted-before-response; byte-identical client responses with auditing off / on / failing-open; denied ids in the log and never in the envelope (I14 tie-in); fail-closed scopes with an injected failing sink (pre-dispatch gate proven by absent upstream requests); transport-default binding; refusal-map totality over the full router; four-canary content test (API key, summary, comment body, attachment data never reach the file); initialize event identity; allowlist reduction and truncation.
All gates green: fmt, clippy
-D warnings, full test suite, cargo-deny, typos.