mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-17 13:45:34 +02:00
Merge PR #3478: fail closed on unavailable secret scanning
Also reject scanner errors, terminate regex options, match actual private-key headers, and verify actual rg invocation and findings.
This commit is contained in:
@@ -36,10 +36,21 @@ scan_added_lines() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if hits="$(printf '%s\n' "$added_lines" | rg -n --pcre2 "$regex" 2>/dev/null)"; then
|
||||
if ! command -v rg >/dev/null 2>&1; then
|
||||
printf '[ECC pre-commit] ripgrep (rg) is required for secret scanning; install it and ensure it is on PATH when Git runs the hook.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if hits="$(printf '%s\n' "$added_lines" | rg -n --pcre2 -- "$regex" 2>/dev/null)"; then
|
||||
printf '\n[ECC pre-commit] Potential secret detected (%s) in %s\n' "$name" "$file" >&2
|
||||
printf '%s\n' "$hits" | head -n 3 >&2
|
||||
has_findings=1
|
||||
else
|
||||
local scan_status=$?
|
||||
if [[ "$scan_status" -ne 1 ]]; then
|
||||
printf '[ECC pre-commit] Secret scanner failed (rg exit %s) for %s; commit blocked because scanning could not complete.\n' "$scan_status" "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -56,7 +67,7 @@ while IFS= read -r file; do
|
||||
scan_added_lines "$file" "GitHub classic token" 'ghp_[A-Za-z0-9]{36}'
|
||||
scan_added_lines "$file" "GitHub fine-grained token" 'github_pat_[A-Za-z0-9_]{20,}'
|
||||
scan_added_lines "$file" "AWS access key" 'AKIA[0-9A-Z]{16}'
|
||||
scan_added_lines "$file" "private key block" '-----BEGIN (RSA|EC|OPENSSH|DSA|PRIVATE) KEY-----'
|
||||
scan_added_lines "$file" "private key block" '-----BEGIN (RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY-----'
|
||||
scan_added_lines "$file" "generic credential assignment" "(?i)\\b(api[_-]?key|secret|password|token)\\b\\s*[:=]\\s*['\\\"][^'\\\"]{12,}['\\\"]"
|
||||
done <<< "$staged_files"
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const TOML = require('@iarna/toml');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const installScript = path.join(repoRoot, 'scripts', 'codex', 'install-global-git-hooks.sh');
|
||||
const preCommitHook = path.join(repoRoot, 'scripts', 'codex-git-hooks', 'pre-commit');
|
||||
const prePushHook = path.join(repoRoot, 'scripts', 'codex-git-hooks', 'pre-push');
|
||||
const pluginCacheCheckScript = path.join(repoRoot, 'scripts', 'codex', 'check-plugin-cache.js');
|
||||
const mergeCodexConfigScript = path.join(repoRoot, 'scripts', 'codex', 'merge-codex-config.js');
|
||||
@@ -164,6 +165,86 @@ if (
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
function runHermeticPreCommit({ content = 'safe text\n', filename = 'sample.txt', scanner = false, env = {} } = {}) {
|
||||
const tempDir = createTempDir('codex-pre-commit-');
|
||||
try {
|
||||
const bashEnv = path.join(tempDir, 'bash-env');
|
||||
const binDir = path.join(tempDir, 'bin');
|
||||
const callsPath = path.join(tempDir, 'scanner-calls');
|
||||
fs.mkdirSync(binDir);
|
||||
const hookEnv = makeHermeticCodexEnv(tempDir, path.join(tempDir, '.codex'), {
|
||||
PATH: process.env.PATH,
|
||||
GIT_CONFIG_NOSYSTEM: '1',
|
||||
MSYS_NO_PATHCONV: '1',
|
||||
ECC_SKIP_GIT_HOOKS: '0',
|
||||
ECC_SKIP_PRECOMMIT: '0',
|
||||
...env,
|
||||
});
|
||||
const initialized = spawnSync('git', ['init', '--quiet'], { cwd: tempDir, env: hookEnv });
|
||||
assert.strictEqual(initialized.status, 0, initialized.stderr?.toString());
|
||||
if (content !== null) {
|
||||
fs.writeFileSync(path.join(tempDir, filename), content);
|
||||
const staged = spawnSync('git', ['add', '--', filename], { cwd: tempDir, env: hookEnv });
|
||||
assert.strictEqual(staged.status, 0, staged.stderr?.toString());
|
||||
}
|
||||
// Keep real Git/awk available, while hiding every installed ripgrep binary.
|
||||
fs.writeFileSync(bashEnv, `
|
||||
git_path="$(command -v git)"
|
||||
awk_path="$(command -v awk)"
|
||||
head_path="$(command -v head)"
|
||||
cat_path="$(command -v cat)"
|
||||
rg_path="$(command -v rg || true)"
|
||||
git() { "$git_path" "$@"; }
|
||||
awk() { "$awk_path" "$@"; }
|
||||
head() { "$head_path" "$@"; }
|
||||
cat() { "$cat_path" "$@"; }
|
||||
unset -f rg
|
||||
PATH="${toBashPath(binDir)}"
|
||||
${scanner ? `rg() {
|
||||
printf '%s\\n' "$*" >> "${toBashPath(callsPath)}"
|
||||
${scanner === 'real' ? '"$rg_path" "$@"' : scanner === 'finding' ? "printf '1:synthetic-scanner-finding\\n'; return 0;" : scanner === 'error' ? 'return 2;' : 'return 1;'}
|
||||
}` : ''}
|
||||
`);
|
||||
const result = runBash(preCommitHook, {
|
||||
env: { ...hookEnv, BASH_ENV: toBashPath(bashEnv) },
|
||||
cwd: tempDir,
|
||||
preservePath: false,
|
||||
});
|
||||
result.scannerCalls = fs.existsSync(callsPath) ? fs.readFileSync(callsPath, 'utf8').trim().split('\n') : [];
|
||||
return result;
|
||||
} finally {
|
||||
cleanup(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, options, status, diagnostic] of [
|
||||
['blocks safe staged text when ripgrep is missing', {}, 1, /ripgrep.*required/i],
|
||||
['blocks a staged token when ripgrep is missing', { content: `token = "ghp_${'a'.repeat(36)}"\n` }, 1, /ripgrep.*required/i],
|
||||
['allows a successful scan with no findings', { scanner: true }, 0, null],
|
||||
['blocks a finding reported by an invoked scanner', { scanner: 'finding' }, 1, /Potential secret detected/],
|
||||
['blocks an invoked scanner error', { scanner: 'error' }, 1, /scanner failed.*exit 2/i],
|
||||
['allows safe text through the real scanner', { scanner: 'real' }, 0, null],
|
||||
['blocks a token through the real scanner', { scanner: 'real', content: `token = "ghp_${'a'.repeat(36)}"\n` }, 1, /Potential secret detected/],
|
||||
['blocks a private key through the real scanner', { scanner: 'real', content: '-----BEGIN PRIVATE KEY-----\n' }, 1, /private key block/],
|
||||
['blocks an RSA private key through the real scanner', { scanner: 'real', content: '-----BEGIN RSA PRIVATE KEY-----\n' }, 1, /private key block/],
|
||||
['allows an empty index without ripgrep', { content: null }, 0, null],
|
||||
['allows excluded lockfiles without ripgrep', { filename: 'yarn.lock' }, 0, null],
|
||||
['preserves the pre-commit bypass warning', { env: { ECC_SKIP_PRECOMMIT: '1' } }, 0, /hook bypassed via env/],
|
||||
['preserves the global bypass warning', { env: { ECC_SKIP_GIT_HOOKS: '1' } }, 0, /hook bypassed via env/],
|
||||
]) {
|
||||
if (test(`pre-commit ${name}`, () => {
|
||||
const result = runHermeticPreCommit(options);
|
||||
assert.strictEqual(result.status, status, `${result.stdout}\n${result.stderr}`);
|
||||
if (diagnostic) assert.match(result.stderr, diagnostic);
|
||||
else assert.strictEqual(result.stderr, '');
|
||||
if (options.scanner) {
|
||||
assert.ok(result.scannerCalls.length > 0, 'The scanner must actually execute');
|
||||
assert.ok(result.scannerCalls.every(call => call.startsWith('-n --pcre2 -- ')), 'Regex must follow the option terminator');
|
||||
}
|
||||
})) passed++;
|
||||
else failed++;
|
||||
}
|
||||
|
||||
function runHermeticPrePush({
|
||||
failScript = null,
|
||||
includeCorepack = true,
|
||||
|
||||
Reference in New Issue
Block a user