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,396 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ /**
7
+ * Foundry PoC Validator (Production-Grade)
8
+ *
9
+ * Hard rules enforced:
10
+ * - HIGH/CRITICAL only emitted if PoC compiles, executes on fork, and proves impact
11
+ * - Impact = fund drain, unauthorized transfer, role takeover, invariant break, or permanent DoS
12
+ * - If PoC fails → finding discarded silently
13
+ * - Severity derived from observed PoC results, not heuristics
14
+ *
15
+ * Notes:
16
+ * - Requires Foundry project with foundry.toml + forge-std
17
+ * - In production mode, forge is mandatory
18
+ */
19
+
20
+ // Impact types that qualify for HIGH/CRITICAL (Immunefi-aligned)
21
+ const IMPACT_PATTERNS = {
22
+ FUND_DRAIN: {
23
+ severity: 'CRITICAL',
24
+ patterns: [
25
+ /balance.*decreased|lost.*funds|drained/i,
26
+ /assertGt\s*\(\s*attackerBalanceAfter\s*,\s*attackerBalanceBefore/i,
27
+ /assertLt\s*\(\s*victimBalanceAfter\s*,\s*victimBalanceBefore/i,
28
+ /profit.*[1-9]\d*/i,
29
+ /stolen.*ether|stolen.*token/i
30
+ ]
31
+ },
32
+ UNAUTHORIZED_TRANSFER: {
33
+ severity: 'CRITICAL',
34
+ patterns: [
35
+ /transfer.*without.*approval/i,
36
+ /unauthorized.*withdrawal/i,
37
+ /assertEq\s*\(\s*attacker.*balance.*,.*victim/i
38
+ ]
39
+ },
40
+ ROLE_TAKEOVER: {
41
+ severity: 'CRITICAL',
42
+ patterns: [
43
+ /owner\s*==\s*attacker/i,
44
+ /hasRole.*attacker.*true/i,
45
+ /admin.*changed|owner.*changed/i,
46
+ /assertEq\s*\(\s*.*\.owner\(\)\s*,\s*attacker/i
47
+ ]
48
+ },
49
+ INVARIANT_BREAK: {
50
+ severity: 'HIGH',
51
+ patterns: [
52
+ /invariant.*broken|invariant.*violated/i,
53
+ /totalSupply.*mismatch/i,
54
+ /shares.*inflated|exchange.*rate.*manipulated/i
55
+ ]
56
+ },
57
+ PERMANENT_DOS: {
58
+ severity: 'HIGH',
59
+ patterns: [
60
+ /permanently.*locked|forever.*frozen/i,
61
+ /cannot.*withdraw|funds.*stuck/i,
62
+ /selfdestruct.*success/i
63
+ ]
64
+ }
65
+ };
66
+
67
+ class PocValidator {
68
+ constructor(options = {}) {
69
+ this.options = {
70
+ enabled: options.enabled || false,
71
+ requirePass: options.requirePass || false,
72
+ productionMode: options.productionMode || false, // Strict mode: require PoC execution proof
73
+ mode: options.mode || 'test', // 'test' (default) or 'build'
74
+ foundryRoot: options.foundryRoot || null,
75
+ keepTemp: options.keepTemp || false,
76
+ forkUrl: options.forkUrl || null, // Optional RPC URL for fork testing
77
+ ...options
78
+ };
79
+ }
80
+
81
+ static isForgeAvailable() {
82
+ const res = spawnSync('forge', ['--version'], { shell: true, stdio: 'ignore' });
83
+ return res.status === 0;
84
+ }
85
+
86
+ static hasPlaceholders(poc) {
87
+ if (!poc) return true;
88
+ const s = String(poc);
89
+ const placeholderPatterns = [
90
+ /0x\.\.\./, // 0x...
91
+ /\bTARGET_ADDRESS\b/,
92
+ /\bTODO\b/i,
93
+ /address\s+constant\s+\w+\s*=\s*address\(0\)/i, // constant address(0)
94
+ ];
95
+ return placeholderPatterns.some(p => p.test(s));
96
+ }
97
+
98
+ static findFoundryRoot(startPath) {
99
+ if (!startPath) return null;
100
+ let current = startPath;
101
+ try {
102
+ const stat = fs.statSync(current);
103
+ if (stat.isFile()) {
104
+ current = path.dirname(current);
105
+ }
106
+ } catch (_) {
107
+ // ignore
108
+ }
109
+
110
+ while (true) {
111
+ const candidate = path.join(current, 'foundry.toml');
112
+ if (fs.existsSync(candidate)) return current;
113
+ const parent = path.dirname(current);
114
+ if (parent === current) break;
115
+ current = parent;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ ensureDir(dirPath) {
121
+ fs.mkdirSync(dirPath, { recursive: true });
122
+ }
123
+
124
+ makeTempTestPath(foundryRoot, finding) {
125
+ const baseDir = path.join(foundryRoot, 'test', '.web3crit');
126
+ this.ensureDir(baseDir);
127
+ const safe = (finding.title || 'Finding')
128
+ .replace(/[^a-zA-Z0-9]+/g, '_')
129
+ .slice(0, 60);
130
+ const fileName = `${Date.now()}_${safe}.t.sol`;
131
+ return path.join(baseDir, fileName);
132
+ }
133
+
134
+ /**
135
+ * Extract impact from forge test output.
136
+ * Returns { impactType, severity, evidence } or null if no impact proven.
137
+ */
138
+ static extractImpact(forgeOutput) {
139
+ if (!forgeOutput) return null;
140
+
141
+ for (const [impactType, config] of Object.entries(IMPACT_PATTERNS)) {
142
+ for (const pattern of config.patterns) {
143
+ const match = forgeOutput.match(pattern);
144
+ if (match) {
145
+ return {
146
+ impactType,
147
+ severity: config.severity,
148
+ evidence: match[0]
149
+ };
150
+ }
151
+ }
152
+ }
153
+
154
+ // Check for test assertions that passed (indicates exploit succeeded)
155
+ if (/PASS.*test/i.test(forgeOutput) && /assert/i.test(forgeOutput)) {
156
+ // Look for value changes in logs
157
+ const valueMatch = forgeOutput.match(/(\d+)\s*(?:ether|wei|tokens?)/i);
158
+ if (valueMatch) {
159
+ return {
160
+ impactType: 'VALUE_EXTRACTION',
161
+ severity: 'HIGH',
162
+ evidence: `Value movement detected: ${valueMatch[0]}`
163
+ };
164
+ }
165
+ }
166
+
167
+ return null;
168
+ }
169
+
170
+ /**
171
+ * Derive severity from observed PoC execution results.
172
+ * Overrides heuristic-based severity with runtime proof.
173
+ */
174
+ static deriveSeverityFromImpact(impact, originalSeverity) {
175
+ if (!impact) {
176
+ // No proven impact → downgrade to INFO (will be filtered out)
177
+ return 'INFO';
178
+ }
179
+
180
+ // Impact-derived severity takes precedence
181
+ return impact.severity;
182
+ }
183
+
184
+ /**
185
+ * Validate a single finding. Returns { ok, reason, details?, impact?, derivedSeverity? }.
186
+ *
187
+ * In production mode:
188
+ * - HIGH/CRITICAL requires PoC execution with proven impact
189
+ * - Severity derived from observed results, not heuristics
190
+ */
191
+ validateFinding(finding, context = {}) {
192
+ const isHighSeverity = ['CRITICAL', 'HIGH'].includes(finding?.severity);
193
+
194
+ // In production mode, HIGH/CRITICAL MUST have PoC validation
195
+ if (this.options.productionMode && isHighSeverity) {
196
+ if (!finding || !finding.foundryPoC) {
197
+ return {
198
+ ok: false,
199
+ reason: 'Production mode: HIGH/CRITICAL requires Foundry PoC',
200
+ derivedSeverity: 'INFO' // Downgrade
201
+ };
202
+ }
203
+ }
204
+
205
+ if (!this.options.enabled && !this.options.productionMode) {
206
+ return { ok: true, reason: 'PoC validation disabled' };
207
+ }
208
+
209
+ if (!finding || !finding.foundryPoC) {
210
+ return { ok: false, reason: 'No Foundry PoC attached to finding' };
211
+ }
212
+
213
+ if (PocValidator.hasPlaceholders(finding.foundryPoC)) {
214
+ return {
215
+ ok: false,
216
+ reason: 'PoC contains placeholders (0x..., TARGET_ADDRESS, TODO, or constant address(0))',
217
+ derivedSeverity: isHighSeverity ? 'INFO' : finding.severity
218
+ };
219
+ }
220
+
221
+ // In production mode, we MUST execute and verify impact
222
+ const mustExecute = this.options.productionMode && isHighSeverity;
223
+
224
+ if (!this.options.requirePass && !mustExecute) {
225
+ return { ok: true, reason: 'PoC basic validation passed (placeholders check only)' };
226
+ }
227
+
228
+ // Strict pass requires forge + foundry project root.
229
+ if (!PocValidator.isForgeAvailable()) {
230
+ if (mustExecute) {
231
+ return {
232
+ ok: false,
233
+ reason: 'Production mode: forge required but not found in PATH',
234
+ derivedSeverity: 'INFO'
235
+ };
236
+ }
237
+ return { ok: false, reason: 'forge not found in PATH (install Foundry to enable PoC pass gating)' };
238
+ }
239
+
240
+ const foundryRoot =
241
+ this.options.foundryRoot ||
242
+ PocValidator.findFoundryRoot(context.scanTargetPath || process.cwd());
243
+
244
+ if (!foundryRoot) {
245
+ if (mustExecute) {
246
+ return {
247
+ ok: false,
248
+ reason: 'Production mode: No foundry.toml found',
249
+ derivedSeverity: 'INFO'
250
+ };
251
+ }
252
+ return { ok: false, reason: 'No foundry.toml found (run inside a Foundry project or pass --foundry-root)' };
253
+ }
254
+
255
+ const testPath = this.makeTempTestPath(foundryRoot, finding);
256
+ const content = String(finding.foundryPoC).trim() + os.EOL;
257
+
258
+ try {
259
+ fs.writeFileSync(testPath, content, { encoding: 'utf8' });
260
+
261
+ // Build args - always use test mode in production for impact verification
262
+ const args = (this.options.mode === 'build' && !mustExecute)
263
+ ? ['build', '--silent']
264
+ : ['test', '--match-path', testPath, '-vvvv']; // Extra verbose for impact extraction
265
+
266
+ // Add fork URL if provided
267
+ if (this.options.forkUrl) {
268
+ args.push('--fork-url', this.options.forkUrl);
269
+ }
270
+
271
+ const res = spawnSync('forge', args, {
272
+ cwd: foundryRoot,
273
+ shell: true,
274
+ encoding: 'utf8',
275
+ maxBuffer: 10 * 1024 * 1024,
276
+ timeout: 120000 // 2 minute timeout
277
+ });
278
+
279
+ const passed = res.status === 0;
280
+ const output = (res.stdout || '') + (res.stderr || '');
281
+
282
+ // Extract impact from test output
283
+ const impact = PocValidator.extractImpact(output);
284
+
285
+ // In production mode, derive severity from observed impact
286
+ let derivedSeverity = finding.severity;
287
+ if (this.options.productionMode && isHighSeverity) {
288
+ derivedSeverity = PocValidator.deriveSeverityFromImpact(impact, finding.severity);
289
+ }
290
+
291
+ // In production mode, require both passing test AND proven impact for HIGH/CRITICAL
292
+ const impactProven = impact !== null;
293
+ const ok = mustExecute
294
+ ? (passed && impactProven)
295
+ : passed;
296
+
297
+ let reason;
298
+ if (ok) {
299
+ reason = impactProven
300
+ ? `PoC executed successfully - Impact proven: ${impact.impactType}`
301
+ : 'forge validation passed';
302
+ } else if (!passed) {
303
+ reason = 'forge validation failed (PoC did not compile or tests failed)';
304
+ } else {
305
+ reason = 'Production mode: PoC executed but no qualifying impact detected';
306
+ }
307
+
308
+ return {
309
+ ok,
310
+ reason,
311
+ impact,
312
+ derivedSeverity,
313
+ details: ok ? undefined : output.slice(0, 4000)
314
+ };
315
+ } finally {
316
+ if (!this.options.keepTemp) {
317
+ try { fs.unlinkSync(testPath); } catch (_) {}
318
+ }
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Validate findings and optionally drop failures.
324
+ *
325
+ * In production mode:
326
+ * - HIGH/CRITICAL without proven impact are silently discarded
327
+ * - Severity is overridden based on PoC execution results
328
+ */
329
+ validateFindings(findings, context = {}) {
330
+ const kept = [];
331
+ const dropped = [];
332
+
333
+ for (const f of findings) {
334
+ const verdict = this.validateFinding(f, context);
335
+
336
+ // In production mode, update severity based on PoC results
337
+ const updatedFinding = { ...f, pocValidation: verdict };
338
+
339
+ if (this.options.productionMode && verdict.derivedSeverity) {
340
+ updatedFinding.originalSeverity = f.severity;
341
+ updatedFinding.severity = verdict.derivedSeverity;
342
+
343
+ // If downgraded from HIGH/CRITICAL, move to dropped
344
+ if (['CRITICAL', 'HIGH'].includes(f.severity) &&
345
+ !['CRITICAL', 'HIGH'].includes(verdict.derivedSeverity)) {
346
+ dropped.push(updatedFinding);
347
+ continue;
348
+ }
349
+ }
350
+
351
+ if (verdict.impact) {
352
+ updatedFinding.provenImpact = verdict.impact;
353
+ }
354
+
355
+ if (verdict.ok) {
356
+ kept.push(updatedFinding);
357
+ } else {
358
+ dropped.push(updatedFinding);
359
+ }
360
+ }
361
+
362
+ return { kept, dropped };
363
+ }
364
+
365
+ /**
366
+ * Production-grade validation for HIGH/CRITICAL findings only.
367
+ * Silently discards findings that don't meet the bar.
368
+ */
369
+ validateForProduction(findings, context = {}) {
370
+ const highSeverityFindings = findings.filter(f =>
371
+ ['CRITICAL', 'HIGH'].includes(f.severity)
372
+ );
373
+ const otherFindings = findings.filter(f =>
374
+ !['CRITICAL', 'HIGH'].includes(f.severity)
375
+ );
376
+
377
+ // All HIGH/CRITICAL must pass PoC validation with proven impact
378
+ const { kept, dropped } = this.validateFindings(highSeverityFindings, context);
379
+
380
+ return {
381
+ // Only return HIGH/CRITICAL that passed validation
382
+ findings: [...kept, ...otherFindings],
383
+ // Track what was dropped for reporting
384
+ droppedHighSeverity: dropped,
385
+ stats: {
386
+ originalHighSeverity: highSeverityFindings.length,
387
+ validatedHighSeverity: kept.length,
388
+ droppedCount: dropped.length
389
+ }
390
+ };
391
+ }
392
+ }
393
+
394
+ module.exports = PocValidator;
395
+
396
+