Skip to content

Commit b1d8aed

Browse files
committed
fix: broaden vulnerability scanner parsing
1 parent 837ed4d commit b1d8aed

3 files changed

Lines changed: 426 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ import SecurityScannerResults from '@/components/security';
479479
<SecurityScannerResults results={scannerJson} />
480480
```
481481

482-
Supported scanner fields include `id`, `ruleId`, `cve`, `title`, `message`, `description`, `severity`, `level`, `package`, `dependency`, `file`, `path`, `line`, `recommendation`, `fix`, and safe `http`/`https` URLs. Arrays, common `{ findings: [...] }` style objects, SARIF `runs[].results`, and npm-audit-style `vulnerabilities` maps are normalized into one warning shape.
482+
Supported scanner fields include `id`, `ruleId`, `check_id`, `check`, `cve`, `swc-id`, `title`, `check_name`, `message`, `description`, `severity`, `level`, `impact`, `package`, `dependency`, `file`, `path`, `filename`, `line`, `lineno`, `recommendation`, `fix`, and safe `http`/`https` URLs. Arrays, common `{ findings: [...] }` style objects, nested Slither-style `results.detectors`, Semgrep `results`, SARIF `runs[].results` with rule metadata, and npm-audit-style `vulnerabilities` or `advisories` maps are normalized into one warning shape.
483483

484484
Severity is normalized case-insensitively: `critical`, `high`, `medium`, `moderate`, `low`, `info`, `warning`, and `error` are supported. `error` maps to `high`; `warning` and `moderate` map to `medium`; unknown values remain `unknown`.
485485

src/components/security/__tests__/vulnerabilityParser.test.ts

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,119 @@ describe('vulnerabilityParser', () => {
127127
});
128128
});
129129

130+
it('uses SARIF rule metadata when individual results omit display fields', () => {
131+
const result = parseVulnerabilityResults({
132+
runs: [
133+
{
134+
tool: {
135+
driver: {
136+
name: 'CodeQL',
137+
rules: [
138+
{
139+
id: 'solidity/reentrancy',
140+
shortDescription: { text: 'Reentrancy risk' },
141+
fullDescription: { text: 'External calls can re-enter state-changing functions.' },
142+
defaultConfiguration: { level: 'error' },
143+
helpUri: 'https://example.com/rules/solidity-reentrancy',
144+
},
145+
],
146+
},
147+
},
148+
results: [
149+
{
150+
ruleId: 'solidity/reentrancy',
151+
locations: [
152+
{
153+
physicalLocation: {
154+
artifactLocation: { uri: 'contracts/Vault.sol' },
155+
region: { startLine: 88 },
156+
},
157+
},
158+
],
159+
},
160+
],
161+
},
162+
],
163+
});
164+
165+
expect(result.error).toBeNull();
166+
expect(result.findings[0]).toMatchObject({
167+
id: 'solidity/reentrancy',
168+
severity: 'high',
169+
title: 'Reentrancy risk',
170+
message: 'External calls can re-enter state-changing functions.',
171+
location: 'contracts/Vault.sol:88',
172+
source: 'CodeQL',
173+
url: 'https://example.com/rules/solidity-reentrancy',
174+
});
175+
});
176+
177+
it('parses nested Slither detector output from smart contract scans', () => {
178+
const result = parseVulnerabilityResults({
179+
success: true,
180+
results: {
181+
detectors: [
182+
{
183+
check: 'reentrancy-eth',
184+
impact: 'High',
185+
confidence: 'Medium',
186+
description: 'Potential reentrancy in withdraw().',
187+
elements: [
188+
{
189+
source_mapping: {
190+
filename_relative: 'contracts/Vault.sol',
191+
lines: [42],
192+
},
193+
},
194+
],
195+
recommendation: 'Use checks-effects-interactions or a reentrancy guard.',
196+
},
197+
],
198+
},
199+
});
200+
201+
expect(result.error).toBeNull();
202+
expect(result.findings[0]).toMatchObject({
203+
id: 'reentrancy-eth',
204+
severity: 'high',
205+
title: 'reentrancy-eth',
206+
message: 'Potential reentrancy in withdraw().',
207+
location: 'contracts/Vault.sol:42',
208+
recommendation: 'Use checks-effects-interactions or a reentrancy guard.',
209+
});
210+
});
211+
212+
it('parses Semgrep JSON and nested metadata safely', () => {
213+
const result = parseVulnerabilityResults({
214+
results: [
215+
{
216+
check_id: 'solidity.lang.security.tx-origin',
217+
path: 'contracts/Auth.sol',
218+
start: { line: 12 },
219+
extra: {
220+
message: 'Avoid tx.origin for authorization.',
221+
severity: 'ERROR',
222+
fix: 'Use msg.sender instead.',
223+
metadata: {
224+
'source-rule-url': 'https://semgrep.dev/r/solidity.lang.security.tx-origin',
225+
},
226+
},
227+
},
228+
],
229+
});
230+
231+
expect(result.error).toBeNull();
232+
expect(result.findings[0]).toMatchObject({
233+
id: 'solidity.lang.security.tx-origin',
234+
severity: 'high',
235+
title: 'solidity.lang.security.tx-origin',
236+
message: 'Avoid tx.origin for authorization.',
237+
location: 'contracts/Auth.sol:12',
238+
recommendation: 'Use msg.sender instead.',
239+
url: 'https://semgrep.dev/r/solidity.lang.security.tx-origin',
240+
});
241+
});
242+
130243
it('summarizes normalized findings', () => {
131244
const result = parseVulnerabilityResults([
132245
{ id: '1', title: 'A', severity: 'critical' },
@@ -146,4 +259,3 @@ describe('vulnerabilityParser', () => {
146259
expect(sanitizeDisplayText('<b>Fix</b> PASSWORD=hunter2')).toBe('Fix PASSWORD=[redacted]');
147260
});
148261
});
149-

0 commit comments

Comments
 (0)