diff --git a/src/index.ts b/src/index.ts index 3fa9be0e..83a8bd78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,8 +15,8 @@ import { getOptionalEnvVariable, readMarkdownFile, shouldRunIaCScanner, + generateCacheKey, } from './util' -import { simpleGit } from 'simple-git' // Global scanner toggles - set to false to disable a scanner globally const enableScaRunning = true @@ -66,15 +66,19 @@ async function runAnalysis() { // Cache the analysis results when scanning the target branch let cacheHit = false - const commit = (await simpleGit().revparse(['HEAD'])).trim() - let cacheKey = `codesec-${commit}` + let cacheKey: string | undefined if (targetScan === 'old') { - const restored = await cache.restoreCache([resultsPath], cacheKey) - if (restored) { - info(`Cache hit for ${cacheKey} — skipping scan`) - cacheHit = true + cacheKey = await generateCacheKey(enableIacRunning, enableScaRunning, targetScan, modifiedFiles) + if (cacheKey) { + const restored = await cache.restoreCache([resultsPath], cacheKey) + if (restored) { + info(`Cache hit for ${cacheKey} — skipping scan`) + cacheHit = true + } else { + info(`Cache miss for ${cacheKey} — running scan`) + } } else { - info(`Cache miss for ${cacheKey} — running scan`) + info('Cache key generation failed — running scan without cache') } } @@ -89,11 +93,21 @@ async function runAnalysis() { ) if (success && targetScan !== 'new') { // Save the analysis results when not scanning the PR source branch - try { - await cache.saveCache([resultsPath], cacheKey) - info(`Saved analysis results for ${cacheKey}`) - } catch (e) { - info(`Failed to save cache for ${cacheKey}: ${(e as Error).message}`) + if (!cacheKey) { + cacheKey = await generateCacheKey( + enableIacRunning, + enableScaRunning, + targetScan, + modifiedFiles + ) + } + if (cacheKey) { + try { + await cache.saveCache([resultsPath], cacheKey) + info(`Saved analysis results for ${cacheKey}`) + } catch (e) { + info(`Failed to save cache for ${cacheKey}: ${(e as Error).message}`) + } } } } else { diff --git a/src/util.ts b/src/util.ts index 4e0ba95c..acd5a5a9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,7 @@ import { error, getInput, info, isDebug } from '@actions/core' import { context } from '@actions/github' import { spawn } from 'child_process' -import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs' +import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'fs' import * as os from 'os' import * as path from 'path' import { simpleGit } from 'simple-git' @@ -157,7 +157,7 @@ export function shouldRunIaCScanner(modifiedFiles: string): boolean { }) } -// runCodesec - Docker-based scanner using codesec:latest image +// runCodesec - Docker-based scanner using codesec:test image // // Modes: // 1. action='scan', scanTarget='new'/'old' -> produces analysis for PR comment @@ -167,22 +167,30 @@ export function shouldRunIaCScanner(modifiedFiles: string): boolean { // Parameters: // - runIac/runSca: which scanners to enable (default false - enable when ready to test) // - scanTarget: 'new', 'old', or 'scan' depending on mode +// - computeCacheKey: if true, runs GENERATE_CACHE_KEY mode instead of scanning export async function runCodesec( action: string, runIac: boolean = false, runSca: boolean = false, reportsDir: string, scanTarget?: string, - modifiedFiles?: string + modifiedFiles?: string, + computeCacheKey: boolean = false ): Promise { const lwAccount = getRequiredEnvVariable('LW_ACCOUNT') const lwApiKey = getRequiredEnvVariable('LW_API_KEY') const lwApiSecret = getRequiredEnvVariable('LW_API_SECRET') if (action === 'scan') { - const containerName = `codesec-scan-${scanTarget || 'default'}` - - info(`Running codesec scan (target: ${scanTarget || 'scan'})`) + const containerName = computeCacheKey + ? `codesec-cache-key-${Date.now()}` + : `codesec-scan-${scanTarget || 'default'}` + + info( + computeCacheKey + ? 'Running codesec cache key generation' + : `Running codesec scan (target: ${scanTarget || 'scan'})` + ) // Create env file with GitHub CI vars for the lacework iac binary const envFile = createEnvFile() @@ -211,12 +219,28 @@ export async function runCodesec( '-e', `SCAN_TARGET=${scanTarget || 'scan'}`, ...(modifiedFiles ? ['-e', `MODIFIED_FILES=${modifiedFiles}`] : []), - 'lacework/codesec:latest', + ...(computeCacheKey ? ['-e', 'GENERATE_CACHE_KEY=true'] : []), + 'lacework/codesec:test', 'scan', ] await callCommand('docker', ...dockerArgs) + if (computeCacheKey) { + // Copy cache key out and cleanup + const outputFile = path.join(reportsDir, 'cache-key.txt') + mkdirSync(reportsDir, { recursive: true }) + await callCommand( + 'docker', + 'container', + 'cp', + `${containerName}:/tmp/scan-results/sca/cache-key.txt`, + outputFile + ) + await callCommand('docker', 'rm', containerName) + return true + } + // Copy results out of container to temp dir if (runSca) { const scaDir = path.join(reportsDir, 'sca') @@ -279,7 +303,7 @@ export async function runCodesec( `RUN_SCA=${runSca}`, '-e', `RUN_IAC=${runIac}`, - 'lacework/codesec:latest', + 'lacework/codesec:test', 'compare', ] @@ -319,3 +343,36 @@ export function readMarkdownFile(filePath: string): string { throw new Error(`Failed to read scanner output file: ${error}`) } } + +export async function generateCacheKey( + runIac: boolean, + runSca: boolean, + scanTarget?: string, + modifiedFiles?: string +): Promise { + const reportsDir = path.join(os.tmpdir(), `codesec-cache-${Date.now()}`) + + try { + await runCodesec('scan', runIac, runSca, reportsDir, scanTarget, modifiedFiles, true) + } catch (e) { + info(`Cache key generation failed: ${(e as Error).message}`) + return undefined + } + + const outputFile = path.join(reportsDir, 'cache-key.txt') + if (!existsSync(outputFile)) { + info('Cache key file not found after generation') + return undefined + } + + const cacheKey = readFileSync(outputFile, 'utf-8').trim() + unlinkSync(outputFile) + + if (!/^[a-f0-9]{64}$/.test(cacheKey)) { + info(`Cache key format invalid: ${cacheKey}`) + return undefined + } + + info(`Generated cache key: ${cacheKey}`) + return `codesec-${cacheKey}` +}