@weave_protocol/mund 0.3.0 → 0.3.1

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.
Files changed (74) hide show
  1. package/README.md +70 -30
  2. package/dist/analyzers/index.d.ts.map +1 -1
  3. package/dist/analyzers/index.js +3 -1
  4. package/dist/analyzers/index.js.map +1 -1
  5. package/dist/canary.d.ts +48 -0
  6. package/dist/canary.d.ts.map +1 -0
  7. package/dist/canary.js +149 -0
  8. package/dist/canary.js.map +1 -0
  9. package/dist/cli/scan.d.ts +2 -1
  10. package/dist/cli/scan.d.ts.map +1 -1
  11. package/dist/cli/scan.js +10 -1
  12. package/dist/cli/scan.js.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/rugpull.d.ts +73 -0
  17. package/dist/rugpull.d.ts.map +1 -0
  18. package/dist/rugpull.js +177 -0
  19. package/dist/rugpull.js.map +1 -0
  20. package/dist/tools/canary-rugpull.d.ts +7 -0
  21. package/dist/tools/canary-rugpull.d.ts.map +1 -0
  22. package/dist/tools/canary-rugpull.js +50 -0
  23. package/dist/tools/canary-rugpull.js.map +1 -0
  24. package/dist/tools/index.d.ts +1 -0
  25. package/dist/tools/index.d.ts.map +1 -1
  26. package/dist/tools/index.js +4 -0
  27. package/dist/tools/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/analyzers/index.ts +3 -1
  30. package/src/canary.ts +165 -0
  31. package/src/cli/scan.ts +10 -1
  32. package/src/index.ts +2 -0
  33. package/src/rugpull.ts +233 -0
  34. package/src/tools/canary-rugpull.ts +75 -0
  35. package/src/tools/index.ts +5 -0
  36. package/tests/analyzers/engine.test.ts +1 -1
  37. package/tests/canary.test.ts +79 -0
  38. package/tests/cli/scan.test.ts +2 -2
  39. package/tests/fixtures/canary/base64.json +9 -0
  40. package/tests/fixtures/canary/clean-output.json +8 -0
  41. package/tests/fixtures/canary/exact-leak.json +9 -0
  42. package/tests/fixtures/canary/lowercased.json +9 -0
  43. package/tests/fixtures/canary/other-token.json +8 -0
  44. package/tests/fixtures/canary/prefix-only.json +8 -0
  45. package/tests/fixtures/canary/reversed.json +9 -0
  46. package/tests/fixtures/canary/spaced-out.json +9 -0
  47. package/tests/fixtures/canary/zero-width.json +9 -0
  48. package/tests/fixtures/rugpull/description-changed/baseline.json +36 -0
  49. package/tests/fixtures/rugpull/description-changed/current.json +36 -0
  50. package/tests/fixtures/rugpull/description-changed/expected.json +11 -0
  51. package/tests/fixtures/rugpull/gains-execution/baseline.json +36 -0
  52. package/tests/fixtures/rugpull/gains-execution/current.json +36 -0
  53. package/tests/fixtures/rugpull/gains-execution/expected.json +18 -0
  54. package/tests/fixtures/rugpull/schema-changed/baseline.json +36 -0
  55. package/tests/fixtures/rugpull/schema-changed/current.json +39 -0
  56. package/tests/fixtures/rugpull/schema-changed/expected.json +11 -0
  57. package/tests/fixtures/rugpull/schema-key-reorder/baseline.json +36 -0
  58. package/tests/fixtures/rugpull/schema-key-reorder/current.json +36 -0
  59. package/tests/fixtures/rugpull/schema-key-reorder/expected.json +5 -0
  60. package/tests/fixtures/rugpull/server-renamed/baseline.json +36 -0
  61. package/tests/fixtures/rugpull/server-renamed/current.json +36 -0
  62. package/tests/fixtures/rugpull/server-renamed/expected.json +11 -0
  63. package/tests/fixtures/rugpull/tool-added/baseline.json +36 -0
  64. package/tests/fixtures/rugpull/tool-added/current.json +44 -0
  65. package/tests/fixtures/rugpull/tool-added/expected.json +11 -0
  66. package/tests/fixtures/rugpull/tool-removed/baseline.json +36 -0
  67. package/tests/fixtures/rugpull/tool-removed/current.json +21 -0
  68. package/tests/fixtures/rugpull/tool-removed/expected.json +11 -0
  69. package/tests/fixtures/rugpull/unchanged/baseline.json +36 -0
  70. package/tests/fixtures/rugpull/unchanged/current.json +36 -0
  71. package/tests/fixtures/rugpull/unchanged/expected.json +5 -0
  72. package/tests/fixtures.test.ts +40 -0
  73. package/tests/rugpull.test.ts +80 -0
  74. package/tests/tools/canary-rugpull.test.ts +57 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Mund - The Guardian Protocol
3
+ * MCP Rug-Pull Detection (OWASP ASI04)
4
+ *
5
+ * A rug pull is an MCP server that passes review at install time and
6
+ * then changes what its tools say or do. Static scanning can't see that,
7
+ * so Mund records a fingerprint of every tool when a server is first
8
+ * approved (the baseline) and diffs later manifests against it.
9
+ *
10
+ * tool description changed -> high (the classic rug pull)
11
+ * tool input schema changed -> medium
12
+ * tool added -> medium
13
+ * tool removed -> low
14
+ * server name changed -> high
15
+ *
16
+ * Any changed or added tool is re-scanned with the MCP analyzer; if it
17
+ * now trips a high/critical finding, or newly gains execution
18
+ * capability, the change is escalated to critical.
19
+ */
20
+ import type { IAnalyzer, SecurityIssue, Severity } from './types.js';
21
+ import { type McpServerManifest } from './analyzers/mcp-server-analyzer.js';
22
+ export type ToolChangeKind = 'description_changed' | 'schema_changed' | 'tool_added' | 'tool_removed' | 'server_renamed';
23
+ export interface ToolFingerprint {
24
+ descriptionHash: string;
25
+ schemaHash: string;
26
+ /** Kept so a diff can show what the description used to say. */
27
+ description: string;
28
+ }
29
+ export interface ManifestBaseline {
30
+ serverId: string;
31
+ serverName: string;
32
+ recordedAt: string;
33
+ tools: Record<string, ToolFingerprint>;
34
+ }
35
+ export interface ToolChange {
36
+ kind: ToolChangeKind;
37
+ tool: string;
38
+ severity: Severity;
39
+ detail: string;
40
+ /** Set when the changed tool now trips the MCP analyzer or gains execution capability. */
41
+ escalated?: string;
42
+ }
43
+ export interface RugPullResult {
44
+ serverId: string;
45
+ status: 'baseline_created' | 'unchanged' | 'changed';
46
+ changes: ToolChange[];
47
+ baselineRecordedAt?: string;
48
+ }
49
+ export declare function defaultBaselinePath(): string;
50
+ export declare function fingerprintManifest(serverId: string, manifest: McpServerManifest): ManifestBaseline;
51
+ /** Compare a manifest against its baseline. Pure: no I/O. */
52
+ export declare function diffManifest(baseline: ManifestBaseline, manifest: McpServerManifest, analyzer?: IAnalyzer): Promise<ToolChange[]>;
53
+ /** Persisted baselines, keyed by server id. */
54
+ export declare class BaselineStore {
55
+ private path;
56
+ constructor(path?: string);
57
+ load(): Promise<Record<string, ManifestBaseline>>;
58
+ get(serverId: string): Promise<ManifestBaseline | undefined>;
59
+ put(baseline: ManifestBaseline): Promise<void>;
60
+ }
61
+ /**
62
+ * Check a manifest against its stored baseline. The first check for a
63
+ * server records the baseline. Pass acceptChanges to re-baseline after
64
+ * a human has reviewed the changes.
65
+ */
66
+ export declare function checkRugPull(serverId: string, manifest: McpServerManifest, opts?: {
67
+ store?: BaselineStore;
68
+ analyzer?: IAnalyzer;
69
+ acceptChanges?: boolean;
70
+ }): Promise<RugPullResult>;
71
+ /** Express rug-pull changes as standard SecurityIssues (for scoring / SARIF). */
72
+ export declare function changesToIssues(result: RugPullResult): SecurityIssue[];
73
+ //# sourceMappingURL=rugpull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rugpull.d.ts","sourceRoot":"","sources":["../src/rugpull.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAqB,KAAK,iBAAiB,EAA0B,MAAM,oCAAoC,CAAC;AAEvH,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAEzH,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,GAAG,WAAW,GAAG,SAAS,CAAC;IACrD,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,mBAAmB,IAAI,MAAM,CAI5C;AAcD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAUnG;AAED,6DAA6D;AAC7D,wBAAsB,YAAY,CAChC,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,GAAE,SAAmC,GAC5C,OAAO,CAAC,UAAU,EAAE,CAAC,CAgDvB;AAoBD,+CAA+C;AAC/C,qBAAa,aAAa;IACZ,OAAO,CAAC,IAAI;gBAAJ,IAAI,GAAE,MAA8B;IAElD,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IASjD,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAI5D,GAAG,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAMrD;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GAClF,OAAO,CAAC,aAAa,CAAC,CAmBxB;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,EAAE,CAYtE"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Mund - The Guardian Protocol
3
+ * MCP Rug-Pull Detection (OWASP ASI04)
4
+ *
5
+ * A rug pull is an MCP server that passes review at install time and
6
+ * then changes what its tools say or do. Static scanning can't see that,
7
+ * so Mund records a fingerprint of every tool when a server is first
8
+ * approved (the baseline) and diffs later manifests against it.
9
+ *
10
+ * tool description changed -> high (the classic rug pull)
11
+ * tool input schema changed -> medium
12
+ * tool added -> medium
13
+ * tool removed -> low
14
+ * server name changed -> high
15
+ *
16
+ * Any changed or added tool is re-scanned with the MCP analyzer; if it
17
+ * now trips a high/critical finding, or newly gains execution
18
+ * capability, the change is escalated to critical.
19
+ */
20
+ import { createHash } from 'crypto';
21
+ import { readFile, writeFile, mkdir } from 'fs/promises';
22
+ import { homedir } from 'os';
23
+ import { dirname, join } from 'path';
24
+ import { McpServerAnalyzer } from './analyzers/mcp-server-analyzer.js';
25
+ const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
26
+ export function defaultBaselinePath() {
27
+ if (process.env.MUND_BASELINE_PATH)
28
+ return process.env.MUND_BASELINE_PATH;
29
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
30
+ return join(base, 'mund', 'mcp-baselines.json');
31
+ }
32
+ /** Stable JSON: object keys sorted so equivalent schemas hash the same. */
33
+ function canonical(value) {
34
+ if (Array.isArray(value))
35
+ return `[${value.map(canonical).join(',')}]`;
36
+ if (value && typeof value === 'object') {
37
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
38
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(',')}}`;
39
+ }
40
+ return JSON.stringify(value ?? null);
41
+ }
42
+ const sha256 = (s) => createHash('sha256').update(s).digest('hex');
43
+ export function fingerprintManifest(serverId, manifest) {
44
+ const tools = {};
45
+ for (const tool of manifest.tools ?? []) {
46
+ tools[tool.name] = {
47
+ descriptionHash: sha256(tool.description ?? ''),
48
+ schemaHash: sha256(canonical(tool.inputSchema ?? {})),
49
+ description: (tool.description ?? '').slice(0, 2000),
50
+ };
51
+ }
52
+ return { serverId, serverName: manifest.name ?? serverId, recordedAt: new Date().toISOString(), tools };
53
+ }
54
+ /** Compare a manifest against its baseline. Pure: no I/O. */
55
+ export async function diffManifest(baseline, manifest, analyzer = new McpServerAnalyzer()) {
56
+ const current = fingerprintManifest(baseline.serverId, manifest);
57
+ const byName = new Map((manifest.tools ?? []).map(t => [t.name, t]));
58
+ const changes = [];
59
+ if (current.serverName !== baseline.serverName) {
60
+ changes.push({
61
+ kind: 'server_renamed', tool: '*', severity: 'high',
62
+ detail: `Server name changed from "${baseline.serverName}" to "${current.serverName}"`,
63
+ });
64
+ }
65
+ for (const [name, now] of Object.entries(current.tools)) {
66
+ const before = baseline.tools[name];
67
+ if (!before) {
68
+ changes.push({ kind: 'tool_added', tool: name, severity: 'medium', detail: `New tool "${name}" not present at baseline` });
69
+ continue;
70
+ }
71
+ if (now.descriptionHash !== before.descriptionHash) {
72
+ changes.push({
73
+ kind: 'description_changed', tool: name, severity: 'high',
74
+ detail: `Description of "${name}" changed after approval`,
75
+ });
76
+ }
77
+ if (now.schemaHash !== before.schemaHash) {
78
+ changes.push({ kind: 'schema_changed', tool: name, severity: 'medium', detail: `Input schema of "${name}" changed after approval` });
79
+ }
80
+ }
81
+ for (const name of Object.keys(baseline.tools)) {
82
+ if (!current.tools[name]) {
83
+ changes.push({ kind: 'tool_removed', tool: name, severity: 'low', detail: `Tool "${name}" removed since baseline` });
84
+ }
85
+ }
86
+ // Escalate changed/added tools that now look dangerous.
87
+ for (const change of changes) {
88
+ if (change.kind === 'tool_removed' || change.kind === 'server_renamed')
89
+ continue;
90
+ const tool = byName.get(change.tool);
91
+ if (!tool)
92
+ continue;
93
+ const reason = await escalationReason(tool, baseline.tools[change.tool], analyzer);
94
+ if (reason) {
95
+ change.severity = 'critical';
96
+ change.escalated = reason;
97
+ }
98
+ }
99
+ return changes.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
100
+ }
101
+ async function escalationReason(tool, before, analyzer) {
102
+ const single = JSON.stringify({ name: 'rugpull-check', tools: [tool] });
103
+ const findings = await analyzer.analyze(single, []);
104
+ const serious = findings.find(f => f.severity === 'critical' || f.severity === 'high');
105
+ if (serious)
106
+ return `Changed tool now triggers "${serious.rule_name}"`;
107
+ const nowExec = McpServerAnalyzer.analyzeCapabilities({ name: 'x', version: '0', tools: [tool] }).execution;
108
+ const wasExec = before
109
+ ? McpServerAnalyzer.analyzeCapabilities({ name: 'x', version: '0', tools: [{ name: tool.name, description: before.description }] }).execution
110
+ : false;
111
+ if (nowExec && !wasExec)
112
+ return 'Tool newly requests command-execution capability';
113
+ return undefined;
114
+ }
115
+ /** Persisted baselines, keyed by server id. */
116
+ export class BaselineStore {
117
+ path;
118
+ constructor(path = defaultBaselinePath()) {
119
+ this.path = path;
120
+ }
121
+ async load() {
122
+ try {
123
+ const data = JSON.parse(await readFile(this.path, 'utf8'));
124
+ return data && typeof data === 'object' && data.version === 1 ? data.baselines : {};
125
+ }
126
+ catch {
127
+ return {};
128
+ }
129
+ }
130
+ async get(serverId) {
131
+ return (await this.load())[serverId];
132
+ }
133
+ async put(baseline) {
134
+ const all = await this.load();
135
+ all[baseline.serverId] = baseline;
136
+ await mkdir(dirname(this.path), { recursive: true });
137
+ await writeFile(this.path, JSON.stringify({ version: 1, baselines: all }, null, 2));
138
+ }
139
+ }
140
+ /**
141
+ * Check a manifest against its stored baseline. The first check for a
142
+ * server records the baseline. Pass acceptChanges to re-baseline after
143
+ * a human has reviewed the changes.
144
+ */
145
+ export async function checkRugPull(serverId, manifest, opts = {}) {
146
+ const store = opts.store ?? new BaselineStore();
147
+ const baseline = await store.get(serverId);
148
+ if (!baseline) {
149
+ await store.put(fingerprintManifest(serverId, manifest));
150
+ return { serverId, status: 'baseline_created', changes: [] };
151
+ }
152
+ const changes = await diffManifest(baseline, manifest, opts.analyzer);
153
+ if (opts.acceptChanges && changes.length > 0) {
154
+ await store.put(fingerprintManifest(serverId, manifest));
155
+ }
156
+ return {
157
+ serverId,
158
+ status: changes.length > 0 ? 'changed' : 'unchanged',
159
+ changes,
160
+ baselineRecordedAt: baseline.recordedAt,
161
+ };
162
+ }
163
+ /** Express rug-pull changes as standard SecurityIssues (for scoring / SARIF). */
164
+ export function changesToIssues(result) {
165
+ return result.changes.map(c => ({
166
+ rule_id: `mcp_rugpull_${c.kind}`,
167
+ rule_name: `MCP rug pull: ${c.detail}`,
168
+ type: 'mcp_rugpull',
169
+ severity: c.severity,
170
+ action: c.severity === 'critical' || c.severity === 'high' ? 'block' : 'alert',
171
+ match: `${result.serverId}:${c.tool}`,
172
+ suggestion: c.escalated
173
+ ? `${c.escalated}. Do not use this server until the change is reviewed.`
174
+ : 'Review the change before continuing to use this server; accept it to update the baseline.',
175
+ }));
176
+ }
177
+ //# sourceMappingURL=rugpull.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rugpull.js","sourceRoot":"","sources":["../src/rugpull.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAErC,OAAO,EAAE,iBAAiB,EAAkD,MAAM,oCAAoC,CAAC;AAkCvH,MAAM,aAAa,GAA6B,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAErG,MAAM,UAAU,mBAAmB;IACjC,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC1E,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,oBAAoB,CAAC,CAAC;AAClD,CAAC;AAED,2EAA2E;AAC3E,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACvE,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QACxG,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC1F,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAE3E,MAAM,UAAU,mBAAmB,CAAC,QAAgB,EAAE,QAA2B;IAC/E,MAAM,KAAK,GAAoC,EAAE,CAAC;IAClD,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YACjB,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;YAC/C,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;YACrD,WAAW,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC;SACrD,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,IAAI,IAAI,QAAQ,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC;AAC1G,CAAC;AAED,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAA0B,EAC1B,QAA2B,EAC3B,WAAsB,IAAI,iBAAiB,EAAE;IAE7C,MAAM,OAAO,GAAG,mBAAmB,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,MAAM,OAAO,GAAiB,EAAE,CAAC;IAEjC,IAAI,OAAO,CAAC,UAAU,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM;YACnD,MAAM,EAAE,6BAA6B,QAAQ,CAAC,UAAU,SAAS,OAAO,CAAC,UAAU,GAAG;SACvF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,IAAI,2BAA2B,EAAE,CAAC,CAAC;YAC3H,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,eAAe,KAAK,MAAM,CAAC,eAAe,EAAE,CAAC;YACnD,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM;gBACzD,MAAM,EAAE,mBAAmB,IAAI,0BAA0B;aAC1D,CAAC,CAAC;QACL,CAAC;QACD,IAAI,GAAG,CAAC,UAAU,KAAK,MAAM,CAAC,UAAU,EAAE,CAAC;YACzC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,IAAI,0BAA0B,EAAE,CAAC,CAAC;QACvI,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,IAAI,0BAA0B,EAAE,CAAC,CAAC;QACvH,CAAC;IACH,CAAC;IAED,wDAAwD;IACxD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB;YAAE,SAAS;QACjF,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;QACnF,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,QAAQ,GAAG,UAAU,CAAC;YAC7B,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AACvF,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,IAAuB,EACvB,MAAmC,EACnC,QAAmB;IAEnB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;IACvF,IAAI,OAAO;QAAE,OAAO,8BAA8B,OAAO,CAAC,SAAS,GAAG,CAAC;IAEvE,MAAM,OAAO,GAAG,iBAAiB,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IAC5G,MAAM,OAAO,GAAG,MAAM;QACpB,CAAC,CAAC,iBAAiB,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,SAAS;QAC7I,CAAC,CAAC,KAAK,CAAC;IACV,IAAI,OAAO,IAAI,CAAC,OAAO;QAAE,OAAO,kDAAkD,CAAC;IACnF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,+CAA+C;AAC/C,MAAM,OAAO,aAAa;IACJ;IAApB,YAAoB,OAAe,mBAAmB,EAAE;QAApC,SAAI,GAAJ,IAAI,CAAgC;IAAG,CAAC;IAE5D,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YAC3D,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACtF,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,QAAgB;QACxB,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,QAA0B;QAClC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC9B,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;QAClC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACtF,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,QAAgB,EAChB,QAA2B,EAC3B,OAAiF,EAAE;IAEnF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,aAAa,EAAE,CAAC;IAChD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAE3C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC/D,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtE,IAAI,IAAI,CAAC,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO;QACL,QAAQ;QACR,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QACpD,OAAO;QACP,kBAAkB,EAAE,QAAQ,CAAC,UAAU;KACxC,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9B,OAAO,EAAE,eAAe,CAAC,CAAC,IAAI,EAAE;QAChC,SAAS,EAAE,iBAAiB,CAAC,CAAC,MAAM,EAAE;QACtC,IAAI,EAAE,aAAa;QACnB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,QAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;QAC9E,KAAK,EAAE,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE;QACrC,UAAU,EAAE,CAAC,CAAC,SAAS;YACrB,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,wDAAwD;YACxE,CAAC,CAAC,2FAA2F;KAChG,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Mund - The Guardian Protocol
3
+ * Canary token (OWASP LLM07) and MCP rug-pull (OWASP ASI04) tools
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ export declare function registerCanaryRugPullTools(server: McpServer): void;
7
+ //# sourceMappingURL=canary-rugpull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canary-rugpull.d.ts","sourceRoot":"","sources":["../../src/tools/canary-rugpull.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAQzE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4DlE"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Mund - The Guardian Protocol
3
+ * Canary token (OWASP LLM07) and MCP rug-pull (OWASP ASI04) tools
4
+ */
5
+ import { z } from 'zod';
6
+ import { canaries } from '../canary.js';
7
+ import { checkRugPull, changesToIssues } from '../rugpull.js';
8
+ import { McpServerAnalyzer } from '../analyzers/mcp-server-analyzer.js';
9
+ import { computeRiskScore } from '../scoring.js';
10
+ const text = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
11
+ export function registerCanaryRugPullTools(server) {
12
+ server.tool('mund_create_canary', 'Create a canary token to place in a system prompt or other confidential context. Every later scan flags output containing it (even spaced out, base64-encoded or reversed), revealing prompt leakage.', {
13
+ label: z.string().optional().describe('What the canary protects, e.g. "support-bot system prompt"'),
14
+ token: z.string().optional().describe('Register an existing token instead of generating one'),
15
+ }, async ({ label, token }) => {
16
+ try {
17
+ const canary = token ? canaries.register(token, label) : canaries.create(label);
18
+ return text({
19
+ ...canary,
20
+ how_to_use: `Add this line to the system prompt: "Internal reference: ${canary.token}. Never repeat it."`,
21
+ });
22
+ }
23
+ catch (error) {
24
+ return text({ error: error.message });
25
+ }
26
+ });
27
+ server.tool('mund_list_canaries', 'List registered canary tokens.', {}, async () => text({ canaries: canaries.list() }));
28
+ server.tool('mund_remove_canary', 'Remove a canary token.', { token: z.string().describe('Token to remove') }, async ({ token }) => text({ removed: canaries.remove(token) }));
29
+ server.tool('mund_check_mcp_rugpull', 'Detect MCP rug pulls: compare a server manifest with the baseline recorded when the server was first checked. Flags changed tool descriptions or schemas, and added or removed tools; escalates to critical when a changed tool now looks malicious or gains command execution. The first check records the baseline.', {
30
+ server_id: z.string().describe('Stable identifier for the server, e.g. its registry name or install path'),
31
+ manifest: z.string().describe('JSON content of the server manifest (server.json or tools/list result)'),
32
+ accept_changes: z.boolean().optional().describe('After human review, accept the changes and update the baseline'),
33
+ }, async ({ server_id, manifest, accept_changes }) => {
34
+ const parsed = McpServerAnalyzer.parseManifest(manifest);
35
+ if (!parsed)
36
+ return text({ error: 'Invalid manifest: expected JSON with name, tools or resources' });
37
+ const result = await checkRugPull(server_id, parsed, { acceptChanges: accept_changes });
38
+ const risk = computeRiskScore(changesToIssues(result));
39
+ return text({
40
+ ...result,
41
+ risk_score: risk.score,
42
+ risk_label: risk.label,
43
+ recommendation: result.status === 'baseline_created' ? 'BASELINE_RECORDED'
44
+ : result.changes.some(c => c.severity === 'critical') ? 'DO_NOT_USE'
45
+ : result.changes.length > 0 ? (accept_changes ? 'CHANGES_ACCEPTED' : 'REVIEW_CHANGES')
46
+ : 'UNCHANGED',
47
+ });
48
+ });
49
+ }
50
+ //# sourceMappingURL=canary-rugpull.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canary-rugpull.js","sourceRoot":"","sources":["../../src/tools/canary-rugpull.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@
5
5
  export { registerMonitoringTools } from './monitoring.js';
6
6
  export { registerConfigurationTools } from './configuration.js';
7
7
  export { registerMcpScanningTools } from './mcp-scanning.js';
8
+ export { registerCanaryRugPullTools } from './canary-rugpull.js';
8
9
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
10
  import type { IStorage, DetectionRule } from '../types.js';
10
11
  import { AnalyzerEngine } from '../analyzers/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAK7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,aAAa,EAAE,EACtB,QAAQ,EAAE,cAAc,EACxB,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,OAAO,GACjB,IAAI,CASN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAMjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,aAAa,EAAE,EACtB,QAAQ,EAAE,cAAc,EACxB,eAAe,EAAE,eAAe,EAChC,SAAS,EAAE,OAAO,GACjB,IAAI,CAYN"}
@@ -5,9 +5,11 @@
5
5
  export { registerMonitoringTools } from './monitoring.js';
6
6
  export { registerConfigurationTools } from './configuration.js';
7
7
  export { registerMcpScanningTools } from './mcp-scanning.js';
8
+ export { registerCanaryRugPullTools } from './canary-rugpull.js';
8
9
  import { registerMonitoringTools } from './monitoring.js';
9
10
  import { registerConfigurationTools } from './configuration.js';
10
11
  import { registerMcpScanningTools } from './mcp-scanning.js';
12
+ import { registerCanaryRugPullTools } from './canary-rugpull.js';
11
13
  /**
12
14
  * Register all Mund MCP tools
13
15
  */
@@ -18,5 +20,7 @@ export function registerAllTools(server, storage, rules, analyzer, notificationH
18
20
  registerConfigurationTools(server, storage, rules);
19
21
  // MCP server scanning tools (new)
20
22
  registerMcpScanningTools(server);
23
+ // Canary tokens (LLM07) and MCP rug-pull detection (ASI04)
24
+ registerCanaryRugPullTools(server);
21
25
  }
22
26
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAM7D;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAiB,EACjB,OAAiB,EACjB,KAAsB,EACtB,QAAwB,EACxB,eAAgC,EAChC,SAAkB;IAElB,wBAAwB;IACxB,uBAAuB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;IAEtF,iCAAiC;IACjC,0BAA0B,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAEnD,kCAAkC;IAClC,wBAAwB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAMjE;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAiB,EACjB,OAAiB,EACjB,KAAsB,EACtB,QAAwB,EACxB,eAAgC,EAChC,SAAkB;IAElB,wBAAwB;IACxB,uBAAuB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;IAEtF,iCAAiC;IACjC,0BAA0B,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAEnD,kCAAkC;IAClC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAEjC,2DAA2D;IAC3D,0BAA0B,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weave_protocol/mund",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "The Guardian Protocol - MCP-based security monitoring for agentic AI systems",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -16,6 +16,7 @@ import { CodeAnalyzer } from './code-analyzer.js';
16
16
  import { InjectionDetector } from './injection-detector.js';
17
17
  import { ExfiltrationDetector } from './exfiltration-detector.js';
18
18
  import { McpServerAnalyzer } from './mcp-server-analyzer.js';
19
+ import { CanaryDetector } from '../canary.js';
19
20
  import type { IAnalyzer } from '../types.js';
20
21
 
21
22
  /**
@@ -28,7 +29,8 @@ export function getAnalyzers(): IAnalyzer[] {
28
29
  new CodeAnalyzer(),
29
30
  new InjectionDetector(),
30
31
  new ExfiltrationDetector(),
31
- new McpServerAnalyzer()
32
+ new McpServerAnalyzer(),
33
+ new CanaryDetector()
32
34
  ];
33
35
  }
34
36
 
package/src/canary.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Mund - The Guardian Protocol
3
+ * Canary Tokens - detect system prompt leakage (OWASP LLM07)
4
+ *
5
+ * Generate a unique marker, place it in your system prompt (or any
6
+ * confidential context), and Mund flags any scanned output that
7
+ * contains it. A model that has been talked into revealing its
8
+ * instructions reveals the canary with them.
9
+ *
10
+ * Matching tolerates the cheap evasions a leaking model or an attacker
11
+ * might apply: case changes, inserted whitespace / zero-width
12
+ * characters / punctuation, base64 encoding, and reversal.
13
+ */
14
+
15
+ import { randomBytes } from 'crypto';
16
+ import { readFileSync } from 'fs';
17
+ import type { DetectionRule, IAnalyzer, SecurityIssue } from './types.js';
18
+
19
+ export interface Canary {
20
+ token: string;
21
+ label: string;
22
+ createdAt: string;
23
+ }
24
+
25
+ export type CanaryMatchKind = 'exact' | 'obfuscated' | 'base64' | 'reversed';
26
+
27
+ const TOKEN_PREFIX = 'MUND-CANARY-';
28
+ const MIN_TOKEN_LENGTH = 12;
29
+ const ZERO_WIDTH = /[​-‍⁠]/g;
30
+
31
+ /** Lowercase and keep only letters and digits. */
32
+ function squash(text: string): string {
33
+ return text.replace(ZERO_WIDTH, '').toLowerCase().replace(/[^a-z0-9]/g, '');
34
+ }
35
+
36
+ export class CanaryRegistry {
37
+ private canaries = new Map<string, Canary>();
38
+
39
+ /** Create a new random canary token. */
40
+ create(label = 'system-prompt'): Canary {
41
+ const canary: Canary = {
42
+ token: TOKEN_PREFIX + randomBytes(8).toString('hex').toUpperCase(),
43
+ label,
44
+ createdAt: new Date().toISOString(),
45
+ };
46
+ this.canaries.set(canary.token, canary);
47
+ return canary;
48
+ }
49
+
50
+ /** Register an existing token (e.g. one already deployed in a prompt). */
51
+ register(token: string, label = 'system-prompt'): Canary {
52
+ if (squash(token).length < MIN_TOKEN_LENGTH) {
53
+ throw new Error(`Canary token must contain at least ${MIN_TOKEN_LENGTH} letters/digits to avoid false positives`);
54
+ }
55
+ const canary: Canary = { token, label, createdAt: new Date().toISOString() };
56
+ this.canaries.set(token, canary);
57
+ return canary;
58
+ }
59
+
60
+ remove(token: string): boolean {
61
+ return this.canaries.delete(token);
62
+ }
63
+
64
+ list(): Canary[] {
65
+ return [...this.canaries.values()];
66
+ }
67
+
68
+ get size(): number {
69
+ return this.canaries.size;
70
+ }
71
+
72
+ /** Find every registered canary that appears in the content. */
73
+ find(content: string): Array<{ canary: Canary; kind: CanaryMatchKind; index: number }> {
74
+ const hits: Array<{ canary: Canary; kind: CanaryMatchKind; index: number }> = [];
75
+ if (this.canaries.size === 0 || !content) return hits;
76
+
77
+ const lower = content.toLowerCase();
78
+ const squashed = squash(content);
79
+ const decoded = decodeBase64Runs(content).map(squash);
80
+
81
+ for (const canary of this.canaries.values()) {
82
+ const needle = squash(canary.token);
83
+ const exactAt = lower.indexOf(canary.token.toLowerCase());
84
+
85
+ if (exactAt !== -1) {
86
+ hits.push({ canary, kind: 'exact', index: exactAt });
87
+ } else if (squashed.includes(needle)) {
88
+ hits.push({ canary, kind: 'obfuscated', index: -1 });
89
+ } else if (decoded.some(d => d.includes(needle))) {
90
+ hits.push({ canary, kind: 'base64', index: -1 });
91
+ } else if (squashed.includes([...needle].reverse().join(''))) {
92
+ hits.push({ canary, kind: 'reversed', index: -1 });
93
+ }
94
+ }
95
+ return hits;
96
+ }
97
+ }
98
+
99
+ /** Decode every plausible base64 run in the text (best effort). */
100
+ function decodeBase64Runs(content: string): string[] {
101
+ const runs = content.match(/[A-Za-z0-9+/]{16,}={0,2}/g) ?? [];
102
+ const out: string[] = [];
103
+ for (const run of runs) {
104
+ try {
105
+ const text = Buffer.from(run, 'base64').toString('utf8');
106
+ if (/^[\x20-\x7E\s]+$/.test(text)) out.push(text);
107
+ } catch {
108
+ // not base64
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /** Shared registry used by the MCP tools and the default analyzer set. */
115
+ export const canaries = new CanaryRegistry();
116
+
117
+ /** Load canaries from MUND_CANARIES (comma-separated) or MUND_CANARY_FILE (one per line). */
118
+ export function loadCanariesFromEnv(registry: CanaryRegistry = canaries): number {
119
+ const tokens: string[] = [];
120
+ if (process.env.MUND_CANARIES) tokens.push(...process.env.MUND_CANARIES.split(','));
121
+ if (process.env.MUND_CANARY_FILE) {
122
+ try {
123
+ tokens.push(...readFileSync(process.env.MUND_CANARY_FILE, 'utf8').split('\n'));
124
+ } catch {
125
+ // missing file: nothing to load
126
+ }
127
+ }
128
+ let loaded = 0;
129
+ for (const t of tokens.map(t => t.trim()).filter(Boolean)) {
130
+ try {
131
+ registry.register(t, 'env');
132
+ loaded++;
133
+ } catch {
134
+ // too short; skip
135
+ }
136
+ }
137
+ return loaded;
138
+ }
139
+
140
+ export class CanaryDetector implements IAnalyzer {
141
+ name = 'CanaryDetector';
142
+ type = 'exfiltration';
143
+
144
+ constructor(private registry: CanaryRegistry = canaries) {}
145
+
146
+ async analyze(content: string, _rules: DetectionRule[]): Promise<SecurityIssue[]> {
147
+ return this.registry.find(content).map(({ canary, kind, index }) => {
148
+ const before = index >= 0 ? content.slice(0, index).split('\n') : null;
149
+ return {
150
+ rule_id: 'canary_leak',
151
+ rule_name: `Canary token leaked (${kind})`,
152
+ type: 'exfiltration',
153
+ severity: 'critical',
154
+ action: 'block',
155
+ match: `${canary.token.slice(0, TOKEN_PREFIX.length + 4)}…`,
156
+ ...(before
157
+ ? { location: { start: index, end: index + canary.token.length, line: before.length, column: before[before.length - 1].length + 1 } }
158
+ : {}),
159
+ suggestion:
160
+ `Content contains the "${canary.label}" canary, so confidential instructions or context are leaking. ` +
161
+ 'Block this output and review the prompt that produced it.',
162
+ };
163
+ });
164
+ }
165
+ }
package/src/cli/scan.ts CHANGED
@@ -16,6 +16,7 @@ import { computeRiskScore, type RiskScore } from '../scoring.js';
16
16
  import { toSarif, type SarifTarget } from '../sarif.js';
17
17
  import { OsvClient, parseNpmLockfile, advisoriesToIssues, type OsvSource } from '../osv.js';
18
18
  import { getVersion } from '../version.js';
19
+ import { canaries, loadCanariesFromEnv } from '../canary.js';
19
20
 
20
21
  export type OutputFormat = 'text' | 'json' | 'sarif';
21
22
  type FailOn = Severity | 'none';
@@ -27,6 +28,7 @@ export interface ScanCliOptions {
27
28
  failOn: FailOn;
28
29
  osv: boolean;
29
30
  offline: boolean;
31
+ canaries: string[];
30
32
  }
31
33
 
32
34
  export interface FileReport extends SarifTarget {
@@ -58,6 +60,8 @@ Options:
58
60
  (critical, high [default], medium, low, none)
59
61
  --no-osv Skip dependency vulnerability lookups
60
62
  --offline Use only the local OSV cache (also: MUND_OFFLINE=1)
63
+ --canary=TOKEN Flag any file containing this canary token (repeatable;
64
+ also MUND_CANARIES / MUND_CANARY_FILE)
61
65
 
62
66
  Examples:
63
67
  mund-mcp scan .
@@ -65,7 +69,7 @@ Examples:
65
69
  `;
66
70
 
67
71
  export function parseScanArgs(args: string[]): ScanCliOptions {
68
- const opts: ScanCliOptions = { paths: [], format: 'text', failOn: 'high', osv: true, offline: false };
72
+ const opts: ScanCliOptions = { paths: [], format: 'text', failOn: 'high', osv: true, offline: false, canaries: [] };
69
73
 
70
74
  for (let i = 0; i < args.length; i++) {
71
75
  const arg = args[i];
@@ -99,6 +103,9 @@ export function parseScanArgs(args: string[]): ScanCliOptions {
99
103
  case '--offline':
100
104
  opts.offline = true;
101
105
  break;
106
+ case '--canary':
107
+ opts.canaries.push(value());
108
+ break;
102
109
  default:
103
110
  if (flag.startsWith('-')) throw new Error(`Unknown option ${flag}`);
104
111
  opts.paths.push(arg);
@@ -111,6 +118,8 @@ export function parseScanArgs(args: string[]): ScanCliOptions {
111
118
 
112
119
  export async function runScan(opts: ScanCliOptions, osvClient?: OsvClient): Promise<ScanReport> {
113
120
  const cwd = process.cwd();
121
+ loadCanariesFromEnv();
122
+ for (const token of opts.canaries) canaries.register(token, 'cli');
114
123
  const engine = new AnalyzerEngine();
115
124
  const files: FileReport[] = [];
116
125
  const lockfiles: string[] = [];
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import { NotificationHub } from './notifications/index.js';
17
17
  import { registerAllTools } from './tools/index.js';
18
18
  import { runScanCli } from './cli/scan.js';
19
19
  import { getVersion } from './version.js';
20
+ import { loadCanariesFromEnv } from './canary.js';
20
21
 
21
22
  // Threat Intelligence imports
22
23
  import { threatIntel } from './threat-intel-manager.js';
@@ -180,6 +181,7 @@ async function createMundServer(config: MundConfig): Promise<McpServer> {
180
181
  }
181
182
 
182
183
  // Register all core tools
184
+ loadCanariesFromEnv();
183
185
  registerAllTools(server, storage, rules, analyzer, notificationHub, config.block_mode);
184
186
 
185
187
  // Register threat intelligence tools