Updates channel prefixes, log messages, comments, and configuration values across daemon, mcp-server, and related packages to complete the rebrand from gsd to sf-run naming. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
184 lines
6.1 KiB
JavaScript
184 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
|
|
const RED = '\x1b[0;31m';
|
|
const YELLOW = '\x1b[1;33m';
|
|
const NC = '\x1b[0m';
|
|
const IGNORE_FILE = '.secretscanignore';
|
|
|
|
const PATTERNS = [
|
|
{ label: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/g },
|
|
{ label: 'Generic API Key', regex: /(api[_-]?key|apikey|api[_-]?secret)[ \t]*[:=][ \t]*['"][0-9a-zA-Z_./-]{20,}['"]/gi },
|
|
{ label: 'Generic Secret', regex: /(secret|token|password|passwd|pwd|credential)[ \t]*[:=][ \t]*['"][^\s'"]{8,}['"]/gi },
|
|
{ label: 'Authorization Header', regex: /(authorization|bearer)[ \t]*[:=][ \t]*['"][^\s'"]{8,}['"]/gi },
|
|
{ label: 'Private Key', regex: /-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)\s+PRIVATE\s+KEY-----/g },
|
|
{ label: 'Database URL', regex: /(mysql|postgres|postgresql|mongodb|redis|amqp|mssql):\/\/[^\s'"]{8,}/gi },
|
|
{ label: 'GitHub Token', regex: /gh[pousr]_[0-9a-zA-Z]{36,}/g },
|
|
{ label: 'GitLab Token', regex: /glpat-[0-9a-zA-Z-]{20,}/g },
|
|
{ label: 'Slack Token', regex: /xox[baprs]-[0-9a-zA-Z-]{10,}/g },
|
|
{ label: 'Slack Webhook', regex: /hooks\.slack\.com\/services\/T[0-9A-Z]{8,}\/B[0-9A-Z]{8,}\/[0-9a-zA-Z]{20,}/g },
|
|
{ label: 'Google API Key', regex: /AIza[0-9A-Za-z_-]{35}/g },
|
|
{ label: 'Stripe Key', regex: /[sr]k_(live|test)_[0-9a-zA-Z]{20,}/g },
|
|
{ label: 'npm Token', regex: /npm_[0-9a-zA-Z]{36,}/g },
|
|
{ label: 'Hex Secret', regex: /(secret|key|token|password)[ \t]*[:=][ \t]*['"]?[0-9a-f]{32,}['"]?/gi },
|
|
{ label: 'Hardcoded Password', regex: /password[ \t]*[:=][ \t]*['"][^'"]{4,}['"]/gi },
|
|
];
|
|
|
|
function runGit(args) {
|
|
try {
|
|
return execFileSync('git', args, {
|
|
encoding: 'utf8',
|
|
shell: process.platform === 'win32',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
});
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
if (argv[0] === '--diff') {
|
|
return { mode: 'diff', ref: argv[1] || 'HEAD' };
|
|
}
|
|
if (argv[0] === '--file') {
|
|
return { mode: 'file', file: argv[1] || '' };
|
|
}
|
|
return { mode: 'staged' };
|
|
}
|
|
|
|
function getFiles(options) {
|
|
if (options.mode === 'diff') {
|
|
return runGit(['diff', '--name-only', '--diff-filter=ACMR', options.ref]);
|
|
}
|
|
if (options.mode === 'file') {
|
|
return options.file;
|
|
}
|
|
return runGit(['diff', '--cached', '--name-only', '--diff-filter=ACMR']);
|
|
}
|
|
|
|
function shouldScan(file) {
|
|
const lower = file.toLowerCase();
|
|
const skippedExtensions = [
|
|
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.woff', '.woff2', '.ttf', '.eot',
|
|
'.zip', '.tar', '.gz', '.tgz', '.bz2', '.7z', '.rar', '.exe', '.dll', '.so', '.dylib',
|
|
'.o', '.a', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.lock', '.map', '.node', '.wasm',
|
|
];
|
|
if (skippedExtensions.some((ext) => lower.endsWith(ext))) return false;
|
|
if (
|
|
lower === '.secretscanignore' ||
|
|
lower === '.gitignore' ||
|
|
lower === '.gitattributes' ||
|
|
lower.startsWith('license') ||
|
|
lower.startsWith('changelog') ||
|
|
lower.endsWith('.md') ||
|
|
lower === 'package-lock.json' ||
|
|
lower === 'pnpm-lock.yaml' ||
|
|
lower === 'bun.lock'
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
lower.startsWith('node_modules/') ||
|
|
lower.startsWith('dist/') ||
|
|
lower.startsWith('coverage/') ||
|
|
lower.startsWith('.sf/')
|
|
) {
|
|
return false;
|
|
}
|
|
if (lower.endsWith('.min.js') || lower.endsWith('.min.css')) return false;
|
|
return true;
|
|
}
|
|
|
|
function getContent(file, mode) {
|
|
if (mode === 'staged') {
|
|
const staged = runGit(['show', `:${file}`]);
|
|
if (staged) return staged;
|
|
}
|
|
try {
|
|
return readFileSync(file, 'utf8');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function loadIgnorePatterns() {
|
|
if (!existsSync(IGNORE_FILE)) return [];
|
|
return readFileSync(IGNORE_FILE, 'utf8')
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter((line) => line && !line.startsWith('#'));
|
|
}
|
|
|
|
function isIgnored(file, lineContent, ignorePatterns) {
|
|
return ignorePatterns.some((pattern) => {
|
|
const splitIndex = pattern.indexOf(':');
|
|
if (splitIndex > 0) {
|
|
const ignoreFile = pattern.slice(0, splitIndex);
|
|
const ignoreRegex = pattern.slice(splitIndex + 1);
|
|
if (file !== ignoreFile) return false;
|
|
try {
|
|
return new RegExp(ignoreRegex, 'i').test(lineContent);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
try {
|
|
return new RegExp(pattern, 'i').test(lineContent);
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
function resetRegex(regex) {
|
|
regex.lastIndex = 0;
|
|
return regex;
|
|
}
|
|
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const files = getFiles(options)
|
|
.split(/\r?\n/)
|
|
.map((file) => file.trim())
|
|
.filter(Boolean);
|
|
|
|
if (files.length === 0) {
|
|
process.stdout.write('secret-scan: no files to scan\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
const ignorePatterns = loadIgnorePatterns();
|
|
let findings = 0;
|
|
|
|
for (const file of files) {
|
|
if (!shouldScan(file)) continue;
|
|
const content = getContent(file, options.mode);
|
|
if (!content) continue;
|
|
|
|
const lines = content.split(/\r?\n/);
|
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
const line = lines[lineIndex];
|
|
for (const pattern of PATTERNS) {
|
|
if (!resetRegex(pattern.regex).test(line)) continue;
|
|
if (isIgnored(file, line, ignorePatterns)) continue;
|
|
|
|
process.stdout.write(`${RED}[SECRET DETECTED]${NC} ${YELLOW}${pattern.label}${NC}\n`);
|
|
process.stdout.write(` File: ${file}:${lineIndex + 1}\n`);
|
|
process.stdout.write(` Line: ${line.slice(0, 120)}...\n\n`);
|
|
findings++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (findings > 0) {
|
|
process.stdout.write(`${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n`);
|
|
process.stdout.write(`${RED}Found ${findings} potential secret(s) in scanned files.${NC}\n`);
|
|
process.stdout.write(`${RED}Commit blocked. Remove the secrets or add exceptions${NC}\n`);
|
|
process.stdout.write(`${RED}to .secretscanignore if these are false positives.${NC}\n`);
|
|
process.stdout.write(`${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write('secret-scan: no secrets detected ✓\n');
|