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.
- package/CONTRIBUTING.md +4 -2
- package/README.md +86 -3
- package/SECURITY.md +58 -10
- package/bin/clawmoat.js +298 -1
- package/clawmoat-0.8.0.tgz +0 -0
- package/docs/blog/386-malicious-skills.html +255 -0
- package/docs/blog/40000-exposed-openclaw-instances.html +194 -0
- package/docs/blog/agent-trust-protocol.html +197 -0
- package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +223 -0
- package/docs/blog/ibm-experts-agent-runtime-protection.html +238 -0
- package/docs/blog/index.html +168 -0
- package/docs/blog/mcp-30-cves-security-crisis.html +279 -0
- package/docs/blog/microsoft-openclaw-workstation-security.html +234 -0
- package/docs/blog/nist-ai-agent-standards-clawmoat.html +369 -0
- package/docs/blog/oasis-websocket-hijack.html +205 -0
- package/docs/blog/ollama-openclaw-security.html +154 -0
- package/docs/blog/openclaw-enterprise-readiness-claw10.html +198 -0
- package/docs/blog/openclaw-security-reckoning-2026.html +361 -0
- package/docs/blog/supply-chain-agents.html +166 -0
- package/docs/blog/supply-chain-agents.md +79 -0
- package/docs/business/index.html +530 -0
- package/docs/business/install.html +247 -0
- package/docs/checklist.html +168 -0
- package/docs/finance/index.html +217 -0
- package/docs/hall-of-fame.html +168 -0
- package/docs/index.html +328 -90
- package/docs/install.sh +557 -0
- package/docs/privacy-policy/index.html +122 -0
- package/docs/scan/index.html +214 -0
- package/docs/sitemap.xml +132 -2
- package/docs/support/index.html +124 -0
- package/docs/terms-of-service/index.html +122 -0
- package/examples/basic-usage.js +38 -0
- package/package.json +1 -1
- package/server/index.js +179 -14
- package/server/index.js.patch +1 -0
- package/src/finance/index.js +585 -0
- package/src/finance/mcp-firewall.js +486 -0
- package/src/guardian/cve-verify.js +129 -0
- package/src/guardian/gateway-monitor.js +590 -0
- package/src/guardian/index.js +3 -1
- package/src/guardian/insider-threat.js +498 -0
- package/src/index.js +3 -0
- 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
|
+
};
|