Build Readiness Audit
Five risks in a security scanner, ranked, with the order to fix them
An excerpt from an audit of ThynkQ's own open-source tool. The scanning core was sound. The risk sat in the parts that guard it.
Summary
Every ranked finding was fixed in mcp-scan 2.1.0.
Nothing here needs a rewrite. Typecheck and lint pass, and all 418 tests pass once the project is built. The five risks sit in the controls around the scanner: a log that can leak, a tamper check that can switch itself off, and a release path with no test gate.
Severity is rated for a local, single-user threat model. A scanner that runs on your own machine is not a server, so a denial of service on your own config file ranks low. Estimates assume one engineer who knows the codebase and include tests.
Architecture sketch
- CLI
src/index.tsCommander entry point. 20 command files in src/commands, including scan, proxy, ci, sbom and watch. - Detect and parse
src/configFinds client config files in home and project directories, then reads JSON, JSONC and TOML. - Scanners
src/scannersSixteen modules run per server. Some reach the network: the npm registry and the GitHub API in a scan, OSV in the library path. - Reports
src/utilsTerminal, JSON, HTML, SARIF and SBOM output, plus an optional webhook post.
- Local state
~/.mcp-scanaudit.log, known-servers.json (config fingerprints) and proxy logs: the state it keeps between runs. The fix command also rewrites client configs and keeps backups. - Release path
.github/workflowsci.yml tests on push. publish.yml publishes to npm with provenance when a v* tag lands.
What is already clean
| Check | Result |
|---|---|
| Typecheck | tsc --noEmit exits 0 |
| Lint | eslint exits 0 |
| Tests | 418 of 418 pass after a build |
| Coverage | about 80% lines and 70% branches |
One catch: on a fresh clone npm test fails 5 tests until you build (risk 05). The numbers above are from after a build.
Top five risks, ranked
The proxy writes raw tool traffic to a world-readable file
- Where
src/commands/proxy.ts:129-131src/commands/proxy.ts:112-116src/commands/proxy.ts:43, 52-54
- Observed
- We piped one JSON-RPC message through mcp-scan proxy. The log landed in ~/.mcp-scan/logs with the default file mode and held the whole message. Masking is off unless a policy file turns it on. The catch block at line 52 writes the unmasked line when parsing fails; we read that path but did not trigger a mask failure.
- Evidence
-rw-r--r-- proxy-<timestamp>.log [CLIENT -> SERVER] {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"arguments":{"note":"sample-value"}}}- Why it matters
- Tool arguments often carry tokens and file contents. On a shared machine, other accounts can read the log. On a single-user laptop the risk is low.
- Fix
- Create the log directory 0700 and the file 0600. In the catch path, log a placeholder, never the raw line. Decide whether masking should default to on.
Tamper detection turns itself off when its store is corrupt
- Where
src/utils/audit-logger.ts:64-66src/utils/audit-logger.ts:90-99src/commands/scan.ts:278
- Observed
- We corrupted known-servers.json, then changed a server's package version in the scanned config twice. Neither run raised server-mutation, and the file was still corrupt afterwards. With a healthy store, the same edit raised it.
- Evidence
MEDIUM server-mutation ✓ All clear (1 server scanned in 8ms) { not json- Why it matters
- The check that watches for edited server configs fails silently, and nothing repairs it. A truncated write or a full disk gets the same result as an attacker.
- Fix
- Warn once when the store cannot be read, quarantine the bad file, and rebuild a baseline on the next run.
The publish workflow skips the tests, and the dependency gate cannot fail
- Where
.github/workflows/publish.yml:23-28.github/workflows/ci.yml:32-33package.json:90package.json:28-31src/config/parser.ts:103src/config/paths.ts:102
- Observed
- A v* tag runs npm ci, build and publish with no test step and no dependency on ci.yml. The CI audit step fails only on critical. npm audit on the runtime tree reports 5 high and 3 moderate packages. smol-toml is a direct dependency pinned at 1.6.1 by the lockfile, inside two advisory ranges (GHSA-7w5x-hrqm-74c2, GHSA-r4xh-jqrq-34v2), and it parses .codex/config.toml from the scanned directory. The lodash override pins 4.17.21, inside its advisory range. We did not reproduce an exploit; the impact is a slow or stuck scan, not code execution. In fairness, the GitLab CI pipeline ran lint, build, test and audit on tags, and the lodash advisory was a documented accepted risk.
- Evidence
8 vulnerabilities (3 moderate, 5 high)
- Why it matters
- A scanner that people run on untrusted project folders should not hang on a hostile file, and a release should not depend on someone remembering to wait for CI.
- Fix
- Add a test job the publish job needs, fail the audit step at high for runtime dependencies, bump smol-toml (a fix is published), and put a review date on the accepted lodash risk. The braces advisory has no published fix yet, so record it as accepted with an owner and a review date.
Upstream releases look like tampering
- Where
src/utils/audit-logger.ts:102-112src/utils/audit-logger.ts:72
- Observed
- The config fingerprint includes the resolved package version. We scanned an unchanged config offline, then online, and four official servers raised a MEDIUM that says the change may indicate unauthorized modification.
- Evidence
Server 'filesystem' configuration has changed since the last scan. This may indicate unauthorized modification. 0 critical 0 high 5 medium 0 low
- Why it matters
- Any unpinned npx server will trip this each time its publisher ships. Alarms that cry wolf get switched off, which feeds the risk above.
- Fix
- Fingerprint the config the user wrote. Report a changed resolved version as an INFO line, not a tamper warning.
npm test fails on a fresh clone until you build
- Where
package.json:36tests/commands/golden.test.ts:9tests/commands/ci.test.ts:5.github/workflows/ci.yml:28-29
- Observed
- Five CLI tests run dist/index.js, but the test script does not build it. CI hides this by running the build first.
- Evidence
Tests 5 failed | 413 passed (418) Tests 418 passed (418)
- Why it matters
- A new contributor sees red on a clean checkout and has to guess why.
- Fix
- Add a pretest script that builds, or have the five tests build once in a global setup.
Fix-first plan
| Order | Fix | Closes | Weeks | Why this order |
|---|---|---|---|---|
| 1 | Release and dependency gates | Risk 03 | 0.25 | Cheapest change with the widest reach: every install passes through the release path. |
| 2 | Proxy log modes and fail-closed masking | Risk 01 | 0.25 | The only finding that can leak data. |
| 3 | Corrupt-store warning and rebuild | Risk 02 | 0.25 | Restores a feature that is silently off today. |
| 4 | Fingerprint the config, not the resolved version | Risk 04 | 0.25 | Needs the store from step 3 to be dependable first. |
| 5 | Build before test, quieter snapshot warning | Risk 05 | 0.1 | Housekeeping once the above has landed. |
Total: about 1.1 weeks for one engineer.
Also noted
- The stale-snapshot warning prints once per server, and tells installed users to run an npm script that only exists in the repository (src/scanners/package-scanner.ts:419).
What you keep if you don't build with us: the full report and fix plan
The audit is a deliverable you pay for: $7,500, 1 week. It comes with:
- A ranked list: what is broken, what it costs to fix, and in what order
- Plain notes on the big technical choices, with the options we weighed
- Security and performance problems found, with what to fix first
- A short summary you can share with partners or investors
This excerpt shows the ranked risks with severity and file evidence, and the order to fix them. It leaves out the architecture decision notes and the executive summary; the architecture sketch above is orientation, not those notes. A web product also gets a performance pass; a command-line tool has no page to measure.
Our IP terms say deliverables transfer to you on receipt of full payment. The standard split is 50/50 on everything up to $59,999. So once the fee is paid, the report and the plan are yours to hand to your own team or another vendor.
The only thing that depends on building with us is the fee credit: 100% of the fee credited toward Launch Build or Product Build if signed within 60 days. Nothing in our published terms makes the report conditional on a build.