feat(ci): add CI/CD integration — --fail-on, --compact, pipeline templates
Add threshold-based exit codes (--fail-on <severity>) and compact output mode (--compact) to scan-orchestrator and CLI. Pipeline templates for GitHub Actions, Azure DevOps, GitLab CI with SARIF upload. CI/CD guide with Schrems II/NSM compliance documentation. npm publish preparation (files whitelist, .npmignore). Policy ci section for distributable CI defaults. Version 6.1.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d642203991
commit
2c33e9cc64
15 changed files with 599 additions and 17 deletions
|
|
@ -46,6 +46,10 @@ const DEFAULT_POLICY = Object.freeze({
|
|||
log_path: null,
|
||||
events: ['trifecta', 'injection', 'secrets', 'destructive'],
|
||||
},
|
||||
ci: {
|
||||
failOn: null,
|
||||
compact: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Cache loaded policy per project root
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import { discoverFiles } from './lib/file-discovery.mjs';
|
|||
import { envelope, resetCounter } from './lib/output.mjs';
|
||||
import { saveBaseline, diffAgainstBaseline, extractFindings } from './lib/diff-engine.mjs';
|
||||
import { toSARIF } from './lib/sarif-formatter.mjs';
|
||||
import { loadPolicy } from './lib/policy-loader.mjs';
|
||||
|
||||
const FAIL_ON_LEVELS = ['critical', 'high', 'medium', 'low'];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// .llm-security-ignore support
|
||||
|
|
@ -123,7 +126,7 @@ const SCANNERS = [
|
|||
// CLI arg parsing — supports --log-file <path>
|
||||
// ---------------------------------------------------------------------------
|
||||
function parseArgs(argv) {
|
||||
const args = { target: null, logFile: null, outputFile: null, baseline: false, saveBaseline: false, format: 'json' };
|
||||
const args = { target: null, logFile: null, outputFile: null, baseline: false, saveBaseline: false, format: 'json', failOn: null, compact: false };
|
||||
for (let i = 2; i < argv.length; i++) {
|
||||
if (argv[i] === '--log-file' && argv[i + 1]) {
|
||||
args.logFile = argv[++i];
|
||||
|
|
@ -135,6 +138,10 @@ function parseArgs(argv) {
|
|||
args.baseline = true;
|
||||
} else if (argv[i] === '--save-baseline') {
|
||||
args.saveBaseline = true;
|
||||
} else if (argv[i] === '--fail-on' && argv[i + 1]) {
|
||||
args.failOn = argv[++i].toLowerCase();
|
||||
} else if (argv[i] === '--compact') {
|
||||
args.compact = true;
|
||||
} else if (!args.target) {
|
||||
args.target = argv[i];
|
||||
}
|
||||
|
|
@ -144,6 +151,25 @@ function parseArgs(argv) {
|
|||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
|
||||
// Policy fallback for CI settings (CLI args take precedence)
|
||||
try {
|
||||
const policyRoot = args.target ? resolve(args.target) : process.cwd();
|
||||
const policy = loadPolicy(policyRoot);
|
||||
if (args.failOn === null && policy.ci && policy.ci.failOn) {
|
||||
args.failOn = policy.ci.failOn;
|
||||
}
|
||||
if (!args.compact && policy.ci && policy.ci.compact) {
|
||||
args.compact = true;
|
||||
}
|
||||
} catch { /* policy loading is best-effort */ }
|
||||
|
||||
// Validate --fail-on value
|
||||
if (args.failOn !== null && !FAIL_ON_LEVELS.includes(args.failOn)) {
|
||||
console.error(`--fail-on must be one of: ${FAIL_ON_LEVELS.join(', ')} (got: ${args.failOn})`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!args.target) {
|
||||
console.error('Usage: node scan-orchestrator.mjs <target-path> [--log-file <path>]');
|
||||
process.exit(1);
|
||||
|
|
@ -254,8 +280,28 @@ async function main() {
|
|||
if (args.outputFile) {
|
||||
writeFileSync(args.outputFile, jsonStr);
|
||||
output.output_file = args.outputFile;
|
||||
// Stdout gets only the compact aggregate (keeps caller context small)
|
||||
if (args.compact) {
|
||||
for (const r of Object.values(results)) {
|
||||
for (const f of r.findings) {
|
||||
const loc = f.file ? ` (${f.file}${f.line ? ':' + f.line : ''})` : '';
|
||||
process.stderr.write(`[${f.severity.toUpperCase()}] ${f.scanner}: ${f.title}${loc}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
process.stdout.write(JSON.stringify({ aggregate: output.aggregate, output_file: args.outputFile }) + '\n');
|
||||
} else if (args.compact && args.format === 'json') {
|
||||
for (const r of Object.values(results)) {
|
||||
for (const f of r.findings) {
|
||||
const loc = f.file ? ` (${f.file}${f.line ? ':' + f.line : ''})` : '';
|
||||
process.stdout.write(`[${f.severity.toUpperCase()}] ${f.scanner}: ${f.title}${loc}\n`);
|
||||
}
|
||||
}
|
||||
const a = output.aggregate;
|
||||
process.stdout.write(
|
||||
`---\nVerdict: ${a.verdict} | Risk: ${a.risk_score}/100 | ` +
|
||||
`Findings: ${a.total_findings} (${a.counts.critical}C ${a.counts.high}H ${a.counts.medium}M ${a.counts.low}L ${a.counts.info}I) | ` +
|
||||
`Duration: ${totalDuration}ms\n`
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(jsonStr);
|
||||
}
|
||||
|
|
@ -271,11 +317,18 @@ async function main() {
|
|||
`[deep-scan] Duration: ${totalDuration}ms\n`
|
||||
);
|
||||
|
||||
// Exit code based on verdict — use exitCode instead of exit() to allow
|
||||
// stdout pipe buffers to drain fully (process.exit() truncates >64KB on macOS)
|
||||
if (agg.verdict === 'BLOCK') process.exitCode = 2;
|
||||
else if (agg.verdict === 'WARNING') process.exitCode = 1;
|
||||
else process.exitCode = 0;
|
||||
// Exit code — use exitCode instead of exit() to allow stdout pipe buffers
|
||||
// to drain fully (process.exit() truncates >64KB on macOS)
|
||||
if (args.failOn !== null) {
|
||||
const threshold = FAIL_ON_LEVELS.indexOf(args.failOn);
|
||||
const exceeded = FAIL_ON_LEVELS.slice(0, threshold + 1)
|
||||
.some(sev => (agg.counts[sev] || 0) > 0);
|
||||
process.exitCode = exceeded ? 1 : 0;
|
||||
} else {
|
||||
if (agg.verdict === 'BLOCK') process.exitCode = 2;
|
||||
else if (agg.verdict === 'WARNING') process.exitCode = 1;
|
||||
else process.exitCode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue