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.

465 lines (464 loc) â€ĸ 23.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EnvScanner = exports.AccessibilityScanner = exports.LinkScanner = exports.SecurityScanner = exports.UbonScan = void 0; const crypto_1 = require("crypto"); const fs_1 = require("fs"); const path_1 = require("path"); const security_scanner_1 = require("./scanners/security-scanner"); const link_scanner_1 = require("./scanners/link-scanner"); const accessibility_scanner_1 = require("./scanners/accessibility-scanner"); const env_scanner_1 = require("./scanners/env-scanner"); const python_security_scanner_1 = require("./scanners/python-security-scanner"); const osv_scanner_1 = require("./scanners/osv-scanner"); const git_history_scanner_1 = require("./scanners/git-history-scanner"); const ast_security_scanner_1 = require("./scanners/ast-security-scanner"); const internal_crawler_1 = require("./scanners/internal-crawler"); const iac_scanner_1 = require("./scanners/iac-scanner"); const rails_security_scanner_1 = require("./scanners/rails-security-scanner"); const glob_1 = require("glob"); const logger_1 = require("./utils/logger"); const chalk_1 = __importDefault(require("chalk")); const rules_1 = require("./types/rules"); const suppressions_1 = require("./utils/suppressions"); class UbonScan { constructor(verbose = false, silent = false, colorMode = 'auto') { this.scanners = []; this.linkScanner = new link_scanner_1.LinkScanner(); this.logger = new logger_1.Logger(verbose, silent, colorMode); this.useColor = this.shouldUseColor(colorMode); } shouldUseColor(mode) { if (mode === 'always') return true; if (mode === 'never') return false; return process.stdout.isTTY && !process.env.NO_COLOR; } colorize(fn, text) { return this.useColor ? fn(text) : text; } brand(text) { return this.useColor ? chalk_1.default.hex('#c99cb3')(text) : text; } async diagnose(options) { this.logger.title('Starting Ubon'); // Auto-detect profile if needed (Python if .py files present) let profile = options.profile || 'auto'; if (profile === 'auto') { const py = await (0, glob_1.glob)('**/*.py', { cwd: options.directory, ignore: ['.venv/**', 'venv/**', 'node_modules/**', 'dist/**', 'build/**', '.next/**', 'examples/**'] }); if (py.length > 0) profile = 'python'; } // Select scanners based on profile this.scanners = this.resolveScanners(profile, options.fast); // Runtime, non-persistent defaults for human-friendly noise reduction if (!options.json && options.profile !== 'python') { // If user didn't set minConfidence, prefer a gentle default if (typeof options.minConfidence !== 'number') { options.minConfidence = 0.8; } } const allResults = []; // Run static file scanners for (const scanner of this.scanners) { this.logger.info(`Running ${scanner.name}...`); try { const results = await scanner.scan(options); allResults.push(...results); this.logger.success(`${scanner.name} completed (${results.length} issues found)`); } catch (error) { this.logger.error(`${scanner.name} failed: ${error}`); } } // Fast mode skips link and OSV scanners if (!options.fast) { this.logger.info(`Running ${this.linkScanner.name}...`); try { const linkResults = await this.linkScanner.scan(options); allResults.push(...linkResults); this.logger.success(`${this.linkScanner.name} completed (${linkResults.length} issues found)`); } catch (error) { this.logger.error(`${this.linkScanner.name} failed: ${error}`); } } else { this.logger.info('⚡ Fast mode: Skipping external link checks'); } // Internal crawler (opt-in) if (options.crawlInternal && !options.fast) { const crawler = new internal_crawler_1.InternalCrawler(); this.logger.info(`Running ${crawler.name}...`); try { const cres = await crawler.scan(options); allResults.push(...cres); this.logger.success(`${crawler.name} completed (${cres.length} issues found)`); } catch (e) { this.logger.error(`${crawler.name} failed: ${e}`); } } // Git history scanner if enabled if (options.gitHistoryDepth && options.gitHistoryDepth > 0) { const hist = new git_history_scanner_1.GitHistoryScanner(); this.logger.info(`Running ${hist.name}...`); try { const hres = await hist.scan(options); allResults.push(...hres); this.logger.success(`${hist.name} completed (${hres.length} issues found)`); } catch (e) { this.logger.error(`${hist.name} failed: ${e}`); } } const filtered = this.filterResults(allResults, options); const withFingerprints = filtered.map(r => ({ ...r, fingerprint: this.computeFingerprint(r) })); const withSuppressions = (0, suppressions_1.applySuppressions)(withFingerprints); const afterBaseline = await this.applyBaseline(withSuppressions, options); const finalResults = this.applyFocusFilters(afterBaseline, options); return this.sortResults(finalResults); } resolveScanners(profile, fast) { const p = profile || 'auto'; if (p === 'python') { const arr = [new python_security_scanner_1.PythonSecurityScanner(), new env_scanner_1.EnvScanner()]; if (!fast) arr.push(new osv_scanner_1.OSVScanner()); return arr; } if (p === 'rails') { const arr = [new rails_security_scanner_1.RailsSecurityScanner()]; return arr; } // vue/react/next fall through to JS scanners // auto/react/next default to JS scanners const jsArr = [new security_scanner_1.SecurityScanner(), new ast_security_scanner_1.AstSecurityScanner(), new accessibility_scanner_1.AccessibilityScanner(), new env_scanner_1.EnvScanner(), new iac_scanner_1.IacScanner()]; if (!fast) jsArr.push(new osv_scanner_1.OSVScanner()); return jsArr; } printResults(results, options) { // Separate suppressed from active results const allResultsWithSuppressions = results; const suppressedCount = results.filter(r => r.suppressed).length; // Apply suppression filtering const activeResults = (0, suppressions_1.filterSuppressedResults)(results, { showSuppressed: options?.showSuppressed, ignoreSuppressed: options?.ignoreSuppressed }); if (activeResults.length === 0 && suppressedCount === 0) { this.logger.success('No issues found! Your app is looking healthy! 🎉'); return; } if (activeResults.length === 0 && suppressedCount > 0) { this.logger.success(`No active issues found! ${suppressedCount} issues suppressed. 🎉`); return; } // Apply filters and limits let filteredResults = this.applyResultFilters(activeResults, options); // Smart suggestion for overwhelm const totalActive = activeResults.length; if (!options?.maxIssues && totalActive > 50) { this.logger.info(this.colorize(chalk_1.default.gray, `Found ${totalActive} issues. Tip: use --max-issues 10 to focus on critical items first.`)); } // Severity-first header const errorCount = filteredResults.filter(r => r.type === 'error').length; const warnCount = filteredResults.filter(r => r.type === 'warning').length; const criticalCount = filteredResults.filter(r => r.severity === 'high').length; const highText = criticalCount > 0 ? this.colorize(chalk_1.default.bgRed.white, ` ${criticalCount} CRITICAL `) : ''; const suppressedText = suppressedCount > 0 ? this.colorize(chalk_1.default.gray, ` ${suppressedCount} suppressed`) : ''; console.log(`\n${this.brand('đŸĒˇ')} ${this.colorize(chalk_1.default.bold, 'Triage')}: ${highText} ${this.colorize(chalk_1.default.red, errorCount + ' errors')}, ${this.colorize(chalk_1.default.yellow, warnCount + ' warnings')}${suppressedText}`); if (filteredResults.length !== activeResults.length) { console.log(`${this.colorize(chalk_1.default.gray, ` (showing ${filteredResults.length} of ${activeResults.length} active issues)`)}`); } this.logger.separator(); this.logger.title(`Found ${filteredResults.length} issues:`); const groupedResults = this.groupResults(filteredResults, options?.groupBy || 'severity'); Object.entries(groupedResults).forEach(([groupKey, groupResults]) => { const icon = this.getGroupIcon(groupKey, options?.groupBy || 'category'); const lotus = this.brand(icon); const count = this.colorize(chalk_1.default.gray, `(${groupResults.length})`); console.log(`\n${lotus} ${this.colorize(chalk_1.default.bold, groupKey.toUpperCase())} ${count}:`); groupResults.forEach(result => { const isError = result.type === 'error'; const icon = isError ? this.colorize(chalk_1.default.red, '●') : this.colorize(chalk_1.default.yellow, '●'); const location = result.file ? this.colorize(chalk_1.default.gray, ` (${result.file}:${result.line})`) : ''; const sev = (result.severity || '').toLowerCase(); const badge = sev === 'high' ? this.colorize(chalk_1.default.bgRed.white, ' HIGH ') : sev === 'medium' ? this.colorize(chalk_1.default.bgYellow.black, ' MED ') : sev === 'low' ? this.colorize(chalk_1.default.bgBlue.white, ' LOW ') : ''; const rule = result.ruleId ? this.colorize(chalk_1.default.gray, ` {${result.ruleId}}`) : ''; const msgColor = isError ? chalk_1.default.red : chalk_1.default.yellow; const suppressedIndicator = result.suppressed ? this.colorize(chalk_1.default.gray, ' [SUPPRESSED]') : ''; const confText = (options?.showConfidence || options?.verbose) ? this.colorize(chalk_1.default.gray, ` (confidence: ${(result.confidence ?? 0).toFixed(2)})`) : ''; console.log(` ${icon} ${badge} ${this.colorize(msgColor, result.message)}${location}${rule}${confText}${suppressedIndicator}`); if (result.fix) { console.log(` ${this.brand('đŸĒˇ')} ${this.colorize(chalk_1.default.green, result.fix)}`); } // Show suppression reason if available if (result.suppressed && result.suppressionReason) { console.log(` ${this.colorize(chalk_1.default.gray, '🔇')} ${this.colorize(chalk_1.default.italic, 'Suppressed: ' + result.suppressionReason)}`); } // Show "why it matters" explanation if enabled if (options?.explain && result.ruleId) { const ruleMeta = rules_1.RULES[result.ruleId]; if (ruleMeta?.impact) { console.log(` ${this.colorize(chalk_1.default.blue, '💡')} ${this.colorize(chalk_1.default.italic, ruleMeta.impact)}`); } } // Show code context if enabled and available if (options?.showContext && result.file && result.line) { const context = this.getCodeContext(result.file, result.line); if (context) { console.log(` ${this.colorize(chalk_1.default.gray, '┌─ Code context:')}`); context.forEach((line, idx) => { const lineNum = (result.line - 2 + idx).toString().padStart(3); const isTarget = idx === 2; // middle line (0-indexed) const marker = isTarget ? this.colorize(chalk_1.default.red, 'â–ē') : this.colorize(chalk_1.default.gray, ' '); const lineColor = isTarget ? chalk_1.default.yellow : chalk_1.default.gray; console.log(` ${this.colorize(chalk_1.default.gray, '│')} ${marker} ${this.colorize(lineColor, lineNum)} ${this.colorize(lineColor, line)}`); }); console.log(` ${this.colorize(chalk_1.default.gray, '└─')}`); } } }); }); this.logger.separator(); this.printSummary(results); } groupByCategory(results) { return results.reduce((acc, result) => { if (!acc[result.category]) { acc[result.category] = []; } acc[result.category].push(result); return acc; }, {}); } applyResultFilters(results, options) { let filtered = [...results]; // Apply severity filter if (options?.minSeverity) { const severityOrder = { low: 1, medium: 2, high: 3 }; const minLevel = severityOrder[options.minSeverity]; filtered = filtered.filter(r => severityOrder[r.severity] >= minLevel); } // Sort by priority: severity (high->low), then type (error->warning->info) filtered.sort((a, b) => { const severityOrder = { high: 3, medium: 2, low: 1 }; const typeOrder = { error: 3, warning: 2, info: 1 }; const sevDiff = severityOrder[b.severity] - severityOrder[a.severity]; if (sevDiff !== 0) return sevDiff; return typeOrder[b.type] - typeOrder[a.type]; }); // Apply max issues limit if (options?.maxIssues && options.maxIssues > 0) { filtered = filtered.slice(0, options.maxIssues); } return filtered; } groupResults(results, groupBy) { return results.reduce((acc, result) => { let key; switch (groupBy) { case 'file': key = result.file || 'unknown'; break; case 'rule': key = result.ruleId || 'unknown'; break; case 'severity': key = result.severity || 'unknown'; break; case 'category': default: key = result.category; break; } if (!acc[key]) { acc[key] = []; } acc[key].push(result); return acc; }, {}); } getCategoryIcon(category) { const icons = { security: '🔒', links: '🔗', accessibility: 'â™ŋ', performance: '⚡', seo: '🔍' }; return icons[category] || '📋'; } getGroupIcon(groupKey, groupBy) { switch (groupBy) { case 'category': return this.getCategoryIcon(groupKey); case 'file': return '📄'; case 'rule': return 'âš–ī¸'; case 'severity': const severityIcons = { high: '🚨', medium: 'âš ī¸', low: 'â„šī¸' }; return severityIcons[groupKey] || '📊'; default: return '📋'; } } getCodeContext(filePath, lineNumber) { try { const content = (0, fs_1.readFileSync)(filePath, 'utf8'); const lines = content.split('\n'); // Get 2 lines before and 2 lines after the target line (5 total) const startLine = Math.max(0, lineNumber - 3); // -3 because lineNumber is 1-indexed const endLine = Math.min(lines.length - 1, lineNumber + 1); // +1 for 2 lines after const contextLines = []; for (let i = startLine; i <= endLine; i++) { contextLines.push(lines[i] || ''); } return contextLines.length > 0 ? contextLines : null; } catch (error) { // File might not exist or be readable return null; } } printSummary(results) { const errors = results.filter(r => r.type === 'error').length; const warnings = results.filter(r => r.type === 'warning').length; console.log(`\n${this.brand('đŸĒˇ')} ${this.colorize(chalk_1.default.bold, 'Summary')}: ${this.colorize(chalk_1.default.red, errors + ' errors')}, ${this.colorize(chalk_1.default.yellow, warnings + ' warnings')}`); if (errors > 0) { this.logger.error('Critical issues found that should be fixed immediately'); } else { this.logger.success('No critical issues found'); } } filterResults(results, options) { let filtered = results; if (options.changedFiles && options.changedFiles.length > 0) { const set = new Set(options.changedFiles.map(f => f.replace(/^[./]+/, ''))); filtered = filtered.filter(r => !r.file || set.has(r.file)); } if (typeof options.minConfidence === 'number') { filtered = filtered.filter(r => (r.confidence ?? 1) >= options.minConfidence); } if (options.enabledRules && options.enabledRules.length > 0) { const set = new Set(options.enabledRules); filtered = filtered.filter(r => set.has(r.ruleId)); } if (options.disabledRules && options.disabledRules.length > 0) { const set = new Set(options.disabledRules); filtered = filtered.filter(r => !set.has(r.ruleId)); } return filtered; } applyFocusFilters(results, options) { let out = results; if (options.focusNew) { // already applied baseline; no-op here since baseline removed old issues } if (options.focusSecurity) { out = out.filter(r => r.category === 'security'); } if (options.focusCritical) { out = out.filter(r => r.severity === 'high'); } if (!options.detailed && typeof options.minConfidence !== 'number') { // gentle noise reduction when not detailed: default minConfidence 0.8 for human runs out = out.filter(r => (r.confidence ?? 1) >= 0.8); } return out; } computeFingerprint(result) { const hash = (0, crypto_1.createHash)('sha256'); const normalizedPath = (result.file || '').replace(/\\/g, '/'); const snippet = (result.match || '').slice(0, 200); hash.update([result.ruleId, normalizedPath, snippet].join('|')); return hash.digest('hex').slice(0, 16); } sortResults(results) { const severityRank = { error: 0, warning: 1, info: 2 }; return [...results].sort((a, b) => { if (severityRank[a.type] !== severityRank[b.type]) return severityRank[a.type] - severityRank[b.type]; if (a.category !== b.category) return a.category.localeCompare(b.category); if ((a.file || '') !== (b.file || '')) return (a.file || '').localeCompare(b.file || ''); if ((a.line || 0) !== (b.line || 0)) return (a.line || 0) - (b.line || 0); return a.ruleId.localeCompare(b.ruleId); }); } async applyBaseline(results, options) { const baselinePath = options.baselinePath || (0, path_1.join)(options.directory, '.ubon.baseline.json'); // Update baseline mode if (options.updateBaseline) { const fingerprints = Array.from(new Set(results.map(r => r.fingerprint))).sort(); const payload = { generatedAt: new Date().toISOString(), fingerprints }; try { (0, fs_1.writeFileSync)(baselinePath, JSON.stringify(payload, null, 2)); this.logger.success(`Baseline updated at ${baselinePath}`); } catch (err) { this.logger.error(`Failed to write baseline: ${err}`); } return []; } const useBaseline = options.useBaseline !== false; // default true if (!useBaseline) return results; // Load baseline fingerprints if exists if (!(0, fs_1.existsSync)(baselinePath)) return results; try { const content = (0, fs_1.readFileSync)(baselinePath, 'utf-8'); const data = JSON.parse(content); const set = new Set(data.fingerprints || []); return results.filter(r => !set.has(r.fingerprint)); } catch { return results; } } } exports.UbonScan = UbonScan; __exportStar(require("./types"), exports); var security_scanner_2 = require("./scanners/security-scanner"); Object.defineProperty(exports, "SecurityScanner", { enumerable: true, get: function () { return security_scanner_2.SecurityScanner; } }); var link_scanner_2 = require("./scanners/link-scanner"); Object.defineProperty(exports, "LinkScanner", { enumerable: true, get: function () { return link_scanner_2.LinkScanner; } }); var accessibility_scanner_2 = require("./scanners/accessibility-scanner"); Object.defineProperty(exports, "AccessibilityScanner", { enumerable: true, get: function () { return accessibility_scanner_2.AccessibilityScanner; } }); var env_scanner_2 = require("./scanners/env-scanner"); Object.defineProperty(exports, "EnvScanner", { enumerable: true, get: function () { return env_scanner_2.EnvScanner; } });