UNPKG

token-guardian

Version:

A comprehensive solution for protecting and managing API tokens and secrets

281 lines (252 loc) 7.43 kB
import { createHash } from 'crypto'; import { readFileSync } from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import { Logger } from '../utils/Logger'; import { TokenPattern } from '../interfaces/TokenPattern'; import { ScanResult } from '../interfaces/ScanResult'; const execAsync = promisify(exec); /** * Default token patterns to scan for */ const DEFAULT_PATTERNS: TokenPattern[] = [ { name: 'AWS Access Key', regex: /(?<![A-Za-z0-9])(AKIA[0-9A-Z]{16})(?![A-Za-z0-9])/, description: 'AWS Access Key ID', entropyThreshold: 3.5, severity: 'high' }, { name: 'AWS Secret Key', regex: /(?<![A-Za-z0-9])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9])/, description: 'AWS Secret Access Key', entropyThreshold: 4.5, severity: 'high' }, { name: 'GitHub Token', regex: /(?<![A-Za-z0-9])(gh[ps]_[a-zA-Z0-9]{36})(?![A-Za-z0-9])/, description: 'GitHub Personal Access Token', entropyThreshold: 4.0, severity: 'high' }, { name: 'Google API Key', regex: /(?<![A-Za-z0-9])(AIza[0-9A-Za-z\\-_]{35})(?![A-Za-z0-9])/, description: 'Google API Key', entropyThreshold: 3.8, severity: 'high' }, { name: 'JWT', regex: /(?<![A-Za-z0-9])([A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*)(?![A-Za-z0-9])/, description: 'JSON Web Token', entropyThreshold: 4.0, severity: 'medium', validate: (token: string) => { try { const [header, payload] = token.split('.').map(part => JSON.parse(Buffer.from(part, 'base64url').toString()) ); return ( typeof header === 'object' && header !== null && typeof payload === 'object' && payload !== null && typeof header.alg === 'string' && typeof header.typ === 'string' ); } catch { return false; } } } ]; /** * Default file patterns to ignore */ const DEFAULT_IGNORE_PATTERNS = [ '**/*.test.{js,ts,jsx,tsx}', '**/*.spec.{js,ts,jsx,tsx}', '**/test/**', '**/tests/**', '**/__tests__/**', '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**' ]; export class GitScanner { private patterns: TokenPattern[]; private ignorePatterns: string[]; private logger: Logger; constructor( patterns: TokenPattern[] = DEFAULT_PATTERNS, ignorePatterns: string[] = DEFAULT_IGNORE_PATTERNS, logger?: Logger ) { this.patterns = patterns; this.ignorePatterns = ignorePatterns; this.logger = logger || new Logger('info'); } /** * Calculate Shannon entropy of a string * @param str String to calculate entropy for * @returns Entropy value */ private calculateEntropy(str: string): number { const len = str.length; const frequencies = new Map<string, number>(); // Calculate character frequencies for (const char of str) { frequencies.set(char, (frequencies.get(char) || 0) + 1); } // Calculate entropy using Shannon's formula return Array.from(frequencies.values()).reduce((entropy, freq) => { const probability = freq / len; return entropy - probability * Math.log2(probability); }, 0); } /** * Check if a file should be ignored * @param filepath File path to check * @returns Whether the file should be ignored */ private shouldIgnoreFile(filepath: string): boolean { return this.ignorePatterns.some(pattern => { if (pattern.startsWith('**/')) { return filepath.includes(pattern.slice(3)); } return filepath === pattern; }); } /** * Get staged files for scanning * @returns List of staged file paths */ private async getStagedFiles(): Promise<string[]> { const { stdout } = await execAsync('git diff --cached --name-only'); return stdout.split('\n').filter(file => file.trim()); } /** * Scan a single line for potential tokens * @param line Line to scan * @param lineNumber Line number * @param filepath File path * @returns Array of found tokens */ private scanLine( line: string, lineNumber: number, filepath: string ): ScanResult[] { const results: ScanResult[] = []; for (const pattern of this.patterns) { const matches = line.matchAll(pattern.regex); for (const match of matches) { const token = match[1] || match[0]; const entropy = this.calculateEntropy(token); // Skip if entropy is too low (likely not a real token) if (entropy < pattern.entropyThreshold) { continue; } // Validate token if pattern has a validator if (pattern.validate && !pattern.validate(token)) { continue; } // Calculate token fingerprint const fingerprint = createHash('sha256') .update(token) .digest('hex') .substring(0, 16); results.push({ type: pattern.name.toLowerCase().replace(/\s+/g, '_'), value: token, description: pattern.description, fingerprint, entropy, location: { file: filepath, line: lineNumber, column: match.index || 0 } }); } } return results; } /** * Scan a file for potential tokens * @param filepath File to scan * @returns Scan results */ private async scanFile(filepath: string): Promise<ScanResult[]> { if (this.shouldIgnoreFile(filepath)) { return []; } try { const content = readFileSync(filepath, 'utf8'); const lines = content.split('\n'); const results: ScanResult[] = []; for (let i = 0; i < lines.length; i++) { const lineResults = this.scanLine(lines[i], i + 1, filepath); results.push(...lineResults); } return results; } catch (error) { this.logger.error(`Error scanning file ${filepath}: ${error}`); return []; } } /** * Run the pre-commit scan * @returns Scan results and whether the commit should be blocked */ public async runPreCommitScan(): Promise<{ results: ScanResult[]; shouldBlock: boolean; }> { const stagedFiles = await this.getStagedFiles(); const allResults: ScanResult[] = []; for (const file of stagedFiles) { const results = await this.scanFile(file); allResults.push(...results); } // Log results if (allResults.length > 0) { this.logger.warn('🚨 Potential tokens found in commit:'); for (const result of allResults) { this.logger.warn(` Token Type: ${result.type} Description: ${result.description} File: ${result.location.file}:${result.location.line} Fingerprint: ${result.fingerprint} Entropy: ${result.entropy.toFixed(2)} Please verify this is not a real token. If it is: 1. Remove the token from the file 2. Rotate the token immediately 3. Check for any unauthorized usage `); } } return { results: allResults, shouldBlock: allResults.length > 0 }; } /** * Add a custom token pattern * @param pattern Pattern to add */ public addPattern(pattern: TokenPattern): void { this.patterns.push(pattern); } /** * Add a custom ignore pattern * @param pattern Pattern to ignore */ public addIgnorePattern(pattern: string): void { this.ignorePatterns.push(pattern); } }