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
JavaScript
;
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