mirror of
https://github.com/imjasonh/govulncheck-action
synced 2026-07-08 16:56:01 +00:00
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.
197 lines
No EOL
6.1 KiB
JavaScript
197 lines
No EOL
6.1 KiB
JavaScript
class SummaryGenerator {
|
|
constructor(core) {
|
|
this.core = core;
|
|
}
|
|
|
|
async generateSummary(vulnerabilities, parser, workingDirectory = '.') {
|
|
const modules = parser.extractUniqueModules(vulnerabilities);
|
|
const callSites = parser.extractCallSites(vulnerabilities);
|
|
|
|
// Group vulnerabilities by module
|
|
const vulnsByModule = new Map();
|
|
for (const vuln of vulnerabilities) {
|
|
if (vuln.finding.trace && vuln.finding.trace[0]) {
|
|
const module = vuln.finding.trace[0].module;
|
|
if (!vulnsByModule.has(module)) {
|
|
vulnsByModule.set(module, []);
|
|
}
|
|
vulnsByModule.get(module).push(vuln);
|
|
}
|
|
}
|
|
|
|
// Sort modules for consistent output
|
|
const sortedModules = Array.from(vulnsByModule.keys()).sort();
|
|
|
|
// Start building the summary
|
|
await this.core.summary
|
|
.addHeading('🔍 Govulncheck Security Report', 1)
|
|
.addEOL();
|
|
|
|
if (vulnerabilities.length === 0) {
|
|
await this.core.summary
|
|
.addRaw('✅ No vulnerabilities found!')
|
|
.addEOL()
|
|
.write();
|
|
return;
|
|
}
|
|
|
|
// Add overview
|
|
await this.core.summary
|
|
.addHeading('📊 Overview', 2)
|
|
.addList([
|
|
`Total vulnerabilities found: ${vulnerabilities.length}`,
|
|
`Vulnerable modules: ${modules.length}`,
|
|
`Vulnerable code locations: ${callSites.length}`
|
|
])
|
|
.addEOL();
|
|
|
|
// Add vulnerable modules section
|
|
await this.core.summary
|
|
.addHeading('📦 Vulnerable Modules', 2)
|
|
.addEOL();
|
|
|
|
for (const module of sortedModules) {
|
|
const moduleVulns = vulnsByModule.get(module);
|
|
const moduleInfo = moduleVulns[0].finding.trace[0];
|
|
|
|
await this.core.summary
|
|
.addHeading(`${module}`, 3)
|
|
.addRaw(`Current version: ${moduleInfo.version}`)
|
|
.addEOL()
|
|
.addEOL();
|
|
|
|
// 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);
|
|
});
|
|
|
|
// Create a table of vulnerabilities
|
|
const tableRows = [['Vulnerability', 'Summary', 'Fixed Version', 'Details']];
|
|
|
|
for (const vuln of sortedVulns) {
|
|
const osvId = vuln.finding.osv;
|
|
const summary = vuln.osvDetails?.summary || 'No summary available';
|
|
const fixedVersion = vuln.finding.fixed_version || 'Not specified';
|
|
|
|
let details = [];
|
|
if (vuln.osvDetails?.aliases) {
|
|
const cves = vuln.osvDetails.aliases.filter(a => a.startsWith('CVE-'));
|
|
if (cves.length > 0) {
|
|
details.push(`CVE: ${cves.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
// For the link, we'll add it as a separate line after the table
|
|
tableRows.push([
|
|
osvId,
|
|
summary.length > 60 ? summary.substring(0, 60) + '...' : summary,
|
|
fixedVersion,
|
|
details.join(', ')
|
|
]);
|
|
}
|
|
|
|
await this.core.summary.addTable(tableRows);
|
|
|
|
// Add links separately since we can't put them in the table
|
|
await this.core.summary.addEOL().addRaw('Links: ');
|
|
for (let i = 0; i < sortedVulns.length; i++) {
|
|
const vuln = sortedVulns[i];
|
|
if (i > 0) await this.core.summary.addRaw(' | ');
|
|
await this.core.summary.addLink(vuln.finding.osv, `https://pkg.go.dev/vuln/${vuln.finding.osv}`);
|
|
}
|
|
await this.core.summary.addEOL().addEOL();
|
|
}
|
|
|
|
// Add vulnerable code locations section
|
|
if (callSites.length > 0) {
|
|
await this.core.summary
|
|
.addHeading('🚨 Vulnerable Code Locations', 2)
|
|
.addEOL();
|
|
|
|
// Group call sites by file
|
|
const callSitesByFile = new Map();
|
|
for (const site of callSites) {
|
|
const filePath = workingDirectory === '.' ? site.filename : `${workingDirectory}/${site.filename}`;
|
|
if (!callSitesByFile.has(filePath)) {
|
|
callSitesByFile.set(filePath, []);
|
|
}
|
|
callSitesByFile.get(filePath).push(site);
|
|
}
|
|
|
|
// Sort files
|
|
const sortedFiles = Array.from(callSitesByFile.keys()).sort();
|
|
|
|
for (const file of sortedFiles) {
|
|
await this.core.summary
|
|
.addHeading(`📄 ${file}`, 3);
|
|
|
|
const sites = callSitesByFile.get(file);
|
|
|
|
for (const site of sites) {
|
|
await this.core.summary
|
|
.addRaw(`• Line ${site.line}: calls ${site.vulnerableFunction}`);
|
|
|
|
if (site.osv) {
|
|
await this.core.summary
|
|
.addRaw(' - ')
|
|
.addLink(site.osv, `https://pkg.go.dev/vuln/${site.osv}`);
|
|
}
|
|
|
|
await this.core.summary.addEOL();
|
|
}
|
|
|
|
await this.core.summary.addEOL();
|
|
}
|
|
}
|
|
|
|
// Add recommendations section
|
|
await this.core.summary
|
|
.addHeading('💡 Recommendations', 2)
|
|
.addEOL();
|
|
|
|
const recommendations = [];
|
|
|
|
// Find modules that need updates
|
|
for (const module of sortedModules) {
|
|
const moduleVulns = vulnsByModule.get(module);
|
|
const fixedVersions = moduleVulns
|
|
.map(v => v.finding.fixed_version)
|
|
.filter(v => v)
|
|
.sort();
|
|
|
|
if (fixedVersions.length > 0) {
|
|
// Get the latest fixed version
|
|
const latestFix = fixedVersions[fixedVersions.length - 1];
|
|
recommendations.push(
|
|
`Update ${module} to version ${latestFix} or later`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (recommendations.length > 0) {
|
|
await this.core.summary.addList(recommendations);
|
|
} else {
|
|
await this.core.summary.addRaw('No specific version recommendations available.');
|
|
}
|
|
|
|
await this.core.summary
|
|
.addEOL()
|
|
.addSeparator()
|
|
.addEOL()
|
|
.addRaw('🔗 Learn more about these vulnerabilities:')
|
|
.addEOL()
|
|
.addRaw('• ')
|
|
.addLink('Go Vulnerability Database', 'https://pkg.go.dev/vuln/')
|
|
.addEOL()
|
|
.addRaw('• ')
|
|
.addLink('Govulncheck Documentation', 'https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck')
|
|
.addEOL();
|
|
|
|
// Write the summary
|
|
await this.core.summary.write();
|
|
}
|
|
}
|
|
|
|
module.exports = SummaryGenerator; |