Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3465d6c
refactor(core): extract domain models and services
tembleking Oct 21, 2025
cbdc224
refactor(app): introduce RunScanUseCase as a facade
tembleking Oct 21, 2025
105f7b0
refactor(infra): implement GitHubActionsInputProvider
tembleking Oct 21, 2025
c7212a3
refactor(infra): implement SysdigCliScanner adapter
tembleking Oct 21, 2025
8b26a08
refactor(infra): implement report presenters
tembleking Oct 21, 2025
6f243f3
refactor(infra): implement FileSystemReportRepository
tembleking Oct 21, 2025
00f5bd9
chore(refactor): final cleanup of legacy code
tembleking Oct 21, 2025
0a80a7a
refactor(core): apply clean architecture and decouple layers
tembleking Oct 21, 2025
006d89d
refactor(use-case): extract methods from RunScanUseCase
tembleking Oct 22, 2025
cf3787c
refactor(errors): introduce specific error classes
tembleking Oct 22, 2025
dbc4b07
chore: do not gitignore dist
tembleking Oct 22, 2025
4013837
refactor(scanner): simplify IScanner interface and align with domain
tembleking Oct 22, 2025
0e6fb80
fix: solve sysdig-cli pulling
tembleking Oct 22, 2025
b3bd6a2
feat: add flexible scanresult domain impl
tembleking Oct 22, 2025
924b01c
refactor(arch): Move report entity to infrastructure layer
tembleking Oct 22, 2025
b2a8b3e
refactor: rename report to JsonScanResultV1
tembleking Oct 22, 2025
40d6a72
refactor: use ScanResult in presenters
tembleking Oct 23, 2025
8a5b095
feat(domain): introduce Version value object for semver comparison
tembleking Oct 23, 2025
c4d39c9
refactor(domain): use Version value object in Package and Vulnerability
tembleking Oct 23, 2025
96ded5e
feat(domain): add suggestedFixVersion to Package
tembleking Oct 23, 2025
cc8f754
feat: implement the suggested fix
tembleking Oct 23, 2025
17245a5
feat: use the suggested fix
tembleking Oct 23, 2025
3d82fb8
refactor(infrastructure): reorganize infrastructure layer
tembleking Oct 23, 2025
3081bb1
Merge branch 'master' into refactor-clean-architecture
tembleking Oct 23, 2025
50ae06a
chore(tests): relocate test fixtures
tembleking Oct 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ jobs:
with:
sysdig-secure-token: ${{ secrets.KUBELAB_SECURE_API_TOKEN }}
mode: iac
iac-scan-path: ./iac_scan_examples
iac-scan-path: ./tests/fixtures/iac/
# Note: This test assumes these policies exist in the target Sysdig Secure account.
use-policies: '"All Posture Findings", "MITRE DEFEND"'

Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
Expand Down
3,025 changes: 2,062 additions & 963 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

125 changes: 23 additions & 102 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,35 @@
import * as core from '@actions/core';
import fs from 'fs';
import { generateSARIFReport } from './src/sarif';
import { cliScannerName, cliScannerResult, cliScannerURL, executeScan, pullScanner, ScanExecutionResult, ScanMode } from './src/scanner';
import { ActionInputs, defaultSecureEndpoint } from './src/action';
import { generateSummary } from './src/summary';
import { Report, FilterOptions, Severity } from './src/report';

function parseCsvList(str?: string): string[] {
if (!str) return [];
return str.split(",").map(s => s.trim()).filter(s => !!s);
}

export class ExecutionError extends Error {
constructor(stdout: string, stderr: string) {
super("execution error\n\nstdout: " + stdout + "\n\nstderr: " + stderr);
}
}

function writeReport(reportData: string) {
fs.writeFileSync("./report.json", reportData);
core.setOutput("scanReport", "./report.json");
}

export async function run() {

import { RunScanUseCase } from './src/application/use-cases/RunScanUseCase';
import { GitHubActionsInputProvider } from './src/infrastructure/github/GitHubActionsInputProvider';
import { SysdigCliScanner } from './src/infrastructure/sysdig/SysdigCliScanner';
import { SarifReportPresenter } from './src/infrastructure/github/SarifReportPresenter';
import { SummaryReportPresenter } from './src/infrastructure/github/SummaryReportPresenter';
import { IReportPresenter } from './src/application/ports/IReportPresenter';

async function run(): Promise<void> {
try {
let opts = ActionInputs.parseActionInputs();
opts.printOptions();
let scanFlags = opts.composeFlags();
const inputProvider = new GitHubActionsInputProvider();
const config = inputProvider.getInputs();

let scanResult: ScanExecutionResult;
// Download CLI Scanner from 'cliScannerURL'
let retCode = await pullScanner(opts.cliScannerURL);
if (retCode == 0) {
// Execute Scanner
scanResult = await executeScan(scanFlags);

retCode = scanResult.ReturnCode;
if (retCode == 0 || retCode == 1) {
// Transform Scan Results to other formats such as SARIF
if (opts.mode == ScanMode.vm) {
await processScanResult(scanResult, opts);
}
} else {
core.error("Terminating scan. Scanner couldn't be executed.")
}
} else {
core.error("Terminating scan. Scanner couldn't be pulled.")
}
const scanner = new SysdigCliScanner();

if (opts.stopOnFailedPolicyEval && retCode == 1) {
core.setFailed(`Stopping because Policy Evaluation was FAILED.`);
} else if (opts.standalone && retCode == 0) {
core.info("Policy Evaluation was OMITTED.");
} else if (retCode == 0) {
core.info("Policy Evaluation was PASSED.");
} else if (opts.stopOnProcessingError && retCode > 1) {
core.setFailed(`Stopping because the scanner terminated with an error.`);
} // else: Don't stop regardless the outcome.
const presenters: IReportPresenter[] = [
new SarifReportPresenter(),
];

} catch (error) {
if (core.getInput('stop-on-processing-error') == 'true') {
core.setFailed(`Unexpected error: ${error instanceof Error ? error.stack : String(error)}`);
if (!config.skipSummary) {
presenters.push(new SummaryReportPresenter());
}
core.error(`Unexpected error: ${error instanceof Error ? error.stack : String(error)}`);
}
}

export async function processScanResult(result: ScanExecutionResult, opts: ActionInputs) {
writeReport(result.Output);

let report: Report;
try {
report = JSON.parse(result.Output);
} catch (error) {
core.error("Error parsing analysis JSON report: " + error + ". Output was: " + result.Output);
throw new ExecutionError(result.Output, result.Error);
}

if (report) {
const filters: FilterOptions = {
minSeverity: (opts.severityAtLeast && opts.severityAtLeast.toLowerCase() !== "any")
? opts.severityAtLeast.toLowerCase() as Severity
: undefined,
packageTypes: parseCsvList(opts.packageTypes),
notPackageTypes: parseCsvList(opts.notPackageTypes),
excludeAccepted: opts.excludeAccepted,
};

core.info("Generating SARIF Report...")
generateSARIFReport(report, opts.groupByPackage, filters);

if (!opts.skipSummary) {
core.info("Generating Summary...")
await generateSummary(opts, report, filters);
const useCase = new RunScanUseCase(scanner, presenters, inputProvider);
await useCase.execute();
} catch (error) {
if (error instanceof Error) {
core.setFailed(error.message);
} else {
core.info("Skipping Summary...")
core.setFailed(`Unknown error: ${error}`);
}
}
}

export {
cliScannerURL,
defaultSecureEndpoint,
pullScanner,
cliScannerName,
executeScan,
cliScannerResult,
};

if (require.main === module) {
run();
}
run();
6 changes: 6 additions & 0 deletions src/application/errors/ReportParsingError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class ReportParsingError extends Error {
constructor(message: string) {
super(message);
this.name = "ReportParsingError";
}
}
6 changes: 6 additions & 0 deletions src/application/errors/ScanExecutionError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class ScanExecutionError extends Error {
constructor(message: string) {
super(message);
this.name = "ScanExecutionError";
}
}
6 changes: 6 additions & 0 deletions src/application/errors/ScannerPullError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class ScannerPullError extends Error {
constructor(message: string) {
super(message);
this.name = "ScannerPullError";
}
}
5 changes: 5 additions & 0 deletions src/application/ports/IInputProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ScanConfig } from "./ScanConfig";

export interface IInputProvider {
getInputs(): ScanConfig;
}
6 changes: 6 additions & 0 deletions src/application/ports/IReportPresenter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { FilterOptions } from "../../domain/services/filtering";
import { ScanResult } from "../../domain/scanresult";

export interface IReportPresenter {
generateReport(data: ScanResult, groupByPackage: boolean, filters?: FilterOptions): void;
}
6 changes: 6 additions & 0 deletions src/application/ports/IScanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ScanResult } from '../../domain/scanresult';
import { ScanConfig } from './ScanConfig';

export interface IScanner {
executeScan(config: ScanConfig): Promise<ScanResult>;
}
31 changes: 31 additions & 0 deletions src/application/ports/ScanConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ScanMode } from './ScannerDTOs';
import { Severity } from '../../domain/scanresult';

export interface ScanConfig {
cliScannerURL: string;
cliScannerVersion?: string;
stopOnFailedPolicyEval: boolean;
stopOnProcessingError: boolean;
standalone: boolean;
skipSummary: boolean;
severityAtLeast?: string;
packageTypes?: string;
notPackageTypes?: string;
excludeAccepted?: boolean;
groupByPackage: boolean;
mode: ScanMode;
imageTag: string;
overridePullString: string;
registryUser: string;
registryPassword: string;
dbPath: string;
skipUpload: boolean;
usePolicies: string;
sysdigSecureToken: string;
sysdigSecureURL: string;
sysdigSkipTLS: boolean;
extraParameters: string;
recursive: boolean;
minimumSeverity: string;
iacScanPath: string;
}
28 changes: 28 additions & 0 deletions src/application/ports/ScannerDTOs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export enum ScanMode {
vm = "vm",
iac = "iac",
}

export namespace ScanMode {
export function fromString(str: string): ScanMode | undefined {
switch (str.toLowerCase()) {
case "vm":
return ScanMode.vm;
case "iac":
return ScanMode.iac;
}
}
}

export interface ScanExecutionResult {
ReturnCode: number;
Output: string;
Error: string;
}

export interface ComposeFlags {
envvars: {
[key: string]: string;
};
flags: string[];
}
132 changes: 132 additions & 0 deletions src/application/use-cases/RunScanUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as core from '@actions/core';
import { ScanMode } from '../ports/ScannerDTOs';
import { IInputProvider } from '../ports/IInputProvider';
import { IScanner } from '../ports/IScanner';
import { IReportPresenter } from '../ports/IReportPresenter';
import { FilterOptions } from '../../domain/services/filtering';
import { Severity } from '../../domain/scanresult';
import { ScanConfig } from '../ports/ScanConfig';
import { ScanExecutionError } from '../errors/ScanExecutionError';

export class RunScanUseCase {
constructor(
private readonly scanner: IScanner,
private readonly reportPresenters: IReportPresenter[],
private readonly inputProvider: IInputProvider
) {}

private parseCsvList(str?: string): string[] {
if (!str) return [];
return str.split(",").map(s => s.trim()).filter(s => !!s);
}

async execute(): Promise<void> {
let config: ScanConfig;
try {
config = this.inputProvider.getInputs();
this.printOptions(config);

const report = await this.scanner.executeScan(config);

if (config.mode === ScanMode.vm && report) {

const filters: FilterOptions = {
minSeverity: (config.severityAtLeast && config.severityAtLeast.toLowerCase() !== "any")
? Severity.fromString(config.severityAtLeast)
: undefined,
packageTypes: this.parseCsvList(config.packageTypes),
notPackageTypes: this.parseCsvList(config.notPackageTypes),
excludeAccepted: config.excludeAccepted,
};

for (const presenter of this.reportPresenters) {
presenter.generateReport(report, config.groupByPackage, filters);
}
}

const policyEvaluation = report?.getEvaluationResult();
if (policyEvaluation?.isFailed()) {
this.setFinalStatus(config, 1);
} else {
this.setFinalStatus(config, 0);
}

} catch (error) {
const errorMessage = `Unexpected error: ${error instanceof Error ? error.stack : String(error)}`;
if (config!.stopOnProcessingError) {
core.setFailed(errorMessage);
} else {
core.error(errorMessage);
}
this.setFinalStatus(config!, 2);
}
}

private setFinalStatus(config: ScanConfig, retCode: number): void {
if (config.stopOnFailedPolicyEval && retCode === 1) {
core.setFailed(`Stopping because Policy Evaluation was FAILED.`);
} else if (config.standalone && retCode === 0) {
core.info("Policy Evaluation was OMITTED.");
} else if (retCode === 0) {
core.info("Policy Evaluation was PASSED.");
} else if (config.stopOnProcessingError && retCode > 1) {
core.setFailed(`Stopping because the scanner terminated with an error.`);
}
}

private printOptions(config: ScanConfig) {
if (config.standalone) {
core.info(`[!] Running in Standalone Mode.`);
}

if (config.sysdigSecureURL) {
core.info('Sysdig Secure URL: ' + config.sysdigSecureURL);
}

if (config.registryUser && config.registryPassword) {
core.info(`Using specified Registry credentials.`);
}



core.info(`Stop on Failed Policy Evaluation: ${config.stopOnFailedPolicyEval}`);

core.info(`Stop on Processing Error: ${config.stopOnProcessingError}`);

if (config.skipUpload) {
core.info(`Skipping scan results upload to Sysdig Secure...`);
}

if (config.dbPath) {
core.info(`DB Path: ${config.dbPath}`);
}

core.info(`Sysdig skip TLS: ${config.sysdigSkipTLS}`);

if (config.severityAtLeast) {
core.info(`Severity level: ${config.severityAtLeast}`);
}

if (config.packageTypes) {
core.info(`Package types included: ${config.packageTypes}`);
}

if (config.notPackageTypes) {
core.info(`Package types excluded: ${config.notPackageTypes}`);
}

if (config.excludeAccepted !== undefined) {
core.info(`Exclude vulnerabilities with accepted risks: ${config.excludeAccepted}`);
}

core.info('Analyzing image: ' + config.imageTag);

if (config.overridePullString) {
core.info(` * Image PullString will be overwritten as ${config.overridePullString}`);
}

if (config.skipSummary) {
core.info("This run will NOT generate a SUMMARY.");
}
}
}
Empty file.
Loading
Loading