mcp-rce-guard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/audit/log.d.ts +75 -0
- package/dist/audit/log.d.ts.map +1 -0
- package/dist/audit/log.js +191 -0
- package/dist/audit/log.js.map +1 -0
- package/dist/canary/tracker.d.ts +38 -0
- package/dist/canary/tracker.d.ts.map +1 -0
- package/dist/canary/tracker.js +40 -0
- package/dist/canary/tracker.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/cve/replay.d.ts +44 -0
- package/dist/cve/replay.d.ts.map +1 -0
- package/dist/cve/replay.js +221 -0
- package/dist/cve/replay.js.map +1 -0
- package/dist/egress/policy.d.ts +27 -0
- package/dist/egress/policy.d.ts.map +1 -0
- package/dist/egress/policy.js +62 -0
- package/dist/egress/policy.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/isolation/cgroups.d.ts +20 -0
- package/dist/isolation/cgroups.d.ts.map +1 -0
- package/dist/isolation/cgroups.js +33 -0
- package/dist/isolation/cgroups.js.map +1 -0
- package/dist/isolation/landlock.d.ts +42 -0
- package/dist/isolation/landlock.d.ts.map +1 -0
- package/dist/isolation/landlock.js +67 -0
- package/dist/isolation/landlock.js.map +1 -0
- package/dist/isolation/platform.d.ts +27 -0
- package/dist/isolation/platform.d.ts.map +1 -0
- package/dist/isolation/platform.js +96 -0
- package/dist/isolation/platform.js.map +1 -0
- package/dist/isolation/sandbox-exec.d.ts +17 -0
- package/dist/isolation/sandbox-exec.d.ts.map +1 -0
- package/dist/isolation/sandbox-exec.js +58 -0
- package/dist/isolation/sandbox-exec.js.map +1 -0
- package/dist/normalize.d.ts +32 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +66 -0
- package/dist/normalize.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/server.js.map +1 -0
- package/dist/state.d.ts +34 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +104 -0
- package/dist/state.js.map +1 -0
- package/dist/tools/audit.d.ts +26 -0
- package/dist/tools/audit.d.ts.map +1 -0
- package/dist/tools/audit.js +97 -0
- package/dist/tools/audit.js.map +1 -0
- package/dist/tools/getAuditLog.d.ts +34 -0
- package/dist/tools/getAuditLog.d.ts.map +1 -0
- package/dist/tools/getAuditLog.js +65 -0
- package/dist/tools/getAuditLog.js.map +1 -0
- package/dist/tools/injectEgress.d.ts +21 -0
- package/dist/tools/injectEgress.d.ts.map +1 -0
- package/dist/tools/injectEgress.js +49 -0
- package/dist/tools/injectEgress.js.map +1 -0
- package/dist/tools/register.d.ts +16 -0
- package/dist/tools/register.d.ts.map +1 -0
- package/dist/tools/register.js +44 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/scanCve.d.ts +14 -0
- package/dist/tools/scanCve.d.ts.map +1 -0
- package/dist/tools/scanCve.js +29 -0
- package/dist/tools/scanCve.js.map +1 -0
- package/dist/tools/trackCanary.d.ts +23 -0
- package/dist/tools/trackCanary.d.ts.map +1 -0
- package/dist/tools/trackCanary.js +44 -0
- package/dist/tools/trackCanary.js.map +1 -0
- package/dist/trust-tiers.d.ts +18 -0
- package/dist/trust-tiers.d.ts.map +1 -0
- package/dist/trust-tiers.js +73 -0
- package/dist/trust-tiers.js.map +1 -0
- package/dist/types.d.ts +187 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +82 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +14 -0
- package/dist/version.js.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthias Meyer (StudioMeyer)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# mcp-rce-guard
|
|
2
|
+
|
|
3
|
+
Policy-synthesis + behavioral CVE-replay + canary-tracking library for MCP servers. Foundation Pillar 9.
|
|
4
|
+
|
|
5
|
+
## v0.1 Scope: Policy-Synthesizer, Not Kernel-Level Sandbox
|
|
6
|
+
|
|
7
|
+
**Be precise about what this is and is not.** v0.1 is a building-block library that synthesizes isolation **policy descriptors** (landlock JSON profiles, sandbox-exec `.sb` scheme files, cgroups-v2 spec maps) and provides **behavioral predicates** that scan subprocess commands for known RCE-vulnerability shapes. It does **not** apply those policies to a running process. The host spawning the subprocess is responsible for translating a descriptor into the platform-specific syscall (Linux landlock, macOS sandbox-exec, cgroups-v2 fs write).
|
|
8
|
+
|
|
9
|
+
**Native enforcement** (NAPI-RS landlock binding, sandbox-exec execa wrap, cgroups-v2 file writes with cleanup-on-exit, cross-platform integration tests) is the **v0.2 tranche** and is not bundled with v0.1. Layered defense relies on the host wiring policy emission to actual enforcement; this library does the first half cleanly.
|
|
10
|
+
|
|
11
|
+
Use this library if you want:
|
|
12
|
+
- A typed, validated way to describe what an MCP subprocess is allowed to read, write, spawn, and talk to.
|
|
13
|
+
- A reproducible scanner for known RCE-vulnerability classes in subprocess commands (MCP-SDK-RCE-2026-04-22, CVE-2026-27124, Nginx-MCP RCE 9.8) plus 5 shell-injection + 3 fullwidth-unicode payload patterns from the simulate_attacker_input corpus.
|
|
14
|
+
- An append-only NDJSON audit log of every isolation decision. Verified tamper-evident signing (Acra-pattern key derivation + rotation + integrated verifier) is on the v0.2 roadmap; v0.1 ships the log unsigned and treats signing as a v0.2 deliverable.
|
|
15
|
+
|
|
16
|
+
Do **not** use v0.1 if you need a sandbox that actually contains a hostile subprocess at the kernel boundary. For that, the v0.1 descriptor needs to be paired with an enforcement helper. v0.2 ships that helper.
|
|
17
|
+
|
|
18
|
+
## What it does (v0.1)
|
|
19
|
+
|
|
20
|
+
- **Process isolation policy synthesis** — emits landlock (Linux >=5.13) policy descriptors, sandbox-exec (macOS) Scheme profiles, cgroups-v2 specs (memory.max, pids.max, cpu.max). Descriptors only; no syscalls are made.
|
|
21
|
+
- **Network egress allowlist** — default-deny policy with wildcard / exact / suffix / port:* matching. Descriptors only; no nftables / packet-filter integration.
|
|
22
|
+
- **CVE replay suite** — behavioral predicates for known MCP-server RCE vectors. Not exploit payloads — predicates that scan a target command for the vulnerable shape.
|
|
23
|
+
- **Cross-server canary tokens** — issue tokens, scan downstream stdout / fs-write / network-egress streams for leaks (MCPHunt arXiv 2604.27819 pattern).
|
|
24
|
+
- **NDJSON append-only audit log** — every tool call appended at `$MCP_RCE_GUARD_HOME/audit.log`. 100MB rotation with max 10 backups. v0.1 ships unsigned (no in-process verifier); v0.2 adds Acra-pattern HMAC chain with key derivation, rotation safety and an integrated verifier.
|
|
25
|
+
- **NFKC + zero-width strip + Bidi-block** normalization shared with Pillar 8 (mcp-stdio-shellguard).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g mcp-rce-guard
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or run via npx:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y mcp-rce-guard serve
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tools (MCP, Spec 2025-06-18)
|
|
40
|
+
|
|
41
|
+
| Tool | Purpose | readOnly | destructive |
|
|
42
|
+
|------|---------|----------|-------------|
|
|
43
|
+
| `register_subprocess` | Register a child MCP server with isolation profile, get back a handle + fingerprint + policy descriptor | yes | no |
|
|
44
|
+
| `audit_subprocess` | Verify requested args match registered allowlist (post-NFKC, smell-test for invisibles) | yes | no |
|
|
45
|
+
| `scan_cve_replay` | Run behavioral predicates against a target server command | yes | no |
|
|
46
|
+
| `track_canary` | Issue a canary token + scan downstream servers for leaks | yes | no |
|
|
47
|
+
| `inject_egress_policy` | Emit network-egress allowlist descriptor for a registered subprocess (v0.1 descriptor-only) | yes | no |
|
|
48
|
+
| `get_audit_log` | Read NDJSON audit log entries with filters | yes | no |
|
|
49
|
+
|
|
50
|
+
> **v0.1 Honesty Note:** all six tools are `readOnlyHint=true, destructiveHint=false` in v0.1 because they emit policy descriptors + append to the audit log but do not modify subprocess state. v0.2 will flip `inject_egress_policy` to `destructiveHint=true` when native enforcement is wired in.
|
|
51
|
+
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
mcp-rce-guard serve # start MCP server on stdio
|
|
56
|
+
mcp-rce-guard platform # detect isolation backend (landlock/sandbox-exec/unsupported)
|
|
57
|
+
mcp-rce-guard scan-cve <cmd> # one-shot CVE replay against a command
|
|
58
|
+
mcp-rce-guard audit-log # tail the audit log
|
|
59
|
+
mcp-rce-guard policy <profile.json> # synthesize landlock+cgroups+sandbox-exec from JSON
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Compatibility matrix
|
|
63
|
+
|
|
64
|
+
| Platform | Isolation backend | Network egress | cgroups |
|
|
65
|
+
|----------|-------------------|----------------|---------|
|
|
66
|
+
| Linux >= 5.13 | landlock | nftables hook (out-of-band) | cgroups-v2 |
|
|
67
|
+
| Linux < 5.13 | unsupported | unsupported | unsupported |
|
|
68
|
+
| macOS 11+ | sandbox-exec (.sb) | sandbox-exec network rules | n/a |
|
|
69
|
+
| Windows | unsupported (planned: AppContainer) | n/a | n/a |
|
|
70
|
+
|
|
71
|
+
## Layer architecture (v0.1)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Pillar 1 (mcp-protocol-conformance) — protocol input shape
|
|
75
|
+
↓
|
|
76
|
+
Pillar 8 (mcp-stdio-shellguard) — argv allowlist + invisible-codepoint detection
|
|
77
|
+
↓
|
|
78
|
+
Pillar 9 (mcp-rce-guard, this lib) — policy synthesis + CVE-replay scan + canary
|
|
79
|
+
↓
|
|
80
|
+
Host spawner (caller responsibility) — applies the policy descriptor via real syscalls
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
In v0.2 the bottom box collapses into Pillar 9 (NAPI-RS landlock binding, sandbox-exec exec wrap, cgroups-v2 fs writes with cleanup). Until then, the host has to bridge the descriptor to the platform.
|
|
84
|
+
|
|
85
|
+
## Trust tiers
|
|
86
|
+
|
|
87
|
+
| Tier | mem | pids | egress | use |
|
|
88
|
+
|------|-----|------|--------|-----|
|
|
89
|
+
| `LOW` | 256MB | 32 | none | untrusted MCP from registry |
|
|
90
|
+
| `MEDIUM` | 512MB | 64 | api-only | known vendor with limited scope |
|
|
91
|
+
| `HIGH` | 1024MB | 128 | broad | first-party MCP with reviewed source |
|
|
92
|
+
| `CRITICAL` | 2048MB | 256 | unrestricted | local OS-integrated MCP, you own the source |
|
|
93
|
+
|
|
94
|
+
## Audit log location
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
$MCP_RCE_GUARD_HOME/audit.log # default: ~/.mcp-rce-guard/audit.log
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Entries are written as one JSON document per line (NDJSON) and read back via
|
|
101
|
+
`JSON.parse` per line, so arbitrary characters inside `args` strings (tabs,
|
|
102
|
+
quotes, embedded escape sequences) round-trip safely. Rotation happens at
|
|
103
|
+
100MB; up to 10 backups (`audit.log.1` .. `audit.log.10`) are kept.
|
|
104
|
+
|
|
105
|
+
v0.1 does **not** sign entries. The previous `MCP_RCE_GUARD_SIGNING_KEY`
|
|
106
|
+
HMAC-SHA256 path was removed pre-publish: there was no in-process verifier,
|
|
107
|
+
no key-length validation, and no rotation strategy, which would have been
|
|
108
|
+
security theatre. v0.2 ships a verified Acra-pattern chain (`KDF(master) →
|
|
109
|
+
per-entry HMAC`, key rotation, an integrated `verify_audit_log` tool).
|
|
110
|
+
|
|
111
|
+
## Notes
|
|
112
|
+
|
|
113
|
+
- v0.1 emits **policy descriptors**. Actual landlock syscall application is the responsibility of the spawning helper (separation-of-concerns: policy synthesis is portable, syscall is platform-specific). A native bindings wrapper is planned for v0.2.
|
|
114
|
+
- CVE fixtures are **behavioral predicates**, not runnable exploits. They check: "does the target command match the shape known to be exploitable?" Not "can we trigger the exploit?"
|
|
115
|
+
- Canary tokens use crypto-random 64-hex bodies inside `__MCP_CANARY_<hex>__` markers. Pattern uniqueness is statistical, not crypto-guaranteed against an adversary that controls the generator.
|
|
116
|
+
|
|
117
|
+
## Known Issues v0.1
|
|
118
|
+
|
|
119
|
+
These are real but accepted limitations of the v0.1 line. They are tracked in
|
|
120
|
+
the v0.2 roadmap below; if any of them blocks your use case, pin to a future
|
|
121
|
+
v0.2.x release once it ships.
|
|
122
|
+
|
|
123
|
+
- **Audit-log rotation has a TOCTOU window** (`src/audit/log.ts`
|
|
124
|
+
`rotateAuditLog`). The size check and the rename are not atomic. In
|
|
125
|
+
multi-process / high-concurrency setups two appenders may both observe
|
|
126
|
+
"needs rotation" and both attempt the rename; the loser's append lands in
|
|
127
|
+
the freshly-created empty active log. Workaround: serialize writes or run
|
|
128
|
+
one writer per process. v0.2 will gate rotation behind `proper-lockfile`.
|
|
129
|
+
- **In-memory state lost on restart** (`src/state.ts`). Registered
|
|
130
|
+
subprocesses and canary chains live in process memory. A process restart
|
|
131
|
+
empties the registry; the operator has to re-register. v0.2 plans a
|
|
132
|
+
SQLite-backed store as an opt-in persistence layer.
|
|
133
|
+
- **`mcp-protocol-validator` CI step is not a hard gate**
|
|
134
|
+
(`.github/workflows/ci.yml`). The validator runs with
|
|
135
|
+
`continue-on-error: true` because the upstream tool is still stabilising;
|
|
136
|
+
failures surface as CI annotations but do not block. v0.2 promotes it to a
|
|
137
|
+
hard gate once upstream is stable.
|
|
138
|
+
- **Audit log is unsigned in v0.1**. See "Audit log location" above. v0.2
|
|
139
|
+
adds a verified Acra-pattern signed chain.
|
|
140
|
+
|
|
141
|
+
## v0.2 Roadmap (parked, no commit date)
|
|
142
|
+
|
|
143
|
+
- pnpm-workspace mit `mcp-rce-guard` + `mcp-rce-demo` + `mcp-rce-fixtures` (Marketplace-Distribution-Hygiene: CVE-Fixtures separate distribution).
|
|
144
|
+
- NAPI-RS Landlock binding fuer Linux (kernel >=5.13). Plain `child_process.spawn` mit prctl + LANDLOCK_CREATE_RULESET ist nicht testbar ohne C-Bridge.
|
|
145
|
+
- macOS sandbox-exec execa wrap mit profile-file lifecycle (deny-network triggert connection-refused, deny-fs-read triggert EACCES).
|
|
146
|
+
- cgroups-v2 fs.writeFile mit Permission-Checks + cleanup-on-exit + signal handlers.
|
|
147
|
+
- Integration tests `tests/integration/linux-landlock.test.ts` + `tests/integration/macos-sandbox-exec.test.ts`.
|
|
148
|
+
- Reference server `mcp-rce-demo` mit 6 Tools die jede Capability live demonstrieren (read_isolated_file, attempt_egress_blocked, spawn_subprocess_audited, replay_cve_demo, inject_canary_demo, get_demo_audit).
|
|
149
|
+
- Latency test "subprocess-Spawn p95 <200ms" auf ubuntu-latest GitHub Actions runner.
|
|
150
|
+
- Sigstore Trusted Publishing OIDC fuer alle 3 npm-Packages.
|
|
151
|
+
- Verified Acra-pattern audit-log chain: KDF master-key → per-entry HMAC, key rotation with overlap window, integrated `verify_audit_log` tool. Replaces the v0.1 HMAC-SHA256 prototype that was removed pre-publish for lacking a verifier.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT — Copyright (c) 2026 Matthias Meyer (StudioMeyer)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only NDJSON audit log.
|
|
3
|
+
*
|
|
4
|
+
* Path: ${MCP_RCE_GUARD_HOME:-~/.mcp-rce-guard}/audit.log
|
|
5
|
+
*
|
|
6
|
+
* Each line is a JSON-serialized AuditLogEntry. The log is append-only with
|
|
7
|
+
* size-based rotation (Reviewer S1024 F6): when the active log exceeds
|
|
8
|
+
* `MCP_RCE_GUARD_AUDIT_MAX_BYTES` (default 100MB), it is atomically renamed
|
|
9
|
+
* to `audit.log.1`, the previous .1 becomes .2, etc., up to .10. Anything
|
|
10
|
+
* beyond .10 is dropped to bound disk usage.
|
|
11
|
+
*
|
|
12
|
+
* Pre-Publish-Audit (2026-05-13 F1/F3/F5):
|
|
13
|
+
* - HMAC-SHA256 signing path removed: the v0.1 implementation lacked a
|
|
14
|
+
* verifier, key-validation and rotation-safety, making it security
|
|
15
|
+
* theatre per Acra-pattern (industry baseline for HMAC audit-log chains).
|
|
16
|
+
* v0.2 will reintroduce a verified Acra-style signed chain.
|
|
17
|
+
* - NDJSON reader now uses `JSON.parse(line.trim())` per line; the previous
|
|
18
|
+
* tab-splitting scheme silently dropped entries whose arg strings
|
|
19
|
+
* contained a literal tab character.
|
|
20
|
+
*/
|
|
21
|
+
import type { AuditLogEntry } from "../types.js";
|
|
22
|
+
export declare function auditLogPath(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Rotate the active log: rename audit.log → audit.log.1 (shifting existing
|
|
25
|
+
* .1 → .2 etc., up to .MAX_ROTATED_BACKUPS). Anything beyond .10 is unlinked.
|
|
26
|
+
* Atomic via fs.rename, which is atomic on the same filesystem on POSIX.
|
|
27
|
+
*
|
|
28
|
+
* Known Issue v0.1 (Pre-Publish-Audit F2): TOCTOU race window between the
|
|
29
|
+
* size-check and the rename. In multi-process / high-concurrency setups, two
|
|
30
|
+
* appenders may both observe "needs rotation" and both attempt the rename;
|
|
31
|
+
* the loser's append lands in the freshly-created empty active log. v0.2
|
|
32
|
+
* will gate rotation behind `proper-lockfile` or an equivalent advisory
|
|
33
|
+
* lockfile. v0.1 callers should serialize writes or accept the race.
|
|
34
|
+
*/
|
|
35
|
+
export declare function rotateAuditLog(): Promise<{
|
|
36
|
+
rotated: boolean;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Append a single audit-log entry. Idempotent against duplicate calls — every
|
|
41
|
+
* call writes a distinct line because `ts` is auto-stamped if missing.
|
|
42
|
+
*
|
|
43
|
+
* Triggers rotation BEFORE writing if the active log already exceeds the
|
|
44
|
+
* configured cap. This is a best-effort guard; under heavy concurrency the
|
|
45
|
+
* log may briefly exceed the cap by one batch (acceptable for audit
|
|
46
|
+
* semantics).
|
|
47
|
+
*/
|
|
48
|
+
export declare function appendAudit(entry: Omit<AuditLogEntry, "ts"> & {
|
|
49
|
+
ts?: string;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Read entries with optional filters.
|
|
53
|
+
*
|
|
54
|
+
* Pre-Publish-Audit F3: each line is parsed as a single JSON document. The
|
|
55
|
+
* earlier implementation split on the last tab character (legacy HMAC suffix
|
|
56
|
+
* carve-out); arg strings containing a literal tab were silently truncated
|
|
57
|
+
* and the surviving prefix failed JSON.parse, dropping the entry. The
|
|
58
|
+
* NDJSON-standard read path used here is robust to any byte content the
|
|
59
|
+
* JSON encoder produced.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readAudit(filter?: {
|
|
62
|
+
subprocessHandle?: string;
|
|
63
|
+
since?: string;
|
|
64
|
+
limit?: number;
|
|
65
|
+
}): Promise<AuditLogEntry[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Test helper: clear the log file. NOT a public API — imported directly
|
|
68
|
+
* from this module by tests; intentionally absent from `src/index.ts`
|
|
69
|
+
* re-exports so external consumers cannot accidentally drop their
|
|
70
|
+
* audit trail (Pre-Publish-Audit F9).
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export declare function clearAuditLog(): Promise<void>;
|
|
75
|
+
//# sourceMappingURL=log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/audit/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAajD,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAiBD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwBrF;AAYD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,OAAO,CAAC,IAAI,CAAC,CAmBf;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,MAAM,CAAC,EAAE;IACvC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CA4B3B;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAYnD"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only NDJSON audit log.
|
|
3
|
+
*
|
|
4
|
+
* Path: ${MCP_RCE_GUARD_HOME:-~/.mcp-rce-guard}/audit.log
|
|
5
|
+
*
|
|
6
|
+
* Each line is a JSON-serialized AuditLogEntry. The log is append-only with
|
|
7
|
+
* size-based rotation (Reviewer S1024 F6): when the active log exceeds
|
|
8
|
+
* `MCP_RCE_GUARD_AUDIT_MAX_BYTES` (default 100MB), it is atomically renamed
|
|
9
|
+
* to `audit.log.1`, the previous .1 becomes .2, etc., up to .10. Anything
|
|
10
|
+
* beyond .10 is dropped to bound disk usage.
|
|
11
|
+
*
|
|
12
|
+
* Pre-Publish-Audit (2026-05-13 F1/F3/F5):
|
|
13
|
+
* - HMAC-SHA256 signing path removed: the v0.1 implementation lacked a
|
|
14
|
+
* verifier, key-validation and rotation-safety, making it security
|
|
15
|
+
* theatre per Acra-pattern (industry baseline for HMAC audit-log chains).
|
|
16
|
+
* v0.2 will reintroduce a verified Acra-style signed chain.
|
|
17
|
+
* - NDJSON reader now uses `JSON.parse(line.trim())` per line; the previous
|
|
18
|
+
* tab-splitting scheme silently dropped entries whose arg strings
|
|
19
|
+
* contained a literal tab character.
|
|
20
|
+
*/
|
|
21
|
+
import { promises as fs } from "node:fs";
|
|
22
|
+
import { existsSync, mkdirSync, statSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join, dirname } from "node:path";
|
|
25
|
+
/**
|
|
26
|
+
* Reviewer S1024 F6: cap on active log size before rotation. Default 100MB.
|
|
27
|
+
* Configurable via env so ops can tune for high-volume tenants.
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
30
|
+
const MAX_ROTATED_BACKUPS = 10;
|
|
31
|
+
function homeDir() {
|
|
32
|
+
return process.env["MCP_RCE_GUARD_HOME"] ?? join(homedir(), ".mcp-rce-guard");
|
|
33
|
+
}
|
|
34
|
+
export function auditLogPath() {
|
|
35
|
+
return join(homeDir(), "audit.log");
|
|
36
|
+
}
|
|
37
|
+
function maxBytes() {
|
|
38
|
+
const raw = process.env["MCP_RCE_GUARD_AUDIT_MAX_BYTES"];
|
|
39
|
+
if (!raw)
|
|
40
|
+
return DEFAULT_MAX_BYTES;
|
|
41
|
+
const parsed = Number.parseInt(raw, 10);
|
|
42
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
43
|
+
return DEFAULT_MAX_BYTES;
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
function ensureDir(path) {
|
|
47
|
+
const dir = dirname(path);
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Rotate the active log: rename audit.log → audit.log.1 (shifting existing
|
|
54
|
+
* .1 → .2 etc., up to .MAX_ROTATED_BACKUPS). Anything beyond .10 is unlinked.
|
|
55
|
+
* Atomic via fs.rename, which is atomic on the same filesystem on POSIX.
|
|
56
|
+
*
|
|
57
|
+
* Known Issue v0.1 (Pre-Publish-Audit F2): TOCTOU race window between the
|
|
58
|
+
* size-check and the rename. In multi-process / high-concurrency setups, two
|
|
59
|
+
* appenders may both observe "needs rotation" and both attempt the rename;
|
|
60
|
+
* the loser's append lands in the freshly-created empty active log. v0.2
|
|
61
|
+
* will gate rotation behind `proper-lockfile` or an equivalent advisory
|
|
62
|
+
* lockfile. v0.1 callers should serialize writes or accept the race.
|
|
63
|
+
*/
|
|
64
|
+
export async function rotateAuditLog() {
|
|
65
|
+
const active = auditLogPath();
|
|
66
|
+
if (!existsSync(active)) {
|
|
67
|
+
return { rotated: false, reason: "no active log" };
|
|
68
|
+
}
|
|
69
|
+
// Shift backups: .9 → .10, .8 → .9, ..., .1 → .2
|
|
70
|
+
for (let i = MAX_ROTATED_BACKUPS; i >= 2; i--) {
|
|
71
|
+
const from = `${active}.${i - 1}`;
|
|
72
|
+
const to = `${active}.${i}`;
|
|
73
|
+
if (existsSync(from)) {
|
|
74
|
+
// If destination exists (overflow at .10), drop it first.
|
|
75
|
+
if (existsSync(to)) {
|
|
76
|
+
await fs.unlink(to);
|
|
77
|
+
}
|
|
78
|
+
await fs.rename(from, to);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Move active → .1
|
|
82
|
+
const dotOne = `${active}.1`;
|
|
83
|
+
if (existsSync(dotOne)) {
|
|
84
|
+
await fs.unlink(dotOne);
|
|
85
|
+
}
|
|
86
|
+
await fs.rename(active, dotOne);
|
|
87
|
+
return { rotated: true };
|
|
88
|
+
}
|
|
89
|
+
function shouldRotate(path) {
|
|
90
|
+
if (!existsSync(path))
|
|
91
|
+
return false;
|
|
92
|
+
try {
|
|
93
|
+
const st = statSync(path);
|
|
94
|
+
return st.size >= maxBytes();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Append a single audit-log entry. Idempotent against duplicate calls — every
|
|
102
|
+
* call writes a distinct line because `ts` is auto-stamped if missing.
|
|
103
|
+
*
|
|
104
|
+
* Triggers rotation BEFORE writing if the active log already exceeds the
|
|
105
|
+
* configured cap. This is a best-effort guard; under heavy concurrency the
|
|
106
|
+
* log may briefly exceed the cap by one batch (acceptable for audit
|
|
107
|
+
* semantics).
|
|
108
|
+
*/
|
|
109
|
+
export async function appendAudit(entry) {
|
|
110
|
+
const finalEntry = {
|
|
111
|
+
ts: entry.ts ?? new Date().toISOString(),
|
|
112
|
+
subprocessHandle: entry.subprocessHandle,
|
|
113
|
+
action: entry.action,
|
|
114
|
+
verdict: entry.verdict,
|
|
115
|
+
...(entry.serverId !== undefined ? { serverId: entry.serverId } : {}),
|
|
116
|
+
...(entry.reason !== undefined ? { reason: entry.reason } : {}),
|
|
117
|
+
...(entry.meta !== undefined ? { meta: entry.meta } : {})
|
|
118
|
+
};
|
|
119
|
+
const path = auditLogPath();
|
|
120
|
+
ensureDir(path);
|
|
121
|
+
if (shouldRotate(path)) {
|
|
122
|
+
await rotateAuditLog();
|
|
123
|
+
}
|
|
124
|
+
// JSON.stringify never emits a literal newline inside the string (all
|
|
125
|
+
// control chars are escaped). One entry == one NDJSON line.
|
|
126
|
+
const line = JSON.stringify(finalEntry);
|
|
127
|
+
await fs.appendFile(path, line + "\n", { mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Read entries with optional filters.
|
|
131
|
+
*
|
|
132
|
+
* Pre-Publish-Audit F3: each line is parsed as a single JSON document. The
|
|
133
|
+
* earlier implementation split on the last tab character (legacy HMAC suffix
|
|
134
|
+
* carve-out); arg strings containing a literal tab were silently truncated
|
|
135
|
+
* and the surviving prefix failed JSON.parse, dropping the entry. The
|
|
136
|
+
* NDJSON-standard read path used here is robust to any byte content the
|
|
137
|
+
* JSON encoder produced.
|
|
138
|
+
*/
|
|
139
|
+
export async function readAudit(filter) {
|
|
140
|
+
const path = auditLogPath();
|
|
141
|
+
if (!existsSync(path))
|
|
142
|
+
return [];
|
|
143
|
+
const raw = await fs.readFile(path, "utf8");
|
|
144
|
+
// Splitting on \n is safe: JSON.stringify escapes embedded newlines as \n
|
|
145
|
+
// (literal two-character sequence) so a real newline only appears at the
|
|
146
|
+
// entry boundary written by appendAudit.
|
|
147
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
148
|
+
const entries = [];
|
|
149
|
+
for (const rawLine of lines) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(rawLine.trim());
|
|
152
|
+
if (filter?.subprocessHandle && parsed.subprocessHandle !== filter.subprocessHandle) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (filter?.since && parsed.ts < filter.since) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
entries.push(parsed);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Skip malformed lines; production: add a counter + WARN log.
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (filter?.limit && entries.length > filter.limit) {
|
|
166
|
+
return entries.slice(-filter.limit);
|
|
167
|
+
}
|
|
168
|
+
return entries;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Test helper: clear the log file. NOT a public API — imported directly
|
|
172
|
+
* from this module by tests; intentionally absent from `src/index.ts`
|
|
173
|
+
* re-exports so external consumers cannot accidentally drop their
|
|
174
|
+
* audit trail (Pre-Publish-Audit F9).
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export async function clearAuditLog() {
|
|
179
|
+
const path = auditLogPath();
|
|
180
|
+
if (existsSync(path)) {
|
|
181
|
+
await fs.unlink(path);
|
|
182
|
+
}
|
|
183
|
+
// Also clear any rotated backups so test runs start clean.
|
|
184
|
+
for (let i = 1; i <= MAX_ROTATED_BACKUPS; i++) {
|
|
185
|
+
const backup = `${path}.${i}`;
|
|
186
|
+
if (existsSync(backup)) {
|
|
187
|
+
await fs.unlink(backup);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/audit/log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canary token tracking for cross-server credential leak detection.
|
|
3
|
+
*
|
|
4
|
+
* Pattern provenance: arXiv 2604.27819 (MCPHunt, April 2026) — taint tracking
|
|
5
|
+
* via canary-token injection across MCP server boundaries. If a token leaves
|
|
6
|
+
* the chain via stdout/network/fs, the controller flags the leak channel.
|
|
7
|
+
*
|
|
8
|
+
* v0.1 ist scanner-only: detect_leaks scant a corpus of textual outputs for
|
|
9
|
+
* the canary token. The actual injection happens at the calling MCP server's
|
|
10
|
+
* tool boundary via track_canary which returns the token + downstream IDs.
|
|
11
|
+
*/
|
|
12
|
+
export type LeakChannel = "stdout" | "network-egress" | "fs-write";
|
|
13
|
+
export interface LeakLocation {
|
|
14
|
+
serverId: string;
|
|
15
|
+
channel: LeakChannel;
|
|
16
|
+
}
|
|
17
|
+
export interface CorpusEntry {
|
|
18
|
+
serverId: string;
|
|
19
|
+
channel: LeakChannel;
|
|
20
|
+
content: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a canary token + pattern. Caller may supply a fixed pattern (for
|
|
24
|
+
* tests) or accept the high-entropy default.
|
|
25
|
+
*/
|
|
26
|
+
export declare function makeCanary(pattern?: string): {
|
|
27
|
+
token: string;
|
|
28
|
+
pattern: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Scan a corpus for canary leaks. Returns one LeakLocation per matching entry.
|
|
32
|
+
*
|
|
33
|
+
* False-positive note: a tool that legitimately echoes a canary in its own
|
|
34
|
+
* structured output would be flagged. Mitigation: callers should redact the
|
|
35
|
+
* token from logs they emit, or pass a unique pattern per chain.
|
|
36
|
+
*/
|
|
37
|
+
export declare function detectLeaks(corpus: readonly CorpusEntry[], pattern: string): LeakLocation[];
|
|
38
|
+
//# sourceMappingURL=tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/canary/tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,gBAAgB,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAM/E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,EAAE,MAAM,GACd,YAAY,EAAE,CAQhB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canary token tracking for cross-server credential leak detection.
|
|
3
|
+
*
|
|
4
|
+
* Pattern provenance: arXiv 2604.27819 (MCPHunt, April 2026) — taint tracking
|
|
5
|
+
* via canary-token injection across MCP server boundaries. If a token leaves
|
|
6
|
+
* the chain via stdout/network/fs, the controller flags the leak channel.
|
|
7
|
+
*
|
|
8
|
+
* v0.1 ist scanner-only: detect_leaks scant a corpus of textual outputs for
|
|
9
|
+
* the canary token. The actual injection happens at the calling MCP server's
|
|
10
|
+
* tool boundary via track_canary which returns the token + downstream IDs.
|
|
11
|
+
*/
|
|
12
|
+
import { generateCanaryToken } from "../state.js";
|
|
13
|
+
/**
|
|
14
|
+
* Build a canary token + pattern. Caller may supply a fixed pattern (for
|
|
15
|
+
* tests) or accept the high-entropy default.
|
|
16
|
+
*/
|
|
17
|
+
export function makeCanary(pattern) {
|
|
18
|
+
const token = generateCanaryToken();
|
|
19
|
+
return {
|
|
20
|
+
token,
|
|
21
|
+
pattern: pattern ?? token
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Scan a corpus for canary leaks. Returns one LeakLocation per matching entry.
|
|
26
|
+
*
|
|
27
|
+
* False-positive note: a tool that legitimately echoes a canary in its own
|
|
28
|
+
* structured output would be flagged. Mitigation: callers should redact the
|
|
29
|
+
* token from logs they emit, or pass a unique pattern per chain.
|
|
30
|
+
*/
|
|
31
|
+
export function detectLeaks(corpus, pattern) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const entry of corpus) {
|
|
34
|
+
if (entry.content.includes(pattern)) {
|
|
35
|
+
out.push({ serverId: entry.serverId, channel: entry.channel });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../src/canary/tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAelD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,MAAM,KAAK,GAAG,mBAAmB,EAAE,CAAC;IACpC,OAAO;QACL,KAAK;QACL,OAAO,EAAE,OAAO,IAAI,KAAK;KAC1B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,MAA8B,EAC9B,OAAe;IAEf,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-rce-guard CLI.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* serve — run the stdio MCP server (same as mcp-rce-guard-server bin)
|
|
6
|
+
* platform — print detected isolation backend + capabilities
|
|
7
|
+
* scan-cve <command...> — run CVE replay fixtures against a candidate command
|
|
8
|
+
* audit-log [--since=..] — print recent audit-log entries
|
|
9
|
+
* policy <profile.json> — emit landlock + sandbox-exec + cgroups specs from a profile
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
declare const program: Command;
|
|
13
|
+
export { program };
|
|
14
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AA8G9B,OAAO,EAAE,OAAO,EAAE,CAAC"}
|