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.

180 lines 6.48 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startServer = startServer; const node_1 = require("vscode-languageserver/node"); const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument"); const path_1 = __importDefault(require("path")); const url_1 = require("url"); const index_1 = require("../index"); const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all); const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument); let workspaceRoot = ''; // Cross-file persistence: keep the most recent scan results per URI even // after a document is closed, so a "go to symbol" jump still surfaces stale // findings instead of a blank squiggle list. const resultsByUri = new Map(); const debounceTimers = new Map(); const DEBOUNCE_MS = 350; function toWorkspacePath(uri) { const filePath = (0, url_1.fileURLToPath)(uri); return path_1.default.relative(workspaceRoot, filePath).replace(/\\/g, '/'); } function toDiagnostic(result) { const line = (result.range?.startLine ?? result.line ?? 1) - 1; const startColumn = (result.range?.startColumn ?? 1) - 1; const endLine = (result.range?.endLine ?? result.line ?? 1) - 1; const endColumn = (result.range?.endColumn ?? startColumn + 1) - 1; const range = node_1.Range.create(line, startColumn, endLine, Math.max(startColumn + 1, endColumn)); const severity = result.severity === 'high' ? node_1.DiagnosticSeverity.Error : result.severity === 'medium' ? node_1.DiagnosticSeverity.Warning : node_1.DiagnosticSeverity.Information; return { range, message: result.message, severity, source: 'ubon', code: result.ruleId }; } async function runScan(document) { if (!workspaceRoot) return; const relativePath = toWorkspacePath(document.uri); const scanner = new index_1.UbonScan(false, true); const results = await scanner.diagnose({ directory: workspaceRoot, changedFiles: [relativePath], profile: 'auto', fast: true, noResultCache: true }); const fileResults = results.filter((r) => r.file === relativePath); resultsByUri.set(document.uri, fileResults); connection.sendDiagnostics({ uri: document.uri, diagnostics: fileResults.map(toDiagnostic) }); } connection.onInitialize((params) => { if (params.rootUri) { workspaceRoot = (0, url_1.fileURLToPath)(params.rootUri); } else if (params.rootPath) { workspaceRoot = params.rootPath; } else { workspaceRoot = process.cwd(); } const result = { capabilities: { textDocumentSync: node_1.TextDocumentSyncKind.Incremental, codeActionProvider: true, hoverProvider: true } }; return result; }); function scheduleScan(document, delayMs = DEBOUNCE_MS) { const existing = debounceTimers.get(document.uri); if (existing) clearTimeout(existing); const timer = setTimeout(() => { debounceTimers.delete(document.uri); runScan(document).catch((error) => { connection.console.error(String(error)); }); }, delayMs); debounceTimers.set(document.uri, timer); } documents.onDidOpen((event) => { // Open is a hard signal — scan immediately so first paint has diagnostics. scheduleScan(event.document, 0); }); documents.onDidSave((event) => { scheduleScan(event.document, 0); }); documents.onDidChangeContent((event) => { // Throttle live edits — a fresh scan on every keystroke is wasteful and // makes the editor visibly stutter on slower projects. scheduleScan(event.document); }); documents.onDidClose((event) => { // Drop the diagnostics envelope from the editor, but keep cached results in // memory so cross-file features (e.g. workspace symbol jumps) still see them. connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); const timer = debounceTimers.get(event.document.uri); if (timer) { clearTimeout(timer); debounceTimers.delete(event.document.uri); } }); connection.onCodeAction((params) => { const document = documents.get(params.textDocument.uri); if (!document) return []; const results = resultsByUri.get(params.textDocument.uri) || []; const actions = []; results.forEach((result) => { if (!result.fixEdits || result.fixEdits.length === 0) return; const edits = result.fixEdits .filter((edit) => edit.file === toWorkspacePath(params.textDocument.uri)) .map((edit) => { const range = node_1.Range.create(edit.startLine - 1, edit.startColumn - 1, edit.endLine - 1, edit.endColumn - 1); return node_1.TextEdit.replace(range, edit.replacement); }); if (edits.length === 0) return; actions.push({ title: `Ubon: ${result.fix || 'Apply suggested fix'}`, kind: node_1.CodeActionKind.QuickFix, diagnostics: params.context.diagnostics, edit: { changes: { [params.textDocument.uri]: edits } } }); }); return actions; }); connection.onHover((params) => { const results = resultsByUri.get(params.textDocument.uri) || []; const line = params.position.line + 1; const hit = results.find((r) => r.line === line); if (!hit) return null; const lines = [ `**ubon · ${hit.ruleId}** — ${hit.severity.toUpperCase()}`, '', hit.message, ]; if (hit.confidenceReason) { lines.push('', `_Why this fired:_ ${hit.confidenceReason} (confidence ${hit.confidence?.toFixed(2) ?? '—'})`); } if (hit.fix) { lines.push('', `**Fix:** ${hit.fix}`); } if (hit.helpUri) { lines.push('', `[Documentation](${hit.helpUri})`); } return { contents: { kind: 'markdown', value: lines.join('\n') } }; }); function startServer() { documents.listen(connection); connection.listen(); } if (require.main === module) { startServer(); } //# sourceMappingURL=server.js.map