mirror of
https://github.com/imjasonh/govulncheck-action
synced 2026-07-08 00:35:45 +00:00
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.
This commit is contained in:
parent
f6e59d4d11
commit
3a117d24f8
9 changed files with 776 additions and 67 deletions
17
.github/workflows/example.yml
vendored
17
.github/workflows/example.yml
vendored
|
|
@ -25,23 +25,6 @@ jobs:
|
|||
with:
|
||||
working-directory: './example'
|
||||
|
||||
- name: Report vulnerability scan results
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Vulnerability Scan Results"
|
||||
echo "- Vulnerabilities found: ${{ steps.govulncheck.outputs.vulnerabilities-found }}"
|
||||
echo "- Vulnerability count: ${{ steps.govulncheck.outputs.vulnerability-count }}"
|
||||
|
||||
if [ "${{ steps.govulncheck.outputs.vulnerabilities-found }}" == "true" ]; then
|
||||
echo ""
|
||||
echo "⚠️ Security vulnerabilities were detected in the example code."
|
||||
echo "This is expected! The example intentionally uses a vulnerable version of golang.org/x/net"
|
||||
echo "to demonstrate how the action creates annotations."
|
||||
else
|
||||
echo ""
|
||||
echo "✅ No vulnerabilities found (this would be unexpected for the example)."
|
||||
fi
|
||||
|
||||
# Note: We don't fail the workflow here since the example is intentionally vulnerable
|
||||
- name: Demonstrate conditional failure
|
||||
run: |
|
||||
|
|
|
|||
412
dist/index.js
vendored
412
dist/index.js
vendored
|
|
@ -8,12 +8,14 @@ const core = __nccwpck_require__(7484);
|
|||
const GovulncheckRunner = __nccwpck_require__(7515);
|
||||
const VulnerabilityParser = __nccwpck_require__(2177);
|
||||
const AnnotationCreator = __nccwpck_require__(2164);
|
||||
const SummaryGenerator = __nccwpck_require__(3228);
|
||||
|
||||
async function run(dependencies = {}) {
|
||||
// Allow dependency injection for testing
|
||||
const govulncheck = dependencies.govulncheck || new GovulncheckRunner();
|
||||
const parser = dependencies.parser || new VulnerabilityParser();
|
||||
const annotator = dependencies.annotator || new AnnotationCreator(core);
|
||||
const summaryGenerator = dependencies.summaryGenerator || new SummaryGenerator(core);
|
||||
|
||||
try {
|
||||
const workingDirectory = core.getInput('working-directory');
|
||||
|
|
@ -47,7 +49,10 @@ async function run(dependencies = {}) {
|
|||
const vulnerabilities = parser.parse(output);
|
||||
|
||||
// Create annotations
|
||||
await annotator.createAnnotations(vulnerabilities, parser, '.');
|
||||
await annotator.createAnnotations(vulnerabilities, parser, workingDirectory);
|
||||
|
||||
// Generate workflow summary
|
||||
await summaryGenerator.generateSummary(vulnerabilities, parser, workingDirectory);
|
||||
|
||||
// Set outputs
|
||||
const hasVulnerabilities = vulnerabilities.length > 0;
|
||||
|
|
@ -98,17 +103,48 @@ class AnnotationCreator {
|
|||
|
||||
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);
|
||||
await this.annotateGoMod(modules, vulnerabilities, workingDirectory, modulesWithCallSites);
|
||||
|
||||
// Annotate source files for call sites
|
||||
this.annotateCallSites(callSites);
|
||||
this.annotateCallSites(callSites, workingDirectory);
|
||||
}
|
||||
|
||||
async annotateGoMod(modules, vulnerabilities, 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 = path.join(workingDirectory, 'go.mod');
|
||||
const goModPath = 'go.mod';
|
||||
let goModContent = '';
|
||||
|
||||
try {
|
||||
|
|
@ -121,8 +157,8 @@ class AnnotationCreator {
|
|||
const goModLines = goModContent.split('\n');
|
||||
|
||||
for (const module of modules) {
|
||||
// Find the vulnerability info for this module
|
||||
const vuln = vulnerabilities.find(v =>
|
||||
// Find all vulnerabilities for this module
|
||||
const moduleVulns = vulnerabilities.filter(v =>
|
||||
v.finding.trace &&
|
||||
v.finding.trace[0] &&
|
||||
v.finding.trace[0].module === module
|
||||
|
|
@ -132,41 +168,145 @@ class AnnotationCreator {
|
|||
for (let i = 0; i < goModLines.length; i++) {
|
||||
if (goModLines[i].includes(module)) {
|
||||
const lineNumber = i + 1;
|
||||
let message = `Vulnerable module: ${module}`;
|
||||
|
||||
if (vuln && vuln.finding.osv) {
|
||||
message += ` (${vuln.finding.osv})`;
|
||||
// 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: 'Security Vulnerability',
|
||||
file: 'go.mod',
|
||||
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) {
|
||||
annotateCallSites(callSites, workingDirectory) {
|
||||
// Group call sites by OSV ID to avoid duplicate annotations
|
||||
const groupedSites = {};
|
||||
for (const site of callSites) {
|
||||
let message = `Vulnerable code path: ${site.function}`;
|
||||
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 += ` (${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: 'Security Vulnerability',
|
||||
file: site.filename,
|
||||
title: title,
|
||||
file: filePath,
|
||||
startLine: site.line,
|
||||
endLine: site.line
|
||||
});
|
||||
|
|
@ -236,6 +376,7 @@ module.exports = GovulncheckRunner;
|
|||
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());
|
||||
|
|
@ -257,7 +398,10 @@ class VulnerabilityParser {
|
|||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
if (json.finding) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -274,7 +418,10 @@ class VulnerabilityParser {
|
|||
for (const jsonStr of jsonObjects) {
|
||||
try {
|
||||
const json = JSON.parse(jsonStr);
|
||||
if (json.finding) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -284,6 +431,13 @@ class VulnerabilityParser {
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -306,14 +460,23 @@ class VulnerabilityParser {
|
|||
|
||||
for (const vuln of vulnerabilities) {
|
||||
const finding = vuln.finding;
|
||||
if (finding.trace) {
|
||||
for (const frame of finding.trace) {
|
||||
if (frame.position && frame.position.filename) {
|
||||
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',
|
||||
osv: finding.osv || null
|
||||
vulnerableFunction: vulnerableFunction,
|
||||
osv: finding.osv || null,
|
||||
osvDetails: vuln.osvDetails || null,
|
||||
fixedVersion: finding.fixed_version || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -328,6 +491,209 @@ module.exports = VulnerabilityParser;
|
|||
|
||||
/***/ }),
|
||||
|
||||
/***/ 3228:
|
||||
/***/ ((module) => {
|
||||
|
||||
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;
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 4914:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ module github.com/imjasonh/govulncheck-action/example
|
|||
|
||||
go 1.20
|
||||
|
||||
require golang.org/x/net v0.30.0
|
||||
require golang.org/x/net v0.0.0-20220906165146-f3363e06e74c
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
func main() {
|
||||
// html.Parse in x/net/html has a vulnerability in the version we depend on. The action should find it.
|
||||
_, err := html.Parse(strings.NewReader(`<html><body><p>Test</p></body></html>`))
|
||||
_, err := html.Parse(strings.NewReader(`<html><body><p>blah</p></body></html>`))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
7
index.js
7
index.js
|
|
@ -2,12 +2,14 @@ const core = require('@actions/core');
|
|||
const GovulncheckRunner = require('./lib/govulncheck');
|
||||
const VulnerabilityParser = require('./lib/parser');
|
||||
const AnnotationCreator = require('./lib/annotator');
|
||||
const SummaryGenerator = require('./lib/summary');
|
||||
|
||||
async function run(dependencies = {}) {
|
||||
// Allow dependency injection for testing
|
||||
const govulncheck = dependencies.govulncheck || new GovulncheckRunner();
|
||||
const parser = dependencies.parser || new VulnerabilityParser();
|
||||
const annotator = dependencies.annotator || new AnnotationCreator(core);
|
||||
const summaryGenerator = dependencies.summaryGenerator || new SummaryGenerator(core);
|
||||
|
||||
try {
|
||||
const workingDirectory = core.getInput('working-directory');
|
||||
|
|
@ -41,7 +43,10 @@ async function run(dependencies = {}) {
|
|||
const vulnerabilities = parser.parse(output);
|
||||
|
||||
// Create annotations
|
||||
await annotator.createAnnotations(vulnerabilities, parser, '.');
|
||||
await annotator.createAnnotations(vulnerabilities, parser, workingDirectory);
|
||||
|
||||
// Generate workflow summary
|
||||
await summaryGenerator.generateSummary(vulnerabilities, parser, workingDirectory);
|
||||
|
||||
// Set outputs
|
||||
const hasVulnerabilities = vulnerabilities.length > 0;
|
||||
|
|
|
|||
167
lib/annotator.js
167
lib/annotator.js
|
|
@ -15,17 +15,48 @@ class AnnotationCreator {
|
|||
|
||||
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);
|
||||
await this.annotateGoMod(modules, vulnerabilities, workingDirectory, modulesWithCallSites);
|
||||
|
||||
// Annotate source files for call sites
|
||||
this.annotateCallSites(callSites);
|
||||
this.annotateCallSites(callSites, workingDirectory);
|
||||
}
|
||||
|
||||
async annotateGoMod(modules, vulnerabilities, 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 = path.join(workingDirectory, 'go.mod');
|
||||
const goModPath = 'go.mod';
|
||||
let goModContent = '';
|
||||
|
||||
try {
|
||||
|
|
@ -38,8 +69,8 @@ class AnnotationCreator {
|
|||
const goModLines = goModContent.split('\n');
|
||||
|
||||
for (const module of modules) {
|
||||
// Find the vulnerability info for this module
|
||||
const vuln = vulnerabilities.find(v =>
|
||||
// Find all vulnerabilities for this module
|
||||
const moduleVulns = vulnerabilities.filter(v =>
|
||||
v.finding.trace &&
|
||||
v.finding.trace[0] &&
|
||||
v.finding.trace[0].module === module
|
||||
|
|
@ -49,41 +80,145 @@ class AnnotationCreator {
|
|||
for (let i = 0; i < goModLines.length; i++) {
|
||||
if (goModLines[i].includes(module)) {
|
||||
const lineNumber = i + 1;
|
||||
let message = `Vulnerable module: ${module}`;
|
||||
|
||||
if (vuln && vuln.finding.osv) {
|
||||
message += ` (${vuln.finding.osv})`;
|
||||
// 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: 'Security Vulnerability',
|
||||
file: 'go.mod',
|
||||
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) {
|
||||
annotateCallSites(callSites, workingDirectory) {
|
||||
// Group call sites by OSV ID to avoid duplicate annotations
|
||||
const groupedSites = {};
|
||||
for (const site of callSites) {
|
||||
let message = `Vulnerable code path: ${site.function}`;
|
||||
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 += ` (${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: 'Security Vulnerability',
|
||||
file: site.filename,
|
||||
title: title,
|
||||
file: filePath,
|
||||
startLine: site.line,
|
||||
endLine: site.line
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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());
|
||||
|
|
@ -22,7 +23,10 @@ class VulnerabilityParser {
|
|||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
if (json.finding) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -39,7 +43,10 @@ class VulnerabilityParser {
|
|||
for (const jsonStr of jsonObjects) {
|
||||
try {
|
||||
const json = JSON.parse(jsonStr);
|
||||
if (json.finding) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -49,6 +56,13 @@ class VulnerabilityParser {
|
|||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -71,14 +85,23 @@ class VulnerabilityParser {
|
|||
|
||||
for (const vuln of vulnerabilities) {
|
||||
const finding = vuln.finding;
|
||||
if (finding.trace) {
|
||||
for (const frame of finding.trace) {
|
||||
if (frame.position && frame.position.filename) {
|
||||
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',
|
||||
osv: finding.osv || null
|
||||
vulnerableFunction: vulnerableFunction,
|
||||
osv: finding.osv || null,
|
||||
osvDetails: vuln.osvDetails || null,
|
||||
fixedVersion: finding.fixed_version || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
197
lib/summary.js
Normal file
197
lib/summary.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue