@weave_protocol/mund 0.2.3 â 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.
- package/README.md +135 -1
- package/dist/analyzers/code-analyzer.d.ts.map +1 -1
- package/dist/analyzers/code-analyzer.js +2 -1
- package/dist/analyzers/code-analyzer.js.map +1 -1
- package/dist/analyzers/exfiltration-detector.d.ts.map +1 -1
- package/dist/analyzers/exfiltration-detector.js +2 -1
- package/dist/analyzers/exfiltration-detector.js.map +1 -1
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +4 -2
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/injection-detector.d.ts.map +1 -1
- package/dist/analyzers/injection-detector.js +2 -1
- package/dist/analyzers/injection-detector.js.map +1 -1
- package/dist/analyzers/mcp-server-analyzer.d.ts +2 -0
- package/dist/analyzers/mcp-server-analyzer.d.ts.map +1 -1
- package/dist/analyzers/mcp-server-analyzer.js.map +1 -1
- package/dist/analyzers/pii-detector.d.ts.map +1 -1
- package/dist/analyzers/pii-detector.js +2 -1
- package/dist/analyzers/pii-detector.js.map +1 -1
- package/dist/analyzers/regex.d.ts +10 -0
- package/dist/analyzers/regex.d.ts.map +1 -0
- package/dist/analyzers/regex.js +17 -0
- package/dist/analyzers/regex.js.map +1 -0
- package/dist/analyzers/secret-scanner.d.ts.map +1 -1
- package/dist/analyzers/secret-scanner.js +2 -1
- package/dist/analyzers/secret-scanner.js.map +1 -1
- package/dist/canary.d.ts +48 -0
- package/dist/canary.d.ts.map +1 -0
- package/dist/canary.js +149 -0
- package/dist/canary.js.map +1 -0
- package/dist/cli/scan.d.ts +50 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +235 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/notifications/email.d.ts +21 -8
- package/dist/notifications/email.d.ts.map +1 -1
- package/dist/notifications/email.js +32 -178
- package/dist/notifications/email.js.map +1 -1
- package/dist/osv.d.ts +94 -0
- package/dist/osv.d.ts.map +1 -0
- package/dist/osv.js +239 -0
- package/dist/osv.js.map +1 -0
- package/dist/rugpull.d.ts +73 -0
- package/dist/rugpull.d.ts.map +1 -0
- package/dist/rugpull.js +177 -0
- package/dist/rugpull.js.map +1 -0
- package/dist/sarif.d.ts +20 -0
- package/dist/sarif.d.ts.map +1 -0
- package/dist/sarif.js +122 -0
- package/dist/sarif.js.map +1 -0
- package/dist/scoring.d.ts +26 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +49 -0
- package/dist/scoring.js.map +1 -0
- package/dist/threat-intel-manager.d.ts.map +1 -1
- package/dist/threat-intel-manager.js +2 -1
- package/dist/threat-intel-manager.js.map +1 -1
- package/dist/tools/canary-rugpull.d.ts +7 -0
- package/dist/tools/canary-rugpull.d.ts.map +1 -0
- package/dist/tools/canary-rugpull.js +50 -0
- package/dist/tools/canary-rugpull.js.map +1 -0
- package/dist/tools/configuration.d.ts.map +1 -1
- package/dist/tools/configuration.js +2 -1
- package/dist/tools/configuration.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp-scanning.d.ts.map +1 -1
- package/dist/tools/mcp-scanning.js +4 -0
- package/dist/tools/mcp-scanning.js.map +1 -1
- package/dist/tools/monitoring.d.ts.map +1 -1
- package/dist/tools/monitoring.js +22 -1
- package/dist/tools/monitoring.js.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -4
- package/src/analyzers/code-analyzer.ts +2 -1
- package/src/analyzers/exfiltration-detector.ts +2 -1
- package/src/analyzers/index.ts +4 -2
- package/src/analyzers/injection-detector.ts +2 -1
- package/src/analyzers/mcp-server-analyzer.ts +2 -0
- package/src/analyzers/pii-detector.ts +2 -1
- package/src/analyzers/regex.ts +17 -0
- package/src/analyzers/secret-scanner.ts +2 -1
- package/src/canary.ts +165 -0
- package/src/cli/scan.ts +260 -0
- package/src/index.ts +13 -4
- package/src/notifications/email.ts +38 -194
- package/src/osv.ts +310 -0
- package/src/rugpull.ts +233 -0
- package/src/sarif.ts +146 -0
- package/src/scoring.ts +64 -0
- package/src/threat-intel-manager.ts +2 -1
- package/src/tools/canary-rugpull.ts +75 -0
- package/src/tools/configuration.ts +2 -1
- package/src/tools/index.ts +5 -0
- package/src/tools/mcp-scanning.ts +5 -0
- package/src/tools/monitoring.ts +23 -1
- package/src/version.ts +20 -0
- package/tests/analyzers/engine.test.ts +60 -0
- package/tests/analyzers/injection-and-code.test.ts +80 -0
- package/tests/analyzers/mcp-server-analyzer.test.ts +82 -0
- package/tests/analyzers/regex.test.ts +35 -0
- package/tests/analyzers/rules.test.ts +54 -0
- package/tests/analyzers/secret-scanner.test.ts +80 -0
- package/tests/canary.test.ts +79 -0
- package/tests/cli/scan.test.ts +92 -0
- package/tests/fixtures/canary/base64.json +9 -0
- package/tests/fixtures/canary/clean-output.json +8 -0
- package/tests/fixtures/canary/exact-leak.json +9 -0
- package/tests/fixtures/canary/lowercased.json +9 -0
- package/tests/fixtures/canary/other-token.json +8 -0
- package/tests/fixtures/canary/prefix-only.json +8 -0
- package/tests/fixtures/canary/reversed.json +9 -0
- package/tests/fixtures/canary/spaced-out.json +9 -0
- package/tests/fixtures/canary/zero-width.json +9 -0
- package/tests/fixtures/rugpull/description-changed/baseline.json +36 -0
- package/tests/fixtures/rugpull/description-changed/current.json +36 -0
- package/tests/fixtures/rugpull/description-changed/expected.json +11 -0
- package/tests/fixtures/rugpull/gains-execution/baseline.json +36 -0
- package/tests/fixtures/rugpull/gains-execution/current.json +36 -0
- package/tests/fixtures/rugpull/gains-execution/expected.json +18 -0
- package/tests/fixtures/rugpull/schema-changed/baseline.json +36 -0
- package/tests/fixtures/rugpull/schema-changed/current.json +39 -0
- package/tests/fixtures/rugpull/schema-changed/expected.json +11 -0
- package/tests/fixtures/rugpull/schema-key-reorder/baseline.json +36 -0
- package/tests/fixtures/rugpull/schema-key-reorder/current.json +36 -0
- package/tests/fixtures/rugpull/schema-key-reorder/expected.json +5 -0
- package/tests/fixtures/rugpull/server-renamed/baseline.json +36 -0
- package/tests/fixtures/rugpull/server-renamed/current.json +36 -0
- package/tests/fixtures/rugpull/server-renamed/expected.json +11 -0
- package/tests/fixtures/rugpull/tool-added/baseline.json +36 -0
- package/tests/fixtures/rugpull/tool-added/current.json +44 -0
- package/tests/fixtures/rugpull/tool-added/expected.json +11 -0
- package/tests/fixtures/rugpull/tool-removed/baseline.json +36 -0
- package/tests/fixtures/rugpull/tool-removed/current.json +21 -0
- package/tests/fixtures/rugpull/tool-removed/expected.json +11 -0
- package/tests/fixtures/rugpull/unchanged/baseline.json +36 -0
- package/tests/fixtures/rugpull/unchanged/current.json +36 -0
- package/tests/fixtures/rugpull/unchanged/expected.json +5 -0
- package/tests/fixtures.test.ts +40 -0
- package/tests/helpers.ts +51 -0
- package/tests/notifications/email.test.ts +68 -0
- package/tests/notifications/hub.test.ts +160 -0
- package/tests/osv.test.ts +117 -0
- package/tests/rugpull.test.ts +80 -0
- package/tests/sarif.test.ts +59 -0
- package/tests/scoring.test.ts +49 -0
- package/tests/tools/canary-rugpull.test.ts +57 -0
- package/tests/tools/monitoring.test.ts +95 -0
- package/vitest.config.ts +8 -0
package/src/cli/scan.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mund - The Guardian Protocol
|
|
3
|
+
* `mund-mcp scan` - scan files on disk and report as text, JSON or SARIF
|
|
4
|
+
*
|
|
5
|
+
* mund-mcp scan [paths...] [--format=text|json|sarif] [--output=FILE]
|
|
6
|
+
* [--fail-on=critical|high|medium|low|none]
|
|
7
|
+
* [--no-osv] [--offline]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile, writeFile, readdir, stat } from 'fs/promises';
|
|
11
|
+
import { basename, join, relative, resolve, sep } from 'path';
|
|
12
|
+
import type { SecurityIssue, Severity } from '../types.js';
|
|
13
|
+
import { DEFAULT_RULES } from '../constants.js';
|
|
14
|
+
import { AnalyzerEngine } from '../analyzers/index.js';
|
|
15
|
+
import { computeRiskScore, type RiskScore } from '../scoring.js';
|
|
16
|
+
import { toSarif, type SarifTarget } from '../sarif.js';
|
|
17
|
+
import { OsvClient, parseNpmLockfile, advisoriesToIssues, type OsvSource } from '../osv.js';
|
|
18
|
+
import { getVersion } from '../version.js';
|
|
19
|
+
import { canaries, loadCanariesFromEnv } from '../canary.js';
|
|
20
|
+
|
|
21
|
+
export type OutputFormat = 'text' | 'json' | 'sarif';
|
|
22
|
+
type FailOn = Severity | 'none';
|
|
23
|
+
|
|
24
|
+
export interface ScanCliOptions {
|
|
25
|
+
paths: string[];
|
|
26
|
+
format: OutputFormat;
|
|
27
|
+
output?: string;
|
|
28
|
+
failOn: FailOn;
|
|
29
|
+
osv: boolean;
|
|
30
|
+
offline: boolean;
|
|
31
|
+
canaries: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FileReport extends SarifTarget {
|
|
35
|
+
risk: RiskScore;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScanReport {
|
|
39
|
+
tool: { name: 'mund'; version: string };
|
|
40
|
+
risk: RiskScore;
|
|
41
|
+
files_scanned: number;
|
|
42
|
+
osv?: { source: OsvSource; lockfiles: number; unchecked: number; error?: string };
|
|
43
|
+
files: FileReport[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.turbo', 'vendor', '__pycache__', '.venv']);
|
|
47
|
+
const LOCKFILES = new Set(['package-lock.json', 'npm-shrinkwrap.json']);
|
|
48
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
49
|
+
const SEVERITY_RANK: Record<Severity, number> = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
50
|
+
|
|
51
|
+
export const SCAN_USAGE = `Usage: mund-mcp scan [paths...] [options]
|
|
52
|
+
|
|
53
|
+
Scan files for secrets, PII, injection, exfiltration and dangerous code,
|
|
54
|
+
plus known-vulnerable npm dependencies (via OSV.dev) in package-lock.json.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--format=FORMAT text (default), json, or sarif
|
|
58
|
+
--output=FILE Write the report to FILE instead of stdout
|
|
59
|
+
--fail-on=LEVEL Exit 1 if any finding is at or above LEVEL
|
|
60
|
+
(critical, high [default], medium, low, none)
|
|
61
|
+
--no-osv Skip dependency vulnerability lookups
|
|
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)
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
mund-mcp scan .
|
|
68
|
+
mund-mcp scan src --format=sarif --output=mund.sarif
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
export function parseScanArgs(args: string[]): ScanCliOptions {
|
|
72
|
+
const opts: ScanCliOptions = { paths: [], format: 'text', failOn: 'high', osv: true, offline: false, canaries: [] };
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < args.length; i++) {
|
|
75
|
+
const arg = args[i];
|
|
76
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, undefined];
|
|
77
|
+
const value = () => {
|
|
78
|
+
const v = inline ?? args[++i];
|
|
79
|
+
if (v === undefined) throw new Error(`${flag} needs a value`);
|
|
80
|
+
return v;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
switch (flag) {
|
|
84
|
+
case '--format': {
|
|
85
|
+
const v = value();
|
|
86
|
+
if (v !== 'text' && v !== 'json' && v !== 'sarif') throw new Error(`Unknown format "${v}" (expected text, json or sarif)`);
|
|
87
|
+
opts.format = v;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case '--output':
|
|
91
|
+
case '-o':
|
|
92
|
+
opts.output = value();
|
|
93
|
+
break;
|
|
94
|
+
case '--fail-on': {
|
|
95
|
+
const v = value();
|
|
96
|
+
if (v !== 'none' && !(v in SEVERITY_RANK)) throw new Error(`Unknown --fail-on level "${v}"`);
|
|
97
|
+
opts.failOn = v as FailOn;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case '--no-osv':
|
|
101
|
+
opts.osv = false;
|
|
102
|
+
break;
|
|
103
|
+
case '--offline':
|
|
104
|
+
opts.offline = true;
|
|
105
|
+
break;
|
|
106
|
+
case '--canary':
|
|
107
|
+
opts.canaries.push(value());
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
if (flag.startsWith('-')) throw new Error(`Unknown option ${flag}`);
|
|
111
|
+
opts.paths.push(arg);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (opts.paths.length === 0) opts.paths.push('.');
|
|
116
|
+
return opts;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function runScan(opts: ScanCliOptions, osvClient?: OsvClient): Promise<ScanReport> {
|
|
120
|
+
const cwd = process.cwd();
|
|
121
|
+
loadCanariesFromEnv();
|
|
122
|
+
for (const token of opts.canaries) canaries.register(token, 'cli');
|
|
123
|
+
const engine = new AnalyzerEngine();
|
|
124
|
+
const files: FileReport[] = [];
|
|
125
|
+
const lockfiles: string[] = [];
|
|
126
|
+
let scanned = 0;
|
|
127
|
+
|
|
128
|
+
for (const file of await collectFiles(opts.paths.map(p => resolve(cwd, p)))) {
|
|
129
|
+
if (LOCKFILES.has(basename(file))) {
|
|
130
|
+
lockfiles.push(file);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const content = await readText(file);
|
|
134
|
+
if (content === null) continue;
|
|
135
|
+
scanned++;
|
|
136
|
+
const issues = await engine.analyzeAll(content, DEFAULT_RULES);
|
|
137
|
+
if (issues.length > 0) files.push({ uri: toUri(cwd, file), issues, risk: computeRiskScore(issues) });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let osv: ScanReport['osv'];
|
|
141
|
+
if (opts.osv && lockfiles.length > 0) {
|
|
142
|
+
const client = osvClient ?? new OsvClient({ offline: opts.offline });
|
|
143
|
+
let source: OsvSource = 'live';
|
|
144
|
+
let unchecked = 0;
|
|
145
|
+
let error: string | undefined;
|
|
146
|
+
|
|
147
|
+
for (const lockfile of lockfiles) {
|
|
148
|
+
const content = await readFile(lockfile, 'utf8');
|
|
149
|
+
let packages;
|
|
150
|
+
try {
|
|
151
|
+
packages = parseNpmLockfile(content);
|
|
152
|
+
} catch {
|
|
153
|
+
continue; // malformed lockfile; nothing to look up
|
|
154
|
+
}
|
|
155
|
+
const result = await client.lookup(packages);
|
|
156
|
+
// Report the weakest source across lockfiles.
|
|
157
|
+
if (result.source === 'unavailable' || (result.source === 'cache' && source === 'live')) source = result.source;
|
|
158
|
+
unchecked += result.unchecked.length;
|
|
159
|
+
error ??= result.error;
|
|
160
|
+
|
|
161
|
+
const issues = advisoriesToIssues(packages, result, content);
|
|
162
|
+
if (issues.length > 0) files.push({ uri: toUri(cwd, lockfile), issues, risk: computeRiskScore(issues) });
|
|
163
|
+
}
|
|
164
|
+
osv = { source, lockfiles: lockfiles.length, unchecked, ...(error ? { error } : {}) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
tool: { name: 'mund', version: getVersion() },
|
|
169
|
+
risk: computeRiskScore(files.flatMap(f => f.issues)),
|
|
170
|
+
files_scanned: scanned,
|
|
171
|
+
...(osv ? { osv } : {}),
|
|
172
|
+
files,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function formatReport(report: ScanReport, format: OutputFormat): string {
|
|
177
|
+
if (format === 'json') return JSON.stringify(report, null, 2);
|
|
178
|
+
if (format === 'sarif') return JSON.stringify(toSarif(report.files, { toolVersion: report.tool.version }), null, 2);
|
|
179
|
+
|
|
180
|
+
const lines: string[] = [];
|
|
181
|
+
for (const file of report.files) {
|
|
182
|
+
lines.push(`${file.uri} (risk ${file.risk.score}/100, ${file.risk.label})`);
|
|
183
|
+
for (const issue of file.issues) {
|
|
184
|
+
const where = issue.location?.line ? `${issue.location.line}:${issue.location.column ?? 1}` : '-';
|
|
185
|
+
lines.push(` ${where.padEnd(8)} ${issue.severity.padEnd(8)} ${issue.rule_name}`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const total = report.files.reduce((n, f) => n + f.issues.length, 0);
|
|
191
|
+
const { counts } = report.risk;
|
|
192
|
+
lines.push(`Scanned ${report.files_scanned} file(s): ${total} finding(s) ` +
|
|
193
|
+
`(${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low)`);
|
|
194
|
+
lines.push(`Risk score: ${report.risk.score}/100 (${report.risk.label})`);
|
|
195
|
+
if (report.osv) {
|
|
196
|
+
const note = report.osv.source === 'live' ? 'live' : `${report.osv.source} - ${report.osv.error ?? ''}`.trim();
|
|
197
|
+
lines.push(`Dependencies: OSV.dev (${note})` + (report.osv.unchecked ? `, ${report.osv.unchecked} package(s) unchecked` : ''));
|
|
198
|
+
}
|
|
199
|
+
return lines.join('\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function shouldFail(report: ScanReport, failOn: FailOn): boolean {
|
|
203
|
+
if (failOn === 'none') return false;
|
|
204
|
+
const threshold = SEVERITY_RANK[failOn];
|
|
205
|
+
return report.files.some(f => f.issues.some((i: SecurityIssue) => SEVERITY_RANK[i.severity] <= threshold));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Entry point for `mund-mcp scan`. Returns the process exit code. */
|
|
209
|
+
export async function runScanCli(args: string[]): Promise<number> {
|
|
210
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
211
|
+
process.stdout.write(SCAN_USAGE);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let opts: ScanCliOptions;
|
|
216
|
+
try {
|
|
217
|
+
opts = parseScanArgs(args);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
process.stderr.write(`mund scan: ${(error as Error).message}\n\n${SCAN_USAGE}`);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const report = await runScan(opts);
|
|
224
|
+
const out = formatReport(report, opts.format) + '\n';
|
|
225
|
+
if (opts.output) await writeFile(opts.output, out);
|
|
226
|
+
else process.stdout.write(out);
|
|
227
|
+
|
|
228
|
+
return shouldFail(report, opts.failOn) ? 1 : 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function collectFiles(roots: string[]): Promise<string[]> {
|
|
232
|
+
const out: string[] = [];
|
|
233
|
+
const walk = async (path: string) => {
|
|
234
|
+
const info = await stat(path).catch(() => null);
|
|
235
|
+
if (!info) return;
|
|
236
|
+
if (info.isFile()) {
|
|
237
|
+
out.push(path);
|
|
238
|
+
} else if (info.isDirectory()) {
|
|
239
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
240
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
241
|
+
if (entry.isDirectory() || entry.isFile()) await walk(join(path, entry.name));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
for (const root of roots) await walk(root);
|
|
246
|
+
return out.sort();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Read a file as UTF-8 text, or null if it's too big or looks binary. */
|
|
250
|
+
async function readText(file: string): Promise<string | null> {
|
|
251
|
+
const info = await stat(file);
|
|
252
|
+
if (info.size > MAX_FILE_BYTES) return null;
|
|
253
|
+
const buf = await readFile(file);
|
|
254
|
+
if (buf.subarray(0, 8000).includes(0)) return null;
|
|
255
|
+
return buf.toString('utf8');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function toUri(cwd: string, file: string): string {
|
|
259
|
+
return relative(cwd, file).split(sep).join('/');
|
|
260
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ import { createStorage } from './storage/index.js';
|
|
|
15
15
|
import { AnalyzerEngine } from './analyzers/index.js';
|
|
16
16
|
import { NotificationHub } from './notifications/index.js';
|
|
17
17
|
import { registerAllTools } from './tools/index.js';
|
|
18
|
+
import { runScanCli } from './cli/scan.js';
|
|
19
|
+
import { getVersion } from './version.js';
|
|
20
|
+
import { loadCanariesFromEnv } from './canary.js';
|
|
18
21
|
|
|
19
22
|
// Threat Intelligence imports
|
|
20
23
|
import { threatIntel } from './threat-intel-manager.js';
|
|
@@ -154,7 +157,7 @@ async function createMundServer(config: MundConfig): Promise<McpServer> {
|
|
|
154
157
|
// Create MCP server
|
|
155
158
|
const server = new McpServer({
|
|
156
159
|
name: 'mund-mcp-server',
|
|
157
|
-
version:
|
|
160
|
+
version: getVersion()
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
// Initialize storage
|
|
@@ -178,6 +181,7 @@ async function createMundServer(config: MundConfig): Promise<McpServer> {
|
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
// Register all core tools
|
|
184
|
+
loadCanariesFromEnv();
|
|
181
185
|
registerAllTools(server, storage, rules, analyzer, notificationHub, config.block_mode);
|
|
182
186
|
|
|
183
187
|
// Register threat intelligence tools
|
|
@@ -218,7 +222,7 @@ async function runHTTP(config: MundConfig): Promise<void> {
|
|
|
218
222
|
|
|
219
223
|
// Health check endpoint
|
|
220
224
|
app.get('/health', (_req, res) => {
|
|
221
|
-
res.json({ status: 'ok', version:
|
|
225
|
+
res.json({ status: 'ok', version: getVersion() });
|
|
222
226
|
});
|
|
223
227
|
|
|
224
228
|
// Threat intel status endpoint
|
|
@@ -254,7 +258,8 @@ function printHelp(): void {
|
|
|
254
258
|
Mund - The Guardian Protocol
|
|
255
259
|
MCP-based security monitoring for agentic AI systems
|
|
256
260
|
|
|
257
|
-
Usage: mund-mcp [options]
|
|
261
|
+
Usage: mund-mcp [options] Run the MCP server
|
|
262
|
+
mund-mcp scan [paths...] Scan files (see: mund-mcp scan --help)
|
|
258
263
|
|
|
259
264
|
Options:
|
|
260
265
|
--help, -h Show this help message
|
|
@@ -314,6 +319,10 @@ For more information, visit: https://github.com/Tyox-all/Weave_Protocol
|
|
|
314
319
|
async function main(): Promise<void> {
|
|
315
320
|
const args = process.argv.slice(2);
|
|
316
321
|
|
|
322
|
+
if (args[0] === 'scan') {
|
|
323
|
+
process.exit(await runScanCli(args.slice(1)));
|
|
324
|
+
}
|
|
325
|
+
|
|
317
326
|
// Parse CLI arguments
|
|
318
327
|
if (args.includes('--help') || args.includes('-h')) {
|
|
319
328
|
printHelp();
|
|
@@ -321,7 +330,7 @@ async function main(): Promise<void> {
|
|
|
321
330
|
}
|
|
322
331
|
|
|
323
332
|
if (args.includes('--version') || args.includes('-v')) {
|
|
324
|
-
console.log(
|
|
333
|
+
console.log(getVersion());
|
|
325
334
|
process.exit(0);
|
|
326
335
|
}
|
|
327
336
|
|
|
@@ -1,210 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mund - The Guardian Protocol
|
|
3
|
-
* Email Notifier -
|
|
3
|
+
* Email Notifier - NOT IMPLEMENTED in v0.2.x.
|
|
4
|
+
*
|
|
5
|
+
* The nodemailer dependency was removed because it carried a
|
|
6
|
+
* high-severity CVE and was never actually wired up (the previous
|
|
7
|
+
* revision had only comments like "In a real implementation, use
|
|
8
|
+
* nodemailer here"). Carrying a prod dep for a feature nobody uses
|
|
9
|
+
* meant inheriting its CVE for no benefit.
|
|
10
|
+
*
|
|
11
|
+
* This stub keeps the INotifier contract so NotificationManager can
|
|
12
|
+
* still wire it up when `notificationConfig.email.smtp_host` is set,
|
|
13
|
+
* but every send() call returns false and logs a one-time warning,
|
|
14
|
+
* so operators see email alerts aren't actually being delivered.
|
|
15
|
+
*
|
|
16
|
+
* When/if email notifications become a real feature:
|
|
17
|
+
* 1. npm install nodemailer@^10 @types/nodemailer
|
|
18
|
+
* 2. Replace this stub with a real implementation
|
|
19
|
+
* 3. Add test fixtures under test/notifications/
|
|
20
|
+
* 4. Remove the console.warn in the constructor
|
|
4
21
|
*/
|
|
5
22
|
|
|
6
|
-
import type { INotifier, NotificationPayload, EmailConfig
|
|
7
|
-
|
|
8
|
-
// Note: In production, use nodemailer
|
|
9
|
-
// For now, we'll implement a basic interface that can be extended
|
|
23
|
+
import type { INotifier, NotificationPayload, EmailConfig } from '../types.js';
|
|
10
24
|
|
|
11
25
|
export class EmailNotifier implements INotifier {
|
|
12
26
|
name = 'EmailNotifier';
|
|
13
|
-
|
|
27
|
+
// Config is retained so a future real implementation can see what
|
|
28
|
+
// shape to expect. Public (not private) because noUnusedLocals flags
|
|
29
|
+
// unread private members even with an underscore prefix.
|
|
30
|
+
readonly config: EmailConfig;
|
|
31
|
+
private warned = false;
|
|
14
32
|
|
|
15
33
|
constructor(config: EmailConfig) {
|
|
16
34
|
this.config = config;
|
|
35
|
+
console.warn(
|
|
36
|
+
'[mund] EmailNotifier configured but NOT IMPLEMENTED in this version. ' +
|
|
37
|
+
'Email alerts will be dropped. Use a webhook, Slack, or Teams notifier ' +
|
|
38
|
+
'for actual delivery. Track at ' +
|
|
39
|
+
'https://github.com/Tyox-all/Weave_Protocol/issues',
|
|
40
|
+
);
|
|
17
41
|
}
|
|
18
42
|
|
|
19
|
-
async send(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const emailContent = this.formatEmail(payload);
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
// In a real implementation, use nodemailer here
|
|
31
|
-
// For now, we'll log the email content
|
|
32
|
-
console.log('Email notification would be sent:');
|
|
33
|
-
console.log('To:', this.config.to_addresses.join(', '));
|
|
34
|
-
console.log('Subject:', emailContent.subject);
|
|
35
|
-
console.log('Body:', emailContent.html.substring(0, 500) + '...');
|
|
36
|
-
|
|
37
|
-
// Uncomment when nodemailer is configured:
|
|
38
|
-
// const transporter = nodemailer.createTransport({
|
|
39
|
-
// host: this.config.smtp_host,
|
|
40
|
-
// port: this.config.smtp_port,
|
|
41
|
-
// secure: this.config.smtp_secure,
|
|
42
|
-
// auth: this.config.smtp_user ? {
|
|
43
|
-
// user: this.config.smtp_user,
|
|
44
|
-
// pass: this.config.smtp_pass
|
|
45
|
-
// } : undefined
|
|
46
|
-
// });
|
|
47
|
-
//
|
|
48
|
-
// await transporter.sendMail({
|
|
49
|
-
// from: this.config.from_address,
|
|
50
|
-
// to: this.config.to_addresses.join(', '),
|
|
51
|
-
// subject: emailContent.subject,
|
|
52
|
-
// html: emailContent.html,
|
|
53
|
-
// text: emailContent.text
|
|
54
|
-
// });
|
|
55
|
-
|
|
56
|
-
return true;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.error('Failed to send email notification:', error);
|
|
59
|
-
return false;
|
|
43
|
+
async send(_payload: NotificationPayload): Promise<boolean> {
|
|
44
|
+
if (!this.warned) {
|
|
45
|
+
console.error(
|
|
46
|
+
'[mund] EmailNotifier.send() called but email delivery is not ' +
|
|
47
|
+
'implemented. Alert dropped. (This warning fires once per process.)',
|
|
48
|
+
);
|
|
49
|
+
this.warned = true;
|
|
60
50
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
private formatEmail(payload: NotificationPayload): { subject: string; html: string; text: string } {
|
|
64
|
-
const { event } = payload;
|
|
65
|
-
const severityEmoji = this.getSeverityEmoji(event.severity);
|
|
66
|
-
const severityColor = this.getSeverityColor(event.severity);
|
|
67
|
-
|
|
68
|
-
const subject = `${severityEmoji} [Mund ${event.severity.toUpperCase()}] ${event.rule_name}`;
|
|
69
|
-
|
|
70
|
-
const html = `
|
|
71
|
-
<!DOCTYPE html>
|
|
72
|
-
<html>
|
|
73
|
-
<head>
|
|
74
|
-
<style>
|
|
75
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
|
76
|
-
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
77
|
-
.header { background: ${severityColor}; color: white; padding: 20px; border-radius: 8px 8px 0 0; }
|
|
78
|
-
.content { background: #f8f9fa; padding: 20px; border: 1px solid #dee2e6; border-top: none; border-radius: 0 0 8px 8px; }
|
|
79
|
-
.snippet { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 4px; font-family: monospace; overflow-x: auto; }
|
|
80
|
-
.details { margin-top: 20px; }
|
|
81
|
-
.detail-row { display: flex; border-bottom: 1px solid #dee2e6; padding: 8px 0; }
|
|
82
|
-
.detail-label { font-weight: bold; width: 120px; flex-shrink: 0; }
|
|
83
|
-
.detail-value { color: #495057; }
|
|
84
|
-
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d; }
|
|
85
|
-
</style>
|
|
86
|
-
</head>
|
|
87
|
-
<body>
|
|
88
|
-
<div class="container">
|
|
89
|
-
<div class="header">
|
|
90
|
-
<h1 style="margin: 0;">${severityEmoji} Mund Security Alert</h1>
|
|
91
|
-
<p style="margin: 10px 0 0 0; opacity: 0.9;">${event.rule_name}</p>
|
|
92
|
-
</div>
|
|
93
|
-
<div class="content">
|
|
94
|
-
<h3>Detected Content</h3>
|
|
95
|
-
<div class="snippet">${this.escapeHtml(event.content_snippet)}</div>
|
|
96
|
-
|
|
97
|
-
<div class="details">
|
|
98
|
-
<h3>Event Details</h3>
|
|
99
|
-
<div class="detail-row">
|
|
100
|
-
<span class="detail-label">Severity:</span>
|
|
101
|
-
<span class="detail-value">${event.severity.toUpperCase()}</span>
|
|
102
|
-
</div>
|
|
103
|
-
<div class="detail-row">
|
|
104
|
-
<span class="detail-label">Type:</span>
|
|
105
|
-
<span class="detail-value">${event.type}</span>
|
|
106
|
-
</div>
|
|
107
|
-
<div class="detail-row">
|
|
108
|
-
<span class="detail-label">Action:</span>
|
|
109
|
-
<span class="detail-value">${event.action_taken}</span>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="detail-row">
|
|
112
|
-
<span class="detail-label">Rule ID:</span>
|
|
113
|
-
<span class="detail-value">${event.rule_id}</span>
|
|
114
|
-
</div>
|
|
115
|
-
<div class="detail-row">
|
|
116
|
-
<span class="detail-label">Event ID:</span>
|
|
117
|
-
<span class="detail-value">${event.id}</span>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="detail-row">
|
|
120
|
-
<span class="detail-label">Timestamp:</span>
|
|
121
|
-
<span class="detail-value">${event.timestamp.toISOString()}</span>
|
|
122
|
-
</div>
|
|
123
|
-
${event.context.tool_name ? `
|
|
124
|
-
<div class="detail-row">
|
|
125
|
-
<span class="detail-label">Tool:</span>
|
|
126
|
-
<span class="detail-value">${event.context.tool_name}</span>
|
|
127
|
-
</div>
|
|
128
|
-
` : ''}
|
|
129
|
-
${event.context.agent_id ? `
|
|
130
|
-
<div class="detail-row">
|
|
131
|
-
<span class="detail-label">Agent:</span>
|
|
132
|
-
<span class="detail-value">${event.context.agent_id}</span>
|
|
133
|
-
</div>
|
|
134
|
-
` : ''}
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<div class="footer">
|
|
138
|
-
This alert was generated by Mund - The Guardian Protocol.<br>
|
|
139
|
-
Review and acknowledge this alert in your security dashboard.
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
</body>
|
|
144
|
-
</html>`;
|
|
145
|
-
|
|
146
|
-
const text = `
|
|
147
|
-
MUND SECURITY ALERT
|
|
148
|
-
====================
|
|
149
|
-
|
|
150
|
-
${event.severity.toUpperCase()}: ${event.rule_name}
|
|
151
|
-
|
|
152
|
-
Detected Content:
|
|
153
|
-
${event.content_snippet}
|
|
154
|
-
|
|
155
|
-
Event Details:
|
|
156
|
-
- Severity: ${event.severity.toUpperCase()}
|
|
157
|
-
- Type: ${event.type}
|
|
158
|
-
- Action: ${event.action_taken}
|
|
159
|
-
- Rule ID: ${event.rule_id}
|
|
160
|
-
- Event ID: ${event.id}
|
|
161
|
-
- Timestamp: ${event.timestamp.toISOString()}
|
|
162
|
-
${event.context.tool_name ? `- Tool: ${event.context.tool_name}` : ''}
|
|
163
|
-
${event.context.agent_id ? `- Agent: ${event.context.agent_id}` : ''}
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
This alert was generated by Mund - The Guardian Protocol.
|
|
167
|
-
`;
|
|
168
|
-
|
|
169
|
-
return { subject, html, text };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private escapeHtml(text: string): string {
|
|
173
|
-
return text
|
|
174
|
-
.replace(/&/g, '&')
|
|
175
|
-
.replace(/</g, '<')
|
|
176
|
-
.replace(/>/g, '>')
|
|
177
|
-
.replace(/"/g, '"')
|
|
178
|
-
.replace(/'/g, ''');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private getSeverityColor(severity: Severity): string {
|
|
182
|
-
const colors: Record<Severity, string> = {
|
|
183
|
-
critical: '#dc3545',
|
|
184
|
-
high: '#fd7e14',
|
|
185
|
-
medium: '#ffc107',
|
|
186
|
-
low: '#17a2b8',
|
|
187
|
-
info: '#6c757d'
|
|
188
|
-
};
|
|
189
|
-
return colors[severity] || colors.info;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private getSeverityEmoji(severity: Severity): string {
|
|
193
|
-
const emojis: Record<Severity, string> = {
|
|
194
|
-
critical: 'đ¨',
|
|
195
|
-
high: 'â ī¸',
|
|
196
|
-
medium: 'âĄ',
|
|
197
|
-
low: 'âšī¸',
|
|
198
|
-
info: 'đ'
|
|
199
|
-
};
|
|
200
|
-
return emojis[severity] || 'đ';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private meetsMinSeverity(eventSeverity: Severity, minSeverity: Severity): boolean {
|
|
204
|
-
const order: Severity[] = ['critical', 'high', 'medium', 'low', 'info'];
|
|
205
|
-
const eventIndex = order.indexOf(eventSeverity);
|
|
206
|
-
const minIndex = order.indexOf(minSeverity);
|
|
207
|
-
return eventIndex <= minIndex;
|
|
51
|
+
return false;
|
|
208
52
|
}
|
|
209
53
|
}
|
|
210
54
|
|