1
0
Fork 0
mirror of https://github.com/imjasonh/govulncheck-action synced 2026-07-18 22:56:04 +00:00

Initial implementation of govulncheck GitHub Action

This action runs govulncheck on Go projects and creates GitHub annotations
for vulnerabilities found in dependencies and code paths.

Features:
- Automated vulnerability scanning with govulncheck
- Smart annotations on go.mod for vulnerable dependencies
- Annotations on source code lines that call vulnerable functions
- Configurable working directory
- Output variables for vulnerability detection

The implementation includes:
- Modular architecture with separate concerns (execution, parsing, annotation)
- Comprehensive test suite with 100% function coverage
- Example vulnerable Go module for demonstration
- GitHub workflow example
- Full documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jason Hall 2025-06-06 23:41:03 -04:00
commit 39e329ab61
Failed to extract signature
19 changed files with 5314 additions and 0 deletions

87
lib/annotator.js Normal file
View file

@ -0,0 +1,87 @@
const fs = require('fs').promises;
const path = require('path');
class AnnotationCreator {
constructor(core, fsModule = fs) {
this.core = core;
this.fs = fsModule;
}
async createAnnotations(vulnerabilities, parser, workingDirectory = '.') {
const modules = parser.extractUniqueModules(vulnerabilities);
const callSites = parser.extractCallSites(vulnerabilities);
// Annotate go.mod for vulnerable modules
await this.annotateGoMod(modules, vulnerabilities, workingDirectory);
// Annotate source files for call sites
this.annotateCallSites(callSites);
}
async annotateGoMod(modules, vulnerabilities, workingDirectory) {
if (modules.length === 0) return;
const goModPath = path.join(workingDirectory, 'go.mod');
let goModContent = '';
try {
goModContent = await this.fs.readFile(goModPath, 'utf8');
} catch (error) {
this.core.warning(`Could not read go.mod: ${error.message}`);
return;
}
const goModLines = goModContent.split('\n');
for (const module of modules) {
// Find the vulnerability info for this module
const vuln = vulnerabilities.find(v =>
v.finding.trace &&
v.finding.trace[0] &&
v.finding.trace[0].module === module
);
// Find line in go.mod
for (let i = 0; i < goModLines.length; i++) {
if (goModLines[i].includes(module)) {
const annotation = {
path: 'go.mod',
start_line: i + 1,
end_line: i + 1,
annotation_level: 'warning',
message: `Vulnerable module: ${module}`,
title: 'Security Vulnerability'
};
if (vuln && vuln.finding.osv) {
annotation.message += ` (${vuln.finding.osv})`;
}
this.core.warning(annotation.message, annotation);
break;
}
}
}
}
annotateCallSites(callSites) {
for (const site of callSites) {
const annotation = {
path: site.filename,
start_line: site.line,
end_line: site.line,
annotation_level: 'warning',
message: `Vulnerable code path: ${site.function}`,
title: 'Security Vulnerability'
};
if (site.osv) {
annotation.message += ` (${site.osv})`;
}
this.core.warning(annotation.message, annotation);
}
}
}
module.exports = AnnotationCreator;

38
lib/govulncheck.js Normal file
View file

@ -0,0 +1,38 @@
const exec = require('@actions/exec');
class GovulncheckRunner {
constructor(execModule = exec) {
this.exec = execModule;
}
async install() {
await this.exec.exec('go', ['install', 'golang.org/x/vuln/cmd/govulncheck@latest']);
}
async run(workingDirectory = './...') {
let output = '';
let errorOutput = '';
const options = {
listeners: {
stdout: (data) => {
output += data.toString();
},
stderr: (data) => {
errorOutput += data.toString();
}
},
ignoreReturnCode: true
};
const exitCode = await this.exec.exec('govulncheck', ['-json', workingDirectory], options);
return {
output,
errorOutput,
exitCode
};
}
}
module.exports = GovulncheckRunner;

58
lib/parser.js Normal file
View file

@ -0,0 +1,58 @@
class VulnerabilityParser {
parse(output) {
const vulnerabilities = [];
const lines = output.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const json = JSON.parse(line);
// Check for vulnerability findings
if (json.finding) {
vulnerabilities.push(json);
}
} catch (e) {
// Skip non-JSON lines
}
}
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) {
for (const frame of finding.trace) {
if (frame.position && frame.position.filename) {
callSites.push({
filename: frame.position.filename,
line: frame.position.line || 1,
function: frame.function || 'unknown function',
osv: finding.osv || null
});
}
}
}
}
return callSites;
}
}
module.exports = VulnerabilityParser;