1
0
Fork 0
mirror of https://github.com/imjasonh/govulncheck-action synced 2026-07-08 16:56:01 +00:00
govulncheck-action/lib/annotator.js
Jason Hall 3a117d24f8
Add vulnerable example and enhance action features
This commit adds a comprehensive example demonstrating the govulncheck action's capabilities, along with several enhancements to improve the user experience.

## Changes

### Example Setup
- Added example/main.go that uses golang.org/x/net v0.0.0-20220906165146-f3363e06e74c (vulnerable version)
- Example intentionally calls html.Parse to trigger vulnerability detection
- Demonstrates how the action creates annotations on vulnerable code

### Enhanced Annotations
- Fixed file path handling when running in subdirectories (e.g., './example')
- Added rich context to annotations including:
  - Vulnerability summaries and CVE numbers
  - Direct links to Go vulnerability database (pkg.go.dev/vuln)
  - Fixed version information
  - Clear indication of which function is vulnerable (e.g., "html.Parse" not "main")
- Sorted vulnerabilities by OSV ID for consistent display

### Suggested Fixes
- When vulnerable code is actually called, creates notice annotations on go.mod
- Shows current vs suggested dependency versions
- Lists which specific vulnerabilities have active call sites
- Provides actionable upgrade recommendations

### Workflow Summary
- Added comprehensive workflow summary using GitHub Actions summary API
- Displays vulnerabilities in formatted tables by module
- Shows vulnerable code locations organized by file
- Includes links to Go vulnerability database entries
- Provides clear recommendations for fixing vulnerabilities

### Bug Fixes
- Fixed JSON parsing to handle both JSON lines and multi-line JSON formats
- Fixed annotation file paths to be relative to repository root
- Improved vulnerability function name extraction from trace data
- Enhanced OSV detail parsing and storage

## Testing
The example workflow demonstrates all features by intentionally using a vulnerable dependency. When run, it will:
1. Detect vulnerabilities in golang.org/x/net
2. Create warning annotations on go.mod and main.go
3. Suggest specific version updates
4. Generate a detailed workflow summary

This provides a complete demonstration of the action's vulnerability detection and reporting capabilities.
2025-06-07 00:58:11 -04:00

229 lines
No EOL
8.3 KiB
JavaScript

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 = '.') {
this.core.info(`Creating annotations for ${vulnerabilities.length} vulnerabilities`);
const modules = parser.extractUniqueModules(vulnerabilities);
const callSites = parser.extractCallSites(vulnerabilities);
this.core.info(`Found ${modules.length} vulnerable modules and ${callSites.length} call sites`);
// Check which modules have actual call sites
const modulesWithCallSites = this.findModulesWithCallSites(vulnerabilities, callSites);
// Annotate go.mod for vulnerable modules
await this.annotateGoMod(modules, vulnerabilities, workingDirectory, modulesWithCallSites);
// Annotate source files for call sites
this.annotateCallSites(callSites, workingDirectory);
}
findModulesWithCallSites(vulnerabilities, callSites) {
const modulesWithCallSites = new Map();
// For each call site, find which module it belongs to
for (const site of callSites) {
const vuln = vulnerabilities.find(v => v.finding.osv === site.osv);
if (vuln && vuln.finding.trace && vuln.finding.trace[0]) {
const module = vuln.finding.trace[0].module;
const version = vuln.finding.trace[0].version;
const fixedVersion = vuln.finding.fixed_version;
if (!modulesWithCallSites.has(module)) {
modulesWithCallSites.set(module, {
currentVersion: version,
fixedVersion: fixedVersion,
osvs: []
});
}
if (!modulesWithCallSites.get(module).osvs.includes(site.osv)) {
modulesWithCallSites.get(module).osvs.push(site.osv);
}
}
}
return modulesWithCallSites;
}
async annotateGoMod(modules, vulnerabilities, workingDirectory, modulesWithCallSites) {
if (modules.length === 0) return;
const goModPath = '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 all vulnerabilities for this module
const moduleVulns = vulnerabilities.filter(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 lineNumber = i + 1;
// Build detailed message
let message = `⚠️ Security vulnerabilities found in ${module}\n\n`;
// Sort vulnerabilities by OSV ID
const sortedVulns = moduleVulns.sort((a, b) => {
const aId = a.finding.osv || '';
const bId = b.finding.osv || '';
return aId.localeCompare(bId);
});
for (const vuln of sortedVulns) {
if (vuln.finding.osv) {
message += `${vuln.finding.osv}`;
if (vuln.osvDetails) {
message += `: ${vuln.osvDetails.summary || 'No summary available'}`;
// Add CVE aliases if available
if (vuln.osvDetails.aliases && vuln.osvDetails.aliases.length > 0) {
const cves = vuln.osvDetails.aliases.filter(a => a.startsWith('CVE-'));
if (cves.length > 0) {
message += ` (${cves.join(', ')})`;
}
}
}
message += `\n`;
// Add link to vulnerability database
message += ` 🔗 https://pkg.go.dev/vuln/${vuln.finding.osv}\n`;
// Add fixed version info
if (vuln.finding.fixed_version) {
message += ` ✅ Fixed in: ${vuln.finding.fixed_version}\n`;
}
message += `\n`;
}
}
// Add upgrade suggestion
if (moduleVulns.length > 0 && moduleVulns[0].finding.fixed_version) {
message += `💡 Recommended action: Update to ${moduleVulns[0].finding.fixed_version} or later`;
}
this.core.info(`Creating annotation for ${module} at go.mod:${lineNumber}`);
// GitHub Actions annotation format
const filePath = workingDirectory === '.' ? 'go.mod' : path.join(workingDirectory, 'go.mod');
this.core.warning(message, {
title: `${moduleVulns.length} vulnerabilities in ${module}`,
file: filePath,
startLine: lineNumber,
endLine: lineNumber
});
// If this module has actual call sites, create a suggested edit
if (modulesWithCallSites.has(module)) {
const moduleInfo = modulesWithCallSites.get(module);
if (moduleInfo.fixedVersion) {
// Create a notice with the suggested fix
const currentLine = goModLines[i];
const suggestedLine = currentLine.replace(moduleInfo.currentVersion, moduleInfo.fixedVersion);
let editMessage = `🔧 Suggested fix: Update this dependency to fix vulnerabilities that are actually being called in your code.\n\n`;
editMessage += `Current: ${currentLine.trim()}\n`;
editMessage += `Suggested: ${suggestedLine.trim()}\n\n`;
editMessage += `This fixes ${moduleInfo.osvs.length} vulnerabilities that have active call sites in your code:\n`;
editMessage += moduleInfo.osvs.sort().map(osv => `${osv}`).join('\n');
this.core.notice(editMessage, {
title: `Fix available for ${module}`,
file: filePath,
startLine: lineNumber,
endLine: lineNumber
});
}
}
break;
}
}
}
}
annotateCallSites(callSites, workingDirectory) {
// Group call sites by OSV ID to avoid duplicate annotations
const groupedSites = {};
for (const site of callSites) {
const key = `${site.filename}:${site.line}:${site.osv}`;
if (!groupedSites[key]) {
groupedSites[key] = site;
}
}
for (const site of Object.values(groupedSites)) {
// Build detailed message
let message = `🚨 Vulnerable code detected\n\n`;
message += `This code calls ${site.vulnerableFunction} which has a known vulnerability.\n\n`;
if (site.osv) {
message += `Vulnerability: ${site.osv}`;
if (site.osvDetails) {
message += ` - ${site.osvDetails.summary || 'No summary available'}`;
// Add CVE aliases if available
if (site.osvDetails.aliases && site.osvDetails.aliases.length > 0) {
const cves = site.osvDetails.aliases.filter(a => a.startsWith('CVE-'));
if (cves.length > 0) {
message += `\nCVE: ${cves.join(', ')}`;
}
}
// Add details if available
if (site.osvDetails.details) {
message += `\n\nDetails: ${site.osvDetails.details}`;
}
}
message += `\n\n🔗 More info: https://pkg.go.dev/vuln/${site.osv}`;
}
if (site.fixedVersion) {
message += `\n\n✅ Fix available: Update the dependency to ${site.fixedVersion} or later`;
}
this.core.info(`Creating annotation for call site at ${site.filename}:${site.line}`);
// GitHub Actions annotation format
// If we're in a subdirectory, prepend it to the filename
const filePath = workingDirectory === '.' ? site.filename : path.join(workingDirectory, site.filename);
const title = site.osv ? `Vulnerable code: ${site.osv}` : 'Vulnerable code detected';
this.core.warning(message, {
title: title,
file: filePath,
startLine: site.line,
endLine: site.line
});
}
}
}
module.exports = AnnotationCreator;