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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cgroups-v2 resource cap synthesis.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 emits a *spec* listing the files-to-write and values. The actual
|
|
5
|
+
* write happens in the spawning helper (separation-of-concerns: we don't
|
|
6
|
+
* want to write to /sys/fs/cgroup from inside a tool handler — that needs
|
|
7
|
+
* elevated privileges that the MCP server typically lacks).
|
|
8
|
+
*/
|
|
9
|
+
export function buildCgroupSpec(cgroupName, profile) {
|
|
10
|
+
const cgroupPath = `/sys/fs/cgroup/${cgroupName}`;
|
|
11
|
+
const writes = [];
|
|
12
|
+
if (profile.memMB !== undefined) {
|
|
13
|
+
// memory.max accepts bytes.
|
|
14
|
+
const bytes = profile.memMB * 1024 * 1024;
|
|
15
|
+
writes.push({ file: `${cgroupPath}/memory.max`, value: String(bytes) });
|
|
16
|
+
}
|
|
17
|
+
if (profile.pidMax !== undefined) {
|
|
18
|
+
writes.push({ file: `${cgroupPath}/pids.max`, value: String(profile.pidMax) });
|
|
19
|
+
}
|
|
20
|
+
if (profile.cpuMs !== undefined) {
|
|
21
|
+
// cpu.max format: "<quota> <period>". 100ms period; quota = cpuMs * 1000 / period_count.
|
|
22
|
+
// Simplified: derive quota_us = cpuMs * 1000 spread over a 100_000us period.
|
|
23
|
+
// For caps on TOTAL CPU time the spawner uses RLIMIT_CPU instead — see spec.
|
|
24
|
+
const periodUs = 100_000;
|
|
25
|
+
const quotaUs = Math.max(1000, Math.min(periodUs, profile.cpuMs * 1000));
|
|
26
|
+
writes.push({
|
|
27
|
+
file: `${cgroupPath}/cpu.max`,
|
|
28
|
+
value: `${quotaUs} ${periodUs}`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return { cgroupPath, writes };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=cgroups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cgroups.js","sourceRoot":"","sources":["../../src/isolation/cgroups.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,MAAM,UAAU,eAAe,CAC7B,UAAkB,EAClB,OAAyB;IAEzB,MAAM,UAAU,GAAG,kBAAkB,UAAU,EAAE,CAAC;IAClD,MAAM,MAAM,GAAyB,EAAE,CAAC;IAExC,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,4BAA4B;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,UAAU,aAAa,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,UAAU,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,yFAAyF;QACzF,6EAA6E;QAC7E,6EAA6E;QAC7E,MAAM,QAAQ,GAAG,OAAO,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC;QACzE,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,GAAG,UAAU,UAAU;YAC7B,KAAK,EAAE,GAAG,OAAO,IAAI,QAAQ,EAAE;SAChC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux landlock policy synthesis.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 emits a *policy descriptor* — a serializable representation of the
|
|
5
|
+
* landlock ruleset — but does NOT directly invoke the prctl/landlock_create_ruleset
|
|
6
|
+
* syscalls. Reason: the landlock syscall interface requires either a tiny native
|
|
7
|
+
* addon or a `libc.dlopen` dance, both of which add binary-distribution complexity.
|
|
8
|
+
*
|
|
9
|
+
* The actual enforcement contract is: the spawning helper uses the `landlock-restrict`
|
|
10
|
+
* tool (bundled in `mcp-rce-fixtures` for v0.1; will become its own bin in v0.2)
|
|
11
|
+
* to apply the policy descriptor before exec'ing the target binary. Same pattern
|
|
12
|
+
* as `firejail` or `bubblewrap` — split policy-eval from enforcement.
|
|
13
|
+
*
|
|
14
|
+
* Anti-Pattern provenance: Nginx-MCP RCE CVSS 9.8 (subprocess inheritert volle
|
|
15
|
+
* parent-permissions). Defense: explicit policy descriptor that lists every
|
|
16
|
+
* allowed fs path + access flag.
|
|
17
|
+
*/
|
|
18
|
+
import type { IsolationProfile } from "../types.js";
|
|
19
|
+
export interface LandlockPolicyDescriptor {
|
|
20
|
+
version: 1;
|
|
21
|
+
ruleset: {
|
|
22
|
+
handledAccessFs: string[];
|
|
23
|
+
rules: Array<{
|
|
24
|
+
path: string;
|
|
25
|
+
access: ("read" | "execute" | "write")[];
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a landlock policy descriptor from an isolation profile.
|
|
31
|
+
*
|
|
32
|
+
* - fsReadOnly entries → read+execute access.
|
|
33
|
+
* - fsWritable entries → read+execute+write access.
|
|
34
|
+
* - Everything else: implicit deny.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildLandlockPolicy(profile: IsolationProfile): LandlockPolicyDescriptor;
|
|
37
|
+
/**
|
|
38
|
+
* Validate that a path appears in the policy. Used by audit_subprocess to
|
|
39
|
+
* detect when the requested binary lives outside the read-only roots.
|
|
40
|
+
*/
|
|
41
|
+
export declare function policyAllowsExec(policy: LandlockPolicyDescriptor, binaryPath: string): boolean;
|
|
42
|
+
//# sourceMappingURL=landlock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landlock.d.ts","sourceRoot":"","sources":["../../src/isolation/landlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE;QACP,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B,KAAK,EAAE,KAAK,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;YACb,MAAM,EAAE,CAAC,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;SAC1C,CAAC,CAAC;KACJ,CAAC;CACH;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,wBAAwB,CA+BvF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,wBAAwB,EAChC,UAAU,EAAE,MAAM,GACjB,OAAO,CAOT"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux landlock policy synthesis.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 emits a *policy descriptor* — a serializable representation of the
|
|
5
|
+
* landlock ruleset — but does NOT directly invoke the prctl/landlock_create_ruleset
|
|
6
|
+
* syscalls. Reason: the landlock syscall interface requires either a tiny native
|
|
7
|
+
* addon or a `libc.dlopen` dance, both of which add binary-distribution complexity.
|
|
8
|
+
*
|
|
9
|
+
* The actual enforcement contract is: the spawning helper uses the `landlock-restrict`
|
|
10
|
+
* tool (bundled in `mcp-rce-fixtures` for v0.1; will become its own bin in v0.2)
|
|
11
|
+
* to apply the policy descriptor before exec'ing the target binary. Same pattern
|
|
12
|
+
* as `firejail` or `bubblewrap` — split policy-eval from enforcement.
|
|
13
|
+
*
|
|
14
|
+
* Anti-Pattern provenance: Nginx-MCP RCE CVSS 9.8 (subprocess inheritert volle
|
|
15
|
+
* parent-permissions). Defense: explicit policy descriptor that lists every
|
|
16
|
+
* allowed fs path + access flag.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Build a landlock policy descriptor from an isolation profile.
|
|
20
|
+
*
|
|
21
|
+
* - fsReadOnly entries → read+execute access.
|
|
22
|
+
* - fsWritable entries → read+execute+write access.
|
|
23
|
+
* - Everything else: implicit deny.
|
|
24
|
+
*/
|
|
25
|
+
export function buildLandlockPolicy(profile) {
|
|
26
|
+
const rules = [];
|
|
27
|
+
for (const path of profile.fsReadOnly) {
|
|
28
|
+
rules.push({ path, access: ["read", "execute"] });
|
|
29
|
+
}
|
|
30
|
+
for (const path of profile.fsWritable) {
|
|
31
|
+
rules.push({ path, access: ["read", "execute", "write"] });
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
version: 1,
|
|
35
|
+
ruleset: {
|
|
36
|
+
handledAccessFs: [
|
|
37
|
+
"execute",
|
|
38
|
+
"write_file",
|
|
39
|
+
"read_file",
|
|
40
|
+
"read_dir",
|
|
41
|
+
"remove_dir",
|
|
42
|
+
"remove_file",
|
|
43
|
+
"make_char",
|
|
44
|
+
"make_dir",
|
|
45
|
+
"make_reg",
|
|
46
|
+
"make_sock",
|
|
47
|
+
"make_fifo",
|
|
48
|
+
"make_block",
|
|
49
|
+
"make_sym"
|
|
50
|
+
],
|
|
51
|
+
rules
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate that a path appears in the policy. Used by audit_subprocess to
|
|
57
|
+
* detect when the requested binary lives outside the read-only roots.
|
|
58
|
+
*/
|
|
59
|
+
export function policyAllowsExec(policy, binaryPath) {
|
|
60
|
+
for (const rule of policy.ruleset.rules) {
|
|
61
|
+
if (binaryPath.startsWith(rule.path) && rule.access.includes("execute")) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=landlock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landlock.js","sourceRoot":"","sources":["../../src/isolation/landlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAeH;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAyB;IAC3D,MAAM,KAAK,GAAiD,EAAE,CAAC;IAE/D,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO;QACL,OAAO,EAAE,CAAC;QACV,OAAO,EAAE;YACP,eAAe,EAAE;gBACf,SAAS;gBACT,YAAY;gBACZ,WAAW;gBACX,UAAU;gBACV,YAAY;gBACZ,aAAa;gBACb,WAAW;gBACX,UAAU;gBACV,UAAU;gBACV,WAAW;gBACX,WAAW;gBACX,YAAY;gBACZ,UAAU;aACX;YACD,KAAK;SACN;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAgC,EAChC,UAAkB;IAElB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACxC,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YACxE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection for isolation backends.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 supports Linux (landlock + cgroups-v2) and macOS (sandbox-exec).
|
|
5
|
+
* Windows is v0.2 backlog (AppContainer + Win Sandbox-API).
|
|
6
|
+
*/
|
|
7
|
+
export type IsolationBackend = "landlock" | "sandbox-exec" | "unsupported";
|
|
8
|
+
export interface PlatformInfo {
|
|
9
|
+
os: NodeJS.Platform;
|
|
10
|
+
backend: IsolationBackend;
|
|
11
|
+
/** Linux kernel version, e.g. "5.15.0". Empty on non-Linux. */
|
|
12
|
+
kernelVersion: string;
|
|
13
|
+
/** True if cgroups-v2 unified hierarchy is mounted at /sys/fs/cgroup. */
|
|
14
|
+
hasCgroupsV2: boolean;
|
|
15
|
+
/** True if the running kernel exposes landlock syscalls (kernel >=5.13). */
|
|
16
|
+
hasLandlock: boolean;
|
|
17
|
+
/** True if /usr/bin/sandbox-exec is present (macOS only). */
|
|
18
|
+
hasSandboxExec: boolean;
|
|
19
|
+
/** Human-readable reason if backend is "unsupported". */
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function detectPlatform(): PlatformInfo;
|
|
23
|
+
/**
|
|
24
|
+
* Test helper: reset the cache. Required because detection uses fs reads.
|
|
25
|
+
*/
|
|
26
|
+
export declare function _resetPlatformCache(): void;
|
|
27
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/isolation/platform.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,CAAC;AAE3E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC;IACpB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,YAAY,EAAE,OAAO,CAAC;IACtB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;IACrB,6DAA6D;IAC7D,cAAc,EAAE,OAAO,CAAC;IACxB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,wBAAgB,cAAc,IAAI,YAAY,CA0C7C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection for isolation backends.
|
|
3
|
+
*
|
|
4
|
+
* v0.1 supports Linux (landlock + cgroups-v2) and macOS (sandbox-exec).
|
|
5
|
+
* Windows is v0.2 backlog (AppContainer + Win Sandbox-API).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { platform } from "node:process";
|
|
9
|
+
let cached = null;
|
|
10
|
+
export function detectPlatform() {
|
|
11
|
+
if (cached)
|
|
12
|
+
return cached;
|
|
13
|
+
const os = platform;
|
|
14
|
+
let backend = "unsupported";
|
|
15
|
+
let kernelVersion = "";
|
|
16
|
+
let hasCgroupsV2 = false;
|
|
17
|
+
let hasLandlock = false;
|
|
18
|
+
let hasSandboxExec = false;
|
|
19
|
+
let reason;
|
|
20
|
+
if (os === "linux") {
|
|
21
|
+
kernelVersion = readKernelVersion();
|
|
22
|
+
hasLandlock = kernelMeetsMin(kernelVersion, 5, 13);
|
|
23
|
+
hasCgroupsV2 = isCgroupsV2();
|
|
24
|
+
if (hasLandlock) {
|
|
25
|
+
backend = "landlock";
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
reason = `Linux kernel ${kernelVersion} does not support landlock (need >=5.13)`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (os === "darwin") {
|
|
32
|
+
hasSandboxExec = existsSync("/usr/bin/sandbox-exec");
|
|
33
|
+
if (hasSandboxExec) {
|
|
34
|
+
backend = "sandbox-exec";
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
reason = "macOS sandbox-exec binary not found at /usr/bin/sandbox-exec";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
reason = `Platform ${os} is not supported in v0.1 (Windows is v0.2 backlog)`;
|
|
42
|
+
}
|
|
43
|
+
const info = {
|
|
44
|
+
os,
|
|
45
|
+
backend,
|
|
46
|
+
kernelVersion,
|
|
47
|
+
hasCgroupsV2,
|
|
48
|
+
hasLandlock,
|
|
49
|
+
hasSandboxExec,
|
|
50
|
+
...(reason !== undefined ? { reason } : {})
|
|
51
|
+
};
|
|
52
|
+
cached = info;
|
|
53
|
+
return info;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Test helper: reset the cache. Required because detection uses fs reads.
|
|
57
|
+
*/
|
|
58
|
+
export function _resetPlatformCache() {
|
|
59
|
+
cached = null;
|
|
60
|
+
}
|
|
61
|
+
function readKernelVersion() {
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync("/proc/version", "utf8");
|
|
64
|
+
const m = raw.match(/Linux version (\d+\.\d+\.\d+)/);
|
|
65
|
+
return m?.[1] ?? "";
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function kernelMeetsMin(version, minMajor, minMinor) {
|
|
72
|
+
const parts = version.split(".").map((p) => parseInt(p, 10));
|
|
73
|
+
const major = parts[0];
|
|
74
|
+
const minor = parts[1];
|
|
75
|
+
if (major === undefined || minor === undefined || Number.isNaN(major) || Number.isNaN(minor)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (major > minMajor)
|
|
79
|
+
return true;
|
|
80
|
+
if (major < minMajor)
|
|
81
|
+
return false;
|
|
82
|
+
return minor >= minMinor;
|
|
83
|
+
}
|
|
84
|
+
function isCgroupsV2() {
|
|
85
|
+
try {
|
|
86
|
+
const mounts = readFileSync("/proc/mounts", "utf8");
|
|
87
|
+
return mounts.split("\n").some((line) => {
|
|
88
|
+
const fields = line.split(/\s+/);
|
|
89
|
+
return fields[2] === "cgroup2";
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/isolation/platform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS sandbox-exec policy synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Emits a Scheme-syntax sandbox profile (.sb) that can be passed to
|
|
5
|
+
* `/usr/bin/sandbox-exec -p` before the target binary. Apple's sandbox-exec
|
|
6
|
+
* is non-public-API but stable since 10.5; v0.1 ships it, v0.2 evaluates
|
|
7
|
+
* App Sandbox via Entitlements as fallback (per Architect-Empfehlung).
|
|
8
|
+
*/
|
|
9
|
+
import type { IsolationProfile } from "../types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build a sandbox-exec profile string from an isolation profile.
|
|
12
|
+
*
|
|
13
|
+
* Default-deny + per-rule allow. The sandbox profile language is Scheme-derived,
|
|
14
|
+
* see `man sandbox-exec` and `/usr/share/sandbox/`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildSandboxProfile(profile: IsolationProfile): string;
|
|
17
|
+
//# sourceMappingURL=sandbox-exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-exec.d.ts","sourceRoot":"","sources":["../../src/isolation/sandbox-exec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAqCrE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS sandbox-exec policy synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Emits a Scheme-syntax sandbox profile (.sb) that can be passed to
|
|
5
|
+
* `/usr/bin/sandbox-exec -p` before the target binary. Apple's sandbox-exec
|
|
6
|
+
* is non-public-API but stable since 10.5; v0.1 ships it, v0.2 evaluates
|
|
7
|
+
* App Sandbox via Entitlements as fallback (per Architect-Empfehlung).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build a sandbox-exec profile string from an isolation profile.
|
|
11
|
+
*
|
|
12
|
+
* Default-deny + per-rule allow. The sandbox profile language is Scheme-derived,
|
|
13
|
+
* see `man sandbox-exec` and `/usr/share/sandbox/`.
|
|
14
|
+
*/
|
|
15
|
+
export function buildSandboxProfile(profile) {
|
|
16
|
+
const lines = [
|
|
17
|
+
"(version 1)",
|
|
18
|
+
"(deny default)",
|
|
19
|
+
"(allow process-fork)",
|
|
20
|
+
"(allow process-exec self)",
|
|
21
|
+
"(allow signal (target self))",
|
|
22
|
+
"(allow sysctl-read)",
|
|
23
|
+
"(allow mach-lookup)",
|
|
24
|
+
"(allow ipc-posix-shm)",
|
|
25
|
+
"(allow file-read-metadata)"
|
|
26
|
+
];
|
|
27
|
+
for (const path of profile.fsReadOnly) {
|
|
28
|
+
lines.push(`(allow file-read* (subpath "${escapeSchemeString(path)}"))`);
|
|
29
|
+
lines.push(`(allow process-exec (subpath "${escapeSchemeString(path)}"))`);
|
|
30
|
+
}
|
|
31
|
+
for (const path of profile.fsWritable) {
|
|
32
|
+
lines.push(`(allow file-read* file-write* (subpath "${escapeSchemeString(path)}"))`);
|
|
33
|
+
}
|
|
34
|
+
// Network egress: default-deny. Allowlist entries are emitted as remote-host
|
|
35
|
+
// matchers. Wildcard "*" permits all (audit-only mode).
|
|
36
|
+
if (profile.egressAllowlist.includes("*")) {
|
|
37
|
+
lines.push("(allow network-outbound)");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
for (const entry of profile.egressAllowlist) {
|
|
41
|
+
const [host, port] = parseHostPort(entry);
|
|
42
|
+
if (host && port) {
|
|
43
|
+
lines.push(`(allow network-outbound (remote ip "${escapeSchemeString(host)}:${port}"))`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n") + "\n";
|
|
48
|
+
}
|
|
49
|
+
function parseHostPort(entry) {
|
|
50
|
+
const m = entry.match(/^([^:]+):(\d+)$/);
|
|
51
|
+
if (!m || !m[1] || !m[2])
|
|
52
|
+
return [null, null];
|
|
53
|
+
return [m[1], m[2]];
|
|
54
|
+
}
|
|
55
|
+
function escapeSchemeString(s) {
|
|
56
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=sandbox-exec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-exec.js","sourceRoot":"","sources":["../../src/isolation/sandbox-exec.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAyB;IAC3D,MAAM,KAAK,GAAa;QACtB,aAAa;QACb,gBAAgB;QAChB,sBAAsB;QACtB,2BAA2B;QAC3B,8BAA8B;QAC9B,qBAAqB;QACrB,qBAAqB;QACrB,uBAAuB;QACvB,4BAA4B;KAC7B,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,+BAA+B,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzE,KAAK,CAAC,IAAI,CAAC,iCAAiC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7E,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,2CAA2C,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvF,CAAC;IAED,6EAA6E;IAC7E,wDAAwD;IACxD,IAAI,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IACzC,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YAC5C,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;YAC1C,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;gBACjB,KAAK,CAAC,IAAI,CACR,uCAAuC,kBAAkB,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,CAC7E,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACjC,CAAC;AAED,SAAS,aAAa,CAAC,KAAa;IAClC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACzC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9C,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,kBAAkB,CAAC,CAAS;IACnC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument normalization for subprocess audit.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: shared with Pillar 8 (mcp-stdio-shellguard) — same NFKC + Zero-Width-Char-Strip
|
|
5
|
+
* + Bidi-Block pipeline. Wiederverwendung garantiert dass Layer-2 Allowlist und Layer-3
|
|
6
|
+
* Sandbox-Audit auf identischer canonical form arbeiten.
|
|
7
|
+
*
|
|
8
|
+
* Anti-Pattern provenance: Fullwidth-Unicode-Bypass (mcp-server-attestation R3, S912),
|
|
9
|
+
* Zero-Width-Joiner-Smuggling (ai-shield Round-2 Finding F2).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a single argument string for safe comparison against an allowlist.
|
|
13
|
+
*
|
|
14
|
+
* Applies in order:
|
|
15
|
+
* 1. NFKC unicode normalization (collapses fullwidth/halfwidth + compat chars).
|
|
16
|
+
* 2. Strip zero-width / format-control codepoints.
|
|
17
|
+
* 3. Strip bidi-control codepoints.
|
|
18
|
+
*
|
|
19
|
+
* Returns the canonical form. Comparison MUST always go through this function.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeArg(input: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Normalize an entire args array. Returns a new array (never mutates input).
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeArgs(args: readonly string[]): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Quick smell-test: does an input contain any zero-width or bidi codepoint?
|
|
28
|
+
* Used by the audit-log to mark suspicious args even when normalization
|
|
29
|
+
* makes them comparable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hasInvisibleCodepoints(input: string): boolean;
|
|
32
|
+
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAuBH;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK7D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument normalization for subprocess audit.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: shared with Pillar 8 (mcp-stdio-shellguard) — same NFKC + Zero-Width-Char-Strip
|
|
5
|
+
* + Bidi-Block pipeline. Wiederverwendung garantiert dass Layer-2 Allowlist und Layer-3
|
|
6
|
+
* Sandbox-Audit auf identischer canonical form arbeiten.
|
|
7
|
+
*
|
|
8
|
+
* Anti-Pattern provenance: Fullwidth-Unicode-Bypass (mcp-server-attestation R3, S912),
|
|
9
|
+
* Zero-Width-Joiner-Smuggling (ai-shield Round-2 Finding F2).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Two distinct regex objects per pattern to avoid the V8 well-known footgun
|
|
13
|
+
* where a `/g`-flagged RegExp carries mutable `lastIndex`. The `_STRIP` form
|
|
14
|
+
* (with `/g`) is used by `.replace()` to remove all occurrences. The `_TEST`
|
|
15
|
+
* form (without `/g`) is used by `.test()` for stateless boolean checks.
|
|
16
|
+
*
|
|
17
|
+
* Bug context: a single `/g` regex used for both `.replace()` (mutates state)
|
|
18
|
+
* AND `.test()` (consults state) makes the test result flip on alternating
|
|
19
|
+
* calls. Reviewer S1024 F3 documented this; regression test in
|
|
20
|
+
* `tests/unit/normalize.test.ts` covers it.
|
|
21
|
+
*/
|
|
22
|
+
const ZERO_WIDTH_STRIP = /[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF]/g;
|
|
23
|
+
const ZERO_WIDTH_TEST = /[\u200B-\u200F\u2028-\u202F\u2060-\u206F\uFEFF]/;
|
|
24
|
+
/**
|
|
25
|
+
* Bidi control codepoints: U+2066..U+2069 (LRI/RLI/FSI/PDI), U+202A..U+202E (LRE/RLE/PDF/LRO/RLO).
|
|
26
|
+
* These are weaponized in trojan-source attacks (CVE-2021-42574).
|
|
27
|
+
*/
|
|
28
|
+
const BIDI_CONTROL_STRIP = /[\u202A-\u202E\u2066-\u2069]/g;
|
|
29
|
+
const BIDI_CONTROL_TEST = /[\u202A-\u202E\u2066-\u2069]/;
|
|
30
|
+
/**
|
|
31
|
+
* Normalize a single argument string for safe comparison against an allowlist.
|
|
32
|
+
*
|
|
33
|
+
* Applies in order:
|
|
34
|
+
* 1. NFKC unicode normalization (collapses fullwidth/halfwidth + compat chars).
|
|
35
|
+
* 2. Strip zero-width / format-control codepoints.
|
|
36
|
+
* 3. Strip bidi-control codepoints.
|
|
37
|
+
*
|
|
38
|
+
* Returns the canonical form. Comparison MUST always go through this function.
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeArg(input) {
|
|
41
|
+
if (typeof input !== "string") {
|
|
42
|
+
throw new TypeError(`normalizeArg expects string, got ${typeof input}`);
|
|
43
|
+
}
|
|
44
|
+
let s = input.normalize("NFKC");
|
|
45
|
+
s = s.replace(ZERO_WIDTH_STRIP, "");
|
|
46
|
+
s = s.replace(BIDI_CONTROL_STRIP, "");
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Normalize an entire args array. Returns a new array (never mutates input).
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeArgs(args) {
|
|
53
|
+
return args.map(normalizeArg);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Quick smell-test: does an input contain any zero-width or bidi codepoint?
|
|
57
|
+
* Used by the audit-log to mark suspicious args even when normalization
|
|
58
|
+
* makes them comparable.
|
|
59
|
+
*/
|
|
60
|
+
export function hasInvisibleCodepoints(input) {
|
|
61
|
+
// IMPORTANT: use the non-/g regex variants. `.test()` on a /g-flagged
|
|
62
|
+
// RegExp consults+mutates `lastIndex`, which makes the result flip on
|
|
63
|
+
// alternating calls with the same input (Reviewer S1024 F3).
|
|
64
|
+
return ZERO_WIDTH_TEST.test(input) || BIDI_CONTROL_TEST.test(input);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=normalize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.js","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,gBAAgB,GAAG,kDAAkD,CAAC;AAC5E,MAAM,eAAe,GAAG,iDAAiD,CAAC;AAE1E;;;GAGG;AACH,MAAM,kBAAkB,GAAG,+BAA+B,CAAC;AAC3D,MAAM,iBAAiB,GAAG,8BAA8B,CAAC;AAEzD;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,oCAAoC,OAAO,KAAK,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;IACtC,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,OAAO,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAa;IAClD,sEAAsE;IACtE,sEAAsE;IACtE,6DAA6D;IAC7D,OAAO,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-rce-guard MCP stdio server (entry-point).
|
|
3
|
+
*
|
|
4
|
+
* Spec: MCP 2025-06-18.
|
|
5
|
+
* Transport: stdio (consistent with Pillar 1 + Pillar 8).
|
|
6
|
+
* Tools: 6 (register_subprocess, audit_subprocess, scan_cve_replay,
|
|
7
|
+
* track_canary, inject_egress_policy, get_audit_log).
|
|
8
|
+
*
|
|
9
|
+
* The server is a thin wrapper around the programmatic tool handlers in
|
|
10
|
+
* src/tools/*.ts — same handlers the library exports.
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
export declare function createServer(): McpServer;
|
|
14
|
+
export declare function main(): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA+CpE,wBAAgB,YAAY,IAAI,SAAS,CAmHxC;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB1C"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-rce-guard MCP stdio server (entry-point).
|
|
4
|
+
*
|
|
5
|
+
* Spec: MCP 2025-06-18.
|
|
6
|
+
* Transport: stdio (consistent with Pillar 1 + Pillar 8).
|
|
7
|
+
* Tools: 6 (register_subprocess, audit_subprocess, scan_cve_replay,
|
|
8
|
+
* track_canary, inject_egress_policy, get_audit_log).
|
|
9
|
+
*
|
|
10
|
+
* The server is a thin wrapper around the programmatic tool handlers in
|
|
11
|
+
* src/tools/*.ts — same handlers the library exports.
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { RegisterSubprocessArgsSchema, AuditSubprocessArgsSchema, ScanCveReplayArgsSchema, TrackCanaryArgsSchema, InjectEgressPolicyArgsSchema, GetAuditLogArgsSchema } from "./types.js";
|
|
16
|
+
import { registerSubprocessTool } from "./tools/register.js";
|
|
17
|
+
import { auditSubprocessTool } from "./tools/audit.js";
|
|
18
|
+
import { scanCveReplayTool } from "./tools/scanCve.js";
|
|
19
|
+
import { trackCanaryTool } from "./tools/trackCanary.js";
|
|
20
|
+
import { injectEgressPolicyTool } from "./tools/injectEgress.js";
|
|
21
|
+
import { getAuditLogTool } from "./tools/getAuditLog.js";
|
|
22
|
+
import { NAME, VERSION } from "./version.js";
|
|
23
|
+
function ok(result) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function err(message) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: message }],
|
|
31
|
+
isError: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function safe(fn) {
|
|
35
|
+
try {
|
|
36
|
+
return ok(await fn());
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
40
|
+
return err(`mcp-rce-guard error: ${msg}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function createServer() {
|
|
44
|
+
const server = new McpServer({ name: NAME, version: VERSION }, {
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools: {}
|
|
47
|
+
},
|
|
48
|
+
instructions: "v0.1 policy-synthesis library for MCP subprocess isolation. Emits landlock / sandbox-exec / cgroups-v2 policy descriptors + runs behavioral CVE-replay predicates + tracks cross-server canary leaks + appends NDJSON audit log. Does NOT call landlock/sandbox-exec/cgroups syscalls in v0.1 — the host spawner translates descriptors into platform syscalls. Native enforcement is v0.2. Use register_subprocess + audit_subprocess to synthesize + validate spawn-specs, scan_cve_replay before connecting to unknown servers, track_canary in multi-server chains, inject_egress_policy for descriptor-only egress policy, get_audit_log for forensic review."
|
|
49
|
+
});
|
|
50
|
+
server.registerTool("register_subprocess", {
|
|
51
|
+
title: "Synthesize isolation policy descriptors",
|
|
52
|
+
description: "Register a subprocess spec (binary + args) under a trust tier with an isolation profile. Returns a stable handle + profile fingerprint + policyDescriptor (landlockRuleset, sandboxExecProfile, cgroupsV2Limits). v0.1 emits descriptors only — the host spawner is responsible for translating them into platform syscalls. Native enforcement is v0.2.",
|
|
53
|
+
inputSchema: RegisterSubprocessArgsSchema.shape,
|
|
54
|
+
annotations: {
|
|
55
|
+
readOnlyHint: true,
|
|
56
|
+
destructiveHint: false,
|
|
57
|
+
idempotentHint: false,
|
|
58
|
+
openWorldHint: false
|
|
59
|
+
}
|
|
60
|
+
}, async (args) => safe(() => registerSubprocessTool(args)));
|
|
61
|
+
server.registerTool("audit_subprocess", {
|
|
62
|
+
title: "Audit a candidate args set",
|
|
63
|
+
description: "Verify candidate args against the registered subprocess. Runs Pillar-8 (NFKC + ZWC + Bidi) normalize + allowlist check. Returns approve / block / quarantine.",
|
|
64
|
+
inputSchema: AuditSubprocessArgsSchema.shape,
|
|
65
|
+
annotations: {
|
|
66
|
+
readOnlyHint: true,
|
|
67
|
+
destructiveHint: false,
|
|
68
|
+
idempotentHint: true,
|
|
69
|
+
openWorldHint: false
|
|
70
|
+
}
|
|
71
|
+
}, async (args) => safe(() => auditSubprocessTool(args)));
|
|
72
|
+
server.registerTool("scan_cve_replay", {
|
|
73
|
+
title: "Run CVE replay fixtures",
|
|
74
|
+
description: "Replay 2026 MCP CVE fixtures (mcp-sdk-rce-2026-04-22, cve-2026-27124, nginx-mcp-rce-9.8) against a candidate command. Returns overall + per-CVE pass/fail.",
|
|
75
|
+
inputSchema: ScanCveReplayArgsSchema.shape,
|
|
76
|
+
annotations: {
|
|
77
|
+
readOnlyHint: true,
|
|
78
|
+
destructiveHint: false,
|
|
79
|
+
idempotentHint: true,
|
|
80
|
+
openWorldHint: false
|
|
81
|
+
}
|
|
82
|
+
}, async (args) => safe(() => scanCveReplayTool(args)));
|
|
83
|
+
server.registerTool("track_canary", {
|
|
84
|
+
title: "Issue canary token + register chain",
|
|
85
|
+
description: "Register a multi-server canary chain. Returns a high-entropy token to inject into the source server's outbound flow. Cross-boundary leaks are detectable via the library detectLeaks() helper. State change is limited to the in-memory chain registry; no subprocess is mutated.",
|
|
86
|
+
inputSchema: TrackCanaryArgsSchema.shape,
|
|
87
|
+
annotations: {
|
|
88
|
+
readOnlyHint: true,
|
|
89
|
+
destructiveHint: false,
|
|
90
|
+
idempotentHint: false,
|
|
91
|
+
openWorldHint: false
|
|
92
|
+
}
|
|
93
|
+
}, async (args) => safe(() => trackCanaryTool(args)));
|
|
94
|
+
server.registerTool("inject_egress_policy", {
|
|
95
|
+
title: "Emit egress policy descriptor",
|
|
96
|
+
description: "Update the egress allowlist + mode (default-deny / audit-only) on a registered subprocess. v0.1 emits a descriptor only — no subprocess state, nftables, or packet-filter is modified by this call. The host spawner translates the descriptor into platform-specific enforcement. v0.2 will flip destructiveHint=true once native enforcement is wired.",
|
|
97
|
+
inputSchema: InjectEgressPolicyArgsSchema.shape,
|
|
98
|
+
annotations: {
|
|
99
|
+
readOnlyHint: true,
|
|
100
|
+
destructiveHint: false,
|
|
101
|
+
idempotentHint: true,
|
|
102
|
+
openWorldHint: false
|
|
103
|
+
}
|
|
104
|
+
}, async (args) => safe(() => injectEgressPolicyTool(args)));
|
|
105
|
+
server.registerTool("get_audit_log", {
|
|
106
|
+
title: "Read audit log",
|
|
107
|
+
description: "Read the append-only NDJSON audit log. Filter by subprocessHandle, since-timestamp, and limit.",
|
|
108
|
+
inputSchema: GetAuditLogArgsSchema.shape,
|
|
109
|
+
annotations: {
|
|
110
|
+
readOnlyHint: true,
|
|
111
|
+
destructiveHint: false,
|
|
112
|
+
idempotentHint: true,
|
|
113
|
+
openWorldHint: false
|
|
114
|
+
}
|
|
115
|
+
}, async (args) => safe(() => getAuditLogTool(args)));
|
|
116
|
+
return server;
|
|
117
|
+
}
|
|
118
|
+
export async function main() {
|
|
119
|
+
const server = createServer();
|
|
120
|
+
const transport = new StdioServerTransport();
|
|
121
|
+
// Graceful shutdown: SIGTERM + SIGINT close transport + flush stderr.
|
|
122
|
+
const shutdown = async (signal) => {
|
|
123
|
+
try {
|
|
124
|
+
await server.close();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// best-effort
|
|
128
|
+
}
|
|
129
|
+
process.stderr.write(`[mcp-rce-guard] received ${signal}, exiting\n`);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
};
|
|
132
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
133
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
134
|
+
await server.connect(transport);
|
|
135
|
+
process.stderr.write(`[mcp-rce-guard] ${VERSION} listening on stdio (6 tools)\n`);
|
|
136
|
+
}
|
|
137
|
+
const isDirect = (() => {
|
|
138
|
+
try {
|
|
139
|
+
const argvUrl = `file://${process.argv[1]}`;
|
|
140
|
+
return argvUrl === import.meta.url || process.argv[1]?.endsWith("server.js");
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
if (isDirect) {
|
|
147
|
+
main().catch((e) => {
|
|
148
|
+
process.stderr.write(`[mcp-rce-guard] fatal: ${e.message}\n`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=server.js.map
|