keyvault-cli
Version:
Secure API key management CLI tool
197 lines (196 loc) ⢠9.67 kB
JavaScript
import fs from 'fs';
import path from 'path';
export class CodeScanner {
async scanDirectory(directoryPath) {
const results = [];
try {
await this.scanDirectoryRecursive(directoryPath, results);
}
catch (error) {
console.error(`Error scanning directory ${directoryPath}:`, error);
}
return results;
}
async scanDirectoryRecursive(directoryPath, results) {
const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directoryPath, entry.name);
if (entry.isDirectory()) {
if (!CodeScanner.IGNORED_DIRECTORIES.includes(entry.name)) {
await this.scanDirectoryRecursive(fullPath, results);
}
}
else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (!CodeScanner.IGNORED_EXTENSIONS.includes(ext)) {
const fileResults = await this.scanFile(fullPath);
results.push(...fileResults);
}
}
}
}
async scanFile(filePath) {
const results = [];
try {
const content = await fs.promises.readFile(filePath, 'utf8');
const lines = content.split('\n');
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const line = lines[lineIndex];
// Skip comments and logs
if (this.isCommentOrLog(line)) {
continue;
}
for (const pattern of CodeScanner.API_KEY_PATTERNS) {
const matches = line.match(pattern.pattern);
if (matches) {
for (const match of matches) {
// Skip obvious false positives
if (this.isFalsePositive(match, line)) {
continue;
}
results.push({
file: filePath,
line: lineIndex + 1,
type: pattern.type,
service: pattern.service,
severity: pattern.severity,
value: this.maskValue(match)
});
}
}
}
}
}
catch (error) {
console.error(`Error scanning file ${filePath}:`, error);
}
return results;
}
isCommentOrLog(line) {
const trimmed = line.trim();
return trimmed.startsWith('//') ||
trimmed.startsWith('#') ||
trimmed.startsWith('/*') ||
trimmed.includes('console.log') ||
trimmed.includes('console.error') ||
trimmed.includes('logger.');
}
isFalsePositive(match, line) {
const falsePositives = [
'example', 'sample', 'test', 'demo', 'placeholder',
'your_key_here', 'your-key-here', 'replace-me',
'sk_test_123', 'pk_test_123', 'AKIA123456789',
'xxxxxxxx', 'aaaaaaaa', '00000000',
'undefined', 'null', 'false', 'true'
];
const lowerMatch = match.toLowerCase();
const lowerLine = line.toLowerCase();
return falsePositives.some(fp => lowerMatch.includes(fp) ||
lowerLine.includes(fp) ||
lowerLine.includes('todo') ||
lowerLine.includes('fixme'));
}
maskValue(value) {
if (value.length <= 8) {
return '*'.repeat(value.length);
}
return value.substring(0, 4) + '*'.repeat(value.length - 8) + value.substring(value.length - 4);
}
generateReport(results) {
if (results.length === 0) {
return 'No API keys or secrets found in the scanned files.';
}
const grouped = results.reduce((acc, result) => {
const key = `${result.service}_${result.severity}`;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(result);
return acc;
}, {});
let report = `Found ${results.length} potential API keys/secrets:\n\n`;
// Sort by severity (critical first)
const severityOrder = ['critical', 'high', 'medium', 'low'];
const sortedGroups = Object.entries(grouped).sort(([a], [b]) => {
const severityA = severityOrder.indexOf(a.split('_')[1]);
const severityB = severityOrder.indexOf(b.split('_')[1]);
return severityA - severityB;
});
for (const [key, groupResults] of sortedGroups) {
const [service, severity] = key.split('_');
const severityIcon = severity === 'critical' ? 'šØ' : severity === 'high' ? 'š“' : severity === 'medium' ? 'š”' : 'š¢';
report += `${severityIcon} ${service.toUpperCase()} (${severity}) - ${groupResults.length} found:\n`;
for (const result of groupResults) {
report += ` š ${result.file}:${result.line} - ${result.type}: ${result.value}\n`;
}
report += '\n';
}
report += '\nā ļø SECURITY RECOMMENDATIONS:\n';
report += '⢠Move all API keys to environment variables\n';
report += '⢠Use a secure vault like KeyVault for credential management\n';
report += '⢠Add sensitive files to .gitignore\n';
report += '⢠Rotate any exposed keys immediately\n';
report += '⢠Enable pre-commit hooks to prevent future leaks\n';
report += '⢠šØ CRITICAL: Never commit private keys or mnemonic seeds!\n';
report += '⢠š For blockchain: Use hardware wallets for production\n';
return report;
}
}
CodeScanner.API_KEY_PATTERNS = [
// AWS
{ pattern: /AKIA[0-9A-Z]{16}/, service: 'aws', type: 'api-key', severity: 'high' },
{ pattern: /aws_secret_access_key\s*=\s*[A-Za-z0-9/+=]{40}/, service: 'aws', type: 'secret', severity: 'high' },
// Stripe
{ pattern: /sk_live_[0-9a-zA-Z]{24}/, service: 'stripe', type: 'api-key', severity: 'high' },
{ pattern: /sk_test_[0-9a-zA-Z]{24}/, service: 'stripe', type: 'api-key', severity: 'medium' },
{ pattern: /pk_live_[0-9a-zA-Z]{24}/, service: 'stripe', type: 'api-key', severity: 'medium' },
// OpenAI
{ pattern: /sk-[a-zA-Z0-9]{48}/, service: 'openai', type: 'api-key', severity: 'high' },
// GitHub
{ pattern: /ghp_[A-Za-z0-9]{36}/, service: 'github', type: 'token', severity: 'high' },
{ pattern: /gho_[A-Za-z0-9]{36}/, service: 'github', type: 'token', severity: 'high' },
{ pattern: /ghu_[A-Za-z0-9]{36}/, service: 'github', type: 'token', severity: 'high' },
// Google Cloud
{ pattern: /AIza[0-9A-Za-z\\-_]{35}/, service: 'google', type: 'api-key', severity: 'high' },
// Slack
{ pattern: /xox[baprs]-[0-9a-zA-Z]{10,48}/, service: 'slack', type: 'token', severity: 'medium' },
// Twilio
{ pattern: /AC[a-fA-F0-9]{32}/, service: 'twilio', type: 'api-key', severity: 'medium' },
// Blockchain & Crypto
// Ethereum private keys (64 hex chars)
{ pattern: /\b[a-fA-F0-9]{64}\b/, service: 'ethereum', type: 'private-key', severity: 'critical' },
// Ethereum addresses
{ pattern: /0x[a-fA-F0-9]{40}/, service: 'ethereum', type: 'wallet-address', severity: 'high' },
// Bitcoin private keys (WIF format)
{ pattern: /[5KL][1-9A-HJ-NP-Za-km-z]{50,51}/, service: 'bitcoin', type: 'private-key', severity: 'critical' },
// Bitcoin addresses (P2PKH)
{ pattern: /[13][a-km-zA-HJ-NP-Z1-9]{25,34}/, service: 'bitcoin', type: 'wallet-address', severity: 'high' },
// Bitcoin addresses (Bech32)
{ pattern: /bc1[a-z0-9]{39,59}/, service: 'bitcoin', type: 'wallet-address', severity: 'high' },
// Solana private keys (base58)
{ pattern: /[1-9A-HJ-NP-Za-km-z]{87,88}/, service: 'solana', type: 'private-key', severity: 'critical' },
// Solana addresses
{ pattern: /[1-9A-HJ-NP-Za-km-z]{32,44}/, service: 'solana', type: 'wallet-address', severity: 'medium' },
// BNB Smart Chain (BSC) addresses
{ pattern: /0x[a-fA-F0-9]{40}/, service: 'binance', type: 'wallet-address', severity: 'high' },
// Polygon addresses (same as Ethereum)
{ pattern: /0x[a-fA-F0-9]{40}/, service: 'polygon', type: 'wallet-address', severity: 'high' },
// Mnemonic seed phrases (12/24 words)
{ pattern: /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/, service: 'crypto', type: 'mnemonic-seed', severity: 'critical' },
// Generic patterns
{ pattern: /['"]\s*[A-Za-z0-9]{32,}\s*['"]/, service: 'generic', type: 'api-key', severity: 'low' },
{ pattern: /api[_-]?key\s*[:=]\s*['"]\s*[A-Za-z0-9]{16,}\s*['"]/, service: 'generic', type: 'api-key', severity: 'medium' },
{ pattern: /secret[_-]?key\s*[:=]\s*['"]\s*[A-Za-z0-9]{16,}\s*['"]/, service: 'generic', type: 'secret', severity: 'medium' },
{ pattern: /token\s*[:=]\s*['"]\s*[A-Za-z0-9]{16,}\s*['"]/, service: 'generic', type: 'token', severity: 'medium' },
];
CodeScanner.IGNORED_EXTENSIONS = [
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico',
'.pdf', '.doc', '.docx', '.zip', '.tar', '.gz',
'.mp4', '.mp3', '.wav', '.avi', '.mov',
'.ttf', '.woff', '.woff2', '.eot',
'.min.js', '.min.css'
];
CodeScanner.IGNORED_DIRECTORIES = [
'node_modules', '.git', '.next', 'dist', 'build',
'coverage', '.nyc_output', 'logs', 'tmp', 'temp'
];