1
0
Fork 0
mirror of https://github.com/imjasonh/govulncheck-action synced 2026-07-09 17:18:14 +00:00
govulncheck-action/lib/parser.js
Jason Hall fe5af5345a
Fix: Parser fails to detect vulnerabilities in multi-line JSON output
## Problem
The govulncheck action was incorrectly reporting "no vulnerabilities found" even when
vulnerabilities were present. This was happening because govulncheck outputs pretty-printed
JSON (multi-line format) rather than JSON lines format.

## Root Cause
The parser had a logic bug where it would set `parsedAsJsonLines = true` whenever it
encountered ANY valid JSON line, including trivial ones like `{`. This prevented the
multi-line JSON parser from ever running, causing all findings to be missed.

## Solution
Changed the parser to only skip multi-line parsing if it actually found meaningful data
(OSV details or findings) in JSON lines format. Now it correctly falls back to multi-line
JSON parsing when needed.

## Additional Changes
Updated the example workflow to fail if no vulnerabilities are detected, since we know
the example code contains vulnerabilities. This serves as a regression test to ensure
the parser continues to work correctly.

## Testing
- Verified the parser now correctly identifies 8 vulnerabilities in the example code
- The action now properly reports vulnerabilities instead of false negatives
- CI checks are now working correctly
- Example workflow now fails if the parser bug resurfaces

This is a critical bug fix that prevents false "no vulnerabilities" reports.

Signed-off-by: Jason Hall <jason@chainguard.dev>
2025-06-07 02:30:32 -04:00

107 lines
No EOL
3.5 KiB
JavaScript

class VulnerabilityParser {
parse(output) {
const vulnerabilities = [];
const osvDetails = {};
// Try to parse as JSON Lines first (one JSON object per line)
const lines = output.split('\n').filter(line => line.trim());
console.log(`Parsing ${lines.length} lines of govulncheck output`);
// Try to parse each line as JSON first (JSON lines format)
let foundValidData = false;
for (const line of lines) {
try {
const json = JSON.parse(line);
if (json.osv) {
// Store OSV details
osvDetails[json.osv.id] = json.osv;
foundValidData = true;
} else if (json.finding) {
console.log(`Found vulnerability: ${JSON.stringify(json.finding.osv || json.finding)}`);
vulnerabilities.push(json);
foundValidData = true;
}
} catch (e) {
// Skip non-JSON lines
}
}
if (!foundValidData) {
// Parse as multi-line JSON objects
// Split by lines that start with '{' at the beginning
const jsonObjects = output.split(/\n(?=\{)/).filter(chunk => chunk.trim());
console.log(`Found ${jsonObjects.length} JSON objects`);
for (const jsonStr of jsonObjects) {
try {
const json = JSON.parse(jsonStr);
if (json.osv) {
// Store OSV details
osvDetails[json.osv.id] = json.osv;
} else if (json.finding) {
console.log(`Found vulnerability: ${JSON.stringify(json.finding.osv || json.finding)}`);
vulnerabilities.push(json);
}
} catch (e) {
console.log(`Failed to parse JSON object: ${e.message}`);
}
}
}
// Attach OSV details to vulnerabilities
for (const vuln of vulnerabilities) {
if (vuln.finding.osv && osvDetails[vuln.finding.osv]) {
vuln.osvDetails = osvDetails[vuln.finding.osv];
}
}
console.log(`Parsed ${vulnerabilities.length} vulnerabilities`);
return vulnerabilities;
}
extractUniqueModules(vulnerabilities) {
const modules = new Set();
for (const vuln of vulnerabilities) {
const finding = vuln.finding;
if (finding.trace && finding.trace.length > 0 && finding.trace[0].module) {
modules.add(finding.trace[0].module);
}
}
return Array.from(modules);
}
extractCallSites(vulnerabilities) {
const callSites = [];
for (const vuln of vulnerabilities) {
const finding = vuln.finding;
if (finding.trace && finding.trace.length > 0) {
// The first frame in the trace is the vulnerable function
const vulnerableFunction = finding.trace[0].function || 'unknown function';
// Look for frames that represent our code (not the vulnerable library)
for (let i = 1; i < finding.trace.length; i++) {
const frame = finding.trace[i];
if (frame.position && frame.position.filename && !frame.position.filename.includes('/')) {
// This frame is in our code (doesn't have path separators like library code)
callSites.push({
filename: frame.position.filename,
line: frame.position.line || 1,
function: frame.function || 'unknown function',
vulnerableFunction: vulnerableFunction,
osv: finding.osv || null,
osvDetails: vuln.osvDetails || null,
fixedVersion: finding.fixed_version || null
});
}
}
}
}
return callSites;
}
}
module.exports = VulnerabilityParser;