diff --git a/src/index.ts b/src/index.ts index 00f79bc5..a1b3111d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,29 @@ import { error, getInput, info, setOutput, warning } from '@actions/core' -import { appendFileSync, existsSync } from 'fs' +import { appendFileSync, existsSync, mkdirSync } from 'fs' +import * as path from 'path' import { downloadArtifact, postCommentIfInPr, resolveExistingCommentIfFound, uploadArtifact, } from './actions' -import { compareResults } from './tool' import { callCommand, - callLaceworkCli, - debug, + codesecRun, getActionRef, getMsSinceStart, getOptionalEnvVariable, getRequiredEnvVariable, getRunUrl, + readMarkdownFile, telemetryCollector, } from './util' -import path from 'path' - -const scaSarifReport = 'scaReport/output.sarif' -const scaReport = 'sca.sarif' -const scaLWJSONReport = 'scaReport/output-lw.json' -const scaDir = 'scaReport' +// Constants for old Lacework CLI flow - kept for reference when re-enabling +// const scaSarifReport = 'scaReport/output.sarif' +// const scaReport = 'sca.sarif' +// const scaLWJSONReport = 'scaReport/output-lw.json' +// const scaDir = 'scaReport' async function runAnalysis() { const target = getInput('target') @@ -43,73 +42,141 @@ async function runAnalysis() { telemetryCollector.addField('tools', 'sca') const toUpload: string[] = [] - // command to print both sarif and lwjson formats - var args = ['sca', 'scan', '.', '-o', scaDir, '--formats', 'sarif,lw-json', '--deployment', 'ci'] - if (target === 'push') { - args.push('--save-results') + // Run codesec Docker scanner + // targetScan: 'new'/'old' for PR mode, 'scan' for push mode (uploads to Lacework UI) + var targetScan = target + if (target == 'push') { + targetScan = 'scan' } - if (debug()) { - args.push('--debug') + const resultsPath = await codesecRun('scan', true, true, targetScan) + + // Upload SCA SARIF from the returned results path + const scaSarifFile = path.join(resultsPath, 'sca', `sca-${targetScan}.sarif`) + if (existsSync(scaSarifFile)) { + info(`Found SCA SARIF file to upload: ${scaSarifFile}`) + toUpload.push(scaSarifFile) + } else { + info(`SCA SARIF file not found at: ${scaSarifFile}`) } - await callLaceworkCli(...args) - // make a copy of the sarif file - args = [scaSarifReport, scaReport] - await callCommand('cp', ...args) - toUpload.push(scaReport) + // Upload IAC JSON from the returned results path + const iacJsonFile = path.join(resultsPath, 'iac', `iac-${targetScan}.json`) + if (existsSync(iacJsonFile)) { + info(`Found IAC JSON file to upload: ${iacJsonFile}`) + toUpload.push(iacJsonFile) + } else { + info(`IAC JSON file not found at: ${iacJsonFile}`) + } const uploadStart = Date.now() const artifactPrefix = getInput('artifact-prefix') - if (artifactPrefix !== '') { - await uploadArtifact(artifactPrefix + '-results-' + target, ...toUpload) - } else { - await uploadArtifact('results-' + target, ...toUpload) - } + const artifactName = + artifactPrefix !== '' ? artifactPrefix + '-results-' + target : 'results-' + target + info(`Uploading artifact '${artifactName}' with ${toUpload.length} file(s)`) + await uploadArtifact(artifactName, ...toUpload) telemetryCollector.addField('duration.upload-artifacts', (Date.now() - uploadStart).toString()) setOutput(`${target}-completed`, true) } async function displayResults() { info('Displaying results') - const downloadStart = Date.now() + + // Download artifacts from previous jobs const artifactOld = await downloadArtifact('results-old') const artifactNew = await downloadArtifact('results-new') - telemetryCollector.addField( - 'duration.download-artifacts', - (Date.now() - downloadStart).toString() + + // Create local scan-results directory for compare + mkdirSync('scan-results/sca', { recursive: true }) + mkdirSync('scan-results/iac', { recursive: true }) + + // Copy SCA SARIF files from artifacts to expected location for compare + info('Copying SCA files from artifacts') + info(` Old SARIF: ${path.join(artifactOld, 'scan-results/sca/sca-old.sarif')}`) + await callCommand( + 'cp', + path.join(artifactOld, 'scan-results/sca/sca-old.sarif'), + 'scan-results/sca/sca-old.sarif' + ) + info(` New SARIF: ${path.join(artifactNew, 'scan-results/sca/sca-new.sarif')}`) + await callCommand( + 'cp', + path.join(artifactNew, 'scan-results/sca/sca-new.sarif'), + 'scan-results/sca/sca-new.sarif' ) - const sarifFileOld = path.join(artifactOld, scaReport) - const sarifFileNew = path.join(artifactNew, scaReport) - const issuesByTool: { [tool: string]: string } = {} - if (existsSync(sarifFileOld) && existsSync(sarifFileNew)) { - issuesByTool['sca'] = await compareResults('sca', sarifFileOld, sarifFileNew) + // Copy IAC JSON files from artifacts to expected location for compare + info('Checking for IAC files in artifacts') + const iacOldPath = path.join(artifactOld, 'scan-results/iac/iac-old.json') + const iacNewPath = path.join(artifactNew, 'scan-results/iac/iac-new.json') + info(` Old IAC: ${iacOldPath} (exists: ${existsSync(iacOldPath)})`) + info(` New IAC: ${iacNewPath} (exists: ${existsSync(iacNewPath)})`) + if (existsSync(iacOldPath) && existsSync(iacNewPath)) { + info(' Copying IAC files') + await callCommand('cp', iacOldPath, 'scan-results/iac/iac-old.json') + await callCommand('cp', iacNewPath, 'scan-results/iac/iac-new.json') } else { - throw new Error('SARIF file not found for SCA') + info(' IAC files not found in artifacts, skipping IAC compare') } - const commentStart = Date.now() - if (Object.values(issuesByTool).some((x) => x.length > 0) && getInput('token').length > 0) { - info('Posting comment to GitHub PR as there were new issues introduced:') - let message = '' - for (const [, issues] of Object.entries(issuesByTool)) { - if (issues.length > 0) { - message += issues - } - } - if (getInput('footer') !== '') { - message += '\n\n' + getInput('footer') - } - info(message) + // Verify SCA files exist (required) + const scaOldExists = existsSync('scan-results/sca/sca-old.sarif') + const scaNewExists = existsSync('scan-results/sca/sca-new.sarif') + + if (!scaOldExists || !scaNewExists) { + throw new Error( + `SARIF files not found for comparison. old=${scaOldExists}, new=${scaNewExists}` + ) + } + + // Run codesec compare mode + await codesecRun('compare', false, false) + + // Read the comparison output + // merged-compare.md exists when both SCA and IAC comparisons succeed + // sca-compare.md exists when only SCA comparison succeeds (partial) + const mergedOutput = 'scan-results/compare/merged-compare.md' + const scaOutput = 'scan-results/compare/sca-compare.md' + + let message: string + if (existsSync(mergedOutput)) { + info('Using merged comparison output') + message = readMarkdownFile(mergedOutput) + } else if (existsSync(scaOutput)) { + info('Using SCA-only comparison output (partial)') + message = readMarkdownFile(scaOutput) + } else { + throw new Error(`Comparison output not found. Tried: ${mergedOutput}, ${scaOutput}`) + } + + // Check if there are new violations (non-zero count in "Found N new potential violations") + const hasViolations = /Found\s+[1-9]\d*\s+/.test(message) + + if (hasViolations && getInput('token').length > 0) { + info('Posting comment to GitHub PR as there were new issues introduced') const commentUrl = await postCommentIfInPr(message) if (commentUrl !== undefined) { setOutput('posted-comment', commentUrl) } } else { + // No new violations or no token - resolve existing comment if found await resolveExistingCommentIfFound() } - telemetryCollector.addField('duration.comment', (Date.now() - commentStart).toString()) - setOutput(`display-completed`, true) + + setOutput('display-completed', true) + + /* + * OLD FLOW - Commented out, to be removed once codesec is fully tested + * + * const downloadStart = Date.now() + * const artifactOld = await downloadArtifact('results-old') + * const artifactNew = await downloadArtifact('results-new') + * const sarifFileOld = path.join(artifactOld, scaReport) + * const sarifFileNew = path.join(artifactNew, scaReport) + * const compareMessage = await compareResults(sarifFileOld, sarifFileNew) + * if (compareMessage.length > 0 && getInput('token').length > 0) { + * await postCommentIfInPr(compareMessage) + * } + */ } async function main() { diff --git a/src/tool.ts b/src/tool.ts index 81935c53..d08edada 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,3 +1,7 @@ +// DISABLED: This file is no longer used - codesecRun('compare') handles comparison +// Keeping commented for reference when migrating back to Lacework CLI + +/* import { endGroup, startGroup } from '@actions/core' import { existsSync, readFileSync } from 'fs' import { callLaceworkCli, debug, generateUILink } from './util' @@ -31,3 +35,4 @@ export async function compareResults( endGroup() return existsSync(`${tool}.md`) ? readFileSync(`${tool}.md`, 'utf8') : '' } +*/ diff --git a/src/util.ts b/src/util.ts index d213c790..f81230db 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,6 +3,8 @@ import { context } from '@actions/github' import { spawn } from 'child_process' import { TelemetryCollector } from './telemetry' import { readFileSync } from 'fs' +import * as path from 'path' +import { mkdirSync, existsSync } from 'fs' export const telemetryCollector = new TelemetryCollector() @@ -110,3 +112,199 @@ export function generateUILink() { return url } + +// codesecRun - Docker-based scanner using codesec:latest image +// Follows the pattern from test-unified-scanner.sh for CI runner compatibility +// +// Modes: +// 1. action='scan', scanTarget='new'/'old' -> produces analysis for PR comment +// 2. action='scan', scanTarget='scan' -> full scan for scheduled events (uploads to Lacework) +// 3. action='compare' -> compares new/old results, generates diff markdown for PR comment +// +// Parameters: +// - runIac/runSca: which scanners to enable (default false - enable when ready to test) +// - scanTarget: 'new', 'old', or 'scan' depending on mode +export async function codesecRun( + action: string, + runIac: boolean = false, + runSca: boolean = false, + scanTarget?: string +): Promise { + const lwAccount = getRequiredEnvVariable('LW_ACCOUNT_NAME') + const lwApiKey = getRequiredEnvVariable('LW_API_KEY') + const lwApiSecret = getRequiredEnvVariable('LW_API_SECRET') + + // Create scan-results directory in workspace (required for artifact upload) + const reportsDir = path.join(process.cwd(), 'scan-results') + + if (action === 'scan') { + // Scan mode: mount repo as /app/src, results go to /tmp/scan-results/ in container + const containerName = `codesec-scan-${scanTarget || 'default'}` + + info(`Running codesec scan (target: ${scanTarget || 'scan'})`) + + // Run the scanner + const dockerArgs = [ + 'run', + '--name', + containerName, + '-v', + `${process.cwd()}:/app/src`, + '-e', + `WORKSPACE=src`, + '-e', + `LW_ACCOUNT=${lwAccount}`, + '-e', + `LW_API_KEY=${lwApiKey}`, + '-e', + `LW_API_SECRET=${lwApiSecret}`, + '-e', + `RUN_SCA=${runSca}`, + '-e', + `RUN_IAC=${runIac}`, + '-e', + `SCAN_TARGET=${scanTarget || 'scan'}`, + 'lacework/codesec:latest', + 'scan', + ] + + await callCommand('docker', ...dockerArgs) + + // Copy results out of container to temp dir + if (runSca) { + const scaDir = path.join(reportsDir, 'sca') + mkdirSync(scaDir, { recursive: true }) + await callCommand( + 'docker', + 'container', + 'cp', + `${containerName}:/tmp/scan-results/sca/sca-${scanTarget || 'scan'}.sarif`, + path.join(scaDir, `sca-${scanTarget || 'scan'}.sarif`) + ) + } + + if (runIac) { + const iacDir = path.join(reportsDir, 'iac') + mkdirSync(iacDir, { recursive: true }) + await callCommand( + 'docker', + 'container', + 'cp', + `${containerName}:/tmp/scan-results/iac/iac-${scanTarget || 'scan'}.json`, + path.join(iacDir, `iac-${scanTarget || 'scan'}.json`) + ) + } + + // Cleanup container + await callCommand('docker', 'rm', containerName) + } else if (action === 'compare') { + // Compare mode: copy scan results into place first, then run compare + const srcDir = path.join(reportsDir, 'sca') + const scaOld = path.join(srcDir, 'sca-old.sarif') + const scaNew = path.join(srcDir, 'sca-new.sarif') + + // Verify required files exist before running compare + if (!existsSync(scaOld) || !existsSync(scaNew)) { + throw new Error( + `Compare requires sca-old.sarif and sca-new.sarif. Found: old=${existsSync( + scaOld + )}, new=${existsSync(scaNew)}` + ) + } + + const containerName = 'codesec-compare' + + info('Running codesec compare') + + // Note: mounts both the repo and the scan-results directory separately + const dockerArgs = [ + 'run', + '--name', + containerName, + '-v', + `${process.cwd()}:/app/src`, + '-v', + `${path.join(process.cwd(), 'scan-results')}:/app/scan-results`, + '-e', + `WORKSPACE=src`, + '-e', + `LW_ACCOUNT=${lwAccount}`, + '-e', + `LW_API_KEY=${lwApiKey}`, + '-e', + `LW_API_SECRET=${lwApiSecret}`, + '-e', + `RUN_SCA=true`, + '-e', + `RUN_IAC=true`, + 'lacework/codesec:latest', + 'compare', + ] + + await callCommand('docker', ...dockerArgs) + + // Copy comparison results out + const compareDir = path.join(reportsDir, 'compare') + mkdirSync(compareDir, { recursive: true }) + + // Copy all available comparison outputs + // merged-compare.md exists when both SCA and IAC comparisons succeed + // sca-compare.md / iac-compare.md exist for individual comparisons + let copiedAny = false + + try { + await callCommand( + 'docker', + 'container', + 'cp', + `${containerName}:/tmp/scan-results/compare/merged-compare.md`, + path.join(compareDir, 'merged-compare.md') + ) + copiedAny = true + } catch { + info('Merged compare output not found (partial compare mode)') + } + + try { + await callCommand( + 'docker', + 'container', + 'cp', + `${containerName}:/tmp/scan-results/compare/sca-compare.md`, + path.join(compareDir, 'sca-compare.md') + ) + copiedAny = true + } catch { + info('SCA compare output not found (may have been skipped)') + } + + try { + await callCommand( + 'docker', + 'container', + 'cp', + `${containerName}:/tmp/scan-results/compare/iac-compare.md`, + path.join(compareDir, 'iac-compare.md') + ) + copiedAny = true + } catch { + info('IAC compare output not found (may have been skipped)') + } + + if (!copiedAny) { + throw new Error('No comparison outputs found in container') + } + + // Cleanup container + await callCommand('docker', 'rm', containerName) + } + return reportsDir +} + +export function readMarkdownFile(filePath: string): string { + try { + return readFileSync(filePath, 'utf-8') + } catch (error) { + throw new Error(`Failed to read scanner output file: ${error}`) + } +}