UNPKG

ubon

Version:

Security scanner for AI-generated apps (Cursor, Lovable, Windsurf, v0). Catches hardcoded secrets, prompt injection, hallucinated imports, Server Actions / Edge runtime mistakes, and the vibe-coded vulnerabilities traditional linters miss.

179 lines 8.54 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EnvScanner = void 0; const glob_1 = require("glob"); const fs_1 = require("fs"); class EnvScanner { name = 'Environment Variables Scanner'; envPatterns = [ { ruleId: 'ENV002', pattern: /^(?!#).*=.*(?:sk-|pk_live_|pk_test_|rk_live_|rk_test_|eyJ)[a-zA-Z0-9_-]+$/gm, message: 'Potential API key in .env file', severity: 'high', fix: 'Ensure this .env file is in .gitignore and not committed' }, { ruleId: 'ENV003', pattern: /^(?!#).*PASSWORD.*=.+$/gmi, message: 'Password stored in .env file', severity: 'high', fix: 'Ensure .env files are never committed to version control' }, { ruleId: 'ENV004', pattern: /^(?!#).*SECRET.*=.+$/gmi, message: 'Secret value in .env file', severity: 'high', fix: 'Verify .env is in .gitignore and use .env.example for documentation' }, { // Values in NEXT_PUBLIC_* / VITE_* / PUBLIC_* / EXPO_PUBLIC_* are baked // into the browser bundle. A database/Redis/Mongo connection string in // one of these is an immediate credential leak. ruleId: 'ENV008', pattern: /^(?!#)\s*(?:NEXT_PUBLIC_|VITE_|PUBLIC_|EXPO_PUBLIC_)[A-Z0-9_]*(?:URL|URI|DSN|HOST|CONN|CONNECTION|ENDPOINT|DATABASE)[A-Z0-9_]*\s*=\s*.*(?:postgres|postgresql|mongodb|redis|mysql|amqp|clickhouse|mssql|rediss):\/\/.+$/gmi, message: 'Client-exposed env var carries a database/service connection URL (leaks to browser bundle)', severity: 'high', fix: 'Rename without the `NEXT_PUBLIC_`/`VITE_`/`PUBLIC_` prefix so the value stays server-only; rotate the credential if it has already shipped.', // ubon-disable-next-line SEC018 documented detector wording, not a credential confidenceReason: 'NEXT_PUBLIC_/VITE_/PUBLIC_ env with a postgres://, mongodb://, redis://… value ships to the client bundle.' } ]; async scan(options) { const results = []; // Check for .env files const envFiles = await (0, glob_1.glob)('.env*', { cwd: options.directory, ignore: ['node_modules/**', '.env.example', '.env.template'] }); // Check if .env files are properly ignored const gitignorePath = `${options.directory}/.gitignore`; const hasGitignore = (0, fs_1.existsSync)(gitignorePath); let gitignoreContent = ''; if (hasGitignore) { try { gitignoreContent = (0, fs_1.readFileSync)(gitignorePath, 'utf-8'); } catch (error) { // Skip if can't read gitignore } } for (const file of envFiles) { // Skip example files if (file.includes('.example') || file.includes('.template')) { continue; } try { const content = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8'); const lines = content.split('\n'); // Check if this .env file is in gitignore const isIgnored = gitignoreContent.includes('.env') || gitignoreContent.includes(file) || gitignoreContent.includes('.env*'); if (!isIgnored && file !== '.env.local') { results.push({ type: 'error', category: 'security', message: `.env file "${file}" may not be in .gitignore`, file, ruleId: 'ENV001', confidence: 0.9, confidenceReason: '.env file found but not listed in .gitignore', severity: 'high', fix: 'Add .env files to .gitignore to prevent accidental commits' }); } // Scan for secrets in env files lines.forEach((line, index) => { this.envPatterns.forEach(({ ruleId, pattern, message, severity, fix, confidenceReason }) => { const m = line.match(pattern); if (m) { results.push({ type: 'warning', category: 'security', message: `${message} in ${file}`, file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, ruleId, confidence: 0.85, confidenceReason: confidenceReason || 'Pattern matches known secret format in .env file', match: m[0]?.slice(0, 200), severity, fix }); } }); }); // Check for real Supabase URLs/keys in .env files if (content.includes('supabase.co') || content.includes('eyJ')) { results.push({ type: 'warning', category: 'security', message: `Supabase credentials in ${file}`, file, ruleId: 'ENV005', confidence: 0.8, confidenceReason: 'File contains supabase.co URL or JWT-like token (eyJ...)', severity: 'medium', fix: 'Ensure this .env file is not committed to version control' }); } } catch (error) { // Skip files that can't be read } } // Check for missing .env.example const hasEnvExample = (0, fs_1.existsSync)(`${options.directory}/.env.example`); const hasEnvFile = envFiles.length > 0; if (hasEnvFile && !hasEnvExample) { results.push({ type: 'warning', category: 'security', message: 'Missing .env.example file for documentation', ruleId: 'ENV006', confidence: 0.7, severity: 'low', fix: 'Create .env.example with placeholder values for team setup' }); } // Drift: keys present in .env but not in .env.example (and vice versa) if (hasEnvFile && hasEnvExample) { try { const example = (0, fs_1.readFileSync)(`${options.directory}/.env.example`, 'utf-8'); const exampleKeys = new Set(example.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).map(l => l.split('=')[0])); const actualKeys = new Set(); for (const file of envFiles) { try { const c = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8'); c.split('\n').forEach(l => { const t = l.trim(); if (t && !t.startsWith('#')) actualKeys.add(t.split('=')[0]); }); } catch { } } const missingInExample = Array.from(actualKeys).filter(k => !exampleKeys.has(k)); const missingInEnv = Array.from(exampleKeys).filter(k => !actualKeys.has(k)); if (missingInExample.length || missingInEnv.length) { results.push({ type: 'warning', category: 'security', message: 'Environment variable drift between .env and .env.example', ruleId: 'ENV007', confidence: 0.7, severity: 'low', fix: 'Align keys across .env and .env.example' }); } } catch { } } return results; } } exports.EnvScanner = EnvScanner; //# sourceMappingURL=env-scanner.js.map