@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.
Files changed (157) hide show
  1. package/README.md +135 -1
  2. package/dist/analyzers/code-analyzer.d.ts.map +1 -1
  3. package/dist/analyzers/code-analyzer.js +2 -1
  4. package/dist/analyzers/code-analyzer.js.map +1 -1
  5. package/dist/analyzers/exfiltration-detector.d.ts.map +1 -1
  6. package/dist/analyzers/exfiltration-detector.js +2 -1
  7. package/dist/analyzers/exfiltration-detector.js.map +1 -1
  8. package/dist/analyzers/index.d.ts.map +1 -1
  9. package/dist/analyzers/index.js +4 -2
  10. package/dist/analyzers/index.js.map +1 -1
  11. package/dist/analyzers/injection-detector.d.ts.map +1 -1
  12. package/dist/analyzers/injection-detector.js +2 -1
  13. package/dist/analyzers/injection-detector.js.map +1 -1
  14. package/dist/analyzers/mcp-server-analyzer.d.ts +2 -0
  15. package/dist/analyzers/mcp-server-analyzer.d.ts.map +1 -1
  16. package/dist/analyzers/mcp-server-analyzer.js.map +1 -1
  17. package/dist/analyzers/pii-detector.d.ts.map +1 -1
  18. package/dist/analyzers/pii-detector.js +2 -1
  19. package/dist/analyzers/pii-detector.js.map +1 -1
  20. package/dist/analyzers/regex.d.ts +10 -0
  21. package/dist/analyzers/regex.d.ts.map +1 -0
  22. package/dist/analyzers/regex.js +17 -0
  23. package/dist/analyzers/regex.js.map +1 -0
  24. package/dist/analyzers/secret-scanner.d.ts.map +1 -1
  25. package/dist/analyzers/secret-scanner.js +2 -1
  26. package/dist/analyzers/secret-scanner.js.map +1 -1
  27. package/dist/canary.d.ts +48 -0
  28. package/dist/canary.d.ts.map +1 -0
  29. package/dist/canary.js +149 -0
  30. package/dist/canary.js.map +1 -0
  31. package/dist/cli/scan.d.ts +50 -0
  32. package/dist/cli/scan.d.ts.map +1 -0
  33. package/dist/cli/scan.js +235 -0
  34. package/dist/cli/scan.js.map +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +12 -4
  37. package/dist/index.js.map +1 -1
  38. package/dist/notifications/email.d.ts +21 -8
  39. package/dist/notifications/email.d.ts.map +1 -1
  40. package/dist/notifications/email.js +32 -178
  41. package/dist/notifications/email.js.map +1 -1
  42. package/dist/osv.d.ts +94 -0
  43. package/dist/osv.d.ts.map +1 -0
  44. package/dist/osv.js +239 -0
  45. package/dist/osv.js.map +1 -0
  46. package/dist/rugpull.d.ts +73 -0
  47. package/dist/rugpull.d.ts.map +1 -0
  48. package/dist/rugpull.js +177 -0
  49. package/dist/rugpull.js.map +1 -0
  50. package/dist/sarif.d.ts +20 -0
  51. package/dist/sarif.d.ts.map +1 -0
  52. package/dist/sarif.js +122 -0
  53. package/dist/sarif.js.map +1 -0
  54. package/dist/scoring.d.ts +26 -0
  55. package/dist/scoring.d.ts.map +1 -0
  56. package/dist/scoring.js +49 -0
  57. package/dist/scoring.js.map +1 -0
  58. package/dist/threat-intel-manager.d.ts.map +1 -1
  59. package/dist/threat-intel-manager.js +2 -1
  60. package/dist/threat-intel-manager.js.map +1 -1
  61. package/dist/tools/canary-rugpull.d.ts +7 -0
  62. package/dist/tools/canary-rugpull.d.ts.map +1 -0
  63. package/dist/tools/canary-rugpull.js +50 -0
  64. package/dist/tools/canary-rugpull.js.map +1 -0
  65. package/dist/tools/configuration.d.ts.map +1 -1
  66. package/dist/tools/configuration.js +2 -1
  67. package/dist/tools/configuration.js.map +1 -1
  68. package/dist/tools/index.d.ts +1 -0
  69. package/dist/tools/index.d.ts.map +1 -1
  70. package/dist/tools/index.js +4 -0
  71. package/dist/tools/index.js.map +1 -1
  72. package/dist/tools/mcp-scanning.d.ts.map +1 -1
  73. package/dist/tools/mcp-scanning.js +4 -0
  74. package/dist/tools/mcp-scanning.js.map +1 -1
  75. package/dist/tools/monitoring.d.ts.map +1 -1
  76. package/dist/tools/monitoring.js +22 -1
  77. package/dist/tools/monitoring.js.map +1 -1
  78. package/dist/version.d.ts +6 -0
  79. package/dist/version.d.ts.map +1 -0
  80. package/dist/version.js +19 -0
  81. package/dist/version.js.map +1 -0
  82. package/package.json +2 -4
  83. package/src/analyzers/code-analyzer.ts +2 -1
  84. package/src/analyzers/exfiltration-detector.ts +2 -1
  85. package/src/analyzers/index.ts +4 -2
  86. package/src/analyzers/injection-detector.ts +2 -1
  87. package/src/analyzers/mcp-server-analyzer.ts +2 -0
  88. package/src/analyzers/pii-detector.ts +2 -1
  89. package/src/analyzers/regex.ts +17 -0
  90. package/src/analyzers/secret-scanner.ts +2 -1
  91. package/src/canary.ts +165 -0
  92. package/src/cli/scan.ts +260 -0
  93. package/src/index.ts +13 -4
  94. package/src/notifications/email.ts +38 -194
  95. package/src/osv.ts +310 -0
  96. package/src/rugpull.ts +233 -0
  97. package/src/sarif.ts +146 -0
  98. package/src/scoring.ts +64 -0
  99. package/src/threat-intel-manager.ts +2 -1
  100. package/src/tools/canary-rugpull.ts +75 -0
  101. package/src/tools/configuration.ts +2 -1
  102. package/src/tools/index.ts +5 -0
  103. package/src/tools/mcp-scanning.ts +5 -0
  104. package/src/tools/monitoring.ts +23 -1
  105. package/src/version.ts +20 -0
  106. package/tests/analyzers/engine.test.ts +60 -0
  107. package/tests/analyzers/injection-and-code.test.ts +80 -0
  108. package/tests/analyzers/mcp-server-analyzer.test.ts +82 -0
  109. package/tests/analyzers/regex.test.ts +35 -0
  110. package/tests/analyzers/rules.test.ts +54 -0
  111. package/tests/analyzers/secret-scanner.test.ts +80 -0
  112. package/tests/canary.test.ts +79 -0
  113. package/tests/cli/scan.test.ts +92 -0
  114. package/tests/fixtures/canary/base64.json +9 -0
  115. package/tests/fixtures/canary/clean-output.json +8 -0
  116. package/tests/fixtures/canary/exact-leak.json +9 -0
  117. package/tests/fixtures/canary/lowercased.json +9 -0
  118. package/tests/fixtures/canary/other-token.json +8 -0
  119. package/tests/fixtures/canary/prefix-only.json +8 -0
  120. package/tests/fixtures/canary/reversed.json +9 -0
  121. package/tests/fixtures/canary/spaced-out.json +9 -0
  122. package/tests/fixtures/canary/zero-width.json +9 -0
  123. package/tests/fixtures/rugpull/description-changed/baseline.json +36 -0
  124. package/tests/fixtures/rugpull/description-changed/current.json +36 -0
  125. package/tests/fixtures/rugpull/description-changed/expected.json +11 -0
  126. package/tests/fixtures/rugpull/gains-execution/baseline.json +36 -0
  127. package/tests/fixtures/rugpull/gains-execution/current.json +36 -0
  128. package/tests/fixtures/rugpull/gains-execution/expected.json +18 -0
  129. package/tests/fixtures/rugpull/schema-changed/baseline.json +36 -0
  130. package/tests/fixtures/rugpull/schema-changed/current.json +39 -0
  131. package/tests/fixtures/rugpull/schema-changed/expected.json +11 -0
  132. package/tests/fixtures/rugpull/schema-key-reorder/baseline.json +36 -0
  133. package/tests/fixtures/rugpull/schema-key-reorder/current.json +36 -0
  134. package/tests/fixtures/rugpull/schema-key-reorder/expected.json +5 -0
  135. package/tests/fixtures/rugpull/server-renamed/baseline.json +36 -0
  136. package/tests/fixtures/rugpull/server-renamed/current.json +36 -0
  137. package/tests/fixtures/rugpull/server-renamed/expected.json +11 -0
  138. package/tests/fixtures/rugpull/tool-added/baseline.json +36 -0
  139. package/tests/fixtures/rugpull/tool-added/current.json +44 -0
  140. package/tests/fixtures/rugpull/tool-added/expected.json +11 -0
  141. package/tests/fixtures/rugpull/tool-removed/baseline.json +36 -0
  142. package/tests/fixtures/rugpull/tool-removed/current.json +21 -0
  143. package/tests/fixtures/rugpull/tool-removed/expected.json +11 -0
  144. package/tests/fixtures/rugpull/unchanged/baseline.json +36 -0
  145. package/tests/fixtures/rugpull/unchanged/current.json +36 -0
  146. package/tests/fixtures/rugpull/unchanged/expected.json +5 -0
  147. package/tests/fixtures.test.ts +40 -0
  148. package/tests/helpers.ts +51 -0
  149. package/tests/notifications/email.test.ts +68 -0
  150. package/tests/notifications/hub.test.ts +160 -0
  151. package/tests/osv.test.ts +117 -0
  152. package/tests/rugpull.test.ts +80 -0
  153. package/tests/sarif.test.ts +59 -0
  154. package/tests/scoring.test.ts +49 -0
  155. package/tests/tools/canary-rugpull.test.ts +57 -0
  156. package/tests/tools/monitoring.test.ts +95 -0
  157. package/vitest.config.ts +8 -0
@@ -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: '0.2.0'
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: '0.2.0' });
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('0.2.0');
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 - Send alerts via email
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, Severity } from '../types.js';
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
- private config: EmailConfig;
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(payload: NotificationPayload): Promise<boolean> {
20
- const { event } = payload;
21
-
22
- // Check minimum severity
23
- if (this.config.min_severity && !this.meetsMinSeverity(event.severity, this.config.min_severity)) {
24
- return true; // Successfully skipped
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, '&amp;')
175
- .replace(/</g, '&lt;')
176
- .replace(/>/g, '&gt;')
177
- .replace(/"/g, '&quot;')
178
- .replace(/'/g, '&#039;');
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