@jungjaehoon/mama-os 0.13.3 → 0.14.2

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 (89) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/agent-loop.d.ts +1 -1
  3. package/dist/agent/agent-loop.d.ts.map +1 -1
  4. package/dist/agent/agent-loop.js +9 -2
  5. package/dist/agent/agent-loop.js.map +1 -1
  6. package/dist/agent/codex-mcp-process.d.ts +1 -0
  7. package/dist/agent/codex-mcp-process.d.ts.map +1 -1
  8. package/dist/agent/codex-mcp-process.js +50 -1
  9. package/dist/agent/codex-mcp-process.js.map +1 -1
  10. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  11. package/dist/agent/gateway-tool-executor.js +101 -1
  12. package/dist/agent/gateway-tool-executor.js.map +1 -1
  13. package/dist/agent/types.d.ts +1 -1
  14. package/dist/agent/types.d.ts.map +1 -1
  15. package/dist/agent/types.js.map +1 -1
  16. package/dist/api/auth-middleware.d.ts +46 -0
  17. package/dist/api/auth-middleware.d.ts.map +1 -0
  18. package/dist/api/auth-middleware.js +206 -0
  19. package/dist/api/auth-middleware.js.map +1 -0
  20. package/dist/api/cron-handler.d.ts.map +1 -1
  21. package/dist/api/cron-handler.js +10 -0
  22. package/dist/api/cron-handler.js.map +1 -1
  23. package/dist/api/graph-api.d.ts.map +1 -1
  24. package/dist/api/graph-api.js +53 -57
  25. package/dist/api/graph-api.js.map +1 -1
  26. package/dist/api/index.d.ts +2 -2
  27. package/dist/api/index.d.ts.map +1 -1
  28. package/dist/api/index.js +32 -1
  29. package/dist/api/index.js.map +1 -1
  30. package/dist/api/token-handler.d.ts +4 -4
  31. package/dist/api/token-handler.d.ts.map +1 -1
  32. package/dist/api/token-handler.js.map +1 -1
  33. package/dist/auth/oauth-manager.d.ts.map +1 -1
  34. package/dist/auth/oauth-manager.js +16 -1
  35. package/dist/auth/oauth-manager.js.map +1 -1
  36. package/dist/cli/commands/start.d.ts.map +1 -1
  37. package/dist/cli/commands/start.js +244 -27
  38. package/dist/cli/commands/start.js.map +1 -1
  39. package/dist/gateways/attachment-utils.d.ts.map +1 -1
  40. package/dist/gateways/attachment-utils.js +292 -4
  41. package/dist/gateways/attachment-utils.js.map +1 -1
  42. package/dist/gateways/channel-history.d.ts +3 -3
  43. package/dist/gateways/channel-history.d.ts.map +1 -1
  44. package/dist/gateways/channel-history.js +38 -2
  45. package/dist/gateways/channel-history.js.map +1 -1
  46. package/dist/gateways/plugin-loader.d.ts.map +1 -1
  47. package/dist/gateways/plugin-loader.js +12 -0
  48. package/dist/gateways/plugin-loader.js.map +1 -1
  49. package/dist/gateways/session-store.d.ts +2 -2
  50. package/dist/gateways/session-store.d.ts.map +1 -1
  51. package/dist/gateways/session-store.js.map +1 -1
  52. package/dist/gateways/slack.d.ts.map +1 -1
  53. package/dist/gateways/slack.js +0 -2
  54. package/dist/gateways/slack.js.map +1 -1
  55. package/dist/multi-agent/swarm/swarm-db.d.ts +12 -12
  56. package/dist/multi-agent/swarm/swarm-db.d.ts.map +1 -1
  57. package/dist/multi-agent/swarm/swarm-db.js +2 -2
  58. package/dist/multi-agent/swarm/swarm-db.js.map +1 -1
  59. package/dist/multi-agent/swarm/swarm-manager.d.ts +2 -2
  60. package/dist/multi-agent/swarm/swarm-manager.d.ts.map +1 -1
  61. package/dist/multi-agent/swarm/swarm-manager.js.map +1 -1
  62. package/dist/multi-agent/swarm/swarm-task-runner.d.ts.map +1 -1
  63. package/dist/multi-agent/swarm/swarm-task-runner.js.map +1 -1
  64. package/dist/observability/metrics-store.d.ts.map +1 -1
  65. package/dist/observability/metrics-store.js +2 -2
  66. package/dist/observability/metrics-store.js.map +1 -1
  67. package/dist/scheduler/cron-scheduler.d.ts.map +1 -1
  68. package/dist/scheduler/cron-scheduler.js +7 -0
  69. package/dist/scheduler/cron-scheduler.js.map +1 -1
  70. package/dist/scheduler/heartbeat.d.ts +1 -0
  71. package/dist/scheduler/heartbeat.d.ts.map +1 -1
  72. package/dist/scheduler/heartbeat.js +9 -1
  73. package/dist/scheduler/heartbeat.js.map +1 -1
  74. package/dist/scheduler/schedule-store.d.ts +2 -2
  75. package/dist/scheduler/schedule-store.d.ts.map +1 -1
  76. package/dist/scheduler/schedule-store.js.map +1 -1
  77. package/dist/security/security-monitor.d.ts +34 -0
  78. package/dist/security/security-monitor.d.ts.map +1 -0
  79. package/dist/security/security-monitor.js +662 -0
  80. package/dist/security/security-monitor.js.map +1 -0
  81. package/dist/security/trusted-proxy.d.ts +9 -0
  82. package/dist/security/trusted-proxy.d.ts.map +1 -0
  83. package/dist/security/trusted-proxy.js +36 -0
  84. package/dist/security/trusted-proxy.js.map +1 -0
  85. package/dist/sqlite.d.ts +29 -0
  86. package/dist/sqlite.d.ts.map +1 -0
  87. package/dist/sqlite.js +124 -0
  88. package/dist/sqlite.js.map +1 -0
  89. package/package.json +5 -6
@@ -0,0 +1,662 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getTarpitDelayMs = getTarpitDelayMs;
37
+ exports.isHoneypotPath = isHoneypotPath;
38
+ exports.setSecurityAlertSender = setSecurityAlertSender;
39
+ exports.resetSecurityMonitorForTests = resetSecurityMonitorForTests;
40
+ exports.flushSecurityMonitor = flushSecurityMonitor;
41
+ exports.formatSecurityAlert = formatSecurityAlert;
42
+ exports.recordSecurityEvent = recordSecurityEvent;
43
+ exports.createSecurityMiddleware = createSecurityMiddleware;
44
+ exports.getCloudflareCustomListCsvPath = getCloudflareCustomListCsvPath;
45
+ exports.getCloudflareWafExpressionPath = getCloudflareWafExpressionPath;
46
+ exports.getDenylistJsonPath = getDenylistJsonPath;
47
+ exports.getDenylistTxtPath = getDenylistTxtPath;
48
+ exports.getIncidentLogDir = getIncidentLogDir;
49
+ exports.getSecurityLogPath = getSecurityLogPath;
50
+ const promises_1 = require("node:fs/promises");
51
+ const node_os_1 = require("node:os");
52
+ const node_path_1 = require("node:path");
53
+ const debugLogger = __importStar(require("@jungjaehoon/mama-core/debug-logger"));
54
+ const trusted_proxy_js_1 = require("./trusted-proxy.js");
55
+ const { DebugLogger } = debugLogger;
56
+ const logger = new DebugLogger('SecurityMonitor');
57
+ const ALERT_COOLDOWN_MS = Number(process.env.MAMA_SECURITY_ALERT_COOLDOWN_MS || 60_000);
58
+ const HONEYPOT_DELAY_MS = Number(process.env.MAMA_HONEYPOT_DELAY_MS || 2_000);
59
+ const MAX_TARPIT_DELAY_MS = Number(process.env.MAMA_TARPIT_MAX_DELAY_MS || 5_000);
60
+ const SECURITY_ENRICHMENT_TIMEOUT_MS = Number(process.env.MAMA_SECURITY_ENRICHMENT_TIMEOUT_MS || 4_000);
61
+ const MAX_SECURITY_CACHE_ENTRIES = Number(process.env.MAMA_SECURITY_CACHE_MAX_ENTRIES || 1_024);
62
+ const SECURITY_CACHE_ENTRY_TTL_MS = Number(process.env.MAMA_SECURITY_CACHE_ENTRY_TTL_MS || 60 * 60 * 1000);
63
+ const DENYLIST_LOCK_TIMEOUT_MS = Number(process.env.MAMA_DENYLIST_LOCK_TIMEOUT_MS || 5_000);
64
+ const DENYLIST_LOCK_STALE_MS = Number(process.env.MAMA_DENYLIST_LOCK_STALE_MS || 30_000);
65
+ const suspicionScores = new Map();
66
+ const attributionCache = new Map();
67
+ const HONEYPOT_PATTERNS = [
68
+ /^\/\.git(?:\/|$)/i,
69
+ /^\/\.env(?:\.|$)/i,
70
+ /^\/\.DS_Store$/i,
71
+ /^\/wp-login\.php$/i,
72
+ /^\/xmlrpc\.php$/i,
73
+ /^\/phpmyadmin(?:\/|$)/i,
74
+ /^\/server-status$/i,
75
+ /^\/nginx_status$/i,
76
+ /^\/backup(?:\.(?:zip|tar|gz|sql|bak))?$/i,
77
+ /^\/mama-memory\.db$/i,
78
+ /^\/debug$/i,
79
+ /^\/metrics$/i,
80
+ ];
81
+ let alertSender = null;
82
+ const lastAlertAt = new Map();
83
+ const incidentIds = new Map();
84
+ const pendingTasks = new Set();
85
+ let denylistWriteChain = Promise.resolve();
86
+ const incidentUpdateChains = new Map();
87
+ function buildFingerprint(event) {
88
+ return JSON.stringify([
89
+ event.type,
90
+ event.clientAddress || event.remoteAddress || 'unknown',
91
+ event.path || 'unknown',
92
+ ]);
93
+ }
94
+ function normalizeClientKey(clientAddress) {
95
+ if (!clientAddress || clientAddress === 'unknown') {
96
+ return 'unknown';
97
+ }
98
+ return clientAddress.replace(/[^a-zA-Z0-9_.:-]/g, '_');
99
+ }
100
+ function getIncidentId(event) {
101
+ const clientKey = normalizeClientKey(event.clientAddress || event.remoteAddress);
102
+ const existing = incidentIds.get(clientKey);
103
+ if (existing) {
104
+ return existing;
105
+ }
106
+ const incidentId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${clientKey}`;
107
+ incidentIds.set(clientKey, incidentId);
108
+ return incidentId;
109
+ }
110
+ function getRiskWeight(severity) {
111
+ switch (severity) {
112
+ case 'critical':
113
+ return 4;
114
+ case 'warn':
115
+ return 2;
116
+ default:
117
+ return 1;
118
+ }
119
+ }
120
+ function pruneSuspicionScores(now = Date.now()) {
121
+ for (const [key, value] of suspicionScores.entries()) {
122
+ if (now - value.updatedAt > SECURITY_CACHE_ENTRY_TTL_MS) {
123
+ suspicionScores.delete(key);
124
+ }
125
+ }
126
+ while (suspicionScores.size > MAX_SECURITY_CACHE_ENTRIES) {
127
+ const oldestKey = suspicionScores.keys().next().value;
128
+ if (!oldestKey) {
129
+ break;
130
+ }
131
+ suspicionScores.delete(oldestKey);
132
+ }
133
+ }
134
+ function pruneAttributionCache(now = Date.now()) {
135
+ for (const [key, value] of attributionCache.entries()) {
136
+ if (now - value.updatedAt > SECURITY_CACHE_ENTRY_TTL_MS) {
137
+ attributionCache.delete(key);
138
+ }
139
+ }
140
+ while (attributionCache.size > MAX_SECURITY_CACHE_ENTRIES) {
141
+ const oldestKey = attributionCache.keys().next().value;
142
+ if (!oldestKey) {
143
+ break;
144
+ }
145
+ attributionCache.delete(oldestKey);
146
+ }
147
+ }
148
+ function getClientAddressFromRequestLike(req) {
149
+ return (0, trusted_proxy_js_1.getForwardedClientAddress)(req);
150
+ }
151
+ function getPathFromRequestLike(req) {
152
+ const rawUrl = req.originalUrl || req.url || '/';
153
+ try {
154
+ return new URL(rawUrl, `http://${String(req.headers.host || 'localhost')}`).pathname;
155
+ }
156
+ catch {
157
+ return rawUrl;
158
+ }
159
+ }
160
+ function rememberSuspiciousClient(clientAddress, severity) {
161
+ if (!clientAddress || clientAddress === 'unknown' || (0, trusted_proxy_js_1.isLocalAddress)(clientAddress)) {
162
+ return;
163
+ }
164
+ const now = Date.now();
165
+ pruneSuspicionScores(now);
166
+ const existing = suspicionScores.get(clientAddress);
167
+ const nextScore = Math.min(20, (existing?.score || 0) + getRiskWeight(severity));
168
+ suspicionScores.set(clientAddress, { score: nextScore, updatedAt: now });
169
+ }
170
+ function getTarpitDelayMs(clientAddress) {
171
+ if (!clientAddress || clientAddress === 'unknown' || (0, trusted_proxy_js_1.isLocalAddress)(clientAddress)) {
172
+ return 0;
173
+ }
174
+ const now = Date.now();
175
+ pruneSuspicionScores(now);
176
+ const entry = suspicionScores.get(clientAddress);
177
+ if (!entry || entry.score < 3) {
178
+ return 0;
179
+ }
180
+ entry.updatedAt = now;
181
+ return Math.min(MAX_TARPIT_DELAY_MS, entry.score * 250);
182
+ }
183
+ function isHoneypotPath(pathname) {
184
+ return HONEYPOT_PATTERNS.some((pattern) => pattern.test(pathname));
185
+ }
186
+ function shouldSendAlert(event) {
187
+ if (!alertSender) {
188
+ return false;
189
+ }
190
+ const fingerprint = buildFingerprint(event);
191
+ const now = Date.now();
192
+ const last = lastAlertAt.get(fingerprint) || 0;
193
+ if (now - last < ALERT_COOLDOWN_MS) {
194
+ return false;
195
+ }
196
+ lastAlertAt.set(fingerprint, now);
197
+ return true;
198
+ }
199
+ function trackBackgroundTask(task) {
200
+ pendingTasks.add(task);
201
+ void task.finally(() => {
202
+ pendingTasks.delete(task);
203
+ });
204
+ }
205
+ async function appendSecurityLog(event) {
206
+ await (0, promises_1.mkdir)(getSecurityLogDir(), { recursive: true });
207
+ await (0, promises_1.appendFile)(getSecurityLogPath(), `${JSON.stringify(event)}\n`, 'utf8');
208
+ }
209
+ function extractAbuseEmails(rdap) {
210
+ const entities = Array.isArray(rdap.entities) ? rdap.entities : [];
211
+ const emails = new Set();
212
+ for (const entity of entities) {
213
+ if (!entity || typeof entity !== 'object') {
214
+ continue;
215
+ }
216
+ const roles = Array.isArray(entity.roles)
217
+ ? entity.roles
218
+ : [];
219
+ const isAbuseRelated = roles.some((role) => typeof role === 'string' &&
220
+ ['abuse', 'technical', 'administrative', 'noc'].includes(role.toLowerCase()));
221
+ if (!isAbuseRelated) {
222
+ continue;
223
+ }
224
+ const vcardArray = entity.vcardArray;
225
+ if (!Array.isArray(vcardArray) || vcardArray.length < 2 || !Array.isArray(vcardArray[1])) {
226
+ continue;
227
+ }
228
+ for (const entry of vcardArray[1]) {
229
+ if (!Array.isArray(entry) || entry[0] !== 'email' || typeof entry[3] !== 'string') {
230
+ continue;
231
+ }
232
+ emails.add(entry[3]);
233
+ }
234
+ }
235
+ return [...emails];
236
+ }
237
+ async function lookupNetworkAttribution(ip) {
238
+ if (!ip || ip === 'unknown' || (0, trusted_proxy_js_1.isLocalAddress)(ip)) {
239
+ return null;
240
+ }
241
+ const now = Date.now();
242
+ pruneAttributionCache(now);
243
+ if (attributionCache.has(ip)) {
244
+ const cached = attributionCache.get(ip);
245
+ cached.updatedAt = now;
246
+ return cached.value;
247
+ }
248
+ if (process.env.MAMA_SECURITY_ENRICHMENT === 'false') {
249
+ attributionCache.set(ip, { value: null, updatedAt: now });
250
+ return null;
251
+ }
252
+ const controller = new AbortController();
253
+ const timeout = setTimeout(() => controller.abort(), SECURITY_ENRICHMENT_TIMEOUT_MS);
254
+ try {
255
+ const res = await fetch(`https://rdap.org/ip/${encodeURIComponent(ip)}`, {
256
+ signal: controller.signal,
257
+ headers: { Accept: 'application/rdap+json, application/json' },
258
+ });
259
+ if (!res.ok) {
260
+ attributionCache.set(ip, { value: null, updatedAt: now });
261
+ return null;
262
+ }
263
+ const rdap = (await res.json());
264
+ const cidrEntries = Array.isArray(rdap.cidr0_cidrs)
265
+ ? rdap.cidr0_cidrs
266
+ : [];
267
+ const cidr = cidrEntries
268
+ .map((entry) => entry.v4prefix && entry.length
269
+ ? `${entry.v4prefix}/${entry.length}`
270
+ : entry.v6prefix && entry.length
271
+ ? `${entry.v6prefix}/${entry.length}`
272
+ : null)
273
+ .find(Boolean);
274
+ const attribution = {
275
+ source: 'rdap',
276
+ networkName: typeof rdap.name === 'string' ? rdap.name : null,
277
+ organization: typeof rdap.port43 === 'string'
278
+ ? rdap.port43
279
+ : typeof rdap.objectClassName === 'string'
280
+ ? rdap.objectClassName
281
+ : null,
282
+ country: typeof rdap.country === 'string' ? rdap.country : null,
283
+ cidr: typeof cidr === 'string' ? cidr : null,
284
+ abuseEmails: extractAbuseEmails(rdap),
285
+ asn: typeof rdap.asn === 'string'
286
+ ? rdap.asn
287
+ : typeof rdap.startAutnum === 'number'
288
+ ? `AS${rdap.startAutnum}`
289
+ : null,
290
+ handle: typeof rdap.handle === 'string' ? rdap.handle : null,
291
+ };
292
+ pruneAttributionCache(now);
293
+ attributionCache.set(ip, { value: attribution, updatedAt: now });
294
+ return attribution;
295
+ }
296
+ catch (error) {
297
+ logger.warn('Network attribution lookup failed', { ip, error: String(error) });
298
+ attributionCache.set(ip, { value: null, updatedAt: now });
299
+ return null;
300
+ }
301
+ finally {
302
+ clearTimeout(timeout);
303
+ }
304
+ }
305
+ function formatAbuseReport(incidentId, incidentSummary) {
306
+ return [
307
+ '# Abuse Report Draft',
308
+ '',
309
+ `- Incident ID: ${incidentId}`,
310
+ `- Generated At: ${new Date().toISOString()}`,
311
+ `- Severity: ${incidentSummary.highestSeverity}`,
312
+ `- Suspected Source IP: ${incidentSummary.clientAddress || 'unknown'}`,
313
+ `- First Seen: ${incidentSummary.firstSeen}`,
314
+ `- Last Seen: ${incidentSummary.lastSeen}`,
315
+ `- Event Count: ${incidentSummary.eventCount}`,
316
+ '',
317
+ '## Summary',
318
+ incidentSummary.summary,
319
+ '',
320
+ '## Network Attribution',
321
+ `- ASN: ${incidentSummary.attribution?.asn || 'unknown'}`,
322
+ `- Network: ${incidentSummary.attribution?.networkName || 'unknown'}`,
323
+ `- Organization: ${incidentSummary.attribution?.organization || 'unknown'}`,
324
+ `- Country: ${incidentSummary.attribution?.country || 'unknown'}`,
325
+ `- CIDR: ${incidentSummary.attribution?.cidr || 'unknown'}`,
326
+ `- Handle: ${incidentSummary.attribution?.handle || 'unknown'}`,
327
+ `- Abuse Contacts: ${incidentSummary.attribution?.abuseEmails?.join(', ') || 'unknown'}`,
328
+ '',
329
+ '## Evidence',
330
+ `- Client Address: ${incidentSummary.clientAddress || 'unknown'}`,
331
+ `- Remote Address: ${incidentSummary.remoteAddress || 'unknown'}`,
332
+ `- Forwarded For: ${incidentSummary.forwardedFor || 'unknown'}`,
333
+ `- Cloudflare Connecting IP: ${incidentSummary.cfConnectingIp || 'unknown'}`,
334
+ `- Cloudflare Ray: ${incidentSummary.cfRay || 'unknown'}`,
335
+ `- Latest Path: ${incidentSummary.path || 'unknown'}`,
336
+ `- Latest Method: ${incidentSummary.method || 'unknown'}`,
337
+ '',
338
+ '## Recommended Submission',
339
+ '- Submit this report to your hosting provider, Cloudflare abuse contact, or upstream ISP abuse desk.',
340
+ `- Attach the JSON evidence file: ${incidentSummary.evidenceJsonPath}`,
341
+ `- Attach the timeline log: ${incidentSummary.timelinePath}`,
342
+ `- Review denylist candidates: ${incidentSummary.denylistCandidatePath}`,
343
+ `- Cloudflare custom list CSV: ${incidentSummary.cloudflareCustomListPath}`,
344
+ `- Cloudflare WAF rule expression: ${incidentSummary.cloudflareWafExpressionPath}`,
345
+ '',
346
+ '## Latest Event Details',
347
+ `\`\`\`json`,
348
+ JSON.stringify(incidentSummary.latestEvent, null, 2),
349
+ '```',
350
+ '',
351
+ ].join('\n');
352
+ }
353
+ function getSecurityLogDir() {
354
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.mama', 'logs');
355
+ }
356
+ function getSecurityLogPath() {
357
+ return (0, node_path_1.join)(getSecurityLogDir(), 'security-events.jsonl');
358
+ }
359
+ function getIncidentLogDir() {
360
+ return (0, node_path_1.join)(getSecurityLogDir(), 'security-incidents');
361
+ }
362
+ function getDenylistJsonPath() {
363
+ return (0, node_path_1.join)(getSecurityLogDir(), 'security-denylist-candidates.json');
364
+ }
365
+ function getDenylistTxtPath() {
366
+ return (0, node_path_1.join)(getSecurityLogDir(), 'security-denylist-candidates.txt');
367
+ }
368
+ function getCloudflareCustomListCsvPath() {
369
+ return (0, node_path_1.join)(getSecurityLogDir(), 'cloudflare-ip-list.csv');
370
+ }
371
+ function getCloudflareWafExpressionPath() {
372
+ return (0, node_path_1.join)(getSecurityLogDir(), 'cloudflare-waf-expression.txt');
373
+ }
374
+ function getDenylistLockPath() {
375
+ return (0, node_path_1.join)(getSecurityLogDir(), 'security-denylist.lock');
376
+ }
377
+ async function acquireDenylistLock() {
378
+ await (0, promises_1.mkdir)(getSecurityLogDir(), { recursive: true });
379
+ const startedAt = Date.now();
380
+ while (Date.now() - startedAt < DENYLIST_LOCK_TIMEOUT_MS) {
381
+ try {
382
+ const handle = await (0, promises_1.open)(getDenylistLockPath(), 'wx');
383
+ return async () => {
384
+ await handle.close().catch(() => undefined);
385
+ await (0, promises_1.unlink)(getDenylistLockPath()).catch(() => undefined);
386
+ };
387
+ }
388
+ catch (error) {
389
+ const code = error.code;
390
+ if (code !== 'EEXIST') {
391
+ throw error;
392
+ }
393
+ try {
394
+ const existing = await (0, promises_1.stat)(getDenylistLockPath());
395
+ if (Date.now() - existing.mtimeMs > DENYLIST_LOCK_STALE_MS) {
396
+ await (0, promises_1.unlink)(getDenylistLockPath()).catch(() => undefined);
397
+ continue;
398
+ }
399
+ }
400
+ catch {
401
+ continue;
402
+ }
403
+ await new Promise((resolve) => setTimeout(resolve, 50));
404
+ }
405
+ }
406
+ throw new Error('Timed out acquiring denylist lock');
407
+ }
408
+ async function withSerializedDenylistWrite(operation) {
409
+ const previous = denylistWriteChain;
410
+ let releaseQueue;
411
+ denylistWriteChain = new Promise((resolve) => {
412
+ releaseQueue = resolve;
413
+ });
414
+ await previous.catch(() => undefined);
415
+ let releaseLock = null;
416
+ try {
417
+ releaseLock = await acquireDenylistLock();
418
+ }
419
+ catch (error) {
420
+ releaseQueue();
421
+ throw error;
422
+ }
423
+ try {
424
+ return await operation();
425
+ }
426
+ finally {
427
+ await releaseLock?.().catch(() => undefined);
428
+ releaseQueue();
429
+ }
430
+ }
431
+ async function withSerializedIncidentUpdate(incidentId, operation) {
432
+ const previous = incidentUpdateChains.get(incidentId) || Promise.resolve();
433
+ let releaseQueue;
434
+ const nextChain = new Promise((resolve) => {
435
+ releaseQueue = resolve;
436
+ });
437
+ incidentUpdateChains.set(incidentId, nextChain);
438
+ await previous.catch(() => undefined);
439
+ try {
440
+ return await operation();
441
+ }
442
+ finally {
443
+ releaseQueue();
444
+ if (incidentUpdateChains.get(incidentId) === nextChain) {
445
+ incidentUpdateChains.delete(incidentId);
446
+ }
447
+ }
448
+ }
449
+ async function readIncidentSummary(path) {
450
+ try {
451
+ const content = await (0, promises_1.readFile)(path, 'utf8');
452
+ return JSON.parse(content);
453
+ }
454
+ catch {
455
+ return null;
456
+ }
457
+ }
458
+ async function writeDenylistCandidate(summary) {
459
+ const ip = summary.clientAddress;
460
+ if (!ip || ip === 'unknown' || (0, trusted_proxy_js_1.isLocalAddress)(ip)) {
461
+ return;
462
+ }
463
+ await withSerializedDenylistWrite(async () => {
464
+ const denylistJsonPath = getDenylistJsonPath();
465
+ const denylistTxtPath = getDenylistTxtPath();
466
+ const raw = await (0, promises_1.readFile)(denylistJsonPath, 'utf8').catch(() => '[]');
467
+ let candidates = [];
468
+ try {
469
+ candidates = JSON.parse(raw);
470
+ }
471
+ catch {
472
+ candidates = [];
473
+ }
474
+ const existing = candidates.find((candidate) => candidate.ip === ip);
475
+ const updated = {
476
+ ip,
477
+ firstSeen: existing?.firstSeen || summary.firstSeen,
478
+ lastSeen: summary.lastSeen,
479
+ eventCount: Math.max(existing?.eventCount || 0, summary.eventCount),
480
+ highestSeverity: existing && getRiskWeight(existing.highestSeverity) > getRiskWeight(summary.highestSeverity)
481
+ ? existing.highestSeverity
482
+ : summary.highestSeverity,
483
+ reasons: Array.from(new Set([...(existing?.reasons || []), summary.summary])),
484
+ suggestedAction: 'Review and block via Cloudflare/WAF if malicious activity is confirmed.',
485
+ attribution: summary.attribution || existing?.attribution || null,
486
+ };
487
+ const next = candidates.filter((candidate) => candidate.ip !== ip);
488
+ next.push(updated);
489
+ next.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
490
+ await (0, promises_1.mkdir)(getSecurityLogDir(), { recursive: true });
491
+ await (0, promises_1.writeFile)(denylistJsonPath, JSON.stringify(next, null, 2), 'utf8');
492
+ await (0, promises_1.writeFile)(denylistTxtPath, `${next.map((candidate) => candidate.ip).join('\n')}\n`, 'utf8');
493
+ await writeCloudflareExports(next);
494
+ });
495
+ }
496
+ async function writeCloudflareExports(candidates) {
497
+ const csvLines = candidates.map((candidate) => {
498
+ const reason = candidate.reasons[0] || 'security-monitor candidate';
499
+ const description = `${candidate.highestSeverity}:${reason}`
500
+ .replace(/[\r\n]+/g, ' ')
501
+ .replace(/,/g, ';')
502
+ .slice(0, 500);
503
+ return `${candidate.ip},${description}`;
504
+ });
505
+ const expressionLines = [
506
+ '# Cloudflare WAF custom rule expression',
507
+ '# Create/import a custom IP list using cloudflare-ip-list.csv, then use:',
508
+ 'ip.src in $mama_security_blocklist',
509
+ ];
510
+ await (0, promises_1.writeFile)(getCloudflareCustomListCsvPath(), `${csvLines.join('\n')}\n`, 'utf8');
511
+ await (0, promises_1.writeFile)(getCloudflareWafExpressionPath(), `${expressionLines.join('\n')}\n`, 'utf8');
512
+ }
513
+ async function preserveEvidence(event) {
514
+ const incidentId = getIncidentId(event);
515
+ const incidentDir = (0, node_path_1.join)(getIncidentLogDir(), incidentId);
516
+ const summaryPath = (0, node_path_1.join)(incidentDir, 'incident.json');
517
+ const timelinePath = (0, node_path_1.join)(incidentDir, 'timeline.jsonl');
518
+ const abuseReportPath = (0, node_path_1.join)(incidentDir, 'abuse-report.md');
519
+ const attribution = await lookupNetworkAttribution(event.clientAddress || event.remoteAddress || '');
520
+ await (0, promises_1.mkdir)(incidentDir, { recursive: true });
521
+ await (0, promises_1.appendFile)(timelinePath, `${JSON.stringify(event)}\n`, 'utf8');
522
+ return await withSerializedIncidentUpdate(incidentId, async () => {
523
+ const previous = await readIncidentSummary(summaryPath);
524
+ const highestSeverity = previous && getRiskWeight(previous.highestSeverity) > getRiskWeight(event.severity)
525
+ ? previous.highestSeverity
526
+ : event.severity;
527
+ const summary = {
528
+ incidentId,
529
+ clientAddress: event.clientAddress || previous?.clientAddress || null,
530
+ remoteAddress: event.remoteAddress || previous?.remoteAddress || null,
531
+ forwardedFor: event.forwardedFor || previous?.forwardedFor || null,
532
+ cfConnectingIp: event.cfConnectingIp || previous?.cfConnectingIp || null,
533
+ cfRay: event.cfRay || previous?.cfRay || null,
534
+ path: event.path || previous?.path || null,
535
+ method: event.method || previous?.method || null,
536
+ firstSeen: previous?.firstSeen || event.timestamp || new Date().toISOString(),
537
+ lastSeen: event.timestamp || new Date().toISOString(),
538
+ eventCount: (previous?.eventCount || 0) + 1,
539
+ highestSeverity,
540
+ summary: `${event.message} (${event.type})`,
541
+ latestEvent: event,
542
+ attribution: previous?.attribution || attribution,
543
+ evidenceJsonPath: summaryPath,
544
+ timelinePath,
545
+ abuseReportPath,
546
+ denylistCandidatePath: getDenylistJsonPath(),
547
+ cloudflareCustomListPath: getCloudflareCustomListCsvPath(),
548
+ cloudflareWafExpressionPath: getCloudflareWafExpressionPath(),
549
+ };
550
+ await (0, promises_1.writeFile)(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
551
+ await (0, promises_1.writeFile)(abuseReportPath, formatAbuseReport(incidentId, summary), 'utf8');
552
+ await writeDenylistCandidate(summary);
553
+ return summary;
554
+ });
555
+ }
556
+ function setSecurityAlertSender(sender) {
557
+ alertSender = sender;
558
+ if (sender) {
559
+ logger.info('Security alert sender registered');
560
+ }
561
+ }
562
+ function resetSecurityMonitorForTests() {
563
+ alertSender = null;
564
+ lastAlertAt.clear();
565
+ suspicionScores.clear();
566
+ incidentIds.clear();
567
+ attributionCache.clear();
568
+ denylistWriteChain = Promise.resolve();
569
+ incidentUpdateChains.clear();
570
+ }
571
+ async function flushSecurityMonitor() {
572
+ if (pendingTasks.size === 0) {
573
+ return;
574
+ }
575
+ await Promise.allSettled([...pendingTasks]);
576
+ }
577
+ function formatSecurityAlert(event) {
578
+ const incidentId = getIncidentId(event);
579
+ const incidentDir = (0, node_path_1.join)(getIncidentLogDir(), incidentId);
580
+ const lines = [
581
+ `🚨 [Security] ${event.message}`,
582
+ `type: ${event.type}`,
583
+ `severity: ${event.severity}`,
584
+ `client: ${event.clientAddress || event.remoteAddress || 'unknown'}`,
585
+ ];
586
+ if (event.path) {
587
+ lines.push(`path: ${event.path}`);
588
+ }
589
+ if (event.method) {
590
+ lines.push(`method: ${event.method}`);
591
+ }
592
+ if (event.cfRay) {
593
+ lines.push(`cfRay: ${event.cfRay}`);
594
+ }
595
+ if (event.details && Object.keys(event.details).length > 0) {
596
+ lines.push(`details: ${JSON.stringify(event.details)}`);
597
+ }
598
+ lines.push(`evidence: ${(0, node_path_1.join)(incidentDir, 'incident.json')}`);
599
+ lines.push(`abuse_draft: ${(0, node_path_1.join)(incidentDir, 'abuse-report.md')}`);
600
+ lines.push(`denylist: ${getDenylistJsonPath()}`);
601
+ lines.push(`cloudflare_csv: ${getCloudflareCustomListCsvPath()}`);
602
+ lines.push(`cloudflare_waf: ${getCloudflareWafExpressionPath()}`);
603
+ return lines.join('\n');
604
+ }
605
+ function recordSecurityEvent(event) {
606
+ const normalized = {
607
+ ...event,
608
+ timestamp: event.timestamp || new Date().toISOString(),
609
+ };
610
+ rememberSuspiciousClient(normalized.clientAddress || normalized.remoteAddress, normalized.severity);
611
+ logger.warn(`[SECURITY] ${normalized.message}`, normalized);
612
+ trackBackgroundTask(appendSecurityLog(normalized).catch((error) => {
613
+ logger.error('Failed to append security event log', error);
614
+ }));
615
+ trackBackgroundTask(preserveEvidence(normalized)
616
+ .then(() => undefined)
617
+ .catch((error) => {
618
+ logger.error('Failed to preserve security evidence', error);
619
+ }));
620
+ if (!shouldSendAlert(normalized) || !alertSender) {
621
+ return;
622
+ }
623
+ trackBackgroundTask(alertSender(normalized).catch((error) => {
624
+ logger.error('Failed to deliver security alert', error);
625
+ }));
626
+ }
627
+ function createSecurityMiddleware() {
628
+ return async (req, res, next) => {
629
+ const clientAddress = getClientAddressFromRequestLike(req);
630
+ const pathname = getPathFromRequestLike(req);
631
+ if (isHoneypotPath(pathname) && !(0, trusted_proxy_js_1.isLocalAddress)(clientAddress)) {
632
+ const delayMs = Math.max(HONEYPOT_DELAY_MS, getTarpitDelayMs(clientAddress));
633
+ recordSecurityEvent({
634
+ type: 'honeypot_hit',
635
+ severity: 'critical',
636
+ message: 'Honeypot path accessed',
637
+ clientAddress,
638
+ remoteAddress: req.socket?.remoteAddress || null,
639
+ forwardedFor: typeof req.headers['x-forwarded-for'] === 'string'
640
+ ? req.headers['x-forwarded-for']
641
+ : null,
642
+ cfConnectingIp: typeof req.headers['cf-connecting-ip'] === 'string'
643
+ ? req.headers['cf-connecting-ip']
644
+ : null,
645
+ cfRay: typeof req.headers['cf-ray'] === 'string' ? req.headers['cf-ray'] : null,
646
+ method: req.method,
647
+ path: pathname,
648
+ viaTunnel: !!(req.headers['cf-connecting-ip'] || req.headers['cf-ray']),
649
+ details: { delayMs },
650
+ });
651
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
652
+ res.status(404).type('text/plain').send('Not Found');
653
+ return;
654
+ }
655
+ const delayMs = getTarpitDelayMs(clientAddress);
656
+ if (delayMs > 0 && pathname !== '/health') {
657
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
658
+ }
659
+ next();
660
+ };
661
+ }
662
+ //# sourceMappingURL=security-monitor.js.map