veritas-kanban/scripts/check-gitleaks.mjs
Brad Groux b1367aa33c
security: add continuous scanning gates (#1180)
* ci: add continuous security gates

* fix: scope runner context to gitleaks step
2026-08-23 12:06:20 -05:00

84 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
import {
copyFileSync,
lstatSync,
mkdirSync,
mkdtempSync,
readlinkSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const binary = process.env.GITLEAKS_BIN || 'gitleaks';
function runGitleaks(args, cwd) {
return spawnSync(binary, args, { cwd, encoding: 'utf8', env: process.env });
}
function fail(message, result) {
console.error(message);
if (result?.error?.code === 'ENOENT') {
console.error(`Gitleaks binary not found at ${binary}. Install gitleaks or set GITLEAKS_BIN.`);
} else if (result?.stderr) {
console.error(result.stderr.trim());
}
process.exit(1);
}
function stageScannableSource() {
const repositoryRoot = process.cwd();
const listing = spawnSync(
'git',
['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
{ cwd: repositoryRoot, encoding: 'utf8' }
);
if (listing.status !== 0) fail('Could not inventory Git-managed source files.', listing);
const stagingDirectory = mkdtempSync(join(tmpdir(), 'veritas-gitleaks-source-'));
for (const relativePath of listing.stdout.split('\0').filter(Boolean)) {
const source = resolve(repositoryRoot, relativePath);
const destination = resolve(stagingDirectory, relativePath);
mkdirSync(dirname(destination), { recursive: true });
const stats = lstatSync(source);
if (stats.isSymbolicLink()) {
writeFileSync(destination, readlinkSync(source));
} else if (stats.isFile()) {
copyFileSync(source, destination);
}
}
return stagingDirectory;
}
const sourceDirectory = stageScannableSource();
const fixtureDirectory = mkdtempSync(join(tmpdir(), 'veritas-gitleaks-regression-'));
try {
const repositoryScan = runGitleaks(
['dir', '.', '--no-banner', '--redact=100', '--gitleaks-ignore-path', '.gitleaksignore'],
sourceDirectory
);
if (repositoryScan.status !== 0) {
fail('Gitleaks found a secret outside the reviewed fingerprint baseline.', repositoryScan);
}
const syntheticToken = ['sk', '_live_', '51Z9Y8X7W6V5U4T3S2Q1P0N9'].join('');
writeFileSync(join(fixtureDirectory, 'new-secret.env'), `payment_token=${syntheticToken}\n`);
const regressionScan = runGitleaks(
['dir', '.', '--no-banner', '--redact=100', '--gitleaks-ignore-path', '/dev/null'],
fixtureDirectory
);
if (regressionScan.status !== 1) {
fail(
`Gitleaks regression fixture was not rejected (expected exit 1, got ${String(regressionScan.status)}).`,
regressionScan
);
}
} finally {
rmSync(sourceDirectory, { recursive: true, force: true });
rmSync(fixtureDirectory, { recursive: true, force: true });
}
console.log('Gitleaks accepted the reviewed baseline and rejected a newly introduced test secret.');