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