2025-06-06 23:41:03 -04:00
const core = require ( '@actions/core' ) ;
2025-11-13 17:57:14 +00:00
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
2025-06-06 23:41:03 -04:00
const GovulncheckRunner = require ( './lib/govulncheck' ) ;
const VulnerabilityParser = require ( './lib/parser' ) ;
const AnnotationCreator = require ( './lib/annotator' ) ;
2025-06-07 00:58:11 -04:00
const SummaryGenerator = require ( './lib/summary' ) ;
2025-06-06 23:41:03 -04:00
2025-11-13 17:57:14 +00:00
/ * *
* Parse working directories from the input , supporting both comma and space delimiters .
* Filters out duplicates and empty strings .
* @ param { string } input - The working - directory input string
* @ returns { string [ ] } - Array of unique , non - empty directory paths
* /
function parseWorkingDirectories ( input ) {
if ( ! input ) {
return [ '.' ] ;
}
// Split by both comma and space, filter empty strings
const dirs = input
. split ( /[,\s]+/ )
. map ( dir => dir . trim ( ) )
. filter ( dir => dir . length > 0 ) ;
// Remove duplicates
const uniqueDirs = [ ... new Set ( dirs ) ] ;
return uniqueDirs . length > 0 ? uniqueDirs : [ '.' ] ;
}
/ * *
* Check if a directory exists
* @ param { string } dir - Directory path to check
* @ param { object } fsModule - File system module ( for testing )
* @ returns { boolean } - True if directory exists and is a directory
* /
function directoryExists ( dir , fsModule = fs ) {
try {
const stats = fsModule . statSync ( dir ) ;
return stats . isDirectory ( ) ;
} catch ( error ) {
return false ;
}
}
2025-06-06 23:41:03 -04:00
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 ) ;
2025-06-07 00:58:11 -04:00
const summaryGenerator = dependencies . summaryGenerator || new SummaryGenerator ( core ) ;
2025-11-13 17:57:14 +00:00
const fsModule = dependencies . fs || fs ;
2025-06-06 23:41:03 -04:00
try {
2025-11-13 17:57:14 +00:00
const workingDirectoryInput = core . getInput ( 'working-directory' ) ;
const workingDirectories = parseWorkingDirectories ( workingDirectoryInput ) ;
2025-06-06 23:41:03 -04:00
2025-11-13 17:57:14 +00:00
core . info ( ` Processing ${ workingDirectories . length } working director ${ workingDirectories . length === 1 ? 'y' : 'ies' } : ${ workingDirectories . join ( ', ' ) } ` ) ;
2025-06-06 23:41:03 -04:00
2025-11-13 17:57:14 +00:00
// Install govulncheck once (not per directory)
2025-06-06 23:41:03 -04:00
await govulncheck . install ( ) ;
2025-11-13 17:57:14 +00:00
const allVulnerabilities = [ ] ;
const originalCwd = process . cwd ( ) ;
// Process each directory
for ( const workingDirectory of workingDirectories ) {
// Resolve to absolute path
const absolutePath = path . isAbsolute ( workingDirectory )
? workingDirectory
: path . join ( originalCwd , workingDirectory ) ;
// Check if directory exists
if ( ! directoryExists ( absolutePath , fsModule ) ) {
core . warning ( ` Directory not found, skipping: ${ workingDirectory } ` ) ;
continue ;
}
// Change to working directory
if ( workingDirectory !== '.' ) {
core . info ( ` Changing working directory to: ${ workingDirectory } ` ) ;
process . chdir ( absolutePath ) ;
}
try {
// Run govulncheck with JSON output
core . info ( ` Running govulncheck in ${ workingDirectory } ... ` ) ;
const { output , errorOutput } = await govulncheck . run ( ) ;
if ( errorOutput ) {
core . warning ( ` govulncheck stderr in ${ workingDirectory } : ${ errorOutput } ` ) ;
// Check for critical errors that indicate govulncheck couldn't run properly
if ( errorOutput . includes ( 'missing go.sum entry' ) ||
errorOutput . includes ( 'could not import' ) ||
errorOutput . includes ( 'invalid package name' ) ) {
throw new Error ( ` govulncheck failed in ${ workingDirectory } due to missing dependencies. Please run 'go mod tidy' to update go.mod and go.sum files. \n \n Error: ${ errorOutput } ` ) ;
}
}
// Log raw output for debugging
core . info ( ` Raw govulncheck output length in ${ workingDirectory } : ${ output . length } characters ` ) ;
if ( output . length < 5000 ) {
core . info ( ` Raw output: ${ output } ` ) ;
} else {
core . info ( ` Raw output (first 1000 chars): ${ output . substring ( 0 , 1000 ) } ... ` ) ;
}
// Parse JSON output
const vulnerabilities = parser . parse ( output ) ;
// Add directory context to each vulnerability
vulnerabilities . forEach ( vuln => {
vuln . workingDirectory = workingDirectory ;
} ) ;
allVulnerabilities . push ( ... vulnerabilities ) ;
core . info ( ` Found ${ vulnerabilities . length } vulnerabilities in ${ workingDirectory } ` ) ;
} finally {
// Always return to original directory
process . chdir ( originalCwd ) ;
2025-06-07 02:08:43 -04:00
}
2025-06-06 23:41:03 -04:00
}
2025-11-13 17:57:14 +00:00
// Filter duplicate vulnerabilities based on OSV ID
const uniqueVulnerabilities = [ ] ;
const seenOsvIds = new Set ( ) ;
for ( const vuln of allVulnerabilities ) {
const osvId = vuln . finding ? . osv ;
if ( osvId && ! seenOsvIds . has ( osvId ) ) {
seenOsvIds . add ( osvId ) ;
uniqueVulnerabilities . push ( vuln ) ;
} else if ( ! osvId ) {
// Include vulnerabilities without OSV IDs (shouldn't happen, but be safe)
uniqueVulnerabilities . push ( vuln ) ;
}
2025-06-06 23:58:11 -04:00
}
2025-11-13 17:57:14 +00:00
core . info ( ` Total unique vulnerabilities across all directories: ${ uniqueVulnerabilities . length } ` ) ;
// For annotations and summaries, we'll use the first working directory as context
// or '.' if no directories were processed
const contextDirectory = workingDirectories . length > 0 ? workingDirectories [ 0 ] : '.' ;
2025-06-06 23:41:03 -04:00
// Create annotations
2025-11-13 17:57:14 +00:00
await annotator . createAnnotations ( uniqueVulnerabilities , parser , contextDirectory ) ;
2025-06-07 00:58:11 -04:00
// Generate workflow summary
2025-11-13 17:57:14 +00:00
await summaryGenerator . generateSummary ( uniqueVulnerabilities , parser , contextDirectory ) ;
2025-06-06 23:41:03 -04:00
// Set outputs
2025-11-13 17:57:14 +00:00
const hasVulnerabilities = uniqueVulnerabilities . length > 0 ;
2025-06-06 23:41:03 -04:00
core . setOutput ( 'vulnerabilities-found' , hasVulnerabilities . toString ( ) ) ;
2025-11-13 17:57:14 +00:00
core . setOutput ( 'vulnerability-count' , uniqueVulnerabilities . length . toString ( ) ) ;
2025-06-06 23:41:03 -04:00
if ( hasVulnerabilities ) {
2025-11-13 17:57:14 +00:00
core . warning ( ` Found ${ uniqueVulnerabilities . length } unique vulnerabilities across all directories ` ) ;
2025-06-06 23:41:03 -04:00
} else {
core . info ( 'No vulnerabilities found' ) ;
}
2025-11-13 17:57:14 +00:00
return { vulnerabilities : uniqueVulnerabilities , hasVulnerabilities } ;
2025-06-06 23:41:03 -04:00
} catch ( error ) {
core . setFailed ( error . message ) ;
throw error ;
}
}
// Only run if this is the main module
if ( require . main === module ) {
run ( ) ;
}
2025-11-13 17:57:14 +00:00
module . exports = { run , parseWorkingDirectories , directoryExists } ;