scout — Security Model & Assurance Case¶
Status: Living document. Last full review: 2026-09-11.
Owner: Sebastien Rousseau (@sebastienrousseau).
Scope: the scout binary, the library packages it is built on, the
release pipeline that produces its artefacts, and the on-disk state it
keeps (the token store and any report directory).
This document is scout's assurance case: a structured argument that the project is secure to a stated level, with the evidence that backs each claim. It is deliberately narrower and more explicit than a marketing-style "security policy". Reviewers, packagers, and downstream users should be able to read this document and understand what scout protects, what it does not protect, what could go wrong, and what compensating controls exist.
1. What scout is¶
scout is a Go command-line tool that connects to a remote Model Context Protocol server the way an agent would — with credentials an operator supplies — and reports, step by step, what it observed. It makes real requests: OAuth discovery and token exchange, the MCP handshake, catalog listing, tool invocations, resource reads, prompt renders, repeated and parallel calls.
It is:
- A client of the server under test. It never listens on the network
except for the loopback redirect that
scout loginopens for the duration of one authorization. - A holder of credentials for the duration of a run, and of a user
token between runs when
scout loginwas used. - A writer of reports that describe a server in detail and can, with
--capture-bodies, include what it returned.
2. Trust boundaries¶
scout operates across four trust boundaries:
| # | Boundary | Direction | What crosses it |
|---|---|---|---|
| 1 | Operator's shell → scout process |
in | CLI flags, SCOUT_* environment, the config file, the token store |
| 2 | scout → authorization server |
out | metadata fetches, registration, token requests carrying client secrets |
| 3 | scout → MCP server |
out/in | JSON-RPC over HTTPS with a bearer token; every byte of the reply is untrusted |
| 4 | scout → report directory and stdout |
out | findings, catalog, telemetry, optionally captured bodies |
The threat model is about limiting blast radius: scout runs with credentials the operator chose against servers the operator chose. It cannot make a server safe; it can make sure that running it does not leak the credential, does not damage the server's data, and does not let the server's output reach the report unbounded.
3. Security properties (claims)¶
We claim the following properties. Each is followed by the evidence.
C1. No credential reaches the report, the telemetry, or the logs¶
Argument. Every HTTP exchange passes through one recorder
(internal/telemetry), and every operator-supplied secret is registered
with its redactor before the client is built. The redactor masks by
header name, by URL and form parameter name, and structurally inside JSON
bodies, where it also registers the values it finds — so a token issued in
the middle of a run is masked in every later event. Content types are not
trusted; a body that starts with { is treated as JSON. The stderr
banner and the report's credential summary print the source of each
credential (flag, environment variable, profile, store), never its value.
Evidence.
probe.RuncallsRecorder.Redactor.Addfor every value increds.Credentials.Secrets()before constructing the client.internal/telemetry/recorder_test.goasserts thatAuthorization,X-Api-KeyandSet-Cookieare masked, that a registered secret in a request body is masked, thatcode=in a query string is masked, and that the HAR export contains no secret.TestFullRunClientCredentialsininternal/probeserialises every recorded event after a full nine-phase run and fails if the operator's client secret, the dynamically registered client secret or any issued access token appears.internal/creds/creds_test.goassertsDescribe()leaks no secret.- The token store is written
0600in a0700directory, atomically (internal/creds/store.go).
C2. Unauthenticated probes are actually unauthenticated¶
Argument. The client's transport chain adds the operator's bearer token, fixed headers and basic credentials to every request. Two checks — first contact, and the invalid-token rejection probe — only mean something if they arrive with none of those. A second transport to the same endpoint, carrying nothing but the trace header, exists for them.
Evidence. Session.Bare in internal/probe/probe.go;
TestServerAcceptingGarbageTokenIsCritical fails a server that answers
200 to a made-up token even when the operator supplied a valid one.
ADR-0001.
C3. scout does not mutate the server under test unless told to¶
Argument. diagnostics.Policy decides what may be invoked. By
default only tools declaring readOnlyHint: true run. A tool without
annotations is destructive under the MCP specification's defaults and is
skipped. --allow-mutations unlocks tools that declare
destructiveHint: false; --allow-destructive unlocks everything and is
documented as dangerous. Resources are read and prompts rendered, both
of which the specification defines as non-mutating. Requests are throttled
to --rps (default 2) including the parallel burst, unless --allow-load
is passed.
Evidence. TestPolicyDefaults in diagnostics; the fake server in
internal/probe/fake_test.go panics if its unannotated delete_all tool
is called, and every probe test runs under the default policy.
ADR-0004.
C4. Server output cannot flood or corrupt the report¶
Argument. Every string the server chooses — tool names, descriptions,
error text, content — is bounded before it enters a finding, and response
bodies are capped at 1 MiB on the raw transport and at BodyCap (64 KiB)
in captured telemetry. Findings never embed server text unescaped into
the Markdown renderer's table cells.
Evidence. truncate in internal/probe; io.LimitReader in
transport.Streamable.Do; Recorder.BodyCap; esc in
internal/report/render_md.go.
C5. The release artefacts you download are the artefacts we built¶
Argument. Every release is built by a GitHub-hosted runner from a semantic-version tag, signed keylessly with cosign (Sigstore), and accompanied by a SLSA build provenance attestation and a CycloneDX SBOM, all published together to the same GitHub Release.
Evidence. .github/workflows/release.yml and .goreleaser.yaml;
every action is SHA-pinned per OpenSSF Scorecard Pinned-Dependencies.
C6. The parsing boundaries are fuzzed¶
Argument. Every byte of a WWW-Authenticate header, an SSE stream or a
tool's JSON Schema comes from the server under test. The parsers for
each are fuzz targets run on every push.
Evidence. FuzzParseWWWAuthenticate (auth), FuzzReadSSE
(transport), FuzzValidate and FuzzArguments (diagnostics),
driven by scripts/fuzz.sh and .github/workflows/fuzz.yml.
4. Threats considered and out of scope¶
In scope¶
- Credential in a report shared with a third party: mitigated by C1.
--capture-bodiesis off by default because a server's own response content is not scout's to redact. - A server that accepts any token looking healthy: mitigated by C2; reported as a critical finding.
- Running scout against production and deleting something: mitigated by C3.
- Hostile server output (oversized bodies, injected Markdown, control characters in tool names): mitigated by C4.
- Supply chain against release: mitigated by SHA-pinned actions, cosign and SLSA (C5).
- Dependency compromise: mitigated by Dependabot on
go.mod,govulncheckin CI, and a two-dependency module;go.sumlocks transitive hashes.
Out of scope¶
- A server that logs the credentials scout sends. Boundary 2 and 3 are the operator's choice; scout cannot prevent the far side from recording what it was sent.
- Compromise of the maintainer's laptop or GitHub account. The maintainer's account is the root of trust; if it is compromised, a malicious release could be signed and shipped. Sigstore's Rekor transparency log makes such a release publicly auditable after the fact but does not prevent it. Users concerned about this scenario should pin to a specific release tag and checksum.
- A malicious config file or token store on the operator's machine. Both are read with the operator's own permissions; an attacker who can write them can already run commands as the operator.
- Denial of service against the server under test. The throttle and
the bounded burst are there to avoid it by accident;
--allow-loadremoves the throttle on purpose and is the operator's decision. - Rate-limit exhaustion of the operator's own quota on a shared authorization server.
5. Assumptions¶
- The credentials the operator supplies are scoped to the server under
test. scout requests the scope the server's challenge names, or what
--scopesays, and nothing broader. - The operator's clock is roughly correct (needed for TLS validation and token expiry arithmetic).
- The operator runs a supported OS: recent Linux, macOS ≥ 14, or Windows 11.
- A report directory is treated with the same care as the server's own
responses when
--capture-bodiesis on.
6. Compensating controls (bus factor + solo maintainer)¶
scout has a single maintainer. This is a real risk to sustained security response. Mitigations:
- Public assurance case (this doc): a successor maintainer or reviewing packager can pick up where the current maintainer left off without back-channel context.
- Documented signing key location:
MAINTAINERS.mdrecords where the release-signing key is published; a successor can publish a new key and users can reason about the transition. - Documented external services:
MAINTAINERS.mdcatalogues every external account (organisation, ghcr.io, Homebrew tap, AUR) so continuity is auditable rather than tribal. - Fork-and-continue is explicit: GPL-3.0 licensing + the six-month
unresponsive-maintainer clause in
GOVERNANCE.mdnormalise the community-fork path.
7. Review and update¶
This document is re-reviewed on every release that touches:
- Any file in
internal/telemetry/,internal/creds/,auth/ortransport/. - Any change to
diagnostics.Policy. - Any file in
.github/workflows/. - Any new dependency.
Otherwise it is re-reviewed annually.
If you have questions or believe a claim above is not adequately supported by the linked evidence, please file a security advisory per SECURITY.md.