web3crit-scanner 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +685 -0
  2. package/bin/web3crit +10 -0
  3. package/package.json +59 -0
  4. package/src/analyzers/control-flow.js +256 -0
  5. package/src/analyzers/data-flow.js +720 -0
  6. package/src/analyzers/exploit-chain.js +751 -0
  7. package/src/analyzers/immunefi-classifier.js +515 -0
  8. package/src/analyzers/poc-validator.js +396 -0
  9. package/src/analyzers/solodit-enricher.js +1122 -0
  10. package/src/cli.js +546 -0
  11. package/src/detectors/access-control-enhanced.js +458 -0
  12. package/src/detectors/base-detector.js +213 -0
  13. package/src/detectors/callback-reentrancy.js +362 -0
  14. package/src/detectors/cross-contract-reentrancy.js +697 -0
  15. package/src/detectors/delegatecall.js +167 -0
  16. package/src/detectors/deprecated-functions.js +62 -0
  17. package/src/detectors/flash-loan.js +408 -0
  18. package/src/detectors/frontrunning.js +553 -0
  19. package/src/detectors/gas-griefing.js +701 -0
  20. package/src/detectors/governance-attacks.js +366 -0
  21. package/src/detectors/integer-overflow.js +487 -0
  22. package/src/detectors/oracle-manipulation.js +524 -0
  23. package/src/detectors/permit-exploits.js +368 -0
  24. package/src/detectors/precision-loss.js +408 -0
  25. package/src/detectors/price-manipulation-advanced.js +548 -0
  26. package/src/detectors/proxy-vulnerabilities.js +651 -0
  27. package/src/detectors/readonly-reentrancy.js +473 -0
  28. package/src/detectors/rebasing-token-vault.js +416 -0
  29. package/src/detectors/reentrancy-enhanced.js +359 -0
  30. package/src/detectors/selfdestruct.js +259 -0
  31. package/src/detectors/share-manipulation.js +412 -0
  32. package/src/detectors/signature-replay.js +409 -0
  33. package/src/detectors/storage-collision.js +446 -0
  34. package/src/detectors/timestamp-dependence.js +494 -0
  35. package/src/detectors/toctou.js +427 -0
  36. package/src/detectors/token-standard-compliance.js +465 -0
  37. package/src/detectors/unchecked-call.js +214 -0
  38. package/src/detectors/vault-inflation.js +421 -0
  39. package/src/index.js +42 -0
  40. package/src/package-lock.json +2874 -0
  41. package/src/package.json +39 -0
  42. package/src/scanner-enhanced.js +816 -0
@@ -0,0 +1,1122 @@
1
+ /**
2
+ * Solodit API Integration for Web3CRIT Scanner
3
+ *
4
+ * Correlates detected vulnerabilities with real-world exploits and disclosed bugs
5
+ * from the Solodit vulnerability database.
6
+ *
7
+ * Features:
8
+ * - Query by vulnerability type, root cause, affected protocol, and code pattern
9
+ * - Validate findings as "confirmed in the wild" or "theoretical"
10
+ * - Assign confidence scores based on match quality
11
+ * - Read-only, production-safe, with graceful error handling
12
+ * - Intelligent caching to minimize API calls
13
+ *
14
+ * Solodit Categories Mapping:
15
+ * - Access Control: unprotected functions, missing modifiers
16
+ * - Reentrancy: classic, cross-function, cross-contract, read-only
17
+ * - Oracle Manipulation: price feed attacks, TWAP manipulation
18
+ * - Flash Loan: balance manipulation, governance attacks
19
+ * - Arithmetic: overflow, underflow, precision loss
20
+ * - Logic Errors: state machine bugs, incorrect assumptions
21
+ */
22
+
23
+ const https = require('https');
24
+ const http = require('http');
25
+
26
+ // Vulnerability type mapping to Solodit categories
27
+ const VULN_TYPE_TO_SOLODIT_CATEGORY = {
28
+ // Reentrancy variants
29
+ 'reentrancy': ['reentrancy', 'cross-function-reentrancy'],
30
+ 'cross-function-reentrancy': ['reentrancy', 'cross-function-reentrancy'],
31
+ 'cross-contract-reentrancy': ['reentrancy', 'cross-contract-reentrancy'],
32
+ 'read-only-reentrancy': ['reentrancy', 'read-only-reentrancy'],
33
+ 'callback-reentrancy': ['reentrancy', 'erc777-callback', 'erc1155-callback'],
34
+
35
+ // Access control
36
+ 'access-control': ['access-control', 'missing-access-control', 'privilege-escalation'],
37
+ 'missing-access-control': ['access-control', 'missing-access-control'],
38
+ 'broken-access-control': ['access-control', 'broken-access-control'],
39
+ 'tx-origin': ['access-control', 'tx-origin-authentication'],
40
+
41
+ // Oracle/Price manipulation
42
+ 'oracle-manipulation': ['oracle-manipulation', 'price-manipulation', 'twap-manipulation'],
43
+ 'flash-loan': ['flash-loan', 'price-manipulation', 'oracle-manipulation'],
44
+ 'flash-loan-oracle': ['flash-loan', 'oracle-manipulation'],
45
+ 'flash-loan-oracle-manipulation': ['flash-loan', 'oracle-manipulation'],
46
+ 'stale-price': ['oracle-manipulation', 'stale-price', 'chainlink'],
47
+ 'spot-price-manipulation': ['price-manipulation', 'spot-price'],
48
+
49
+ // Proxy/Upgrade
50
+ 'proxy': ['proxy', 'upgradeable', 'storage-collision'],
51
+ 'unprotected-initializer': ['proxy', 'unprotected-initializer', 'initialization'],
52
+ 'unauthorized-upgrade': ['proxy', 'unauthorized-upgrade', 'uups'],
53
+ 'storage-collision': ['proxy', 'storage-collision'],
54
+
55
+ // Signature/Replay
56
+ 'signature-replay': ['signature', 'replay-attack', 'missing-nonce'],
57
+ 'permit-replay': ['signature', 'permit', 'erc20-permit'],
58
+ 'missing-deadline': ['signature', 'deadline', 'permit'],
59
+
60
+ // Fund handling
61
+ 'unchecked-call': ['unchecked-return', 'low-level-call'],
62
+ 'delegatecall': ['delegatecall', 'proxy', 'code-injection'],
63
+ 'delegatecall-injection': ['delegatecall', 'code-injection'],
64
+ 'selfdestruct': ['selfdestruct', 'force-ether', 'contract-destruction'],
65
+
66
+ // DeFi specific
67
+ 'vault-inflation': ['vault', 'first-depositor', 'share-inflation'],
68
+ 'share-manipulation': ['vault', 'share-manipulation', 'erc4626'],
69
+ 'first-depositor': ['vault', 'first-depositor', 'donation-attack'],
70
+ 'donation-attack': ['donation-attack', 'vault', 'share-manipulation'],
71
+
72
+ // Governance
73
+ 'governance': ['governance', 'voting', 'flash-loan-governance'],
74
+ 'governance-reentrancy': ['governance', 'reentrancy'],
75
+
76
+ // Arithmetic
77
+ 'integer-overflow': ['arithmetic', 'overflow', 'underflow'],
78
+ 'precision-loss': ['arithmetic', 'precision-loss', 'rounding'],
79
+
80
+ // Other
81
+ 'frontrunning': ['frontrunning', 'mev', 'sandwich'],
82
+ 'toctou': ['toctou', 'race-condition'],
83
+ 'timestamp-dependence': ['timestamp', 'block-timestamp']
84
+ };
85
+
86
+ // Root cause patterns for deeper matching
87
+ const ROOT_CAUSE_PATTERNS = {
88
+ 'external-call-before-state-update': /external.*call.*before.*state|state.*after.*call|reentrancy/i,
89
+ 'missing-access-control': /no.*access.*control|missing.*modifier|public.*sensitive|unprotected/i,
90
+ 'unchecked-external-input': /user.*input|untrusted.*input|external.*input|tainted/i,
91
+ 'flash-loan-price-manipulation': /flash.*loan|price.*manipulat|oracle.*manipulat/i,
92
+ 'arithmetic-precision': /precision|rounding|division.*before.*multiplic|truncat/i,
93
+ 'signature-validation': /signature|ecrecover|nonce|replay|permit/i,
94
+ 'initialization-race': /initializ|uninitializ|proxy|upgrade/i,
95
+ 'state-inconsistency': /inconsistent.*state|state.*corrupt|toctou|race.*condition/i
96
+ };
97
+
98
+ /**
99
+ * Built-in knowledge base of notable exploits for offline mode
100
+ * Data sourced from public post-mortems and security research
101
+ */
102
+ const OFFLINE_EXPLOIT_DATABASE = [
103
+ // Reentrancy exploits
104
+ {
105
+ id: 'OKB-001',
106
+ title: 'The DAO Hack - Classic Reentrancy',
107
+ protocol: 'The DAO',
108
+ categories: ['reentrancy'],
109
+ rootCause: 'external-call-before-state-update',
110
+ keywords: ['reentrancy', 'withdraw', 'call', 'balance', 'recursive'],
111
+ codePatterns: ['external-call-with-value', 'low-level-call'],
112
+ severity: 'CRITICAL',
113
+ exploited: true,
114
+ lossAmount: 60000000,
115
+ exploitDate: '2016-06-17',
116
+ references: ['https://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/']
117
+ },
118
+ {
119
+ id: 'OKB-002',
120
+ title: 'Cream Finance Reentrancy via AMP Token',
121
+ protocol: 'Cream Finance',
122
+ categories: ['reentrancy', 'erc777-callback'],
123
+ rootCause: 'external-call-before-state-update',
124
+ keywords: ['reentrancy', 'erc777', 'callback', 'lending', 'borrow'],
125
+ codePatterns: ['external-call-with-value'],
126
+ severity: 'CRITICAL',
127
+ exploited: true,
128
+ lossAmount: 18800000,
129
+ exploitDate: '2021-08-30',
130
+ references: ['https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5']
131
+ },
132
+ {
133
+ id: 'OKB-003',
134
+ title: 'Curve Finance Read-Only Reentrancy',
135
+ protocol: 'Curve Finance',
136
+ categories: ['reentrancy', 'read-only-reentrancy'],
137
+ rootCause: 'external-call-before-state-update',
138
+ keywords: ['reentrancy', 'read-only', 'view', 'price', 'oracle', 'curve'],
139
+ codePatterns: ['external-call-with-value', 'balance-based-logic'],
140
+ severity: 'CRITICAL',
141
+ exploited: true,
142
+ lossAmount: 47000000,
143
+ exploitDate: '2023-07-30',
144
+ references: ['https://hackmd.io/@LlamaRisk/BJzSKHNjn']
145
+ },
146
+
147
+ // Flash loan / Oracle manipulation
148
+ {
149
+ id: 'OKB-010',
150
+ title: 'bZx Flash Loan Oracle Manipulation',
151
+ protocol: 'bZx',
152
+ categories: ['flash-loan', 'oracle-manipulation', 'price-manipulation'],
153
+ rootCause: 'flash-loan-price-manipulation',
154
+ keywords: ['flash', 'loan', 'oracle', 'price', 'manipulation', 'borrow'],
155
+ codePatterns: ['balance-based-logic'],
156
+ severity: 'CRITICAL',
157
+ exploited: true,
158
+ lossAmount: 8100000,
159
+ exploitDate: '2020-02-15',
160
+ references: ['https://peckshield.medium.com/bzx-hack-full-disclosure-with-detailed-profit-analysis-e6b1fa9b18fc']
161
+ },
162
+ {
163
+ id: 'OKB-011',
164
+ title: 'Harvest Finance Flash Loan Attack',
165
+ protocol: 'Harvest Finance',
166
+ categories: ['flash-loan', 'price-manipulation'],
167
+ rootCause: 'flash-loan-price-manipulation',
168
+ keywords: ['flash', 'loan', 'vault', 'price', 'manipulation', 'arbitrage'],
169
+ codePatterns: ['balance-based-logic'],
170
+ severity: 'CRITICAL',
171
+ exploited: true,
172
+ lossAmount: 34000000,
173
+ exploitDate: '2020-10-26',
174
+ references: ['https://medium.com/harvest-finance/harvest-flashloan-economic-attack-post-mortem-3cf900d65217']
175
+ },
176
+ {
177
+ id: 'OKB-012',
178
+ title: 'Euler Finance Flash Loan Attack',
179
+ protocol: 'Euler Finance',
180
+ categories: ['flash-loan', 'oracle-manipulation'],
181
+ rootCause: 'flash-loan-price-manipulation',
182
+ keywords: ['flash', 'loan', 'donate', 'liquidation', 'collateral'],
183
+ codePatterns: ['balance-based-logic'],
184
+ severity: 'CRITICAL',
185
+ exploited: true,
186
+ lossAmount: 197000000,
187
+ exploitDate: '2023-03-13',
188
+ references: ['https://www.euler.finance/blog/euler-protocol-attack-post-mortem']
189
+ },
190
+
191
+ // Access control
192
+ {
193
+ id: 'OKB-020',
194
+ title: 'Poly Network Access Control Bypass',
195
+ protocol: 'Poly Network',
196
+ categories: ['access-control', 'missing-access-control'],
197
+ rootCause: 'missing-access-control',
198
+ keywords: ['access', 'control', 'keeper', 'admin', 'cross-chain'],
199
+ codePatterns: ['unprotected-sender'],
200
+ severity: 'CRITICAL',
201
+ exploited: true,
202
+ lossAmount: 610000000,
203
+ exploitDate: '2021-08-10',
204
+ references: ['https://slowmist.medium.com/the-root-cause-of-poly-network-being-hacked-ec2ee1b0c68f']
205
+ },
206
+ {
207
+ id: 'OKB-021',
208
+ title: 'Ronin Bridge Validator Key Compromise',
209
+ protocol: 'Ronin Network',
210
+ categories: ['access-control'],
211
+ rootCause: 'missing-access-control',
212
+ keywords: ['bridge', 'validator', 'multisig', 'admin', 'key'],
213
+ codePatterns: [],
214
+ severity: 'CRITICAL',
215
+ exploited: true,
216
+ lossAmount: 624000000,
217
+ exploitDate: '2022-03-23',
218
+ references: ['https://roninblockchain.substack.com/p/community-alert-ronin-validators']
219
+ },
220
+
221
+ // Proxy/Upgrade vulnerabilities
222
+ {
223
+ id: 'OKB-030',
224
+ title: 'Wormhole Uninitialized Proxy',
225
+ protocol: 'Wormhole',
226
+ categories: ['proxy', 'unprotected-initializer'],
227
+ rootCause: 'initialization-race',
228
+ keywords: ['proxy', 'initialize', 'guardian', 'bridge', 'upgrade'],
229
+ codePatterns: [],
230
+ severity: 'CRITICAL',
231
+ exploited: true,
232
+ lossAmount: 320000000,
233
+ exploitDate: '2022-02-02',
234
+ references: ['https://extropy-io.medium.com/solana-wormhole-bridge-exploit-technical-analysis-3c1c0c99e8b8']
235
+ },
236
+ {
237
+ id: 'OKB-031',
238
+ title: 'Audius Uninitialized Proxy Storage',
239
+ protocol: 'Audius',
240
+ categories: ['proxy', 'unprotected-initializer', 'storage-collision'],
241
+ rootCause: 'initialization-race',
242
+ keywords: ['proxy', 'initialize', 'storage', 'governance', 'voting'],
243
+ codePatterns: [],
244
+ severity: 'CRITICAL',
245
+ exploited: true,
246
+ lossAmount: 6000000,
247
+ exploitDate: '2022-07-24',
248
+ references: ['https://blog.audius.co/article/audius-governance-takeover-post-mortem-7-23-22']
249
+ },
250
+
251
+ // Signature replay
252
+ {
253
+ id: 'OKB-040',
254
+ title: 'Wintermute Profanity Key Vulnerability',
255
+ protocol: 'Wintermute',
256
+ categories: ['signature', 'access-control'],
257
+ rootCause: 'signature-validation',
258
+ keywords: ['signature', 'key', 'vanity', 'private', 'brute-force'],
259
+ codePatterns: ['ecrecover'],
260
+ severity: 'CRITICAL',
261
+ exploited: true,
262
+ lossAmount: 160000000,
263
+ exploitDate: '2022-09-20',
264
+ references: ['https://rekt.news/wintermute-rekt/']
265
+ },
266
+ {
267
+ id: 'OKB-041',
268
+ title: 'Nomad Bridge Signature Bypass',
269
+ protocol: 'Nomad',
270
+ categories: ['signature', 'access-control'],
271
+ rootCause: 'signature-validation',
272
+ keywords: ['bridge', 'signature', 'merkle', 'root', 'verification'],
273
+ codePatterns: [],
274
+ severity: 'CRITICAL',
275
+ exploited: true,
276
+ lossAmount: 190000000,
277
+ exploitDate: '2022-08-01',
278
+ references: ['https://medium.com/nomad-xyz-blog/nomad-bridge-hack-root-cause-analysis-875ad2e5aacd']
279
+ },
280
+
281
+ // First depositor / Share manipulation
282
+ {
283
+ id: 'OKB-050',
284
+ title: 'ERC4626 First Depositor Inflation Attack',
285
+ protocol: 'Various ERC4626 Vaults',
286
+ categories: ['vault', 'first-depositor', 'share-inflation'],
287
+ rootCause: 'arithmetic-precision',
288
+ keywords: ['vault', 'share', 'deposit', 'first', 'inflation', 'rounding', 'erc4626'],
289
+ codePatterns: ['balance-based-logic'],
290
+ severity: 'HIGH',
291
+ exploited: true,
292
+ lossAmount: 0,
293
+ exploitDate: '2022-01-01',
294
+ references: ['https://blog.openzeppelin.com/a-]vulnerability-in-erc4626-vaults']
295
+ },
296
+
297
+ // Governance attacks
298
+ {
299
+ id: 'OKB-060',
300
+ title: 'Beanstalk Flash Loan Governance Attack',
301
+ protocol: 'Beanstalk',
302
+ categories: ['governance', 'flash-loan', 'flash-loan-governance'],
303
+ rootCause: 'flash-loan-price-manipulation',
304
+ keywords: ['governance', 'flash', 'loan', 'vote', 'proposal', 'snapshot'],
305
+ codePatterns: [],
306
+ severity: 'CRITICAL',
307
+ exploited: true,
308
+ lossAmount: 182000000,
309
+ exploitDate: '2022-04-17',
310
+ references: ['https://bean.money/blog/beanstalk-governance-exploit']
311
+ },
312
+
313
+ // Unchecked calls
314
+ {
315
+ id: 'OKB-070',
316
+ title: 'King of the Ether Unchecked Send',
317
+ protocol: 'King of the Ether',
318
+ categories: ['unchecked-return', 'low-level-call'],
319
+ rootCause: 'unchecked-external-input',
320
+ keywords: ['send', 'transfer', 'unchecked', 'return', 'value'],
321
+ codePatterns: ['low-level-call'],
322
+ severity: 'HIGH',
323
+ exploited: true,
324
+ lossAmount: 0,
325
+ exploitDate: '2016-02-06',
326
+ references: ['https://www.kingoftheether.com/postmortem.html']
327
+ }
328
+ ];
329
+
330
+ /**
331
+ * Offline vulnerability matcher using built-in knowledge base
332
+ */
333
+ class OfflineVulnerabilityMatcher {
334
+ constructor() {
335
+ this.database = OFFLINE_EXPLOIT_DATABASE;
336
+ }
337
+
338
+ /**
339
+ * Search for matching vulnerabilities in offline database
340
+ */
341
+ search(finding, categories, keywords, rootCause, codePatterns) {
342
+ const results = [];
343
+
344
+ for (const entry of this.database) {
345
+ let score = 0;
346
+ const matchReasons = [];
347
+
348
+ // Category match (0-30 points)
349
+ const categoryOverlap = categories.filter(c =>
350
+ entry.categories.some(ec => ec.toLowerCase().includes(c.toLowerCase()) ||
351
+ c.toLowerCase().includes(ec.toLowerCase()))
352
+ );
353
+ if (categoryOverlap.length > 0) {
354
+ score += Math.min(30, categoryOverlap.length * 15);
355
+ matchReasons.push(`category: ${categoryOverlap.join(', ')}`);
356
+ }
357
+
358
+ // Root cause match (0-25 points)
359
+ if (rootCause !== 'unknown' && entry.rootCause === rootCause) {
360
+ score += 25;
361
+ matchReasons.push(`root cause: ${rootCause}`);
362
+ }
363
+
364
+ // Keyword match (0-25 points)
365
+ const keywordOverlap = keywords.filter(k =>
366
+ entry.keywords.some(ek => ek.includes(k) || k.includes(ek))
367
+ );
368
+ if (keywordOverlap.length > 0) {
369
+ score += Math.min(25, keywordOverlap.length * 5);
370
+ matchReasons.push(`keywords: ${keywordOverlap.length} matches`);
371
+ }
372
+
373
+ // Code pattern match (0-20 points)
374
+ const patternOverlap = codePatterns.filter(p =>
375
+ entry.codePatterns.some(ep => ep.includes(p) || p.includes(ep))
376
+ );
377
+ if (patternOverlap.length > 0) {
378
+ score += Math.min(20, patternOverlap.length * 10);
379
+ matchReasons.push(`patterns: ${patternOverlap.join(', ')}`);
380
+ }
381
+
382
+ // Only include if score is above threshold
383
+ if (score >= 20) {
384
+ results.push({
385
+ ...entry,
386
+ matchConfidence: {
387
+ score: Math.min(100, score),
388
+ normalized: Math.min(1, score / 100),
389
+ reasons: matchReasons
390
+ }
391
+ });
392
+ }
393
+ }
394
+
395
+ // Sort by score descending
396
+ results.sort((a, b) => b.matchConfidence.score - a.matchConfidence.score);
397
+
398
+ return { results };
399
+ }
400
+ }
401
+
402
+ class SoloditEnricher {
403
+ /**
404
+ * @param {Object} options
405
+ * @param {string} options.apiKey - Solodit API key (or use SOLODIT_API_KEY env var)
406
+ * @param {string} options.baseUrl - Solodit API base URL
407
+ * @param {boolean} options.enabled - Enable/disable enrichment
408
+ * @param {boolean} options.verbose - Verbose logging
409
+ * @param {number} options.timeout - API request timeout in ms
410
+ * @param {number} options.cacheTTL - Cache TTL in ms
411
+ * @param {number} options.maxRetries - Max API retry attempts
412
+ * @param {number} options.minConfidenceThreshold - Minimum match confidence to include (0-1)
413
+ */
414
+ constructor(options = {}) {
415
+ this.apiKey = options.apiKey || process.env.SOLODIT_API_KEY || null;
416
+ this.baseUrl = options.baseUrl || process.env.SOLODIT_API_URL || 'https://api.solodit.xyz/v1';
417
+ this.verbose = options.verbose || false;
418
+ this.timeout = options.timeout || 10000; // 10 seconds
419
+ this.cacheTTL = options.cacheTTL || 3600000; // 1 hour
420
+ this.maxRetries = options.maxRetries || 2;
421
+ this.minConfidenceThreshold = options.minConfidenceThreshold || 0.3;
422
+
423
+ // Offline mode: use built-in knowledge base when no API key
424
+ this.offlineMode = options.offlineMode || !this.apiKey;
425
+ this.offlineMatcher = new OfflineVulnerabilityMatcher();
426
+
427
+ // Enable if explicitly requested OR if API key is available
428
+ // Also enable in offline mode for local matching
429
+ this.enabled = options.enabled === true || (options.enabled !== false && this.apiKey !== null);
430
+
431
+ // If enabled but no API key, force offline mode
432
+ if (this.enabled && !this.apiKey) {
433
+ this.offlineMode = true;
434
+ this.log('info', 'Solodit enrichment enabled in offline mode (using built-in knowledge base)');
435
+ }
436
+
437
+ // In-memory cache with TTL
438
+ this.cache = new Map();
439
+ this.cacheTimestamps = new Map();
440
+
441
+ // Statistics
442
+ this.stats = {
443
+ queriesTotal: 0,
444
+ queriesSuccessful: 0,
445
+ queriesFailed: 0,
446
+ cacheHits: 0,
447
+ findingsEnriched: 0,
448
+ confirmedInWild: 0,
449
+ theoretical: 0,
450
+ offlineMode: this.offlineMode
451
+ };
452
+
453
+ // Validate API key format if provided
454
+ if (this.apiKey && !this.isValidApiKey(this.apiKey)) {
455
+ this.log('warn', 'Invalid Solodit API key format - falling back to offline mode');
456
+ this.offlineMode = true;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Validate API key format (basic check)
462
+ */
463
+ isValidApiKey(key) {
464
+ return typeof key === 'string' && key.length >= 16 && /^[a-zA-Z0-9_-]+$/.test(key);
465
+ }
466
+
467
+ /**
468
+ * Log with verbosity control
469
+ */
470
+ log(level, message, data = null) {
471
+ if (!this.verbose && level !== 'error') return;
472
+
473
+ const prefix = `[Solodit ${level.toUpperCase()}]`;
474
+ if (data) {
475
+ console.log(prefix, message, data);
476
+ } else {
477
+ console.log(prefix, message);
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Check if cache entry is valid
483
+ */
484
+ isCacheValid(key) {
485
+ if (!this.cache.has(key)) return false;
486
+ const timestamp = this.cacheTimestamps.get(key) || 0;
487
+ return Date.now() - timestamp < this.cacheTTL;
488
+ }
489
+
490
+ /**
491
+ * Get from cache
492
+ */
493
+ getFromCache(key) {
494
+ if (this.isCacheValid(key)) {
495
+ this.stats.cacheHits++;
496
+ return this.cache.get(key);
497
+ }
498
+ return null;
499
+ }
500
+
501
+ /**
502
+ * Set cache entry
503
+ */
504
+ setCache(key, value) {
505
+ this.cache.set(key, value);
506
+ this.cacheTimestamps.set(key, Date.now());
507
+ }
508
+
509
+ /**
510
+ * Generate cache key for a finding
511
+ */
512
+ generateCacheKey(finding) {
513
+ const attackVector = this.normalizeAttackVector(finding.attackVector || finding.detector);
514
+ const titleHash = this.simpleHash(finding.title || '');
515
+ const descHash = this.simpleHash((finding.description || '').substring(0, 200));
516
+ return `${attackVector}:${titleHash}:${descHash}`;
517
+ }
518
+
519
+ /**
520
+ * Simple string hash for cache keys
521
+ */
522
+ simpleHash(str) {
523
+ let hash = 0;
524
+ for (let i = 0; i < str.length; i++) {
525
+ const char = str.charCodeAt(i);
526
+ hash = ((hash << 5) - hash) + char;
527
+ hash = hash & hash;
528
+ }
529
+ return Math.abs(hash).toString(36);
530
+ }
531
+
532
+ /**
533
+ * Normalize attack vector for API queries
534
+ */
535
+ normalizeAttackVector(vector) {
536
+ if (!vector) return 'unknown';
537
+ return vector.toLowerCase()
538
+ .replace(/[^a-z0-9-]/g, '-')
539
+ .replace(/-+/g, '-')
540
+ .replace(/^-|-$/g, '');
541
+ }
542
+
543
+ /**
544
+ * Extract keywords from a finding for search
545
+ */
546
+ extractKeywords(finding) {
547
+ const keywords = new Set();
548
+
549
+ // Extract from attack vector
550
+ const vector = finding.attackVector || '';
551
+ if (vector) {
552
+ vector.split(/[-_\s]/).forEach(w => {
553
+ if (w.length > 2) keywords.add(w.toLowerCase());
554
+ });
555
+ }
556
+
557
+ // Extract from title
558
+ const title = finding.title || '';
559
+ title.split(/[\s\-_()]+/).forEach(w => {
560
+ const cleaned = w.toLowerCase().replace(/[^a-z0-9]/g, '');
561
+ if (cleaned.length > 3) keywords.add(cleaned);
562
+ });
563
+
564
+ // Extract from description (first 500 chars)
565
+ const desc = (finding.description || '').substring(0, 500);
566
+ const descWords = desc.match(/\b[a-zA-Z]{4,}\b/g) || [];
567
+ descWords.slice(0, 20).forEach(w => keywords.add(w.toLowerCase()));
568
+
569
+ // Add known DeFi terms if present
570
+ const defiTerms = ['vault', 'pool', 'swap', 'stake', 'lending', 'borrow',
571
+ 'collateral', 'liquidat', 'oracle', 'price', 'flash',
572
+ 'permit', 'approve', 'transfer', 'mint', 'burn'];
573
+ defiTerms.forEach(term => {
574
+ if (desc.toLowerCase().includes(term)) {
575
+ keywords.add(term);
576
+ }
577
+ });
578
+
579
+ return Array.from(keywords).slice(0, 15);
580
+ }
581
+
582
+ /**
583
+ * Identify root cause from finding
584
+ */
585
+ identifyRootCause(finding) {
586
+ const combined = `${finding.title || ''} ${finding.description || ''}`.toLowerCase();
587
+
588
+ for (const [cause, pattern] of Object.entries(ROOT_CAUSE_PATTERNS)) {
589
+ if (pattern.test(combined)) {
590
+ return cause;
591
+ }
592
+ }
593
+
594
+ return 'unknown';
595
+ }
596
+
597
+ /**
598
+ * Extract code pattern signature for matching
599
+ */
600
+ extractCodePattern(finding) {
601
+ const code = finding.code || '';
602
+ const patterns = [];
603
+
604
+ // Detect common vulnerable patterns
605
+ if (/\.call\{.*value/.test(code)) patterns.push('external-call-with-value');
606
+ if (/\.call\(/.test(code)) patterns.push('low-level-call');
607
+ if (/delegatecall/.test(code)) patterns.push('delegatecall');
608
+ if (/selfdestruct/.test(code)) patterns.push('selfdestruct');
609
+ if (/tx\.origin/.test(code)) patterns.push('tx-origin');
610
+ if (/ecrecover/.test(code)) patterns.push('ecrecover');
611
+ if (/balanceOf/.test(code) && /\.call/.test(code)) patterns.push('balance-based-logic');
612
+ if (/block\.timestamp/.test(code)) patterns.push('timestamp-dependent');
613
+ if (/msg\.sender/.test(code) && !/require|modifier/.test(code)) patterns.push('unprotected-sender');
614
+
615
+ return patterns;
616
+ }
617
+
618
+ /**
619
+ * Make HTTP request to Solodit API
620
+ */
621
+ async makeRequest(endpoint, method = 'GET', body = null) {
622
+ return new Promise((resolve, reject) => {
623
+ const url = new URL(endpoint, this.baseUrl);
624
+ const isHttps = url.protocol === 'https:';
625
+ const httpModule = isHttps ? https : http;
626
+
627
+ const options = {
628
+ hostname: url.hostname,
629
+ port: url.port || (isHttps ? 443 : 80),
630
+ path: url.pathname + url.search,
631
+ method: method,
632
+ headers: {
633
+ 'Authorization': `Bearer ${this.apiKey}`,
634
+ 'Content-Type': 'application/json',
635
+ 'Accept': 'application/json',
636
+ 'User-Agent': 'Web3CRIT-Scanner/6.0.0'
637
+ },
638
+ timeout: this.timeout
639
+ };
640
+
641
+ const req = httpModule.request(options, (res) => {
642
+ let data = '';
643
+
644
+ res.on('data', chunk => data += chunk);
645
+ res.on('end', () => {
646
+ if (res.statusCode >= 200 && res.statusCode < 300) {
647
+ try {
648
+ resolve(JSON.parse(data));
649
+ } catch (e) {
650
+ reject(new Error(`Invalid JSON response: ${e.message}`));
651
+ }
652
+ } else if (res.statusCode === 401) {
653
+ reject(new Error('Solodit API authentication failed - check API key'));
654
+ } else if (res.statusCode === 429) {
655
+ reject(new Error('Solodit API rate limit exceeded'));
656
+ } else {
657
+ reject(new Error(`Solodit API error: ${res.statusCode} ${res.statusMessage}`));
658
+ }
659
+ });
660
+ });
661
+
662
+ req.on('error', (e) => reject(e));
663
+ req.on('timeout', () => {
664
+ req.destroy();
665
+ reject(new Error('Solodit API request timeout'));
666
+ });
667
+
668
+ if (body) {
669
+ req.write(JSON.stringify(body));
670
+ }
671
+
672
+ req.end();
673
+ });
674
+ }
675
+
676
+ /**
677
+ * Search Solodit for similar vulnerabilities
678
+ * Uses API when available, falls back to offline knowledge base
679
+ */
680
+ async searchVulnerabilities(finding, retryCount = 0) {
681
+ const attackVector = this.normalizeAttackVector(finding.attackVector || finding.detector);
682
+ const categories = VULN_TYPE_TO_SOLODIT_CATEGORY[attackVector] || [attackVector];
683
+ const keywords = this.extractKeywords(finding);
684
+ const rootCause = this.identifyRootCause(finding);
685
+ const codePatterns = this.extractCodePattern(finding);
686
+
687
+ this.stats.queriesTotal++;
688
+
689
+ // Use offline matcher if in offline mode
690
+ if (this.offlineMode) {
691
+ this.log('info', `Offline search for: ${attackVector}`);
692
+ const results = this.offlineMatcher.search(finding, categories, keywords, rootCause, codePatterns);
693
+ this.stats.queriesSuccessful++;
694
+ return results;
695
+ }
696
+
697
+ // Online API query
698
+ const query = {
699
+ categories: categories,
700
+ keywords: keywords,
701
+ rootCause: rootCause,
702
+ codePatterns: codePatterns,
703
+ severity: finding.severity,
704
+ limit: 10
705
+ };
706
+
707
+ try {
708
+ const response = await this.makeRequest('/vulnerabilities/search', 'POST', query);
709
+ this.stats.queriesSuccessful++;
710
+ return response;
711
+ } catch (error) {
712
+ if (retryCount < this.maxRetries) {
713
+ this.log('warn', `Retry ${retryCount + 1}/${this.maxRetries}: ${error.message}`);
714
+ await this.sleep(1000 * (retryCount + 1)); // Exponential backoff
715
+ return this.searchVulnerabilities(finding, retryCount + 1);
716
+ }
717
+
718
+ // Fall back to offline mode on API failure
719
+ this.log('warn', `API failed, falling back to offline mode: ${error.message}`);
720
+ this.stats.queriesFailed++;
721
+ const offlineResults = this.offlineMatcher.search(finding, categories, keywords, rootCause, codePatterns);
722
+ return offlineResults;
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Calculate match confidence between finding and Solodit result
728
+ */
729
+ calculateMatchConfidence(finding, soloditResult) {
730
+ let confidence = 0;
731
+ let matchReasons = [];
732
+
733
+ // Category match (0-25 points)
734
+ const findingCategories = VULN_TYPE_TO_SOLODIT_CATEGORY[
735
+ this.normalizeAttackVector(finding.attackVector)
736
+ ] || [];
737
+ const resultCategories = soloditResult.categories || [];
738
+ const categoryOverlap = findingCategories.filter(c =>
739
+ resultCategories.some(rc => rc.toLowerCase().includes(c.toLowerCase()))
740
+ );
741
+ if (categoryOverlap.length > 0) {
742
+ confidence += Math.min(25, categoryOverlap.length * 10);
743
+ matchReasons.push(`category match: ${categoryOverlap.join(', ')}`);
744
+ }
745
+
746
+ // Root cause match (0-25 points)
747
+ const findingRootCause = this.identifyRootCause(finding);
748
+ const resultRootCause = (soloditResult.rootCause || '').toLowerCase();
749
+ if (findingRootCause !== 'unknown' && resultRootCause.includes(findingRootCause.replace(/-/g, ' '))) {
750
+ confidence += 25;
751
+ matchReasons.push(`root cause match: ${findingRootCause}`);
752
+ }
753
+
754
+ // Keyword overlap (0-20 points)
755
+ const findingKeywords = this.extractKeywords(finding);
756
+ const resultKeywords = (soloditResult.keywords || []).map(k => k.toLowerCase());
757
+ const keywordOverlap = findingKeywords.filter(k =>
758
+ resultKeywords.some(rk => rk.includes(k) || k.includes(rk))
759
+ );
760
+ if (keywordOverlap.length > 0) {
761
+ confidence += Math.min(20, keywordOverlap.length * 4);
762
+ matchReasons.push(`keyword overlap: ${keywordOverlap.length} terms`);
763
+ }
764
+
765
+ // Code pattern match (0-20 points)
766
+ const findingPatterns = this.extractCodePattern(finding);
767
+ const resultPatterns = soloditResult.codePatterns || [];
768
+ const patternOverlap = findingPatterns.filter(p =>
769
+ resultPatterns.some(rp => rp.toLowerCase().includes(p.toLowerCase()))
770
+ );
771
+ if (patternOverlap.length > 0) {
772
+ confidence += Math.min(20, patternOverlap.length * 10);
773
+ matchReasons.push(`code pattern match: ${patternOverlap.join(', ')}`);
774
+ }
775
+
776
+ // Severity alignment (0-10 points)
777
+ const severityMap = { 'CRITICAL': 4, 'HIGH': 3, 'MEDIUM': 2, 'LOW': 1 };
778
+ const findingSev = severityMap[finding.severity] || 0;
779
+ const resultSev = severityMap[soloditResult.severity?.toUpperCase()] || 0;
780
+ if (Math.abs(findingSev - resultSev) <= 1) {
781
+ confidence += 10;
782
+ matchReasons.push('severity aligned');
783
+ }
784
+
785
+ return {
786
+ score: Math.min(100, confidence),
787
+ normalized: Math.min(1, confidence / 100),
788
+ reasons: matchReasons
789
+ };
790
+ }
791
+
792
+ /**
793
+ * Determine if finding is "confirmed in the wild" based on Solodit matches
794
+ */
795
+ determineValidationStatus(finding, matches, topMatchConfidence) {
796
+ // Confirmed in the wild: high confidence match with real exploit
797
+ if (topMatchConfidence >= 0.7 && matches.some(m => m.exploited === true)) {
798
+ return {
799
+ status: 'confirmed_in_wild',
800
+ label: 'Confirmed in the Wild',
801
+ description: 'Similar vulnerability exploited in production',
802
+ confidence: topMatchConfidence
803
+ };
804
+ }
805
+
806
+ // Confirmed pattern: high confidence match with disclosed bug
807
+ if (topMatchConfidence >= 0.6 && matches.length > 0) {
808
+ return {
809
+ status: 'confirmed_pattern',
810
+ label: 'Confirmed Pattern',
811
+ description: 'Matches known vulnerability pattern from audits',
812
+ confidence: topMatchConfidence
813
+ };
814
+ }
815
+
816
+ // Likely valid: medium confidence match
817
+ if (topMatchConfidence >= 0.4 && matches.length > 0) {
818
+ return {
819
+ status: 'likely_valid',
820
+ label: 'Likely Valid',
821
+ description: 'Similar to known vulnerabilities',
822
+ confidence: topMatchConfidence
823
+ };
824
+ }
825
+
826
+ // Theoretical: no strong matches
827
+ return {
828
+ status: 'theoretical',
829
+ label: 'Theoretical',
830
+ description: 'No strong matches in vulnerability database',
831
+ confidence: topMatchConfidence
832
+ };
833
+ }
834
+
835
+ /**
836
+ * Enrich a single finding with Solodit data
837
+ */
838
+ async enrichFinding(finding) {
839
+ // Check cache first
840
+ const cacheKey = this.generateCacheKey(finding);
841
+ const cached = this.getFromCache(cacheKey);
842
+ if (cached !== null) {
843
+ this.log('info', `Cache hit for ${finding.title}`);
844
+ return { ...finding, soloditMetadata: cached };
845
+ }
846
+
847
+ // Search Solodit
848
+ const searchResults = await this.searchVulnerabilities(finding);
849
+
850
+ if (!searchResults || !searchResults.results || searchResults.results.length === 0) {
851
+ // No matches found
852
+ const metadata = {
853
+ matched: false,
854
+ validationStatus: this.determineValidationStatus(finding, [], 0),
855
+ searchedAt: new Date().toISOString(),
856
+ matchConfidence: 0
857
+ };
858
+ this.setCache(cacheKey, metadata);
859
+ this.stats.theoretical++;
860
+ return { ...finding, soloditMetadata: metadata };
861
+ }
862
+
863
+ // Calculate confidence for each match
864
+ const scoredMatches = searchResults.results.map(result => {
865
+ const confidence = this.calculateMatchConfidence(finding, result);
866
+ return { ...result, matchConfidence: confidence };
867
+ });
868
+
869
+ // Sort by confidence
870
+ scoredMatches.sort((a, b) => b.matchConfidence.score - a.matchConfidence.score);
871
+
872
+ // Get top match
873
+ const topMatch = scoredMatches[0];
874
+ const topConfidence = topMatch.matchConfidence.normalized;
875
+
876
+ // Filter to only include matches above threshold
877
+ const relevantMatches = scoredMatches.filter(
878
+ m => m.matchConfidence.normalized >= this.minConfidenceThreshold
879
+ );
880
+
881
+ // Determine validation status
882
+ const validationStatus = this.determineValidationStatus(finding, relevantMatches, topConfidence);
883
+
884
+ // Build enrichment metadata
885
+ const metadata = {
886
+ matched: relevantMatches.length > 0,
887
+ matchConfidence: topConfidence,
888
+ validationStatus: validationStatus,
889
+ topMatch: relevantMatches.length > 0 ? {
890
+ id: topMatch.id,
891
+ title: topMatch.title,
892
+ protocol: topMatch.protocol,
893
+ severity: topMatch.severity,
894
+ exploited: topMatch.exploited || false,
895
+ bountyAmount: topMatch.bountyAmount,
896
+ disclosedAt: topMatch.disclosedAt,
897
+ references: (topMatch.references || []).slice(0, 3),
898
+ matchReasons: topMatch.matchConfidence.reasons
899
+ } : null,
900
+ similarFindings: relevantMatches.slice(1, 4).map(m => ({
901
+ id: m.id,
902
+ title: m.title,
903
+ protocol: m.protocol,
904
+ confidence: m.matchConfidence.normalized
905
+ })),
906
+ realWorldExploits: relevantMatches
907
+ .filter(m => m.exploited === true)
908
+ .slice(0, 3)
909
+ .map(m => ({
910
+ id: m.id,
911
+ title: m.title,
912
+ protocol: m.protocol,
913
+ lossAmount: m.lossAmount,
914
+ exploitDate: m.exploitDate
915
+ })),
916
+ relatedAudits: relevantMatches
917
+ .filter(m => m.source === 'audit')
918
+ .slice(0, 3)
919
+ .map(m => ({
920
+ id: m.id,
921
+ title: m.title,
922
+ auditor: m.auditor,
923
+ protocol: m.protocol
924
+ })),
925
+ searchedAt: new Date().toISOString()
926
+ };
927
+
928
+ // Update stats
929
+ this.stats.findingsEnriched++;
930
+ if (validationStatus.status === 'confirmed_in_wild') {
931
+ this.stats.confirmedInWild++;
932
+ } else if (validationStatus.status === 'theoretical') {
933
+ this.stats.theoretical++;
934
+ }
935
+
936
+ // Cache the result
937
+ this.setCache(cacheKey, metadata);
938
+
939
+ return { ...finding, soloditMetadata: metadata };
940
+ }
941
+
942
+ /**
943
+ * Enrich all findings with Solodit data
944
+ */
945
+ async enrichFindings(findings) {
946
+ if (!this.enabled) {
947
+ this.log('info', 'Solodit enrichment disabled (no API key or explicitly disabled)');
948
+ return findings;
949
+ }
950
+
951
+ if (!findings || findings.length === 0) {
952
+ return findings;
953
+ }
954
+
955
+ this.log('info', `Enriching ${findings.length} findings with Solodit data...`);
956
+
957
+ const enriched = [];
958
+
959
+ // Process findings with rate limiting
960
+ for (let i = 0; i < findings.length; i++) {
961
+ const finding = findings[i];
962
+
963
+ try {
964
+ const enrichedFinding = await this.enrichFinding(finding);
965
+ enriched.push(enrichedFinding);
966
+
967
+ // Rate limiting: small delay between API calls
968
+ if (i < findings.length - 1 && !this.isCacheValid(this.generateCacheKey(findings[i + 1]))) {
969
+ await this.sleep(200); // 200ms between uncached requests
970
+ }
971
+ } catch (error) {
972
+ this.log('error', `Failed to enrich finding "${finding.title}": ${error.message}`);
973
+ // Add finding without enrichment on error
974
+ enriched.push(finding);
975
+ }
976
+ }
977
+
978
+ this.log('info', `Enrichment complete. Stats: ${JSON.stringify(this.stats)}`);
979
+
980
+ return enriched;
981
+ }
982
+
983
+ /**
984
+ * Get enrichment statistics
985
+ */
986
+ getStats() {
987
+ return {
988
+ ...this.stats,
989
+ cacheSize: this.cache.size,
990
+ enabled: this.enabled,
991
+ offlineMode: this.offlineMode,
992
+ knowledgeBaseSize: this.offlineMode ? OFFLINE_EXPLOIT_DATABASE.length : null
993
+ };
994
+ }
995
+
996
+ /**
997
+ * Clear cache
998
+ */
999
+ clearCache() {
1000
+ this.cache.clear();
1001
+ this.cacheTimestamps.clear();
1002
+ }
1003
+
1004
+ /**
1005
+ * Sleep utility
1006
+ */
1007
+ sleep(ms) {
1008
+ return new Promise(resolve => setTimeout(resolve, ms));
1009
+ }
1010
+
1011
+ /**
1012
+ * Test API connectivity
1013
+ */
1014
+ async testConnection() {
1015
+ if (!this.enabled) {
1016
+ return { success: false, error: 'Solodit enrichment not enabled' };
1017
+ }
1018
+
1019
+ try {
1020
+ const response = await this.makeRequest('/health', 'GET');
1021
+ return { success: true, response };
1022
+ } catch (error) {
1023
+ return { success: false, error: error.message };
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Solodit Finding Formatter
1030
+ * Formats enriched findings for display
1031
+ */
1032
+ class SoloditFormatter {
1033
+ /**
1034
+ * Format validation status badge
1035
+ */
1036
+ static formatValidationBadge(metadata) {
1037
+ if (!metadata || !metadata.validationStatus) {
1038
+ return '[UNKNOWN]';
1039
+ }
1040
+
1041
+ const status = metadata.validationStatus;
1042
+ switch (status.status) {
1043
+ case 'confirmed_in_wild':
1044
+ return `[CONFIRMED IN WILD - ${Math.round(status.confidence * 100)}%]`;
1045
+ case 'confirmed_pattern':
1046
+ return `[CONFIRMED PATTERN - ${Math.round(status.confidence * 100)}%]`;
1047
+ case 'likely_valid':
1048
+ return `[LIKELY VALID - ${Math.round(status.confidence * 100)}%]`;
1049
+ case 'theoretical':
1050
+ return `[THEORETICAL]`;
1051
+ default:
1052
+ return `[UNKNOWN]`;
1053
+ }
1054
+ }
1055
+
1056
+ /**
1057
+ * Format Solodit metadata for CLI output
1058
+ */
1059
+ static formatForCLI(metadata) {
1060
+ if (!metadata || !metadata.matched) {
1061
+ return null;
1062
+ }
1063
+
1064
+ const lines = [];
1065
+
1066
+ // Validation status
1067
+ lines.push(` Solodit: ${this.formatValidationBadge(metadata)}`);
1068
+
1069
+ // Top match
1070
+ if (metadata.topMatch) {
1071
+ lines.push(` Best Match: "${metadata.topMatch.title}" (${metadata.topMatch.protocol})`);
1072
+ if (metadata.topMatch.exploited) {
1073
+ lines.push(` - EXPLOITED in production`);
1074
+ }
1075
+ if (metadata.topMatch.bountyAmount) {
1076
+ lines.push(` - Bounty: $${metadata.topMatch.bountyAmount.toLocaleString()}`);
1077
+ }
1078
+ }
1079
+
1080
+ // Real-world exploits
1081
+ if (metadata.realWorldExploits && metadata.realWorldExploits.length > 0) {
1082
+ lines.push(` Related Exploits:`);
1083
+ metadata.realWorldExploits.forEach(exp => {
1084
+ const loss = exp.lossAmount ? ` ($${exp.lossAmount.toLocaleString()} loss)` : '';
1085
+ lines.push(` - ${exp.protocol}: ${exp.title}${loss}`);
1086
+ });
1087
+ }
1088
+
1089
+ return lines.join('\n');
1090
+ }
1091
+
1092
+ /**
1093
+ * Format Solodit metadata for JSON report
1094
+ */
1095
+ static formatForReport(metadata) {
1096
+ if (!metadata) {
1097
+ return null;
1098
+ }
1099
+
1100
+ return {
1101
+ validationStatus: metadata.validationStatus?.label || 'Unknown',
1102
+ matchConfidence: metadata.matchConfidence,
1103
+ confirmedInWild: metadata.validationStatus?.status === 'confirmed_in_wild',
1104
+ topMatch: metadata.topMatch ? {
1105
+ title: metadata.topMatch.title,
1106
+ protocol: metadata.topMatch.protocol,
1107
+ exploited: metadata.topMatch.exploited,
1108
+ bountyAmount: metadata.topMatch.bountyAmount,
1109
+ references: metadata.topMatch.references
1110
+ } : null,
1111
+ realWorldExploits: metadata.realWorldExploits || [],
1112
+ relatedAudits: metadata.relatedAudits || [],
1113
+ similarFindings: metadata.similarFindings || []
1114
+ };
1115
+ }
1116
+ }
1117
+
1118
+ module.exports = SoloditEnricher;
1119
+ module.exports.SoloditFormatter = SoloditFormatter;
1120
+ module.exports.OfflineVulnerabilityMatcher = OfflineVulnerabilityMatcher;
1121
+ module.exports.VULN_TYPE_TO_SOLODIT_CATEGORY = VULN_TYPE_TO_SOLODIT_CATEGORY;
1122
+ module.exports.OFFLINE_EXPLOIT_DATABASE = OFFLINE_EXPLOIT_DATABASE;