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,515 @@
1
+ /**
2
+ * Immunefi Severity Classifier
3
+ * Maps vulnerability findings to Immunefi bug bounty payout categories
4
+ * Only reports findings that qualify for High/Critical payouts
5
+ *
6
+ * Immunefi Severity Levels:
7
+ * - Critical: Direct theft of funds, permanent freezing, protocol insolvency, governance takeover
8
+ * - High: Theft of unclaimed yield, temporary freezing, griefing with capital requirements
9
+ * - Medium/Low: Filtered out for high-TVL focus
10
+ */
11
+
12
+ const IMMUNEFI_CATEGORIES = {
13
+ CRITICAL: {
14
+ DIRECT_THEFT: {
15
+ id: 'direct-theft',
16
+ name: 'Direct Theft of Funds',
17
+ description: 'Attacker can steal user deposits, protocol reserves, or treasury funds',
18
+ minScore: 90,
19
+ requiredElements: ['value_extraction', 'no_auth_bypass_needed_OR_auth_bypass_present']
20
+ },
21
+ PERMANENT_FREEZE: {
22
+ id: 'permanent-freeze',
23
+ name: 'Permanent Freezing of Funds',
24
+ description: 'Funds become permanently inaccessible (not temporary DoS)',
25
+ minScore: 85,
26
+ requiredElements: ['fund_lock', 'no_recovery_path']
27
+ },
28
+ PROTOCOL_INSOLVENCY: {
29
+ id: 'protocol-insolvency',
30
+ name: 'Protocol Insolvency',
31
+ description: 'Protocol becomes unable to honor withdrawals or obligations',
32
+ minScore: 90,
33
+ requiredElements: ['accounting_manipulation', 'systemic_impact']
34
+ },
35
+ GOVERNANCE_TAKEOVER: {
36
+ id: 'governance-takeover',
37
+ name: 'Governance Takeover',
38
+ description: 'Attacker gains control of protocol governance/admin functions',
39
+ minScore: 85,
40
+ requiredElements: ['admin_access', 'persistent_control']
41
+ },
42
+ UNAUTHORIZED_MINTING: {
43
+ id: 'unauthorized-minting',
44
+ name: 'Unauthorized Token Minting',
45
+ description: 'Attacker can mint tokens without proper authorization',
46
+ minScore: 90,
47
+ requiredElements: ['mint_capability', 'value_extraction']
48
+ }
49
+ },
50
+ HIGH: {
51
+ YIELD_THEFT: {
52
+ id: 'yield-theft',
53
+ name: 'Theft of Unclaimed Yield',
54
+ description: 'Attacker can steal yield/rewards that belong to other users',
55
+ minScore: 75,
56
+ requiredElements: ['yield_extraction', 'other_user_impact']
57
+ },
58
+ TEMPORARY_FREEZE: {
59
+ id: 'temporary-freeze',
60
+ name: 'Temporary Freezing of Funds',
61
+ description: 'Funds locked temporarily but recoverable (DoS with fund impact)',
62
+ minScore: 70,
63
+ requiredElements: ['fund_lock', 'recovery_possible']
64
+ },
65
+ MANIPULATION_PROFIT: {
66
+ id: 'manipulation-profit',
67
+ name: 'Price/State Manipulation for Profit',
68
+ description: 'Attacker profits by manipulating prices or protocol state',
69
+ minScore: 75,
70
+ requiredElements: ['state_manipulation', 'profit_extraction']
71
+ },
72
+ PRIVILEGE_ESCALATION: {
73
+ id: 'privilege-escalation',
74
+ name: 'Privilege Escalation',
75
+ description: 'Attacker gains elevated permissions without authorization',
76
+ minScore: 70,
77
+ requiredElements: ['auth_bypass', 'elevated_access']
78
+ }
79
+ }
80
+ };
81
+
82
+ // Maps vulnerability types to potential Immunefi categories
83
+ const VULNERABILITY_TO_CATEGORY_MAP = {
84
+ // Reentrancy variants
85
+ 'reentrancy': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY'],
86
+ 'cross-function-reentrancy': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY'],
87
+ 'cross-contract-reentrancy': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY'],
88
+ 'read-only-reentrancy': ['MANIPULATION_PROFIT'],
89
+
90
+ // Access control (multiple name variants)
91
+ 'access-control': ['DIRECT_THEFT', 'GOVERNANCE_TAKEOVER', 'UNAUTHORIZED_MINTING'],
92
+ 'missing-access-control': ['DIRECT_THEFT', 'GOVERNANCE_TAKEOVER', 'UNAUTHORIZED_MINTING'],
93
+ 'broken-access-control': ['DIRECT_THEFT', 'GOVERNANCE_TAKEOVER'],
94
+ 'tx-origin': ['DIRECT_THEFT', 'PRIVILEGE_ESCALATION'],
95
+
96
+ // Oracle/Price manipulation
97
+ 'flash-loan': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY', 'MANIPULATION_PROFIT'],
98
+ 'flash-loan-oracle': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY', 'MANIPULATION_PROFIT'],
99
+ 'flash-loan-oracle-manipulation': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY'],
100
+ 'flash-loan-balance-manipulation': ['DIRECT_THEFT', 'MANIPULATION_PROFIT'],
101
+ 'flash-loan-governance': ['GOVERNANCE_TAKEOVER'],
102
+ 'balance-manipulation': ['DIRECT_THEFT', 'MANIPULATION_PROFIT'],
103
+ 'spot-price-manipulation': ['DIRECT_THEFT', 'MANIPULATION_PROFIT'],
104
+ 'oracle-manipulation': ['DIRECT_THEFT', 'MANIPULATION_PROFIT'],
105
+ 'stale-price': ['MANIPULATION_PROFIT', 'DIRECT_THEFT'],
106
+ 'stale-price-exploitation': ['MANIPULATION_PROFIT', 'DIRECT_THEFT'],
107
+
108
+ // Proxy/Upgrade
109
+ 'unprotected-initializer': ['GOVERNANCE_TAKEOVER', 'DIRECT_THEFT'],
110
+ 'unauthorized-upgrade': ['GOVERNANCE_TAKEOVER', 'DIRECT_THEFT'],
111
+ 'storage-collision': ['DIRECT_THEFT', 'PROTOCOL_INSOLVENCY'],
112
+ 'proxy': ['GOVERNANCE_TAKEOVER', 'DIRECT_THEFT'],
113
+
114
+ // Signature/Replay
115
+ 'signature-replay': ['DIRECT_THEFT', 'PRIVILEGE_ESCALATION'],
116
+ 'permit-dos': ['TEMPORARY_FREEZE'],
117
+ 'permit-replay': ['DIRECT_THEFT'],
118
+ 'missing-deadline': ['MANIPULATION_PROFIT'],
119
+
120
+ // Fund handling
121
+ 'unchecked-call': ['DIRECT_THEFT', 'PERMANENT_FREEZE'],
122
+ 'unchecked-transfer': ['DIRECT_THEFT'],
123
+ 'delegatecall': ['DIRECT_THEFT', 'GOVERNANCE_TAKEOVER'],
124
+ 'delegatecall-injection': ['DIRECT_THEFT', 'GOVERNANCE_TAKEOVER'],
125
+ 'selfdestruct': ['PERMANENT_FREEZE', 'DIRECT_THEFT'],
126
+
127
+ // TOCTOU
128
+ 'toctou': ['DIRECT_THEFT', 'MANIPULATION_PROFIT'],
129
+
130
+ // DeFi specific
131
+ 'vault-inflation': ['DIRECT_THEFT', 'YIELD_THEFT'],
132
+ 'share-manipulation': ['DIRECT_THEFT', 'YIELD_THEFT'],
133
+ 'first-depositor': ['DIRECT_THEFT'],
134
+ 'first-depositor-inflation': ['DIRECT_THEFT'],
135
+ 'donation-attack': ['MANIPULATION_PROFIT', 'YIELD_THEFT'],
136
+ 'sandwich-donation': ['MANIPULATION_PROFIT'],
137
+ 'rounding-exploit': ['YIELD_THEFT'],
138
+
139
+ // Governance
140
+ 'governance': ['GOVERNANCE_TAKEOVER'],
141
+ 'governance-reentrancy': ['GOVERNANCE_TAKEOVER'],
142
+
143
+ // Fallback for unknown (still try to classify based on elements)
144
+ 'unknown': ['DIRECT_THEFT', 'MANIPULATION_PROFIT']
145
+ };
146
+
147
+ class ImmunefiClassifier {
148
+ constructor() {
149
+ this.categories = IMMUNEFI_CATEGORIES;
150
+ this.vulnMap = VULNERABILITY_TO_CATEGORY_MAP;
151
+ }
152
+
153
+ /**
154
+ * Classify a finding into Immunefi payout category
155
+ * Returns null if finding doesn't qualify for High/Critical
156
+ */
157
+ classifyFinding(finding) {
158
+ const attackVector = this.normalizeAttackVector(finding.attackVector || finding.detector);
159
+ const potentialCategories = this.vulnMap[attackVector] || [];
160
+
161
+ if (potentialCategories.length === 0) {
162
+ return null; // No mapping, doesn't qualify
163
+ }
164
+
165
+ // Analyze finding for exploit chain elements
166
+ const exploitElements = this.analyzeExploitElements(finding);
167
+
168
+ // Try to match to highest severity category first
169
+ for (const categoryId of potentialCategories) {
170
+ const category = this.findCategory(categoryId);
171
+ if (!category) continue;
172
+
173
+ const match = this.matchCategory(finding, category, exploitElements);
174
+ if (match.qualifies) {
175
+ return {
176
+ severity: this.getCategorySeverity(categoryId),
177
+ category: category,
178
+ categoryId: categoryId,
179
+ exploitChain: match.exploitChain,
180
+ confidence: match.confidence,
181
+ payoutTier: this.getCategorySeverity(categoryId) === 'CRITICAL' ? 'Critical' : 'High',
182
+ impactDescription: this.generateImpactDescription(category, finding, match)
183
+ };
184
+ }
185
+ }
186
+
187
+ return null; // Doesn't qualify for High/Critical
188
+ }
189
+
190
+ /**
191
+ * Analyze finding for exploit chain elements
192
+ */
193
+ analyzeExploitElements(finding) {
194
+ const elements = new Set();
195
+ const desc = (finding.description || '').toLowerCase();
196
+ const title = (finding.title || '').toLowerCase();
197
+ const combined = desc + ' ' + title;
198
+
199
+ // Value extraction indicators
200
+ if (/drain|steal|extract|profit|arbitrage|theft/.test(combined)) {
201
+ elements.add('value_extraction');
202
+ }
203
+ if (/transfer|withdraw|claim|redeem/.test(combined)) {
204
+ elements.add('value_extraction');
205
+ }
206
+
207
+ // Auth bypass indicators
208
+ if (/no.*access.*control|missing.*modifier|bypass|unauthorized/.test(combined)) {
209
+ elements.add('auth_bypass');
210
+ elements.add('auth_bypass_present');
211
+ }
212
+ // High confidence exploitable = no auth bypass needed (directly callable)
213
+ if (finding.exploitable === true && finding.confidence === 'HIGH') {
214
+ elements.add('no_auth_bypass_needed');
215
+ }
216
+ // Also support reentrant attacks that don't need auth bypass
217
+ if (/reentran|drain|steal|exploit/.test(combined) && finding.exploitable === true) {
218
+ elements.add('no_auth_bypass_needed');
219
+ }
220
+
221
+ // Fund lock indicators
222
+ if (/lock|freeze|stuck|inaccessible|brick/.test(combined)) {
223
+ elements.add('fund_lock');
224
+ }
225
+ if (/permanent|forever|irrecoverable|destroy/.test(combined)) {
226
+ elements.add('no_recovery_path');
227
+ }
228
+ if (/temporary|dos|delay|block/.test(combined)) {
229
+ elements.add('recovery_possible');
230
+ }
231
+
232
+ // Accounting manipulation
233
+ if (/inflation|deflation|accounting|balance.*manipul|share.*manipul/.test(combined)) {
234
+ elements.add('accounting_manipulation');
235
+ }
236
+ if (/insolvency|insolvent|undercollateral/.test(combined)) {
237
+ elements.add('systemic_impact');
238
+ }
239
+
240
+ // Admin/Governance
241
+ if (/owner|admin|governance|upgrade|initializ/.test(combined)) {
242
+ elements.add('admin_access');
243
+ }
244
+ if (/takeover|control|persist|permanent.*access/.test(combined)) {
245
+ elements.add('persistent_control');
246
+ }
247
+ if (/elevat|escalat|privilege/.test(combined)) {
248
+ elements.add('elevated_access');
249
+ }
250
+
251
+ // Minting
252
+ if (/mint|create.*token|issue.*token/.test(combined)) {
253
+ elements.add('mint_capability');
254
+ }
255
+
256
+ // Yield/Rewards
257
+ if (/yield|reward|interest|dividend|fee/.test(combined)) {
258
+ elements.add('yield_extraction');
259
+ }
260
+ if (/other.*user|victim|deposit/.test(combined)) {
261
+ elements.add('other_user_impact');
262
+ }
263
+
264
+ // State manipulation
265
+ if (/manipulat|flash.*loan|oracle|price|state/.test(combined)) {
266
+ elements.add('state_manipulation');
267
+ }
268
+ if (/profit|gain|extract|arbitrage/.test(combined)) {
269
+ elements.add('profit_extraction');
270
+ }
271
+
272
+ return elements;
273
+ }
274
+
275
+ /**
276
+ * Match finding against category requirements
277
+ */
278
+ matchCategory(finding, category, exploitElements) {
279
+ const required = category.requiredElements || [];
280
+ const matched = [];
281
+ const missing = [];
282
+
283
+ for (const element of required) {
284
+ // Handle OR conditions (element1_OR_element2)
285
+ if (element.includes('_OR_')) {
286
+ const alternatives = element.split('_OR_');
287
+ const hasAny = alternatives.some(alt => exploitElements.has(alt));
288
+ if (hasAny) {
289
+ matched.push(element);
290
+ } else {
291
+ missing.push(element);
292
+ }
293
+ } else {
294
+ if (exploitElements.has(element)) {
295
+ matched.push(element);
296
+ } else {
297
+ missing.push(element);
298
+ }
299
+ }
300
+ }
301
+
302
+ // Calculate match quality
303
+ const matchRatio = required.length > 0 ? matched.length / required.length : 0;
304
+ const scoreThreshold = category.minScore || 70;
305
+ const findingScore = finding.exploitabilityScore || 50;
306
+
307
+ // Qualifies if:
308
+ // 1. All required elements present OR high match ratio with high score
309
+ // 2. Finding score meets minimum threshold
310
+ const qualifies = (
311
+ (missing.length === 0 || (matchRatio >= 0.7 && findingScore >= 80)) &&
312
+ findingScore >= scoreThreshold &&
313
+ finding.exploitable !== false
314
+ );
315
+
316
+ return {
317
+ qualifies,
318
+ matchedElements: matched,
319
+ missingElements: missing,
320
+ matchRatio,
321
+ confidence: this.calculateConfidence(matchRatio, finding),
322
+ exploitChain: this.buildExploitChain(finding, category, exploitElements)
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Build concrete exploit chain description
328
+ */
329
+ buildExploitChain(finding, category, elements) {
330
+ const chain = {
331
+ attackerCapabilities: this.inferAttackerCapabilities(finding, elements),
332
+ entryPoint: this.extractEntryPoint(finding),
333
+ vulnerableOperation: this.extractVulnerableOp(finding),
334
+ impact: category.description,
335
+ profitMechanism: this.inferProfitMechanism(finding, elements),
336
+ steps: []
337
+ };
338
+
339
+ // Build step-by-step exploit
340
+ const attackVector = (finding.attackVector || '').toLowerCase();
341
+
342
+ if (attackVector.includes('reentrancy')) {
343
+ chain.steps = [
344
+ 'Attacker deploys malicious contract with fallback/receive function',
345
+ `Attacker calls ${chain.entryPoint || 'vulnerable function'}`,
346
+ 'Vulnerable contract makes external call to attacker',
347
+ 'Attacker reenters before state update completes',
348
+ 'Attacker drains funds through repeated reentry',
349
+ 'State finally updates but funds already extracted'
350
+ ];
351
+ } else if (attackVector.includes('flash-loan') || attackVector.includes('oracle')) {
352
+ chain.steps = [
353
+ 'Attacker obtains flash loan (Aave/dYdX/Balancer)',
354
+ 'Attacker manipulates price oracle (large swap/donation)',
355
+ `Attacker calls ${chain.entryPoint || 'vulnerable function'} at manipulated price`,
356
+ 'Attacker extracts value at favorable rate',
357
+ 'Attacker reverses manipulation (swap back)',
358
+ 'Attacker repays flash loan, keeps profit'
359
+ ];
360
+ } else if (attackVector.includes('access-control')) {
361
+ chain.steps = [
362
+ 'Attacker identifies unprotected privileged function',
363
+ `Attacker calls ${chain.entryPoint || 'admin function'} directly`,
364
+ 'Function executes without authorization check',
365
+ chain.profitMechanism || 'Attacker gains unauthorized access/funds'
366
+ ];
367
+ } else if (attackVector.includes('initializer')) {
368
+ chain.steps = [
369
+ 'Attacker identifies uninitialized proxy or reinitializable contract',
370
+ 'Attacker calls initialize() with attacker-controlled parameters',
371
+ 'Attacker sets themselves as owner/admin',
372
+ 'Attacker uses admin privileges to drain funds or upgrade maliciously'
373
+ ];
374
+ } else if (attackVector.includes('signature') || attackVector.includes('replay')) {
375
+ chain.steps = [
376
+ 'Attacker obtains valid signature from previous transaction',
377
+ 'Attacker replays signature (missing nonce/deadline/chainId)',
378
+ `Signature validates and executes ${chain.entryPoint || 'privileged action'}`,
379
+ 'Attacker profits from unauthorized repeated execution'
380
+ ];
381
+ } else {
382
+ chain.steps = [
383
+ `Identify vulnerable operation in ${chain.entryPoint || 'target function'}`,
384
+ 'Prepare attack transaction',
385
+ 'Execute exploit',
386
+ 'Extract value or gain unauthorized access'
387
+ ];
388
+ }
389
+
390
+ return chain;
391
+ }
392
+
393
+ /**
394
+ * Infer required attacker capabilities
395
+ */
396
+ inferAttackerCapabilities(finding, elements) {
397
+ const capabilities = [];
398
+ const desc = (finding.description || '').toLowerCase();
399
+
400
+ if (elements.has('state_manipulation') || /flash.*loan|manipulat/.test(desc)) {
401
+ capabilities.push('Flash loan access (Aave, dYdX, Balancer)');
402
+ }
403
+ if (/mev|sandwich|frontrun/.test(desc)) {
404
+ capabilities.push('MEV infrastructure (Flashbots, private mempool)');
405
+ }
406
+ if (/reentran/.test(desc)) {
407
+ capabilities.push('Ability to deploy malicious contract');
408
+ }
409
+ if (capabilities.length === 0) {
410
+ capabilities.push('Standard EOA with gas');
411
+ }
412
+
413
+ return capabilities;
414
+ }
415
+
416
+ extractEntryPoint(finding) {
417
+ let loc = finding.location || '';
418
+ if (typeof loc !== 'string') {
419
+ loc = String(loc) || '';
420
+ }
421
+ const funcMatch = loc.match(/Function:\s*(\w+)/);
422
+ return funcMatch ? funcMatch[1] + '()' : null;
423
+ }
424
+
425
+ extractVulnerableOp(finding) {
426
+ return finding.title || finding.detector;
427
+ }
428
+
429
+ inferProfitMechanism(finding, elements) {
430
+ if (elements.has('value_extraction')) {
431
+ return 'Direct fund extraction via vulnerable function';
432
+ }
433
+ if (elements.has('yield_extraction')) {
434
+ return 'Theft of accumulated yield/rewards';
435
+ }
436
+ if (elements.has('profit_extraction')) {
437
+ return 'Arbitrage profit from price manipulation';
438
+ }
439
+ if (elements.has('mint_capability')) {
440
+ return 'Minting tokens and selling on market';
441
+ }
442
+ return 'Unauthorized value extraction';
443
+ }
444
+
445
+ calculateConfidence(matchRatio, finding) {
446
+ const baseConfidence = finding.confidence === 'HIGH' ? 0.9 :
447
+ finding.confidence === 'MEDIUM' ? 0.7 : 0.5;
448
+ return Math.min(1, baseConfidence * (0.5 + matchRatio * 0.5));
449
+ }
450
+
451
+ findCategory(categoryId) {
452
+ for (const severity of ['CRITICAL', 'HIGH']) {
453
+ if (this.categories[severity][categoryId]) {
454
+ return this.categories[severity][categoryId];
455
+ }
456
+ }
457
+ return null;
458
+ }
459
+
460
+ getCategorySeverity(categoryId) {
461
+ if (this.categories.CRITICAL[categoryId]) return 'CRITICAL';
462
+ if (this.categories.HIGH[categoryId]) return 'HIGH';
463
+ return 'MEDIUM';
464
+ }
465
+
466
+ normalizeAttackVector(vector) {
467
+ if (!vector) return '';
468
+ return vector.toLowerCase()
469
+ .replace(/[^a-z0-9-]/g, '-')
470
+ .replace(/-+/g, '-')
471
+ .replace(/^-|-$/g, '');
472
+ }
473
+
474
+ generateImpactDescription(category, finding, match) {
475
+ const entryPoint = this.extractEntryPoint(finding);
476
+ const func = entryPoint || 'the vulnerable function';
477
+
478
+ return `${category.name}: An attacker can exploit ${func} to ${category.description.toLowerCase()}. ` +
479
+ `This qualifies as Immunefi ${this.getCategorySeverity(category.id) || 'High'} severity. ` +
480
+ `Exploit confidence: ${Math.round(match.confidence * 100)}%.`;
481
+ }
482
+
483
+ /**
484
+ * Filter findings to only Immunefi High/Critical
485
+ */
486
+ filterToPayoutLevel(findings) {
487
+ const qualified = [];
488
+
489
+ for (const finding of findings) {
490
+ const classification = this.classifyFinding(finding);
491
+
492
+ if (classification) {
493
+ qualified.push({
494
+ ...finding,
495
+ immunefiClassification: classification,
496
+ severity: classification.severity, // Upgrade severity based on classification
497
+ payoutTier: classification.payoutTier,
498
+ exploitChain: classification.exploitChain
499
+ });
500
+ }
501
+ }
502
+
503
+ // Sort by severity then by exploitability score
504
+ qualified.sort((a, b) => {
505
+ const sevOrder = { 'CRITICAL': 2, 'HIGH': 1 };
506
+ const sevDiff = (sevOrder[b.severity] || 0) - (sevOrder[a.severity] || 0);
507
+ if (sevDiff !== 0) return sevDiff;
508
+ return (b.exploitabilityScore || 0) - (a.exploitabilityScore || 0);
509
+ });
510
+
511
+ return qualified;
512
+ }
513
+ }
514
+
515
+ module.exports = ImmunefiClassifier;