From ffb930eaf594534e4e5f02e843de6d0ed72c732b Mon Sep 17 00:00:00 2001 From: Teodor-Ioan Baltoi Date: Tue, 10 Mar 2026 10:33:15 +0000 Subject: [PATCH] unified scanner docker image unified scanner docker image unified scanner docker image debug + erase macOS runner debug... debug testing clean add back the generateUILink clean --- .github/workflows/integration-test.yml | 24 +-- action.yaml | 13 -- src/index.ts | 167 ++++++++++++-------- src/util.ts | 202 ++++++++++++++++++++++--- 4 files changed, 288 insertions(+), 118 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 458a2243..190bc73e 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -10,28 +10,17 @@ on: workflow_dispatch: env: - LW_ACCOUNT_NAME: ${{ secrets.LW_ACCOUNT_CAT }} - LW_API_KEY: ${{ secrets.LW_API_KEY_CAT }} - LW_API_SECRET: ${{ secrets.LW_API_SECRET_CAT }} + LW_ACCOUNT_NAME: ${{ secrets.LW_ACCOUNT_UEDEMO }} + LW_API_KEY: ${{ secrets.LW_API_KEY_UEDEMO }} + LW_API_SECRET: ${{ secrets.LW_API_SECRET_UEDEMO }} DEBUG: 'true' jobs: build: - strategy: - fail-fast: false - matrix: - os: - - macos-latest - - ubuntu-latest - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - name: Move action run: | mkdir ../action @@ -44,7 +33,6 @@ jobs: target: push sources: ${{ github.workspace }} debug: true - artifact-prefix: ${{ matrix.os }} - name: Check run succeeded env: RUN_OUTPUT: ${{ steps.run-action.outputs.push-completed }} @@ -58,12 +46,12 @@ jobs: - name: Download results uses: actions/download-artifact@v4 with: - name: ${{ matrix.os }}-results-push + name: results-push path: artifact - name: Check results working-directory: artifact run: | - export SCA_RESULTS=`jq '.runs | map (.results | length) | add' sca.sarif` + export SCA_RESULTS=`jq '.runs | map (.results | length) | add' scan-results/sca/sca-scan.sarif` echo "Got $SCA_RESULTS from SCA" if [ "$SCA_RESULTS" -eq 0 ]; then echo "::error::Expected to have $expectedScaResults SCA results!" diff --git a/action.yaml b/action.yaml index 963d01c4..319adce2 100644 --- a/action.yaml +++ b/action.yaml @@ -49,11 +49,6 @@ runs: - if: runner.os == 'Linux' shell: bash run: echo "LACEWORK_START_TIME=$(date --rfc-3339=seconds)" >> $GITHUB_ENV - - if: runner.os == 'macOS' - shell: bash - run: | - brew install coreutils - echo "LACEWORK_START_TIME=$(gdate --rfc-3339=seconds)" >> $GITHUB_ENV - id: init shell: bash env: @@ -63,19 +58,11 @@ runs: echo "Lacework context ID: $LACEWORK_CONTEXT_ID" echo "LACEWORK_CONTEXT_ID=$(echo $LACEWORK_CONTEXT_ID)" >> $GITHUB_ENV echo "LACEWORK_ACTION_REF=$(echo $LACEWORK_ACTION_REF)" >> $GITHUB_ENV - curl https://raw.githubusercontent.com/lacework/go-sdk/main/cli/install.sh | bash - name: Sets LW_LOG var for debug shell: bash if: ${{ inputs.debug == 'true' }} run: | echo "LW_LOG=debug" >> $GITHUB_ENV - - name: Install Lacework CLI component - shell: bash - run: | - lacework --noninteractive -a "${LW_ACCOUNT_NAME}" -k "${LW_API_KEY}" -s "${LW_API_SECRET}" component install sca - lacework --noninteractive -a "${LW_ACCOUNT_NAME}" -k "${LW_API_KEY}" -s "${LW_API_SECRET}" version - env: - CDK_DOWNLOAD_TIMEOUT_MINUTES: 2 - uses: actions/setup-node@v4 with: node-version: 18 diff --git a/src/index.ts b/src/index.ts index c42cc747..80572677 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,13 @@ import { error, getInput, info, setOutput } from '@actions/core' -import { existsSync, readFileSync } from 'fs' +import { existsSync, mkdirSync } from 'fs' +import * as path from 'path' import { downloadArtifact, postCommentIfInPr, resolveExistingCommentIfFound, uploadArtifact, } from './actions' -import { callLaceworkCli, debug, generateUILink, getOptionalEnvVariable } from './util' - -import path from 'path' - -const artifactPrefix = getInput('artifact-prefix') -const sarifReportPath = getInput('code-scanning-path') -const comparisonMarkdownPath = 'comparison.md' +import { callCommand, codesecRun, getOptionalEnvVariable, readMarkdownFile } from './util' async function runAnalysis() { const target = getInput('target') @@ -30,87 +25,125 @@ async function runAnalysis() { info('Analyzing ' + target) const toUpload: string[] = [] - // command to print both sarif and lwjson formats - var args = ['scan', '.', '--formats', 'sarif', '--output', sarifReportPath, '--deployment', 'ci'] - if (target === 'push') { - args.push('--save-results') + // Run codesec Docker scanner + // targetScan: 'new'/'old' for PR mode, 'scan' for push mode (should upload results to db) + 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) - toUpload.push(sarifReportPath) - const uploadStart = Date.now() + // 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}`) + } - await uploadArtifact(getArtifactName(target), ...toUpload) + const artifactPrefix = getInput('artifact-prefix') + const artifactName = + artifactPrefix !== '' ? artifactPrefix + '-results-' + target : 'results-' + target + info(`Uploading artifact '${artifactName}' with ${toUpload.length} file(s)`) + await uploadArtifact(artifactName, ...toUpload) setOutput(`${target}-completed`, true) } -export async function compareResults(oldReport: string, newReport: string): Promise { - const args = [ - 'compare', - '--old', - oldReport, - '--new', - newReport, - '--output', - sarifReportPath, - '--markdown', - comparisonMarkdownPath, - '--markdown-variant', - 'GitHub', - '--deployment', - 'ci', - ] - const uiLink = generateUILink() - if (uiLink) args.push(...['--ui-link', uiLink]) - if (debug()) args.push('--debug') +async function displayResults() { + info('Displaying results') - await callLaceworkCli(...args) - await uploadArtifact(getArtifactName('compare'), sarifReportPath, comparisonMarkdownPath) + // Download artifacts from previous jobs + const artifactOld = await downloadArtifact('results-old') + const artifactNew = await downloadArtifact('results-new') - return existsSync(comparisonMarkdownPath) ? readFileSync(comparisonMarkdownPath, 'utf8') : '' -} + // Create local scan-results directory for compare + mkdirSync('scan-results/sca', { recursive: true }) + mkdirSync('scan-results/iac', { recursive: true }) -async function displayResults() { - info('Displaying results') - const downloadStart = Date.now() - const artifactOld = await downloadArtifact(getArtifactName('old')) - const artifactNew = await downloadArtifact(getArtifactName('new')) - const sarifFileOld = path.join(artifactOld, sarifReportPath) - const sarifFileNew = path.join(artifactNew, sarifReportPath) - - var compareMessage: string - if (existsSync(sarifFileOld) && existsSync(sarifFileNew)) { - compareMessage = await compareResults(sarifFileOld, sarifFileNew) - } else { - throw new Error('SARIF file not found') + // Check and copy files for each scanner type + const scaAvailable = await prepareScannerFiles('sca', artifactOld, artifactNew) + const iacAvailable = await prepareScannerFiles('iac', artifactOld, artifactNew) + + // Need at least one scanner to compare + if (!scaAvailable && !iacAvailable) { + info('No scanner files available for comparison. Nothing to compare.') + setOutput('display-completed', true) + return } - const commentStart = Date.now() - if (compareMessage.length > 0 && getInput('token').length > 0) { - info('Posting comment to GitHub PR as there were new issues introduced:') - if (getInput('footer') !== '') { - compareMessage += '\n\n' + getInput('footer') + // Run codesec compare mode with available scanners + await codesecRun('compare', iacAvailable, scaAvailable) + + // Read comparison output - check all possible outputs + const outputs = [ + 'scan-results/compare/merged-compare.md', + 'scan-results/compare/sca-compare.md', + 'scan-results/compare/iac-compare.md', + ] + + let message: string | null = null + for (const output of outputs) { + if (existsSync(output)) { + info(`Using comparison output: ${output}`) + message = readMarkdownFile(output) + break } - info(compareMessage) - const commentUrl = await postCommentIfInPr(compareMessage) + } + + if (!message) { + info('No comparison output produced. No changes detected.') + setOutput('display-completed', true) + return + } + + // 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() } - setOutput(`display-completed`, true) + + setOutput('display-completed', true) } -function getArtifactName(target: string): string { - var artifactName = 'results-' - if (artifactPrefix !== '') { - artifactName = artifactPrefix + '-' + artifactName +async function prepareScannerFiles( + scanner: 'sca' | 'iac', + artifactOld: string, + artifactNew: string +): Promise { + const ext = scanner === 'sca' ? 'sarif' : 'json' + const oldPath = path.join(artifactOld, 'scan-results', scanner, `${scanner}-old.${ext}`) + const newPath = path.join(artifactNew, 'scan-results', scanner, `${scanner}-new.${ext}`) + + const oldExists = existsSync(oldPath) + const newExists = existsSync(newPath) + + if (!oldExists || !newExists) { + info(`${scanner.toUpperCase()} files not found for compare. old=${oldExists}, new=${newExists}`) + return false } - return artifactName + target + + info(`Copying ${scanner.toUpperCase()} files for compare`) + await callCommand('cp', oldPath, path.join('scan-results', scanner, `${scanner}-old.${ext}`)) + await callCommand('cp', newPath, path.join('scan-results', scanner, `${scanner}-new.${ext}`)) + return true } async function main() { diff --git a/src/util.ts b/src/util.ts index b432e53a..6755f83d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,8 @@ import { error, getInput, info, isDebug } from '@actions/core' import { context } from '@actions/github' import { spawn } from 'child_process' -import { readFileSync } from 'fs' +import { readFileSync, mkdirSync } from 'fs' +import * as path from 'path' export function getMsSinceStart(): string { const now = Date.now() @@ -59,25 +60,6 @@ export function getOptionalEnvVariable(name: string, defaultValue: string) { return value } -export async function callLaceworkCli(...args: string[]) { - const accountName = getRequiredEnvVariable('LW_ACCOUNT_NAME') - const apiKey = getRequiredEnvVariable('LW_API_KEY') - const apiSecret = getRequiredEnvVariable('LW_API_SECRET') - const expandedArgs = [ - '--noninteractive', - '--account', - accountName, - '--api_key', - apiKey, - '--api_secret', - apiSecret, - 'sca', - ...args, - ] - info('Calling lacework ' + expandedArgs.join(' ')) - await callCommand('lacework', ...expandedArgs) -} - export function getOrDefault(name: string, defaultValue: string) { const setTo = getInput(name) if (setTo !== undefined && setTo.length > 0) return setTo @@ -108,3 +90,183 @@ export function generateUILink() { return url } + +// codesecRun - Docker-based scanner using codesec:latest image +// +// 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 + const reportsDir = path.join(process.cwd(), 'scan-results') + + if (action === 'scan') { + 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') { + const containerName = 'codesec-compare' + + info('Running codesec compare') + + // 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=${runSca}`, + '-e', + `RUN_IAC=${runIac}`, + '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}`) + } +}