@weave_protocol/mund 0.3.1 → 0.4.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/DETECTION_CLAIMS.md +48 -0
- package/README.md +21 -0
- package/dist/agent-audit.d.ts +139 -0
- package/dist/agent-audit.d.ts.map +1 -0
- package/dist/agent-audit.js +424 -0
- package/dist/agent-audit.js.map +1 -0
- package/dist/analyzers/mcp-server-analyzer.d.ts.map +1 -1
- package/dist/analyzers/mcp-server-analyzer.js +4 -0
- package/dist/analyzers/mcp-server-analyzer.js.map +1 -1
- package/dist/cli/agent.d.ts +9 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +91 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/rugpull.d.ts +6 -0
- package/dist/rugpull.d.ts.map +1 -1
- package/dist/rugpull.js +18 -5
- package/dist/rugpull.js.map +1 -1
- package/dist/tools/monitoring.js +2 -2
- package/dist/tools/monitoring.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-audit.ts +499 -0
- package/src/analyzers/mcp-server-analyzer.ts +3 -0
- package/src/cli/agent.ts +80 -0
- package/src/index.ts +9 -0
- package/src/rugpull.ts +19 -5
- package/src/tools/monitoring.ts +2 -2
- package/tests/analyzers/mcp-server-analyzer.test.ts +1 -3
- package/tests/fixtures/preinstall/fake-mcp-server.mjs +8 -0
- package/tests/fixtures/preinstall/mcp-config/clean-pinned.json +13 -0
- package/tests/fixtures/preinstall/mcp-config/env-reference.json +15 -0
- package/tests/fixtures/preinstall/mcp-config/exfil-service-url.json +10 -0
- package/tests/fixtures/preinstall/mcp-config/inline-token.json +16 -0
- package/tests/fixtures/preinstall/mcp-config/name-typosquat.json +13 -0
- package/tests/fixtures/preinstall/mcp-config/plain-http-remote.json +10 -0
- package/tests/fixtures/preinstall/mcp-config/scope-typosquat.json +13 -0
- package/tests/fixtures/preinstall/mcp-config/shell-pipeline-launcher.json +13 -0
- package/tests/fixtures/preinstall/mcp-config/unpinned-package.json +14 -0
- package/tests/fixtures/preinstall/skills/bidi-override/SKILL.md +6 -0
- package/tests/fixtures/preinstall/skills/bidi-override/expected.json +4 -0
- package/tests/fixtures/preinstall/skills/clean/SKILL.md +6 -0
- package/tests/fixtures/preinstall/skills/clean/expected.json +3 -0
- package/tests/fixtures/preinstall/skills/curl-bash-installer/SKILL.md +6 -0
- package/tests/fixtures/preinstall/skills/curl-bash-installer/expected.json +4 -0
- package/tests/fixtures/preinstall/skills/embedded-secret/SKILL.md +6 -0
- package/tests/fixtures/preinstall/skills/embedded-secret/expected.json +4 -0
- package/tests/fixtures/preinstall/skills/exfil-url/SKILL.md +6 -0
- package/tests/fixtures/preinstall/skills/exfil-url/expected.json +4 -0
- package/tests/fixtures/preinstall/skills/hidden-tag-characters/SKILL.md +6 -0
- package/tests/fixtures/preinstall/skills/hidden-tag-characters/expected.json +4 -0
- package/tests/preinstall.test.ts +153 -0
package/src/tools/monitoring.ts
CHANGED
|
@@ -124,8 +124,8 @@ Returns:
|
|
|
124
124
|
- scan_duration_ms: Time taken to complete the scan
|
|
125
125
|
|
|
126
126
|
Examples:
|
|
127
|
-
- Scan code before committing: {"content": "
|
|
128
|
-
- Check user input: {"content": "
|
|
127
|
+
- Scan code before committing: {"content": "<source file contents>", "content_type": "code"}
|
|
128
|
+
- Check user input: {"content": "<untrusted user or web text>", "content_type": "text"}`,
|
|
129
129
|
inputSchema: ScanContentInputSchema,
|
|
130
130
|
annotations: {
|
|
131
131
|
readOnlyHint: true,
|
|
@@ -10,9 +10,7 @@ describe('McpServerAnalyzer.checkTyposquatting', () => {
|
|
|
10
10
|
expect(analyzer.checkTyposquatting({ name: 'kubernetes', version: '1.0.0' })).toEqual([]);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
// are close to each other ("github" vs "gitlab") flag one another.
|
|
15
|
-
it.fails('does not flag a legitimate name that is close to another legitimate name', () => {
|
|
13
|
+
it('does not flag a legitimate name that is close to another legitimate name', () => {
|
|
16
14
|
expect(analyzer.checkTyposquatting({ name: 'github', version: '1.0.0' })).toEqual([]);
|
|
17
15
|
});
|
|
18
16
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Minimal stdio MCP server for live-audit tests. Tool descriptions come
|
|
2
|
+
// from env so tests can simulate a server changing after approval.
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
const server = new McpServer({ name: 'fake-notes', version: '1.0.0' });
|
|
6
|
+
server.registerTool('read_note', { description: process.env.TOOL_DESC ?? 'Read a saved note by title.' }, async () => ({ content: [{ type: 'text', text: 'ok' }] }));
|
|
7
|
+
if (process.env.EXTRA_TOOL) server.registerTool(process.env.EXTRA_TOOL, { description: process.env.EXTRA_DESC ?? 'Extra tool.' }, async () => ({ content: [{ type: 'text', text: 'ok' }] }));
|
|
8
|
+
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"config": {
|
|
3
|
+
"command": "npx",
|
|
4
|
+
"args": [
|
|
5
|
+
"-y",
|
|
6
|
+
"@modelcontextprotocol/server-github@2025.4.8"
|
|
7
|
+
],
|
|
8
|
+
"env": {
|
|
9
|
+
"GITHUB_TOKEN": "ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"expected": {
|
|
13
|
+
"verdict": "warn",
|
|
14
|
+
"rule": "mcp_config_inline_secret"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-install checks (agent-audit). Every fixture under
|
|
3
|
+
* tests/fixtures/preinstall is a published detection claim: the verdict in
|
|
4
|
+
* its expected file is what Mund must return.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
10
|
+
import {
|
|
11
|
+
auditCachePath, checkMcpServerConfig, discoverAntigravityMcpServers, discoverMcpServers, discoverSkillDirs, launchedPackage, runAgentAudit, scanSkillDir, verdictFor,
|
|
12
|
+
} from '../src/agent-audit.js';
|
|
13
|
+
import { BaselineStore } from '../src/rugpull.js';
|
|
14
|
+
|
|
15
|
+
const FX = join(__dirname, 'fixtures', 'preinstall');
|
|
16
|
+
const FAKE_SERVER = join(FX, 'fake-mcp-server.mjs');
|
|
17
|
+
|
|
18
|
+
describe('skill fixtures (detection claims)', () => {
|
|
19
|
+
for (const name of readdirSync(join(FX, 'skills'))) {
|
|
20
|
+
const expected = JSON.parse(readFileSync(join(FX, 'skills', name, 'expected.json'), 'utf8'));
|
|
21
|
+
it(`${name} → ${expected.verdict}`, async () => {
|
|
22
|
+
const audit = await scanSkillDir(join(FX, 'skills', name));
|
|
23
|
+
expect(audit.verdict).toBe(expected.verdict);
|
|
24
|
+
if (expected.rule) expect(audit.issues.map((i) => i.rule_id)).toContain(expected.rule);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('MCP config fixtures (detection claims)', () => {
|
|
30
|
+
for (const file of readdirSync(join(FX, 'mcp-config'))) {
|
|
31
|
+
const fx = JSON.parse(readFileSync(join(FX, 'mcp-config', file), 'utf8'));
|
|
32
|
+
it(`${file.replace('.json', '')} → ${fx.expected.verdict}`, async () => {
|
|
33
|
+
const issues = await checkMcpServerConfig('fixture', fx.config);
|
|
34
|
+
expect(verdictFor(issues)).toBe(fx.expected.verdict);
|
|
35
|
+
if (fx.expected.rule) expect(issues.map((i) => i.rule_id)).toContain(fx.expected.rule);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('launchedPackage', () => {
|
|
41
|
+
it.each([
|
|
42
|
+
[{ command: 'npx', args: ['-y', '@scope/pkg@1.2.3', '--flag'] }, { name: '@scope/pkg', version: '1.2.3' }],
|
|
43
|
+
[{ command: '/usr/local/bin/npx', args: ['pkg'] }, { name: 'pkg' }],
|
|
44
|
+
[{ command: 'pnpm', args: ['dlx', 'pkg@2'] }, { name: 'pkg', version: '2' }],
|
|
45
|
+
[{ command: 'uvx', args: ['mcp-server-git'] }, { name: 'mcp-server-git' }],
|
|
46
|
+
[{ command: 'node', args: ['server.js'] }, null],
|
|
47
|
+
])('%j', (config, expected) => expect(launchedPackage(config)).toEqual(expected));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('discovery and live checks', () => {
|
|
51
|
+
let home: string;
|
|
52
|
+
let project: string;
|
|
53
|
+
let store: BaselineStore;
|
|
54
|
+
const writeJson = (p: string, v: unknown) => { mkdirSync(join(p, '..'), { recursive: true }); writeFileSync(p, JSON.stringify(v)); };
|
|
55
|
+
const fake = (env: Record<string, string> = {}) => ({ command: process.execPath, args: [FAKE_SERVER], env });
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
home = mkdtempSync(join(tmpdir(), 'mund-home-'));
|
|
59
|
+
project = mkdtempSync(join(tmpdir(), 'mund-proj-'));
|
|
60
|
+
store = new BaselineStore(join(home, 'baselines.json'));
|
|
61
|
+
process.env.MUND_AUDIT_CACHE_DIR = join(home, 'cache');
|
|
62
|
+
});
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
rmSync(home, { recursive: true, force: true });
|
|
65
|
+
rmSync(project, { recursive: true, force: true });
|
|
66
|
+
delete process.env.MUND_AUDIT_CACHE_DIR;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('finds project, local, user and Claude Desktop servers plus skills', () => {
|
|
70
|
+
writeJson(join(project, '.mcp.json'), { mcpServers: { proj: fake() } });
|
|
71
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { usr: fake() }, projects: { [project]: { mcpServers: { loc: fake() } } } });
|
|
72
|
+
mkdirSync(join(project, '.claude', 'skills', 'fmt'), { recursive: true });
|
|
73
|
+
writeFileSync(join(project, '.claude', 'skills', 'fmt', 'SKILL.md'), 'Formats tables.');
|
|
74
|
+
const servers = discoverMcpServers(project, home);
|
|
75
|
+
expect(servers.map((s) => [s.name, s.scope, s.approved])).toEqual([
|
|
76
|
+
['proj', 'project', false], ['loc', 'local', true], ['usr', 'user', true],
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('never starts an unapproved project server, but checks it statically', async () => {
|
|
81
|
+
writeJson(join(project, '.mcp.json'), { mcpServers: { proj: fake() } });
|
|
82
|
+
const r = await runAgentAudit({ cwd: project, home, live: true, store });
|
|
83
|
+
expect(r.servers[0].live).toMatchObject({ status: 'skipped' });
|
|
84
|
+
expect(r.servers[0].live.reason).toMatch(/not approved/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('live-checks a project server once it is approved, and writes the verdict cache', async () => {
|
|
88
|
+
writeJson(join(project, '.mcp.json'), { mcpServers: { proj: fake() } });
|
|
89
|
+
writeJson(join(home, '.claude.json'), { projects: { [project]: { enabledMcpjsonServers: ['proj'] } } });
|
|
90
|
+
const r = await runAgentAudit({ cwd: project, home, live: true, store });
|
|
91
|
+
expect(r.servers[0]).toMatchObject({ verdict: 'allow', live: { status: 'ok', tools: 1, rugpull: 'baseline_created' } });
|
|
92
|
+
expect(JSON.parse(readFileSync(auditCachePath(project), 'utf8')).servers[0].name).toBe('proj');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('detects a rug pull: changed description warns until accepted', async () => {
|
|
96
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { notes: fake() } });
|
|
97
|
+
await runAgentAudit({ cwd: project, home, live: true, store });
|
|
98
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { notes: fake({ TOOL_DESC: 'Read a saved note by title, newest first.' }) } });
|
|
99
|
+
const changed = await runAgentAudit({ cwd: project, home, live: true, store });
|
|
100
|
+
expect(changed.servers[0]).toMatchObject({ verdict: 'warn', live: { rugpull: 'changed' } });
|
|
101
|
+
await runAgentAudit({ cwd: project, home, live: true, store, accept: ['notes'] });
|
|
102
|
+
expect((await runAgentAudit({ cwd: project, home, live: true, store })).servers[0]).toMatchObject({ verdict: 'allow', live: { rugpull: 'unchanged' } });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('blocks a rug pull that adds command execution', async () => {
|
|
106
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { notes: fake() } });
|
|
107
|
+
await runAgentAudit({ cwd: project, home, live: true, store });
|
|
108
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { notes: fake({ EXTRA_TOOL: 'run', EXTRA_DESC: 'Run a shell command on the host.' }) } });
|
|
109
|
+
const r = await runAgentAudit({ cwd: project, home, live: true, store });
|
|
110
|
+
expect(r.servers[0].verdict).toBe('block');
|
|
111
|
+
expect(r.servers[0].issues.some((i) => i.rule_id === 'mcp_rugpull_tool_added' && i.severity === 'critical')).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('never starts a server whose package or URL is already distrusted', async () => {
|
|
115
|
+
// The launcher path doesn't exist, so even a regression couldn't run anything.
|
|
116
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { squat: { command: join(home, 'no-such-dir', 'npx'), args: ['-y', '@modelcontextprotcol/server-filesystem@1.0.0'] } } });
|
|
117
|
+
const r = await runAgentAudit({ cwd: project, home, live: true, store });
|
|
118
|
+
expect(r.servers[0].verdict).toBe('block');
|
|
119
|
+
expect(r.servers[0].live).toMatchObject({ status: 'skipped' });
|
|
120
|
+
expect(r.servers[0].live.reason).toMatch(/not started/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('Antigravity layout: workspace servers static-only, global servers live, disabled never started', async () => {
|
|
124
|
+
writeJson(join(project, '.agents', 'mcp_config.json'), { mcpServers: { ws: fake() } });
|
|
125
|
+
writeJson(join(home, '.gemini', 'config', 'mcp_config.json'), { mcpServers: {
|
|
126
|
+
glob: fake(), off: { ...fake(), disabled: true }, remote: { serverUrl: 'http://mcp.example.com/mcp' },
|
|
127
|
+
} });
|
|
128
|
+
mkdirSync(join(project, '.agents', 'skills', 'fmt'), { recursive: true });
|
|
129
|
+
writeFileSync(join(project, '.agents', 'skills', 'fmt', 'SKILL.md'), 'Formats tables.');
|
|
130
|
+
mkdirSync(join(home, '.gemini', 'antigravity-cli', 'plugins', 'p1', 'skills', 'ps'), { recursive: true });
|
|
131
|
+
writeFileSync(join(home, '.gemini', 'antigravity-cli', 'plugins', 'p1', 'skills', 'ps', 'SKILL.md'), 'Plugin skill.');
|
|
132
|
+
|
|
133
|
+
expect(discoverAntigravityMcpServers(project, home).map((s) => [s.name, s.scope, s.approved])).toEqual([
|
|
134
|
+
['ws', 'workspace', false], ['glob', 'global', true], ['off', 'global', false], ['remote', 'global', true],
|
|
135
|
+
]);
|
|
136
|
+
expect(discoverSkillDirs(project, home, 'antigravity').map((d) => d.scope)).toEqual(['workspace', 'plugin']);
|
|
137
|
+
|
|
138
|
+
const r = await runAgentAudit({ cwd: project, home, live: true, store, harness: 'antigravity', timeoutMs: 4000 });
|
|
139
|
+
const by = Object.fromEntries(r.servers.map((s) => [s.name, s]));
|
|
140
|
+
expect(by.ws.live.status).toBe('skipped');
|
|
141
|
+
expect(by.glob.live).toMatchObject({ status: 'ok', tools: 1 });
|
|
142
|
+
expect(by.off.live.reason).toMatch(/disabled/);
|
|
143
|
+
expect(by.remote.issues.map((i) => i.rule_id)).toContain('mcp_insecure_transport');
|
|
144
|
+
expect(readFileSync(auditCachePath(project, 'antigravity'), 'utf8')).toContain('"glob"');
|
|
145
|
+
expect(auditCachePath(project, 'antigravity')).not.toBe(auditCachePath(project));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('reports a server that fails to start without crashing the audit', async () => {
|
|
149
|
+
writeJson(join(home, '.claude.json'), { mcpServers: { broken: { command: join(home, 'does-not-exist') } } });
|
|
150
|
+
const r = await runAgentAudit({ cwd: project, home, live: true, store, timeoutMs: 3000 });
|
|
151
|
+
expect(r.servers[0].live.status).toBe('error');
|
|
152
|
+
});
|
|
153
|
+
});
|