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,458 @@
1
+ const BaseDetector = require('./base-detector');
2
+
3
+ /**
4
+ * Enhanced Access Control Detector
5
+ * Validates what modifiers actually do instead of just checking existence
6
+ * Detects broken access control logic
7
+ */
8
+ class AccessControlEnhancedDetector extends BaseDetector {
9
+ constructor() {
10
+ super(
11
+ 'Access Control Vulnerability (Enhanced)',
12
+ 'Advanced access control validation - checks modifier logic, not just presence',
13
+ 'CRITICAL'
14
+ );
15
+ this.cfg = null;
16
+ this.dataFlow = null;
17
+ }
18
+
19
+ /**
20
+ * Override detect to use CFG analysis
21
+ */
22
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
23
+ this.cfg = cfg;
24
+ this.dataFlow = dataFlow;
25
+ this.sourceCode = sourceCode;
26
+ this.fileName = fileName;
27
+ this.sourceLines = sourceCode.split('\n');
28
+ this.findings = [];
29
+
30
+ if (!cfg) {
31
+ // Fallback to basic detection
32
+ return super.detect(ast, sourceCode, fileName);
33
+ }
34
+
35
+ // Analyze each function for access control issues
36
+ for (const [funcKey, funcInfo] of cfg.functions) {
37
+ this.analyzeFunctionAccessControl(funcKey, funcInfo);
38
+ }
39
+
40
+ // Analyze modifiers for broken logic
41
+ for (const [modKey, modInfo] of cfg.modifiers) {
42
+ this.analyzeModifierLogic(modKey, modInfo);
43
+ }
44
+
45
+ return this.findings;
46
+ }
47
+
48
+ /**
49
+ * Analyze function access control
50
+ */
51
+ analyzeFunctionAccessControl(funcKey, funcInfo) {
52
+ // Skip constructors
53
+ if (funcInfo.isConstructor) {
54
+ return;
55
+ }
56
+
57
+ // Check if function needs access control
58
+ if (this.needsAccessControl(funcInfo)) {
59
+ const protection = this.analyzeAccessProtection(funcInfo);
60
+
61
+ if (protection.level === 'none') {
62
+ this.reportMissingAccessControl(funcInfo, protection);
63
+ } else if (protection.level === 'weak') {
64
+ this.reportWeakAccessControl(funcInfo, protection);
65
+ } else if (protection.level === 'broken') {
66
+ this.reportBrokenAccessControl(funcInfo, protection);
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Determine if function needs access control
73
+ */
74
+ needsAccessControl(funcInfo) {
75
+ // Private/internal functions don't need explicit access control
76
+ if (funcInfo.visibility === 'private' || funcInfo.visibility === 'internal') {
77
+ return false;
78
+ }
79
+
80
+ // View/pure functions with no state changes don't need access control
81
+ if ((funcInfo.stateMutability === 'view' || funcInfo.stateMutability === 'pure') &&
82
+ funcInfo.stateWrites.length === 0) {
83
+ return false;
84
+ }
85
+
86
+ // Check if function has internal access control (require/assert checks in body)
87
+ if (this.hasInternalAccessControl(funcInfo)) {
88
+ return false;
89
+ }
90
+
91
+ // Check if function is a self-service withdrawal pattern (users withdraw own funds)
92
+ if (this.isSelfServiceWithdrawal(funcInfo)) {
93
+ return false;
94
+ }
95
+
96
+ // Check if function performs sensitive operations
97
+ return this.hasSensitiveOperations(funcInfo);
98
+ }
99
+
100
+ /**
101
+ * Check if function has internal access control checks
102
+ * (access control via require/assert inside function body instead of modifier)
103
+ */
104
+ hasInternalAccessControl(funcInfo) {
105
+ if (!funcInfo.node || !funcInfo.node.body) return false;
106
+
107
+ const funcCode = this.getCodeSnippet(funcInfo.node.loc);
108
+ if (!funcCode) return false;
109
+
110
+ // Check for common internal access control patterns
111
+ const internalAccessPatterns = [
112
+ /require\s*\(\s*msg\.sender\s*==\s*\w+/i, // require(msg.sender == owner)
113
+ /require\s*\(\s*\w+\s*==\s*msg\.sender/i, // require(pendingOwner == msg.sender)
114
+ /assert\s*\(\s*msg\.sender\s*==\s*\w+/i,
115
+ /if\s*\(\s*msg\.sender\s*!=\s*\w+\)\s*revert/i, // if (msg.sender != owner) revert
116
+ /onlyOwner|onlyAdmin|onlyRole/i, // Modifier names in function
117
+ ];
118
+
119
+ return internalAccessPatterns.some(pattern => pattern.test(funcCode));
120
+ }
121
+
122
+ /**
123
+ * Check if function is a self-service withdrawal pattern
124
+ * (users can only withdraw their own funds based on msg.sender)
125
+ */
126
+ isSelfServiceWithdrawal(funcInfo) {
127
+ const funcNameLower = funcInfo.name.toLowerCase().replace(/[_\s]/g, '');
128
+
129
+ // Check if function name suggests self-service pattern
130
+ const selfServiceNames = ['withdraw', 'withdrawfunds', 'claim', 'redeem', 'collect'];
131
+ if (!selfServiceNames.some(name => funcNameLower.includes(name))) {
132
+ return false;
133
+ }
134
+
135
+ if (!funcInfo.node || !funcInfo.node.body) return false;
136
+
137
+ const funcCode = this.getCodeSnippet(funcInfo.node.loc);
138
+ if (!funcCode) return false;
139
+
140
+ // Check if function accesses user-specific storage using msg.sender
141
+ // e.g., balances[msg.sender] or pendingWithdrawals[msg.sender]
142
+ const selfServicePatterns = [
143
+ /\[\s*msg\.sender\s*\]/, // mapping[msg.sender]
144
+ /balances\s*\[\s*msg\.sender\s*\]/i,
145
+ /pending\w*\s*\[\s*msg\.sender\s*\]/i,
146
+ ];
147
+
148
+ return selfServicePatterns.some(pattern => pattern.test(funcCode));
149
+ }
150
+
151
+ /**
152
+ * Check if function performs sensitive operations
153
+ */
154
+ hasSensitiveOperations(funcInfo) {
155
+ // Check for dangerous operations
156
+ const dangerousOps = ['delegatecall', 'selfdestruct', 'suicide'];
157
+ if (funcInfo.externalCalls.some(call => dangerousOps.includes(call.type))) {
158
+ return true;
159
+ }
160
+
161
+ // Check for direct value-moving operations by inspecting code for transfer/mint/burn patterns.
162
+ // This is intentionally conservative to avoid name-only false positives (e.g., withdrawInfo()).
163
+ if (funcInfo.node && funcInfo.node.loc) {
164
+ const code = (this.getCodeSnippet(funcInfo.node.loc) || '').toLowerCase();
165
+ const valueMovingPatterns = [
166
+ /\.transfer\s*\(/, // ERC20/ETH transfer-like
167
+ /\.transferfrom\s*\(/, // ERC20 transferFrom
168
+ /\.send\s*\(/, // ETH send
169
+ /\.call\s*\{[^}]*value/i, // call{value: ...}
170
+ /\b_mint\s*\(/,
171
+ /\bmint\s*\(/,
172
+ /\b_burn\s*\(/,
173
+ /\bburn\s*\(/,
174
+ /\bselfdestruct\s*\(/,
175
+ ];
176
+ if (valueMovingPatterns.some(p => p.test(code))) {
177
+ return true;
178
+ }
179
+ }
180
+
181
+ // Check if modifies critical state
182
+ if (this.modifiesCriticalState(funcInfo)) {
183
+ return true;
184
+ }
185
+
186
+ return false;
187
+ }
188
+
189
+ /**
190
+ * Check if function modifies critical state variables
191
+ */
192
+ modifiesCriticalState(funcInfo) {
193
+ const criticalVars = ['owner', 'admin', 'paused', 'implementation'];
194
+
195
+ return funcInfo.stateWrites.some(write => {
196
+ const varName = write.variable.toLowerCase();
197
+ return criticalVars.some(cv => varName.includes(cv));
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Analyze what access protection a function has
203
+ */
204
+ analyzeAccessProtection(funcInfo) {
205
+ if (funcInfo.modifiers.length === 0) {
206
+ return {
207
+ level: 'none',
208
+ reason: 'No access control modifiers'
209
+ };
210
+ }
211
+
212
+ // Analyze each modifier
213
+ const modifierAnalysis = funcInfo.modifiers.map(modName => {
214
+ const modKey = `${funcInfo.contract}.${modName}`;
215
+ const modInfo = this.cfg.modifiers.get(modKey);
216
+
217
+ if (!modInfo) {
218
+ return {
219
+ name: modName,
220
+ effective: 'unknown',
221
+ reason: 'Modifier not found in contract'
222
+ };
223
+ }
224
+
225
+ return this.evaluateModifierEffectiveness(modInfo);
226
+ });
227
+
228
+ // Determine overall protection level
229
+ const hasEffectiveProtection = modifierAnalysis.some(m => m.effective === 'strong');
230
+ const hasBrokenProtection = modifierAnalysis.some(m => m.effective === 'broken');
231
+ const hasWeakProtection = modifierAnalysis.some(m => m.effective === 'weak');
232
+
233
+ if (hasBrokenProtection) {
234
+ return {
235
+ level: 'broken',
236
+ modifiers: modifierAnalysis,
237
+ reason: 'Modifier exists but logic is broken'
238
+ };
239
+ } else if (hasEffectiveProtection) {
240
+ return {
241
+ level: 'strong',
242
+ modifiers: modifierAnalysis
243
+ };
244
+ } else if (hasWeakProtection) {
245
+ return {
246
+ level: 'weak',
247
+ modifiers: modifierAnalysis,
248
+ reason: 'Modifier provides weak protection'
249
+ };
250
+ }
251
+
252
+ return {
253
+ level: 'none',
254
+ modifiers: modifierAnalysis,
255
+ reason: 'Modifiers do not provide access control'
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Evaluate how effective a modifier is at access control
261
+ */
262
+ evaluateModifierEffectiveness(modInfo) {
263
+ if (!modInfo.checksAccess) {
264
+ return {
265
+ name: modInfo.name,
266
+ effective: 'none',
267
+ reason: 'Modifier does not check access'
268
+ };
269
+ }
270
+
271
+ // Check for common broken patterns
272
+ const brokenPatterns = this.detectBrokenPatterns(modInfo);
273
+ if (brokenPatterns.length > 0) {
274
+ return {
275
+ name: modInfo.name,
276
+ effective: 'broken',
277
+ reason: `Broken pattern: ${brokenPatterns.join(', ')}`
278
+ };
279
+ }
280
+
281
+ // Check for weak patterns
282
+ const weakPatterns = this.detectWeakPatterns(modInfo);
283
+ if (weakPatterns.length > 0) {
284
+ return {
285
+ name: modInfo.name,
286
+ effective: 'weak',
287
+ reason: `Weak pattern: ${weakPatterns.join(', ')}`
288
+ };
289
+ }
290
+
291
+ // Modifier appears to provide strong protection
292
+ return {
293
+ name: modInfo.name,
294
+ effective: 'strong',
295
+ reason: modInfo.checksOwnership ? 'Checks ownership' :
296
+ modInfo.checksRole ? 'Checks role' :
297
+ 'Checks access control'
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Detect broken access control patterns
303
+ */
304
+ detectBrokenPatterns(modInfo) {
305
+ const broken = [];
306
+
307
+ // Check if modifier has empty body
308
+ if (modInfo.requireStatements.length === 0) {
309
+ broken.push('Empty modifier - no checks');
310
+ }
311
+
312
+ // Check for always-true conditions
313
+ modInfo.requireStatements.forEach(stmt => {
314
+ if (stmt === 'true' || stmt === '1 == 1') {
315
+ broken.push('Always-true condition');
316
+ }
317
+ });
318
+
319
+ // Check for tx.origin instead of msg.sender (phishing risk)
320
+ modInfo.requireStatements.forEach(stmt => {
321
+ if (stmt.includes('tx.origin') && !stmt.includes('msg.sender')) {
322
+ broken.push('Uses tx.origin (vulnerable to phishing)');
323
+ }
324
+ });
325
+
326
+ return broken;
327
+ }
328
+
329
+ /**
330
+ * Detect weak access control patterns
331
+ */
332
+ detectWeakPatterns(modInfo) {
333
+ const weak = [];
334
+
335
+ // Check for balance-based access control (flash loan vulnerable)
336
+ modInfo.requireStatements.forEach(stmt => {
337
+ if (stmt.includes('balanceOf') || stmt.includes('.balance >')) {
338
+ weak.push('Balance-based access control (flash loan risk)');
339
+ }
340
+ });
341
+
342
+ // Check for timestamp-based access (miner manipulation)
343
+ modInfo.requireStatements.forEach(stmt => {
344
+ if (stmt.includes('block.timestamp') || stmt.includes('now')) {
345
+ weak.push('Timestamp-based check (miner manipulation risk)');
346
+ }
347
+ });
348
+
349
+ return weak;
350
+ }
351
+
352
+ /**
353
+ * Analyze modifier logic for vulnerabilities
354
+ */
355
+ analyzeModifierLogic(modKey, modInfo) {
356
+ // Check for modifier that claims to provide access control but doesn't
357
+ const broken = this.detectBrokenPatterns(modInfo);
358
+
359
+ if (broken.length > 0) {
360
+ this.reportBrokenModifier(modInfo, broken);
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Report missing access control
366
+ */
367
+ reportMissingAccessControl(funcInfo, protection) {
368
+ this.addFinding({
369
+ title: 'Missing Access Control on Sensitive Function',
370
+ description: `Function '${funcInfo.name}' performs sensitive operations without access control. ${protection.reason}. This allows ANY user to call this function.`,
371
+ location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
372
+ line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
373
+ column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
374
+ code: this.getCodeSnippet(funcInfo.node.loc),
375
+ severity: 'CRITICAL',
376
+ confidence: 'HIGH',
377
+ exploitable: true,
378
+ recommendation: 'Add access control modifier (e.g., onlyOwner) to restrict who can call this function. Use OpenZeppelin Ownable or AccessControl contracts.',
379
+ references: [
380
+ 'https://docs.openzeppelin.com/contracts/4.x/access-control',
381
+ 'https://swcregistry.io/docs/SWC-105'
382
+ ]
383
+ });
384
+ }
385
+
386
+ /**
387
+ * Report weak access control
388
+ */
389
+ reportWeakAccessControl(funcInfo, protection) {
390
+ const modifierDetails = protection.modifiers.map(m =>
391
+ `${m.name}: ${m.reason}`
392
+ ).join('; ');
393
+
394
+ this.addFinding({
395
+ title: 'Weak Access Control Pattern',
396
+ description: `Function '${funcInfo.name}' uses weak access control that can be bypassed. ${modifierDetails}. This may be exploitable.`,
397
+ location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
398
+ line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
399
+ column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
400
+ code: this.getCodeSnippet(funcInfo.node.loc),
401
+ severity: 'HIGH',
402
+ confidence: 'MEDIUM',
403
+ exploitable: true,
404
+ recommendation: 'Replace weak access control with proper ownership or role-based checks. Avoid balance-based or timestamp-based access control.',
405
+ references: [
406
+ 'https://docs.openzeppelin.com/contracts/4.x/access-control'
407
+ ]
408
+ });
409
+ }
410
+
411
+ /**
412
+ * Report broken access control
413
+ */
414
+ reportBrokenAccessControl(funcInfo, protection) {
415
+ const modifierDetails = protection.modifiers.map(m =>
416
+ `${m.name}: ${m.reason}`
417
+ ).join('; ');
418
+
419
+ this.addFinding({
420
+ title: 'Broken Access Control - Logic Error',
421
+ description: `Function '${funcInfo.name}' has access control modifier BUT it contains logic errors that make it ineffective. ${modifierDetails}. CRITICAL: Access control appears to exist but does not work.`,
422
+ location: `Contract: ${funcInfo.contract}, Function: ${funcInfo.name}`,
423
+ line: funcInfo.node.loc ? funcInfo.node.loc.start.line : 0,
424
+ column: funcInfo.node.loc ? funcInfo.node.loc.start.column : 0,
425
+ code: this.getCodeSnippet(funcInfo.node.loc),
426
+ severity: 'CRITICAL',
427
+ confidence: 'HIGH',
428
+ exploitable: true,
429
+ recommendation: 'Fix the modifier logic immediately. This is more dangerous than missing access control because developers may assume the function is protected.',
430
+ references: [
431
+ 'https://docs.openzeppelin.com/contracts/4.x/access-control'
432
+ ]
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Report broken modifier
438
+ */
439
+ reportBrokenModifier(modInfo, issues) {
440
+ this.addFinding({
441
+ title: 'Broken Access Control Modifier',
442
+ description: `Modifier '${modInfo.name}' contains logic errors: ${issues.join(', ')}. Functions using this modifier are NOT protected.`,
443
+ location: `Contract: ${modInfo.contract}, Modifier: ${modInfo.name}`,
444
+ line: modInfo.node.loc ? modInfo.node.loc.start.line : 0,
445
+ column: modInfo.node.loc ? modInfo.node.loc.start.column : 0,
446
+ code: this.getCodeSnippet(modInfo.node.loc),
447
+ severity: 'CRITICAL',
448
+ confidence: 'HIGH',
449
+ exploitable: true,
450
+ recommendation: 'Fix modifier logic immediately. All functions using this modifier are vulnerable.',
451
+ references: [
452
+ 'https://docs.soliditylang.org/en/latest/contracts.html#function-modifiers'
453
+ ]
454
+ });
455
+ }
456
+ }
457
+
458
+ module.exports = AccessControlEnhancedDetector;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Base Detector Class (Enhanced)
3
+ * Provides foundation for all vulnerability detectors with:
4
+ * - Exploitability scoring (0-100)
5
+ * - Attack vector classification
6
+ * - Foundry PoC support for high-confidence findings
7
+ */
8
+ class BaseDetector {
9
+ constructor(name, description, severity) {
10
+ this.name = name;
11
+ this.description = description;
12
+ this.severity = severity;
13
+ this.findings = [];
14
+ }
15
+
16
+ async detect(ast, sourceCode, fileName, cfg, dataFlow) {
17
+ this.findings = [];
18
+ this.ast = ast;
19
+ this.sourceCode = sourceCode;
20
+ this.fileName = fileName;
21
+ this.sourceLines = sourceCode.split('\n');
22
+ this.cfg = cfg;
23
+ this.dataFlow = dataFlow;
24
+
25
+ // Traverse the AST
26
+ this.traverse(ast);
27
+
28
+ return this.findings;
29
+ }
30
+
31
+ traverse(node) {
32
+ if (!node) return;
33
+
34
+ // Visit the node
35
+ const methodName = `visit${node.type}`;
36
+ if (typeof this[methodName] === 'function') {
37
+ this[methodName](node);
38
+ }
39
+
40
+ // Traverse children
41
+ for (const key in node) {
42
+ if (key === 'loc' || key === 'range') continue;
43
+
44
+ const child = node[key];
45
+ if (Array.isArray(child)) {
46
+ child.forEach(c => this.traverse(c));
47
+ } else if (child && typeof child === 'object' && child.type) {
48
+ this.traverse(child);
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Add a vulnerability finding with enhanced classification
55
+ *
56
+ * @param {Object} vulnerability - Finding details
57
+ * @param {string} vulnerability.title - Short title
58
+ * @param {string} vulnerability.description - Detailed description
59
+ * @param {string} vulnerability.severity - CRITICAL, HIGH, MEDIUM, LOW, INFO
60
+ * @param {string} vulnerability.confidence - HIGH, MEDIUM, LOW
61
+ * @param {boolean} vulnerability.exploitable - Is this realistically exploitable
62
+ * @param {number} vulnerability.exploitabilityScore - 0-100 score (optional)
63
+ * @param {string} vulnerability.attackVector - Attack classification (optional)
64
+ * @param {string} vulnerability.foundryPoC - Foundry test code for high-confidence findings (optional)
65
+ */
66
+ addFinding(vulnerability) {
67
+ // Calculate exploitability score if not provided
68
+ const exploitabilityScore = vulnerability.exploitabilityScore ||
69
+ this.calculateExploitabilityScore(vulnerability);
70
+
71
+ // Determine if this is a high-confidence finding worthy of PoC
72
+ const isHighConfidence = this.isHighConfidenceFinding(vulnerability, exploitabilityScore);
73
+
74
+ this.findings.push({
75
+ detector: this.name,
76
+ severity: vulnerability.severity || this.severity,
77
+ confidence: vulnerability.confidence || 'MEDIUM',
78
+ exploitable: vulnerability.exploitable !== undefined ? vulnerability.exploitable : true,
79
+ exploitabilityScore: exploitabilityScore,
80
+ attackVector: vulnerability.attackVector || this.classifyAttackVector(vulnerability),
81
+ title: vulnerability.title,
82
+ description: vulnerability.description,
83
+ location: vulnerability.location,
84
+ fileName: this.fileName,
85
+ line: vulnerability.line,
86
+ column: vulnerability.column,
87
+ code: vulnerability.code,
88
+ recommendation: vulnerability.recommendation,
89
+ references: vulnerability.references || [],
90
+ // Only include PoC for high-confidence findings
91
+ foundryPoC: isHighConfidence ? vulnerability.foundryPoC : undefined,
92
+ isHighConfidence: isHighConfidence
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Calculate exploitability score based on finding characteristics
98
+ * Score: 0-100 where higher = more likely exploitable
99
+ *
100
+ * HARDENED: More aggressive scoring to filter noise
101
+ * - Require HIGH confidence for high scores
102
+ * - Penalize theoretical/best-practice issues
103
+ * - Bonus for concrete attack vectors
104
+ */
105
+ calculateExploitabilityScore(vulnerability) {
106
+ let score = 40; // Base score (lowered from 50)
107
+
108
+ // Severity impact (unchanged)
109
+ const severityScores = {
110
+ 'CRITICAL': 30,
111
+ 'HIGH': 20,
112
+ 'MEDIUM': 5, // Reduced from 10
113
+ 'LOW': -10, // Penalty
114
+ 'INFO': -30 // Stronger penalty
115
+ };
116
+ score += severityScores[vulnerability.severity] || 0;
117
+
118
+ // Confidence impact (increased importance)
119
+ const confidenceScores = {
120
+ 'HIGH': 25, // Increased from 20
121
+ 'MEDIUM': 0,
122
+ 'LOW': -25 // Stronger penalty
123
+ };
124
+ score += confidenceScores[vulnerability.confidence] || 0;
125
+
126
+ // Exploitable flag (stronger impact)
127
+ if (vulnerability.exploitable === false) {
128
+ score -= 40; // Increased penalty
129
+ } else if (vulnerability.exploitable === true) {
130
+ score += 10; // Bonus for confirmed exploitable
131
+ }
132
+
133
+ // Bonus for concrete attack vector (not 'unknown')
134
+ if (vulnerability.attackVector && vulnerability.attackVector !== 'unknown') {
135
+ score += 5;
136
+ }
137
+
138
+ // Bonus for having Foundry PoC
139
+ if (vulnerability.foundryPoC) {
140
+ score += 10;
141
+ }
142
+
143
+ // Clamp to 0-100
144
+ return Math.max(0, Math.min(100, score));
145
+ }
146
+
147
+ /**
148
+ * Determine if finding is high-confidence (worthy of PoC)
149
+ */
150
+ isHighConfidenceFinding(vulnerability, exploitabilityScore) {
151
+ // Must have high confidence AND be exploitable AND score >= 70
152
+ if (vulnerability.confidence !== 'HIGH') return false;
153
+ if (vulnerability.exploitable === false) return false;
154
+ if (exploitabilityScore < 70) return false;
155
+
156
+ // CRITICAL or HIGH severity
157
+ const highSeverity = ['CRITICAL', 'HIGH'].includes(vulnerability.severity);
158
+ if (!highSeverity) return false;
159
+
160
+ return true;
161
+ }
162
+
163
+ /**
164
+ * Classify the attack vector based on finding characteristics
165
+ */
166
+ classifyAttackVector(vulnerability) {
167
+ const title = (vulnerability.title || '').toLowerCase();
168
+ const desc = (vulnerability.description || '').toLowerCase();
169
+ const combined = title + ' ' + desc;
170
+
171
+ // Attack vector classification
172
+ const vectors = [
173
+ { pattern: /reentrancy|reentrant/i, vector: 'reentrancy' },
174
+ { pattern: /overflow|underflow/i, vector: 'integer-overflow' },
175
+ { pattern: /flash.?loan|oracle.?manipul/i, vector: 'flash-loan' },
176
+ { pattern: /front.?run|sandwich|mev/i, vector: 'frontrunning' },
177
+ { pattern: /access.?control|unauthorized|permission/i, vector: 'access-control' },
178
+ { pattern: /dos|denial|gas.?grief/i, vector: 'denial-of-service' },
179
+ { pattern: /timestamp|block\.number/i, vector: 'timestamp-manipulation' },
180
+ { pattern: /delegate.?call/i, vector: 'delegatecall' },
181
+ { pattern: /selfdestruct|self.?destruct/i, vector: 'selfdestruct' },
182
+ { pattern: /signature|replay/i, vector: 'signature-replay' },
183
+ { pattern: /unchecked.?call|call.?return/i, vector: 'unchecked-call' }
184
+ ];
185
+
186
+ for (const { pattern, vector } of vectors) {
187
+ if (pattern.test(combined)) {
188
+ return vector;
189
+ }
190
+ }
191
+
192
+ return 'unknown';
193
+ }
194
+
195
+ getCodeSnippet(loc) {
196
+ if (!loc || !loc.start) return '';
197
+
198
+ const startLine = loc.start.line - 1;
199
+ const endLine = loc.end ? loc.end.line - 1 : startLine;
200
+
201
+ return this.sourceLines.slice(
202
+ Math.max(0, startLine),
203
+ Math.min(this.sourceLines.length, endLine + 1)
204
+ ).join('\n');
205
+ }
206
+
207
+ getLineContent(lineNumber) {
208
+ if (lineNumber < 1 || lineNumber > this.sourceLines.length) return '';
209
+ return this.sourceLines[lineNumber - 1];
210
+ }
211
+ }
212
+
213
+ module.exports = BaseDetector;