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.

320 lines 17.3 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); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateSecurityPosture = exports.InteractiveReporter = exports.HumanReporter = exports.BaseScanner = exports.FrameworkScanner = exports.AIScanner = exports.VibeScanner = exports.LovableSupabaseScanner = exports.AgentSettingsScanner = exports.ReactPatternsScanner = exports.ReactSecurityScanner = exports.ViteScanner = exports.OSVScanner = exports.IacScanner = exports.EnvScanner = exports.DevelopmentScanner = exports.AccessibilityScanner = exports.LinkScanner = exports.AstSecurityScanner = exports.SecurityScanner = exports.UbonScan = void 0; const crypto_1 = require("crypto"); const fs_1 = require("fs"); const path_1 = require("path"); const link_scanner_1 = require("./scanners/link-scanner"); const git_history_scanner_1 = require("./scanners/git-history-scanner"); const internal_crawler_1 = require("./scanners/internal-crawler"); const profiles_1 = require("./core/profiles"); const logger_1 = require("./utils/logger"); const suppressions_1 = require("./utils/suppressions"); const file_source_cache_1 = require("./utils/file-source-cache"); const HumanReporter_1 = require("./reporters/HumanReporter"); const InteractiveReporter_1 = require("./reporters/InteractiveReporter"); /** * `UbonScan` is the orchestration layer: it picks scanners based on the * profile, runs them concurrently, applies fingerprinting / suppressions / * baseline, and hands the final list of `ScanResult`s to a reporter. * * It deliberately knows nothing about *how* results are rendered — that's * the job of `HumanReporter` / `InteractiveReporter` / the JSON writers in * `cli/shared.ts` / the upcoming MCP tools. */ class UbonScan { scanners = []; linkScanner = new link_scanner_1.LinkScanner(); logger; colorMode; humanReporter; interactiveReporter; constructor(verbose = false, silent = false, colorMode = 'auto', quiet = false) { this.logger = new logger_1.Logger(verbose, silent, colorMode, quiet); this.colorMode = colorMode; this.humanReporter = new HumanReporter_1.HumanReporter(this.logger, colorMode); this.interactiveReporter = new InteractiveReporter_1.InteractiveReporter(colorMode); } /** * Toggle quiet mode after construction. Used by `cli/shared.ts` when the * CLI flag `--quiet` is observed but the orchestrator was constructed * earlier (e.g. by the MCP server). Keeps the public ctor signature stable. */ setQuiet(quiet) { this.logger.quiet = quiet; } async diagnose(options) { if (options.quiet) this.setQuiet(true); this.logger.title('Starting Ubon'); const profile = await this.detectProfile(options); this.scanners = this.resolveScanners(profile, options.fast); if (!options.json) { if (typeof options.minConfidence !== 'number') { options.minConfidence = 0.8; } } const allResults = []; const scannerRuns = this.scanners.map(async (scanner) => { this.logger.info(`Running ${scanner.name}...`); try { const results = await scanner.scan(options); this.logger.success(`🪷 ${scanner.name} completed (${results.length} issues found)`); return results; } catch (error) { this.logger.error(`${scanner.name} failed: ${error}`); return []; } }); const scannerResults = await Promise.all(scannerRuns); scannerResults.forEach((r) => allResults.push(...r)); 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'); } 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}`); } } 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 deduped = this.dedupeResults(allResults); const filtered = this.filterResults(deduped, 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); // Drop the in-memory file cache so a long-running process (LSP / MCP // server) doesn't accumulate stale source between scans. file_source_cache_1.FileSourceCache.clear(options.directory); return this.sortResults(finalResults); } async detectProfile(options) { let profile = options.profile || 'auto'; if (profile !== 'auto') return profile; const hasVite = (0, fs_1.existsSync)((0, path_1.join)(options.directory, 'vite.config.ts')) || (0, fs_1.existsSync)((0, path_1.join)(options.directory, 'vite.config.js')); if (hasVite) { try { const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(options.directory, 'package.json'), 'utf-8')); const hasSupabase = pkg.dependencies?.['@supabase/supabase-js'] || pkg.devDependencies?.['@supabase/supabase-js'] || (0, fs_1.existsSync)((0, path_1.join)(options.directory, 'supabase')); const hasReact = pkg.dependencies?.['react']; const hasTailwind = pkg.dependencies?.['tailwindcss'] || pkg.devDependencies?.['tailwindcss']; if (hasSupabase && hasReact && hasTailwind) profile = 'lovable'; } catch { // package.json missing — fall through to other heuristics } } return profile; } resolveScanners(profile, fast) { return (0, profiles_1.resolveScanners)(profile, fast); } async printResults(results, options) { if (options?.interactive) { await this.interactiveReporter.run(results, options); return; } this.humanReporter.print(results, options); } /** @deprecated Use the InteractiveReporter directly. Kept for back-compat. */ async runInteractive(results, options) { await this.interactiveReporter.run(results, options); } 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.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') { out = out.filter((r) => (r.confidence ?? 1) >= 0.8); } return out; } /** * Collapse findings that target the same `(ruleId, file, line)` tuple. * Multiple scanners (e.g. `security-scanner` + `react-security-scanner`) * sometimes match the same AST node and emit the same rule at the same * location; users see the redundancy as noise. Keep the highest-confidence * finding; tie-break on richer `match` context. */ dedupeResults(results) { const keyed = new Map(); for (const r of results) { const key = [r.ruleId, r.file || '', r.line ?? 0].join('|'); const existing = keyed.get(key); if (!existing) { keyed.set(key, r); continue; } const aConf = existing.confidence ?? 0; const bConf = r.confidence ?? 0; if (bConf > aConf) keyed.set(key, r); else if (bConf === aConf && (r.match?.length ?? 0) > (existing.match?.length ?? 0)) keyed.set(key, r); } return Array.from(keyed.values()); } 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 typeRank = { error: 0, warning: 1, info: 2 }; return [...results].sort((a, b) => { if (typeRank[a.type] !== typeRank[b.type]) return typeRank[a.type] - typeRank[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'); 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; if (!useBaseline) return results; 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_1 = require("./scanners/security-scanner"); Object.defineProperty(exports, "SecurityScanner", { enumerable: true, get: function () { return security_scanner_1.SecurityScanner; } }); var ast_security_scanner_1 = require("./scanners/ast-security-scanner"); Object.defineProperty(exports, "AstSecurityScanner", { enumerable: true, get: function () { return ast_security_scanner_1.AstSecurityScanner; } }); var link_scanner_2 = require("./scanners/link-scanner"); Object.defineProperty(exports, "LinkScanner", { enumerable: true, get: function () { return link_scanner_2.LinkScanner; } }); var accessibility_scanner_1 = require("./scanners/accessibility-scanner"); Object.defineProperty(exports, "AccessibilityScanner", { enumerable: true, get: function () { return accessibility_scanner_1.AccessibilityScanner; } }); var development_scanner_1 = require("./scanners/development-scanner"); Object.defineProperty(exports, "DevelopmentScanner", { enumerable: true, get: function () { return development_scanner_1.DevelopmentScanner; } }); var env_scanner_1 = require("./scanners/env-scanner"); Object.defineProperty(exports, "EnvScanner", { enumerable: true, get: function () { return env_scanner_1.EnvScanner; } }); var iac_scanner_1 = require("./scanners/iac-scanner"); Object.defineProperty(exports, "IacScanner", { enumerable: true, get: function () { return iac_scanner_1.IacScanner; } }); var osv_scanner_1 = require("./scanners/osv-scanner"); Object.defineProperty(exports, "OSVScanner", { enumerable: true, get: function () { return osv_scanner_1.OSVScanner; } }); var vite_scanner_1 = require("./scanners/vite-scanner"); Object.defineProperty(exports, "ViteScanner", { enumerable: true, get: function () { return vite_scanner_1.ViteScanner; } }); var react_security_scanner_1 = require("./scanners/react-security-scanner"); Object.defineProperty(exports, "ReactSecurityScanner", { enumerable: true, get: function () { return react_security_scanner_1.ReactSecurityScanner; } }); var react_patterns_scanner_1 = require("./scanners/react-patterns-scanner"); Object.defineProperty(exports, "ReactPatternsScanner", { enumerable: true, get: function () { return react_patterns_scanner_1.ReactPatternsScanner; } }); var agent_settings_scanner_1 = require("./scanners/agent-settings-scanner"); Object.defineProperty(exports, "AgentSettingsScanner", { enumerable: true, get: function () { return agent_settings_scanner_1.AgentSettingsScanner; } }); var lovable_supabase_scanner_1 = require("./scanners/lovable-supabase-scanner"); Object.defineProperty(exports, "LovableSupabaseScanner", { enumerable: true, get: function () { return lovable_supabase_scanner_1.LovableSupabaseScanner; } }); var vibe_scanner_1 = require("./scanners/vibe-scanner"); Object.defineProperty(exports, "VibeScanner", { enumerable: true, get: function () { return vibe_scanner_1.VibeScanner; } }); var ai_scanner_1 = require("./scanners/ai-scanner"); Object.defineProperty(exports, "AIScanner", { enumerable: true, get: function () { return ai_scanner_1.AIScanner; } }); var framework_scanner_1 = require("./scanners/framework-scanner"); Object.defineProperty(exports, "FrameworkScanner", { enumerable: true, get: function () { return framework_scanner_1.FrameworkScanner; } }); var base_scanner_1 = require("./scanners/base-scanner"); Object.defineProperty(exports, "BaseScanner", { enumerable: true, get: function () { return base_scanner_1.BaseScanner; } }); var HumanReporter_2 = require("./reporters/HumanReporter"); Object.defineProperty(exports, "HumanReporter", { enumerable: true, get: function () { return HumanReporter_2.HumanReporter; } }); var InteractiveReporter_2 = require("./reporters/InteractiveReporter"); Object.defineProperty(exports, "InteractiveReporter", { enumerable: true, get: function () { return InteractiveReporter_2.InteractiveReporter; } }); var Posture_1 = require("./core/Posture"); Object.defineProperty(exports, "calculateSecurityPosture", { enumerable: true, get: function () { return Posture_1.calculateSecurityPosture; } }); //# sourceMappingURL=index.js.map