clawmoat 0.5.0 → 0.8.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.
Files changed (44) hide show
  1. package/CONTRIBUTING.md +4 -2
  2. package/README.md +86 -3
  3. package/SECURITY.md +58 -10
  4. package/bin/clawmoat.js +298 -1
  5. package/clawmoat-0.8.0.tgz +0 -0
  6. package/docs/blog/386-malicious-skills.html +255 -0
  7. package/docs/blog/40000-exposed-openclaw-instances.html +194 -0
  8. package/docs/blog/agent-trust-protocol.html +197 -0
  9. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +223 -0
  10. package/docs/blog/ibm-experts-agent-runtime-protection.html +238 -0
  11. package/docs/blog/index.html +168 -0
  12. package/docs/blog/mcp-30-cves-security-crisis.html +279 -0
  13. package/docs/blog/microsoft-openclaw-workstation-security.html +234 -0
  14. package/docs/blog/nist-ai-agent-standards-clawmoat.html +369 -0
  15. package/docs/blog/oasis-websocket-hijack.html +205 -0
  16. package/docs/blog/ollama-openclaw-security.html +154 -0
  17. package/docs/blog/openclaw-enterprise-readiness-claw10.html +198 -0
  18. package/docs/blog/openclaw-security-reckoning-2026.html +361 -0
  19. package/docs/blog/supply-chain-agents.html +166 -0
  20. package/docs/blog/supply-chain-agents.md +79 -0
  21. package/docs/business/index.html +530 -0
  22. package/docs/business/install.html +247 -0
  23. package/docs/checklist.html +168 -0
  24. package/docs/finance/index.html +217 -0
  25. package/docs/hall-of-fame.html +168 -0
  26. package/docs/index.html +328 -90
  27. package/docs/install.sh +557 -0
  28. package/docs/privacy-policy/index.html +122 -0
  29. package/docs/scan/index.html +214 -0
  30. package/docs/sitemap.xml +132 -2
  31. package/docs/support/index.html +124 -0
  32. package/docs/terms-of-service/index.html +122 -0
  33. package/examples/basic-usage.js +38 -0
  34. package/package.json +1 -1
  35. package/server/index.js +179 -14
  36. package/server/index.js.patch +1 -0
  37. package/src/finance/index.js +585 -0
  38. package/src/finance/mcp-firewall.js +486 -0
  39. package/src/guardian/cve-verify.js +129 -0
  40. package/src/guardian/gateway-monitor.js +590 -0
  41. package/src/guardian/index.js +3 -1
  42. package/src/guardian/insider-threat.js +498 -0
  43. package/src/index.js +3 -0
  44. package/src/middleware/openclaw.js +28 -1
@@ -0,0 +1,585 @@
1
+ /**
2
+ * ClawMoat Finance — Financial Security Module for AI Agents
3
+ *
4
+ * Protects financial data, credentials, and transactions when
5
+ * AI agents operate in financial contexts (bookkeeping, invoicing,
6
+ * crypto, banking, payments).
7
+ *
8
+ * Features:
9
+ * - Financial credential detection and protection
10
+ * - Transaction amount guardrails with approval thresholds
11
+ * - PCI-DSS / SOX-ready audit trail formatting
12
+ * - Financial PII scanning (SSN, account numbers, routing numbers)
13
+ * - Crypto wallet protection (seed phrases, private keys, wallet files)
14
+ * - API rate limiting for financial services
15
+ * - Dual-approval workflow for high-value operations
16
+ *
17
+ * @module clawmoat/finance
18
+ * @example
19
+ * const { FinanceGuard } = require('clawmoat/finance');
20
+ * const guard = new FinanceGuard({
21
+ * transactionLimit: 1000, // Require approval above $1000
22
+ * dualApprovalThreshold: 10000, // Two approvals above $10K
23
+ * auditFormat: 'sox', // SOX-compliant audit trail
24
+ * onAlert: (alert) => notifySlack(alert),
25
+ * });
26
+ */
27
+
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+ const os = require('os');
31
+ const crypto = require('crypto');
32
+
33
+ // ─── Financial Credential Patterns ──────────────────────────────
34
+
35
+ /** Patterns that indicate financial credentials in file paths */
36
+ const FINANCIAL_FORBIDDEN_ZONES = [
37
+ // Banking & Payment
38
+ { pattern: /\.stripe\b/i, label: 'Stripe credentials', severity: 'critical', category: 'payment' },
39
+ { pattern: /\.plaid\b/i, label: 'Plaid credentials', severity: 'critical', category: 'banking' },
40
+ { pattern: /\.square\b/i, label: 'Square credentials', severity: 'critical', category: 'payment' },
41
+ { pattern: /braintree/i, label: 'Braintree credentials', severity: 'critical', category: 'payment' },
42
+ { pattern: /paypal/i, label: 'PayPal credentials', severity: 'high', category: 'payment' },
43
+ { pattern: /adyen/i, label: 'Adyen credentials', severity: 'critical', category: 'payment' },
44
+ { pattern: /dwolla/i, label: 'Dwolla credentials', severity: 'critical', category: 'payment' },
45
+
46
+ // Crypto Wallets
47
+ { pattern: /\.bitcoin\b/i, label: 'Bitcoin wallet', severity: 'critical', category: 'crypto' },
48
+ { pattern: /\.ethereum\b/i, label: 'Ethereum wallet', severity: 'critical', category: 'crypto' },
49
+ { pattern: /\.solana\b/i, label: 'Solana wallet', severity: 'critical', category: 'crypto' },
50
+ { pattern: /wallet\.dat/i, label: 'Crypto wallet file', severity: 'critical', category: 'crypto' },
51
+ { pattern: /keystore.*\.json/i, label: 'Crypto keystore', severity: 'critical', category: 'crypto' },
52
+ { pattern: /\.metamask\b/i, label: 'MetaMask data', severity: 'critical', category: 'crypto' },
53
+ { pattern: /\.phantom\b/i, label: 'Phantom wallet', severity: 'critical', category: 'crypto' },
54
+ { pattern: /\.ledger\b/i, label: 'Ledger config', severity: 'high', category: 'crypto' },
55
+ { pattern: /\.trezor\b/i, label: 'Trezor config', severity: 'high', category: 'crypto' },
56
+
57
+ // Accounting Software
58
+ { pattern: /quickbooks/i, label: 'QuickBooks data', severity: 'high', category: 'accounting' },
59
+ { pattern: /xero/i, label: 'Xero credentials', severity: 'high', category: 'accounting' },
60
+ { pattern: /freshbooks/i, label: 'FreshBooks credentials', severity: 'high', category: 'accounting' },
61
+ { pattern: /\.qbo$/i, label: 'QuickBooks Online file', severity: 'high', category: 'accounting' },
62
+ { pattern: /\.qbw$/i, label: 'QuickBooks data file', severity: 'high', category: 'accounting' },
63
+ { pattern: /\.ofx$/i, label: 'Open Financial Exchange file', severity: 'high', category: 'accounting' },
64
+ { pattern: /\.qfx$/i, label: 'Quicken Financial Exchange file', severity: 'high', category: 'accounting' },
65
+
66
+ // Tax & Compliance
67
+ { pattern: /\.tax\b/i, label: 'Tax data', severity: 'high', category: 'tax' },
68
+ { pattern: /turbotax/i, label: 'TurboTax data', severity: 'high', category: 'tax' },
69
+ { pattern: /\.1099\b/i, label: '1099 form data', severity: 'critical', category: 'tax' },
70
+ { pattern: /\.w[29]\b/i, label: 'W-2/W-9 form data', severity: 'critical', category: 'tax' },
71
+
72
+ // Banking Files
73
+ { pattern: /\.bai2?$/i, label: 'BAI bank statement', severity: 'high', category: 'banking' },
74
+ { pattern: /\.mt940$/i, label: 'SWIFT MT940 statement', severity: 'high', category: 'banking' },
75
+ { pattern: /\.ach$/i, label: 'ACH payment file', severity: 'critical', category: 'banking' },
76
+ { pattern: /nacha/i, label: 'NACHA payment file', severity: 'critical', category: 'banking' },
77
+ ];
78
+
79
+ /** Patterns that indicate financial secrets in text content */
80
+ const FINANCIAL_SECRET_PATTERNS = [
81
+ // API Keys
82
+ { pattern: /sk_(test|live)_[a-zA-Z0-9]{24,}/g, label: 'Stripe secret key', severity: 'critical' },
83
+ { pattern: /pk_(test|live)_[a-zA-Z0-9]{24,}/g, label: 'Stripe publishable key', severity: 'high' },
84
+ { pattern: /rk_(test|live)_[a-zA-Z0-9]{24,}/g, label: 'Stripe restricted key', severity: 'critical' },
85
+ { pattern: /whsec_[a-zA-Z0-9]{32,}/g, label: 'Stripe webhook secret', severity: 'critical' },
86
+ { pattern: /access-[a-z0-9]{32,}/g, label: 'Plaid access token', severity: 'critical' },
87
+ { pattern: /sq0[a-z]{3}-[a-zA-Z0-9\-_]{22,}/g, label: 'Square API key', severity: 'critical' },
88
+
89
+ // Crypto
90
+ { pattern: /(?:^|\s)(5[HJK][1-9A-HJ-NP-Za-km-z]{49})(?:\s|$)/g, label: 'Bitcoin private key (WIF)', severity: 'critical' },
91
+ { pattern: /0x[a-fA-F0-9]{64}/g, label: 'Ethereum private key', severity: 'critical' },
92
+ { pattern: /(?:^|\s)([1-9A-HJ-NP-Za-km-z]{87,88})(?:\s|$)/g, label: 'Solana private key', severity: 'critical' },
93
+ { pattern: /(?:abandon|ability|able|about|above)\s+(?:abandon|ability|able|about|above)(?:\s+\w+){10,22}/gi, label: 'Possible BIP-39 seed phrase', severity: 'critical' },
94
+
95
+ // Financial PII
96
+ { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, label: 'SSN (Social Security Number)', severity: 'critical' },
97
+ { pattern: /\b\d{9}\b(?=.*(?:routing|aba|rtn))/gi, label: 'ABA routing number', severity: 'critical' },
98
+ { pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g, label: 'Credit card number', severity: 'critical' },
99
+ { pattern: /\b[0-9]{8,17}\b(?=.*(?:account|acct|checking|savings|routing))/gi, label: 'Bank account number', severity: 'critical' },
100
+ { pattern: /\b\d{2}-\d{7}\b(?=.*(?:ein|tax|employer))/gi, label: 'EIN (Employer ID Number)', severity: 'high' },
101
+ { pattern: /\bIBAN\s*:?\s*[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/gi, label: 'IBAN number', severity: 'critical' },
102
+ { pattern: /\bSWIFT\s*:?\s*[A-Z]{6}[A-Z0-9]{2,5}\b/gi, label: 'SWIFT/BIC code', severity: 'high' },
103
+ ];
104
+
105
+ /** Financial API domains to monitor */
106
+ const FINANCIAL_API_DOMAINS = [
107
+ { domain: 'api.stripe.com', label: 'Stripe', category: 'payment' },
108
+ { domain: 'api.plaid.com', label: 'Plaid', category: 'banking' },
109
+ { domain: 'connect.squareup.com', label: 'Square', category: 'payment' },
110
+ { domain: 'api.braintreegateway.com', label: 'Braintree', category: 'payment' },
111
+ { domain: 'api.paypal.com', label: 'PayPal', category: 'payment' },
112
+ { domain: 'checkout-test.adyen.com', label: 'Adyen', category: 'payment' },
113
+ { domain: 'api.coinbase.com', label: 'Coinbase', category: 'crypto' },
114
+ { domain: 'api.binance.com', label: 'Binance', category: 'crypto' },
115
+ { domain: 'api.kraken.com', label: 'Kraken', category: 'crypto' },
116
+ { domain: 'quickbooks.api.intuit.com', label: 'QuickBooks', category: 'accounting' },
117
+ { domain: 'api.xero.com', label: 'Xero', category: 'accounting' },
118
+ { domain: 'api.freshbooks.com', label: 'FreshBooks', category: 'accounting' },
119
+ { domain: 'api.wise.com', label: 'Wise', category: 'transfer' },
120
+ { domain: 'api.mercury.com', label: 'Mercury', category: 'banking' },
121
+ { domain: 'api.svb.com', label: 'SVB', category: 'banking' },
122
+ ];
123
+
124
+ // ─── FinanceGuard Class ─────────────────────────────────────────
125
+
126
+ class FinanceGuard {
127
+ /**
128
+ * @param {Object} options
129
+ * @param {number} [options.transactionLimit=1000] - Require approval above this amount
130
+ * @param {number} [options.dualApprovalThreshold=10000] - Two approvals above this
131
+ * @param {string} [options.currency='USD'] - Default currency
132
+ * @param {string} [options.auditFormat='sox'] - Audit format: 'sox' | 'pcidss' | 'standard'
133
+ * @param {Function} [options.onAlert] - Alert callback
134
+ * @param {Function} [options.onApprovalRequired] - Called when transaction needs approval
135
+ * @param {string} [options.logPath] - Audit log file path
136
+ * @param {string[]} [options.allowedDomains] - Additional allowed financial domains
137
+ */
138
+ constructor(options = {}) {
139
+ this.transactionLimit = options.transactionLimit ?? 1000;
140
+ this.dualApprovalThreshold = options.dualApprovalThreshold ?? 10000;
141
+ this.currency = options.currency || 'USD';
142
+ this.auditFormat = options.auditFormat || 'sox';
143
+ this.onAlert = options.onAlert || null;
144
+ this.onApprovalRequired = options.onApprovalRequired || null;
145
+ this.logPath = options.logPath || null;
146
+ this.allowedDomains = new Set(options.allowedDomains || []);
147
+
148
+ // State
149
+ this.transactions = [];
150
+ this.alerts = [];
151
+ this.auditLog = [];
152
+ this.apiCallLog = [];
153
+ this.pendingApprovals = new Map();
154
+ }
155
+
156
+ // ─── File Path Protection ─────────────────────────────────────
157
+
158
+ /**
159
+ * Check if a file path accesses financial data.
160
+ * @param {string} filePath - Path to check
161
+ * @returns {Object} { allowed, findings[] }
162
+ */
163
+ checkFilePath(filePath) {
164
+ const normalized = filePath.replace(/\\/g, '/').replace(/^~/, os.homedir());
165
+ const findings = [];
166
+
167
+ for (const zone of FINANCIAL_FORBIDDEN_ZONES) {
168
+ if (zone.pattern.test(normalized)) {
169
+ findings.push({
170
+ type: 'financial_forbidden_zone',
171
+ label: zone.label,
172
+ category: zone.category,
173
+ severity: zone.severity,
174
+ path: filePath,
175
+ });
176
+ }
177
+ }
178
+
179
+ if (findings.length > 0) {
180
+ this._audit('file_access_blocked', { path: filePath, findings });
181
+ if (findings.some(f => f.severity === 'critical')) {
182
+ this._alert({
183
+ type: 'financial_credential_access',
184
+ severity: 'critical',
185
+ message: `Agent attempted to access financial data: ${findings[0].label}`,
186
+ details: { path: filePath, findings },
187
+ });
188
+ }
189
+ }
190
+
191
+ return {
192
+ allowed: findings.length === 0,
193
+ findings,
194
+ };
195
+ }
196
+
197
+ // ─── Content Scanning ─────────────────────────────────────────
198
+
199
+ /**
200
+ * Scan text content for financial secrets and PII.
201
+ * @param {string} text - Content to scan
202
+ * @returns {Object} { safe, findings[], redacted }
203
+ */
204
+ scanContent(text) {
205
+ if (!text || typeof text !== 'string') {
206
+ return { safe: true, findings: [], redacted: text || '' };
207
+ }
208
+
209
+ const findings = [];
210
+ let redacted = text;
211
+
212
+ for (const pattern of FINANCIAL_SECRET_PATTERNS) {
213
+ // Reset regex state
214
+ pattern.pattern.lastIndex = 0;
215
+ let match;
216
+ while ((match = pattern.pattern.exec(text)) !== null) {
217
+ findings.push({
218
+ type: 'financial_secret',
219
+ label: pattern.label,
220
+ severity: pattern.severity,
221
+ match: match[0].substring(0, 8) + '***REDACTED***',
222
+ position: match.index,
223
+ });
224
+ // Redact in output
225
+ const replacement = `[REDACTED:${pattern.label}]`;
226
+ redacted = redacted.replace(match[0], replacement);
227
+ }
228
+ }
229
+
230
+ if (findings.length > 0) {
231
+ this._audit('financial_secret_detected', { findingCount: findings.length, labels: findings.map(f => f.label) });
232
+ for (const f of findings) {
233
+ if (f.severity === 'critical') {
234
+ this._alert({
235
+ type: 'financial_secret_leak',
236
+ severity: 'critical',
237
+ message: `Financial secret detected in agent output: ${f.label}`,
238
+ details: f,
239
+ });
240
+ }
241
+ }
242
+ }
243
+
244
+ return {
245
+ safe: findings.length === 0,
246
+ findings,
247
+ redacted,
248
+ };
249
+ }
250
+
251
+ // ─── Transaction Guardrails ───────────────────────────────────
252
+
253
+ /**
254
+ * Evaluate a financial transaction for approval.
255
+ * @param {Object} transaction
256
+ * @param {number} transaction.amount - Transaction amount
257
+ * @param {string} [transaction.currency] - Currency code
258
+ * @param {string} transaction.type - 'payment' | 'transfer' | 'refund' | 'subscription' | 'invoice'
259
+ * @param {string} [transaction.recipient] - Recipient identifier
260
+ * @param {string} [transaction.description] - Description
261
+ * @param {string} [transaction.initiator] - Who/what initiated (agent, skill, user)
262
+ * @returns {Object} { approved, requiresApproval, requiresDualApproval, reason, transactionId }
263
+ */
264
+ evaluateTransaction(transaction) {
265
+ const txId = crypto.randomBytes(8).toString('hex');
266
+ const amount = Math.abs(transaction.amount || 0);
267
+ const currency = transaction.currency || this.currency;
268
+ const now = Date.now();
269
+
270
+ const record = {
271
+ transactionId: txId,
272
+ timestamp: now,
273
+ amount,
274
+ currency,
275
+ type: transaction.type || 'unknown',
276
+ recipient: transaction.recipient || 'unknown',
277
+ description: transaction.description || '',
278
+ initiator: transaction.initiator || 'agent',
279
+ status: 'pending',
280
+ };
281
+
282
+ this.transactions.push(record);
283
+
284
+ // Check daily aggregate
285
+ const todayStart = new Date().setHours(0, 0, 0, 0);
286
+ const todayTotal = this.transactions
287
+ .filter(t => t.timestamp >= todayStart && t.status === 'approved')
288
+ .reduce((sum, t) => sum + t.amount, 0);
289
+
290
+ const result = {
291
+ transactionId: txId,
292
+ amount,
293
+ currency,
294
+ approved: false,
295
+ requiresApproval: false,
296
+ requiresDualApproval: false,
297
+ reason: '',
298
+ dailyTotal: todayTotal,
299
+ };
300
+
301
+ // Evaluate
302
+ if (amount >= this.dualApprovalThreshold) {
303
+ result.requiresDualApproval = true;
304
+ result.requiresApproval = true;
305
+ result.reason = `Amount $${amount.toLocaleString()} exceeds dual-approval threshold ($${this.dualApprovalThreshold.toLocaleString()})`;
306
+ record.status = 'pending_dual_approval';
307
+ this.pendingApprovals.set(txId, { approvals: [], required: 2, record });
308
+ } else if (amount >= this.transactionLimit) {
309
+ result.requiresApproval = true;
310
+ result.reason = `Amount $${amount.toLocaleString()} exceeds single-approval threshold ($${this.transactionLimit.toLocaleString()})`;
311
+ record.status = 'pending_approval';
312
+ this.pendingApprovals.set(txId, { approvals: [], required: 1, record });
313
+ } else {
314
+ result.approved = true;
315
+ result.reason = 'Within auto-approval limits';
316
+ record.status = 'approved';
317
+ }
318
+
319
+ this._audit('transaction_evaluated', { ...result, type: record.type, recipient: record.recipient });
320
+
321
+ if (result.requiresApproval) {
322
+ this._alert({
323
+ type: result.requiresDualApproval ? 'dual_approval_required' : 'approval_required',
324
+ severity: result.requiresDualApproval ? 'critical' : 'high',
325
+ message: result.reason,
326
+ details: { transactionId: txId, amount, currency, type: record.type, recipient: record.recipient },
327
+ });
328
+
329
+ if (this.onApprovalRequired) {
330
+ this.onApprovalRequired(result);
331
+ }
332
+ }
333
+
334
+ return result;
335
+ }
336
+
337
+ /**
338
+ * Approve a pending transaction.
339
+ * @param {string} transactionId
340
+ * @param {string} approver - Who is approving
341
+ * @returns {Object} { approved, remainingApprovals }
342
+ */
343
+ approveTransaction(transactionId, approver) {
344
+ const pending = this.pendingApprovals.get(transactionId);
345
+ if (!pending) {
346
+ return { approved: false, error: 'Transaction not found or already resolved' };
347
+ }
348
+
349
+ // Prevent same person approving twice
350
+ if (pending.approvals.includes(approver)) {
351
+ return { approved: false, error: 'Same approver cannot approve twice' };
352
+ }
353
+
354
+ pending.approvals.push(approver);
355
+ const remaining = pending.required - pending.approvals.length;
356
+
357
+ if (remaining <= 0) {
358
+ pending.record.status = 'approved';
359
+ this.pendingApprovals.delete(transactionId);
360
+ this._audit('transaction_approved', { transactionId, approvers: pending.approvals });
361
+ return { approved: true, remainingApprovals: 0 };
362
+ }
363
+
364
+ this._audit('transaction_partial_approval', { transactionId, approver, remaining });
365
+ return { approved: false, remainingApprovals: remaining };
366
+ }
367
+
368
+ /**
369
+ * Deny a pending transaction.
370
+ * @param {string} transactionId
371
+ * @param {string} denier - Who is denying
372
+ * @param {string} [reason] - Reason for denial
373
+ * @returns {Object} { denied }
374
+ */
375
+ denyTransaction(transactionId, denier, reason) {
376
+ const pending = this.pendingApprovals.get(transactionId);
377
+ if (!pending) {
378
+ return { denied: false, error: 'Transaction not found or already resolved' };
379
+ }
380
+
381
+ pending.record.status = 'denied';
382
+ this.pendingApprovals.delete(transactionId);
383
+ this._audit('transaction_denied', { transactionId, denier, reason });
384
+ return { denied: true };
385
+ }
386
+
387
+ // ─── API Call Monitoring ──────────────────────────────────────
388
+
389
+ /**
390
+ * Monitor an outbound API call to a financial service.
391
+ * @param {Object} call
392
+ * @param {string} call.url - API URL
393
+ * @param {string} [call.method] - HTTP method
394
+ * @param {string} [call.initiator] - What triggered the call
395
+ * @returns {Object} { allowed, service, category, alerts }
396
+ */
397
+ monitorApiCall(call) {
398
+ let url;
399
+ try {
400
+ url = new URL(call.url);
401
+ } catch {
402
+ return { allowed: true, service: null, category: null, alerts: [] };
403
+ }
404
+
405
+ const domain = url.hostname;
406
+ const matchedService = FINANCIAL_API_DOMAINS.find(s => domain.includes(s.domain));
407
+ const alerts = [];
408
+
409
+ if (matchedService) {
410
+ const record = {
411
+ timestamp: Date.now(),
412
+ domain,
413
+ service: matchedService.label,
414
+ category: matchedService.category,
415
+ method: call.method || 'unknown',
416
+ path: url.pathname,
417
+ initiator: call.initiator || 'agent',
418
+ };
419
+
420
+ this.apiCallLog.push(record);
421
+ this._audit('financial_api_call', record);
422
+
423
+ // Check for dangerous operations
424
+ const isDangerous = /\/charges|\/transfers|\/payouts|\/send|\/withdraw/i.test(url.pathname);
425
+ if (isDangerous && (call.method || '').toUpperCase() === 'POST') {
426
+ const alert = {
427
+ type: 'financial_api_mutation',
428
+ severity: 'high',
429
+ message: `Agent making POST to financial API: ${matchedService.label} ${url.pathname}`,
430
+ details: record,
431
+ };
432
+ this._alert(alert);
433
+ alerts.push(alert);
434
+ }
435
+
436
+ // Rate limiting check
437
+ const recentCalls = this.apiCallLog.filter(
438
+ c => c.service === matchedService.label && c.timestamp > Date.now() - 60000
439
+ );
440
+ if (recentCalls.length > 30) {
441
+ const alert = {
442
+ type: 'financial_api_rate',
443
+ severity: 'warning',
444
+ message: `High rate of calls to ${matchedService.label}: ${recentCalls.length} in last 60s`,
445
+ details: { service: matchedService.label, callCount: recentCalls.length },
446
+ };
447
+ this._alert(alert);
448
+ alerts.push(alert);
449
+ }
450
+ }
451
+
452
+ return {
453
+ allowed: !this.allowedDomains.size || this.allowedDomains.has(domain) || !matchedService,
454
+ service: matchedService?.label || null,
455
+ category: matchedService?.category || null,
456
+ alerts,
457
+ };
458
+ }
459
+
460
+ // ─── Compliance Report ────────────────────────────────────────
461
+
462
+ /**
463
+ * Generate a compliance-ready audit report.
464
+ * @param {Object} [options]
465
+ * @param {string} [options.format] - 'sox' | 'pcidss' | 'standard'
466
+ * @param {number} [options.fromTimestamp] - Start time
467
+ * @param {number} [options.toTimestamp] - End time
468
+ * @returns {Object} Formatted audit report
469
+ */
470
+ generateReport(options = {}) {
471
+ const format = options.format || this.auditFormat;
472
+ const from = options.fromTimestamp || 0;
473
+ const to = options.toTimestamp || Date.now();
474
+
475
+ const filteredLog = this.auditLog.filter(e => e.timestamp >= from && e.timestamp <= to);
476
+ const filteredTx = this.transactions.filter(t => t.timestamp >= from && t.timestamp <= to);
477
+
478
+ const report = {
479
+ generatedAt: new Date().toISOString(),
480
+ format,
481
+ period: {
482
+ from: new Date(from).toISOString(),
483
+ to: new Date(to).toISOString(),
484
+ },
485
+ summary: {
486
+ totalTransactions: filteredTx.length,
487
+ approvedTransactions: filteredTx.filter(t => t.status === 'approved').length,
488
+ deniedTransactions: filteredTx.filter(t => t.status === 'denied').length,
489
+ pendingTransactions: filteredTx.filter(t => t.status.startsWith('pending')).length,
490
+ totalAmount: filteredTx.filter(t => t.status === 'approved').reduce((s, t) => s + t.amount, 0),
491
+ totalAlerts: this.alerts.filter(a => a.timestamp >= from && a.timestamp <= to).length,
492
+ criticalAlerts: this.alerts.filter(a => a.timestamp >= from && a.timestamp <= to && a.severity === 'critical').length,
493
+ financialApiCalls: this.apiCallLog.filter(c => c.timestamp >= from && c.timestamp <= to).length,
494
+ },
495
+ transactions: filteredTx,
496
+ alerts: this.alerts.filter(a => a.timestamp >= from && a.timestamp <= to),
497
+ auditEntries: filteredLog.length,
498
+ };
499
+
500
+ if (format === 'sox') {
501
+ report.soxCompliance = {
502
+ separationOfDuties: this.dualApprovalThreshold > 0,
503
+ auditTrailComplete: filteredLog.length > 0,
504
+ transactionLimitsEnforced: this.transactionLimit > 0,
505
+ unauthorizedAccessAttempts: this.alerts.filter(a => a.type === 'financial_credential_access').length,
506
+ };
507
+ }
508
+
509
+ if (format === 'pcidss') {
510
+ report.pciCompliance = {
511
+ cardDataDetected: this.alerts.filter(a =>
512
+ a.details?.label?.includes('Credit card')
513
+ ).length,
514
+ credentialLeaks: this.alerts.filter(a => a.type === 'financial_secret_leak').length,
515
+ accessControlEnforced: true,
516
+ auditTrailEnabled: !!this.logPath,
517
+ };
518
+ }
519
+
520
+ this._audit('compliance_report_generated', { format, entries: filteredLog.length });
521
+
522
+ return report;
523
+ }
524
+
525
+ // ─── Utility Methods ──────────────────────────────────────────
526
+
527
+ /** Get all alerts */
528
+ getAlerts(minSeverity) {
529
+ if (!minSeverity) return [...this.alerts];
530
+ const levels = { low: 0, warning: 1, high: 2, critical: 3 };
531
+ const min = levels[minSeverity] || 0;
532
+ return this.alerts.filter(a => (levels[a.severity] || 0) >= min);
533
+ }
534
+
535
+ /** Get summary stats */
536
+ getSummary() {
537
+ return {
538
+ transactions: this.transactions.length,
539
+ pendingApprovals: this.pendingApprovals.size,
540
+ alerts: this.alerts.length,
541
+ criticalAlerts: this.alerts.filter(a => a.severity === 'critical').length,
542
+ apiCalls: this.apiCallLog.length,
543
+ auditEntries: this.auditLog.length,
544
+ };
545
+ }
546
+
547
+ /** Reset all state */
548
+ reset() {
549
+ this.transactions = [];
550
+ this.alerts = [];
551
+ this.auditLog = [];
552
+ this.apiCallLog = [];
553
+ this.pendingApprovals.clear();
554
+ }
555
+
556
+ /** @private */
557
+ _alert(alert) {
558
+ alert.timestamp = alert.timestamp || Date.now();
559
+ this.alerts.push(alert);
560
+ if (this.onAlert) this.onAlert(alert);
561
+ }
562
+
563
+ /** @private */
564
+ _audit(action, details) {
565
+ const entry = {
566
+ timestamp: Date.now(),
567
+ action,
568
+ details,
569
+ _iso: new Date().toISOString(),
570
+ };
571
+ this.auditLog.push(entry);
572
+ if (this.logPath) {
573
+ try {
574
+ fs.appendFileSync(this.logPath, JSON.stringify(entry) + '\n');
575
+ } catch { /* don't let logging break functionality */ }
576
+ }
577
+ }
578
+ }
579
+
580
+ module.exports = {
581
+ FinanceGuard,
582
+ FINANCIAL_FORBIDDEN_ZONES,
583
+ FINANCIAL_SECRET_PATTERNS,
584
+ FINANCIAL_API_DOMAINS,
585
+ };