@weave_protocol/mund 0.2.2 â 0.3.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/README.md +94 -0
- 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.js +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/cli/scan.d.ts +49 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +226 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -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/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/configuration.d.ts.map +1 -1
- package/dist/tools/configuration.js +2 -1
- package/dist/tools/configuration.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 +5 -8
- package/src/analyzers/code-analyzer.ts +2 -1
- package/src/analyzers/exfiltration-detector.ts +2 -1
- package/src/analyzers/index.ts +1 -1
- 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/cli/scan.ts +251 -0
- package/src/index.ts +11 -4
- package/src/notifications/email.ts +38 -194
- package/src/osv.ts +310 -0
- package/src/sarif.ts +146 -0
- package/src/scoring.ts +64 -0
- package/src/threat-intel-manager.ts +2 -1
- package/src/tools/configuration.ts +2 -1
- 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/cli/scan.test.ts +92 -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/sarif.test.ts +59 -0
- package/tests/scoring.test.ts +49 -0
- package/tests/tools/monitoring.test.ts +95 -0
- package/vitest.config.ts +8 -0
package/src/cli/scan.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
|
|
20
|
+
export type OutputFormat = 'text' | 'json' | 'sarif';
|
|
21
|
+
type FailOn = Severity | 'none';
|
|
22
|
+
|
|
23
|
+
export interface ScanCliOptions {
|
|
24
|
+
paths: string[];
|
|
25
|
+
format: OutputFormat;
|
|
26
|
+
output?: string;
|
|
27
|
+
failOn: FailOn;
|
|
28
|
+
osv: boolean;
|
|
29
|
+
offline: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FileReport extends SarifTarget {
|
|
33
|
+
risk: RiskScore;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ScanReport {
|
|
37
|
+
tool: { name: 'mund'; version: string };
|
|
38
|
+
risk: RiskScore;
|
|
39
|
+
files_scanned: number;
|
|
40
|
+
osv?: { source: OsvSource; lockfiles: number; unchecked: number; error?: string };
|
|
41
|
+
files: FileReport[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.turbo', 'vendor', '__pycache__', '.venv']);
|
|
45
|
+
const LOCKFILES = new Set(['package-lock.json', 'npm-shrinkwrap.json']);
|
|
46
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
47
|
+
const SEVERITY_RANK: Record<Severity, number> = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
48
|
+
|
|
49
|
+
export const SCAN_USAGE = `Usage: mund-mcp scan [paths...] [options]
|
|
50
|
+
|
|
51
|
+
Scan files for secrets, PII, injection, exfiltration and dangerous code,
|
|
52
|
+
plus known-vulnerable npm dependencies (via OSV.dev) in package-lock.json.
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--format=FORMAT text (default), json, or sarif
|
|
56
|
+
--output=FILE Write the report to FILE instead of stdout
|
|
57
|
+
--fail-on=LEVEL Exit 1 if any finding is at or above LEVEL
|
|
58
|
+
(critical, high [default], medium, low, none)
|
|
59
|
+
--no-osv Skip dependency vulnerability lookups
|
|
60
|
+
--offline Use only the local OSV cache (also: MUND_OFFLINE=1)
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
mund-mcp scan .
|
|
64
|
+
mund-mcp scan src --format=sarif --output=mund.sarif
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export function parseScanArgs(args: string[]): ScanCliOptions {
|
|
68
|
+
const opts: ScanCliOptions = { paths: [], format: 'text', failOn: 'high', osv: true, offline: false };
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
const arg = args[i];
|
|
72
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, undefined];
|
|
73
|
+
const value = () => {
|
|
74
|
+
const v = inline ?? args[++i];
|
|
75
|
+
if (v === undefined) throw new Error(`${flag} needs a value`);
|
|
76
|
+
return v;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
switch (flag) {
|
|
80
|
+
case '--format': {
|
|
81
|
+
const v = value();
|
|
82
|
+
if (v !== 'text' && v !== 'json' && v !== 'sarif') throw new Error(`Unknown format "${v}" (expected text, json or sarif)`);
|
|
83
|
+
opts.format = v;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case '--output':
|
|
87
|
+
case '-o':
|
|
88
|
+
opts.output = value();
|
|
89
|
+
break;
|
|
90
|
+
case '--fail-on': {
|
|
91
|
+
const v = value();
|
|
92
|
+
if (v !== 'none' && !(v in SEVERITY_RANK)) throw new Error(`Unknown --fail-on level "${v}"`);
|
|
93
|
+
opts.failOn = v as FailOn;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case '--no-osv':
|
|
97
|
+
opts.osv = false;
|
|
98
|
+
break;
|
|
99
|
+
case '--offline':
|
|
100
|
+
opts.offline = true;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
if (flag.startsWith('-')) throw new Error(`Unknown option ${flag}`);
|
|
104
|
+
opts.paths.push(arg);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (opts.paths.length === 0) opts.paths.push('.');
|
|
109
|
+
return opts;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function runScan(opts: ScanCliOptions, osvClient?: OsvClient): Promise<ScanReport> {
|
|
113
|
+
const cwd = process.cwd();
|
|
114
|
+
const engine = new AnalyzerEngine();
|
|
115
|
+
const files: FileReport[] = [];
|
|
116
|
+
const lockfiles: string[] = [];
|
|
117
|
+
let scanned = 0;
|
|
118
|
+
|
|
119
|
+
for (const file of await collectFiles(opts.paths.map(p => resolve(cwd, p)))) {
|
|
120
|
+
if (LOCKFILES.has(basename(file))) {
|
|
121
|
+
lockfiles.push(file);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const content = await readText(file);
|
|
125
|
+
if (content === null) continue;
|
|
126
|
+
scanned++;
|
|
127
|
+
const issues = await engine.analyzeAll(content, DEFAULT_RULES);
|
|
128
|
+
if (issues.length > 0) files.push({ uri: toUri(cwd, file), issues, risk: computeRiskScore(issues) });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let osv: ScanReport['osv'];
|
|
132
|
+
if (opts.osv && lockfiles.length > 0) {
|
|
133
|
+
const client = osvClient ?? new OsvClient({ offline: opts.offline });
|
|
134
|
+
let source: OsvSource = 'live';
|
|
135
|
+
let unchecked = 0;
|
|
136
|
+
let error: string | undefined;
|
|
137
|
+
|
|
138
|
+
for (const lockfile of lockfiles) {
|
|
139
|
+
const content = await readFile(lockfile, 'utf8');
|
|
140
|
+
let packages;
|
|
141
|
+
try {
|
|
142
|
+
packages = parseNpmLockfile(content);
|
|
143
|
+
} catch {
|
|
144
|
+
continue; // malformed lockfile; nothing to look up
|
|
145
|
+
}
|
|
146
|
+
const result = await client.lookup(packages);
|
|
147
|
+
// Report the weakest source across lockfiles.
|
|
148
|
+
if (result.source === 'unavailable' || (result.source === 'cache' && source === 'live')) source = result.source;
|
|
149
|
+
unchecked += result.unchecked.length;
|
|
150
|
+
error ??= result.error;
|
|
151
|
+
|
|
152
|
+
const issues = advisoriesToIssues(packages, result, content);
|
|
153
|
+
if (issues.length > 0) files.push({ uri: toUri(cwd, lockfile), issues, risk: computeRiskScore(issues) });
|
|
154
|
+
}
|
|
155
|
+
osv = { source, lockfiles: lockfiles.length, unchecked, ...(error ? { error } : {}) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
tool: { name: 'mund', version: getVersion() },
|
|
160
|
+
risk: computeRiskScore(files.flatMap(f => f.issues)),
|
|
161
|
+
files_scanned: scanned,
|
|
162
|
+
...(osv ? { osv } : {}),
|
|
163
|
+
files,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatReport(report: ScanReport, format: OutputFormat): string {
|
|
168
|
+
if (format === 'json') return JSON.stringify(report, null, 2);
|
|
169
|
+
if (format === 'sarif') return JSON.stringify(toSarif(report.files, { toolVersion: report.tool.version }), null, 2);
|
|
170
|
+
|
|
171
|
+
const lines: string[] = [];
|
|
172
|
+
for (const file of report.files) {
|
|
173
|
+
lines.push(`${file.uri} (risk ${file.risk.score}/100, ${file.risk.label})`);
|
|
174
|
+
for (const issue of file.issues) {
|
|
175
|
+
const where = issue.location?.line ? `${issue.location.line}:${issue.location.column ?? 1}` : '-';
|
|
176
|
+
lines.push(` ${where.padEnd(8)} ${issue.severity.padEnd(8)} ${issue.rule_name}`);
|
|
177
|
+
}
|
|
178
|
+
lines.push('');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const total = report.files.reduce((n, f) => n + f.issues.length, 0);
|
|
182
|
+
const { counts } = report.risk;
|
|
183
|
+
lines.push(`Scanned ${report.files_scanned} file(s): ${total} finding(s) ` +
|
|
184
|
+
`(${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low)`);
|
|
185
|
+
lines.push(`Risk score: ${report.risk.score}/100 (${report.risk.label})`);
|
|
186
|
+
if (report.osv) {
|
|
187
|
+
const note = report.osv.source === 'live' ? 'live' : `${report.osv.source} - ${report.osv.error ?? ''}`.trim();
|
|
188
|
+
lines.push(`Dependencies: OSV.dev (${note})` + (report.osv.unchecked ? `, ${report.osv.unchecked} package(s) unchecked` : ''));
|
|
189
|
+
}
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function shouldFail(report: ScanReport, failOn: FailOn): boolean {
|
|
194
|
+
if (failOn === 'none') return false;
|
|
195
|
+
const threshold = SEVERITY_RANK[failOn];
|
|
196
|
+
return report.files.some(f => f.issues.some((i: SecurityIssue) => SEVERITY_RANK[i.severity] <= threshold));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Entry point for `mund-mcp scan`. Returns the process exit code. */
|
|
200
|
+
export async function runScanCli(args: string[]): Promise<number> {
|
|
201
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
202
|
+
process.stdout.write(SCAN_USAGE);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let opts: ScanCliOptions;
|
|
207
|
+
try {
|
|
208
|
+
opts = parseScanArgs(args);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
process.stderr.write(`mund scan: ${(error as Error).message}\n\n${SCAN_USAGE}`);
|
|
211
|
+
return 2;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const report = await runScan(opts);
|
|
215
|
+
const out = formatReport(report, opts.format) + '\n';
|
|
216
|
+
if (opts.output) await writeFile(opts.output, out);
|
|
217
|
+
else process.stdout.write(out);
|
|
218
|
+
|
|
219
|
+
return shouldFail(report, opts.failOn) ? 1 : 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function collectFiles(roots: string[]): Promise<string[]> {
|
|
223
|
+
const out: string[] = [];
|
|
224
|
+
const walk = async (path: string) => {
|
|
225
|
+
const info = await stat(path).catch(() => null);
|
|
226
|
+
if (!info) return;
|
|
227
|
+
if (info.isFile()) {
|
|
228
|
+
out.push(path);
|
|
229
|
+
} else if (info.isDirectory()) {
|
|
230
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
231
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
232
|
+
if (entry.isDirectory() || entry.isFile()) await walk(join(path, entry.name));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
for (const root of roots) await walk(root);
|
|
237
|
+
return out.sort();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Read a file as UTF-8 text, or null if it's too big or looks binary. */
|
|
241
|
+
async function readText(file: string): Promise<string | null> {
|
|
242
|
+
const info = await stat(file);
|
|
243
|
+
if (info.size > MAX_FILE_BYTES) return null;
|
|
244
|
+
const buf = await readFile(file);
|
|
245
|
+
if (buf.subarray(0, 8000).includes(0)) return null;
|
|
246
|
+
return buf.toString('utf8');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function toUri(cwd: string, file: string): string {
|
|
250
|
+
return relative(cwd, file).split(sep).join('/');
|
|
251
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ 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';
|
|
18
20
|
|
|
19
21
|
// Threat Intelligence imports
|
|
20
22
|
import { threatIntel } from './threat-intel-manager.js';
|
|
@@ -154,7 +156,7 @@ async function createMundServer(config: MundConfig): Promise<McpServer> {
|
|
|
154
156
|
// Create MCP server
|
|
155
157
|
const server = new McpServer({
|
|
156
158
|
name: 'mund-mcp-server',
|
|
157
|
-
version:
|
|
159
|
+
version: getVersion()
|
|
158
160
|
});
|
|
159
161
|
|
|
160
162
|
// Initialize storage
|
|
@@ -218,7 +220,7 @@ async function runHTTP(config: MundConfig): Promise<void> {
|
|
|
218
220
|
|
|
219
221
|
// Health check endpoint
|
|
220
222
|
app.get('/health', (_req, res) => {
|
|
221
|
-
res.json({ status: 'ok', version:
|
|
223
|
+
res.json({ status: 'ok', version: getVersion() });
|
|
222
224
|
});
|
|
223
225
|
|
|
224
226
|
// Threat intel status endpoint
|
|
@@ -254,7 +256,8 @@ function printHelp(): void {
|
|
|
254
256
|
Mund - The Guardian Protocol
|
|
255
257
|
MCP-based security monitoring for agentic AI systems
|
|
256
258
|
|
|
257
|
-
Usage: mund-mcp [options]
|
|
259
|
+
Usage: mund-mcp [options] Run the MCP server
|
|
260
|
+
mund-mcp scan [paths...] Scan files (see: mund-mcp scan --help)
|
|
258
261
|
|
|
259
262
|
Options:
|
|
260
263
|
--help, -h Show this help message
|
|
@@ -314,6 +317,10 @@ For more information, visit: https://github.com/Tyox-all/Weave_Protocol
|
|
|
314
317
|
async function main(): Promise<void> {
|
|
315
318
|
const args = process.argv.slice(2);
|
|
316
319
|
|
|
320
|
+
if (args[0] === 'scan') {
|
|
321
|
+
process.exit(await runScanCli(args.slice(1)));
|
|
322
|
+
}
|
|
323
|
+
|
|
317
324
|
// Parse CLI arguments
|
|
318
325
|
if (args.includes('--help') || args.includes('-h')) {
|
|
319
326
|
printHelp();
|
|
@@ -321,7 +328,7 @@ async function main(): Promise<void> {
|
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
if (args.includes('--version') || args.includes('-v')) {
|
|
324
|
-
console.log(
|
|
331
|
+
console.log(getVersion());
|
|
325
332
|
process.exit(0);
|
|
326
333
|
}
|
|
327
334
|
|
|
@@ -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
|
|