UNPKG

ubon

Version:

Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.

163 lines (162 loc) 7.19 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 { constructor() { this.name = 'Environment Variables Scanner'; this.envPatterns = [ { 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' }, { pattern: /^(?!#).*PASSWORD.*=.+$/gmi, message: 'Password stored in .env file', severity: 'high', fix: 'Ensure .env files are never committed to version control' }, { pattern: /^(?!#).*SECRET.*=.+$/gmi, message: 'Secret value in .env file', severity: 'high', fix: 'Verify .env is in .gitignore and use .env.example for documentation' } ]; } 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, 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(({ pattern, message, severity, fix }, envIndex) => { 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: `ENV${String(envIndex + 2).padStart(3, '0')}`, confidence: 0.85, 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, 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;