|
1 | 1 | #!/usr/bin/env node |
2 | 2 | /** |
3 | | - * Cross-platform secrets detection using Gitleaks |
4 | | - * Works on Windows, macOS, and Linux |
| 3 | + * Cross-platform secrets detection using Gitleaks. |
| 4 | + * Supports Docker and Podman (including Colima, OrbStack, Podman Machine). |
| 5 | + * Works on Windows, macOS, and Linux. |
5 | 6 | * |
6 | | - * This script runs Gitleaks in a Docker container for local validation. |
7 | | - * CI uses the official gitleaks-action@v2 for better GitHub integration. |
8 | | - * Both share the same .gitleaks.toml configuration. |
| 7 | + * Usage: |
| 8 | + * node scripts/validate-secrets.js # scan working directory |
9 | 9 | */ |
10 | 10 |
|
11 | | -import { spawn } from 'child_process'; |
12 | | -import { platform } from 'os'; |
13 | | -import { resolve } from 'path'; |
14 | | -import { existsSync } from 'fs'; |
| 11 | +import { spawn, execSync } from 'child_process' |
| 12 | +import { existsSync } from 'fs' |
| 13 | +import { platform } from 'os' |
| 14 | +import { resolve } from 'path' |
15 | 15 |
|
16 | | -const isWindows = platform() === 'win32'; |
17 | | -const projectPath = resolve(process.cwd()); |
| 16 | +const GITLEAKS_IMAGE = 'ghcr.io/gitleaks/gitleaks:v8.30.1' |
| 17 | +const isWindows = platform() === 'win32' |
| 18 | +const projectPath = resolve(process.cwd()) |
| 19 | +const configPath = resolve(projectPath, '.gitleaks.toml') |
| 20 | +const hasConfig = existsSync(configPath) |
18 | 21 |
|
19 | | -// Check if .gitleaks.toml exists |
20 | | -const configPath = resolve(projectPath, '.gitleaks.toml'); |
21 | | -const hasConfig = existsSync(configPath); |
| 22 | +function commandExists(cmd) { |
| 23 | + try { |
| 24 | + execSync(isWindows ? `where ${cmd}` : `which ${cmd}`, { stdio: 'ignore' }) |
| 25 | + return true |
| 26 | + } catch { |
| 27 | + return false |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +function daemonRunning(engine) { |
| 32 | + try { |
| 33 | + execSync(`${engine} info`, { stdio: 'ignore' }) |
| 34 | + return true |
| 35 | + } catch { |
| 36 | + return false |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +function detectEngine() { |
| 41 | + for (const engine of ['docker', 'podman']) { |
| 42 | + if (commandExists(engine)) return engine |
| 43 | + } |
| 44 | + return null |
| 45 | +} |
| 46 | + |
| 47 | +function hintForStoppedDaemon(engine) { |
| 48 | + if (engine === 'docker') { |
| 49 | + if (commandExists('colima')) return "Run 'colima start' to enable secrets detection locally" |
| 50 | + if (commandExists('orbstack')) return 'Start OrbStack to enable secrets detection locally' |
| 51 | + } |
| 52 | + if (engine === 'podman') { |
| 53 | + return "Run 'podman machine start' to enable secrets detection locally" |
| 54 | + } |
| 55 | + return 'Start your container engine to enable secrets detection locally' |
| 56 | +} |
| 57 | + |
| 58 | +const engine = detectEngine() |
| 59 | + |
| 60 | +if (!engine) { |
| 61 | + console.log('No container engine found (docker/podman) - skipping secrets detection') |
| 62 | + console.log('Install Docker or Podman to enable local secrets scanning') |
| 63 | + process.exit(1) |
| 64 | +} |
| 65 | + |
| 66 | +if (!daemonRunning(engine)) { |
| 67 | + const engineLabel = engine.charAt(0).toUpperCase() + engine.slice(1) |
| 68 | + console.error(`${engineLabel} daemon is not running`) |
| 69 | + console.error(hintForStoppedDaemon(engine)) |
| 70 | + process.exit(1) |
| 71 | +} |
22 | 72 |
|
23 | 73 | const args = [ |
24 | 74 | 'run', |
25 | 75 | '--rm', |
26 | 76 | '-v', |
27 | | - `${projectPath}:/path`, |
28 | | - 'ghcr.io/gitleaks/gitleaks:v8.30.1', |
29 | | - 'detect', |
30 | | - '--source=/path', |
| 77 | + `${projectPath}:/workspace`, |
| 78 | + GITLEAKS_IMAGE, |
| 79 | + 'dir', |
| 80 | + '--no-banner', |
31 | 81 | '--verbose', |
32 | | - '--no-git' |
33 | | -]; |
| 82 | +] |
34 | 83 |
|
35 | | -// Add config file if it exists |
36 | 84 | if (hasConfig) { |
37 | | - args.push('--config=/path/.gitleaks.toml'); |
| 85 | + args.push('--config=/workspace/.gitleaks.toml') |
38 | 86 | } |
39 | 87 |
|
40 | | -console.log('Running Gitleaks secrets detection...'); |
| 88 | +args.push('/workspace') |
| 89 | + |
| 90 | +console.log('Checking for secrets with Gitleaks...') |
41 | 91 |
|
42 | | -const gitleaks = spawn('docker', args, { |
| 92 | +const gitleaks = spawn(engine, args, { |
43 | 93 | stdio: 'inherit', |
44 | | - shell: isWindows |
45 | | -}); |
| 94 | + shell: isWindows, |
| 95 | +}) |
46 | 96 |
|
47 | 97 | gitleaks.on('close', (code) => { |
48 | | - process.exit(code); |
49 | | -}); |
| 98 | + if (code !== 0) { |
| 99 | + console.error('Secrets detected! Please remove sensitive data before committing.') |
| 100 | + } |
| 101 | + process.exit(code) |
| 102 | +}) |
50 | 103 |
|
51 | 104 | gitleaks.on('error', (err) => { |
52 | | - console.error('Failed to run Gitleaks:', err.message); |
53 | | - process.exit(1); |
54 | | -}); |
| 105 | + console.error(`Failed to run Gitleaks via ${engine}:`, err.message) |
| 106 | + process.exit(1) |
| 107 | +}) |
0 commit comments