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.
- package/README.md +685 -0
- package/bin/web3crit +10 -0
- package/package.json +59 -0
- package/src/analyzers/control-flow.js +256 -0
- package/src/analyzers/data-flow.js +720 -0
- package/src/analyzers/exploit-chain.js +751 -0
- package/src/analyzers/immunefi-classifier.js +515 -0
- package/src/analyzers/poc-validator.js +396 -0
- package/src/analyzers/solodit-enricher.js +1122 -0
- package/src/cli.js +546 -0
- package/src/detectors/access-control-enhanced.js +458 -0
- package/src/detectors/base-detector.js +213 -0
- package/src/detectors/callback-reentrancy.js +362 -0
- package/src/detectors/cross-contract-reentrancy.js +697 -0
- package/src/detectors/delegatecall.js +167 -0
- package/src/detectors/deprecated-functions.js +62 -0
- package/src/detectors/flash-loan.js +408 -0
- package/src/detectors/frontrunning.js +553 -0
- package/src/detectors/gas-griefing.js +701 -0
- package/src/detectors/governance-attacks.js +366 -0
- package/src/detectors/integer-overflow.js +487 -0
- package/src/detectors/oracle-manipulation.js +524 -0
- package/src/detectors/permit-exploits.js +368 -0
- package/src/detectors/precision-loss.js +408 -0
- package/src/detectors/price-manipulation-advanced.js +548 -0
- package/src/detectors/proxy-vulnerabilities.js +651 -0
- package/src/detectors/readonly-reentrancy.js +473 -0
- package/src/detectors/rebasing-token-vault.js +416 -0
- package/src/detectors/reentrancy-enhanced.js +359 -0
- package/src/detectors/selfdestruct.js +259 -0
- package/src/detectors/share-manipulation.js +412 -0
- package/src/detectors/signature-replay.js +409 -0
- package/src/detectors/storage-collision.js +446 -0
- package/src/detectors/timestamp-dependence.js +494 -0
- package/src/detectors/toctou.js +427 -0
- package/src/detectors/token-standard-compliance.js +465 -0
- package/src/detectors/unchecked-call.js +214 -0
- package/src/detectors/vault-inflation.js +421 -0
- package/src/index.js +42 -0
- package/src/package-lock.json +2874 -0
- package/src/package.json +39 -0
- 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;
|