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.
156 lines • 5.71 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadTsconfigPaths = loadTsconfigPaths;
exports.resolvesViaTsconfigPaths = resolvesViaTsconfigPaths;
const fs_1 = require("fs");
const path_1 = require("path");
const cache = new Map();
const CANDIDATE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs', 'index.cjs'];
function stripJsonComments(text) {
// String-aware JSONC stripper. Naive global regexes choke on patterns like
// `"@/*": ["./*"]` where `/*` / `*/` appear inside string literals.
let out = '';
let i = 0;
let inString = false;
let stringChar = '';
while (i < text.length) {
const ch = text[i];
const next = text[i + 1];
if (inString) {
out += ch;
if (ch === '\\' && i + 1 < text.length) {
out += text[i + 1];
i += 2;
continue;
}
if (ch === stringChar)
inString = false;
i++;
continue;
}
if (ch === '"' || ch === '\'') {
inString = true;
stringChar = ch;
out += ch;
i++;
continue;
}
if (ch === '/' && next === '/') {
while (i < text.length && text[i] !== '\n')
i++;
continue;
}
if (ch === '/' && next === '*') {
i += 2;
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
i++;
i += 2;
continue;
}
out += ch;
i++;
}
return out;
}
function readTsconfigOnce(tsconfigPath, seen) {
if (seen.has(tsconfigPath))
return null;
seen.add(tsconfigPath);
if (!(0, fs_1.existsSync)(tsconfigPath))
return null;
try {
const raw = (0, fs_1.readFileSync)(tsconfigPath, 'utf-8');
const parsed = JSON.parse(stripJsonComments(raw));
const own = parsed?.compilerOptions || {};
const extendsPath = parsed?.extends;
if (typeof extendsPath === 'string' && (extendsPath.startsWith('.') || extendsPath.startsWith('/'))) {
const base = readTsconfigOnce((0, path_1.resolve)((0, path_1.dirname)(tsconfigPath), extendsPath.endsWith('.json') ? extendsPath : `${extendsPath}.json`), seen);
if (base) {
return {
baseUrl: own.baseUrl ?? base.baseUrl,
paths: { ...(base.paths || {}), ...(own.paths || {}) }
};
}
}
return own;
}
catch {
return null;
}
}
function loadTsconfigPaths(directory) {
const key = (0, path_1.resolve)(directory);
if (cache.has(key))
return cache.get(key) ?? null;
const tsconfigPath = (0, path_1.join)(key, 'tsconfig.json');
const co = readTsconfigOnce(tsconfigPath, new Set());
if (!co || (!co.baseUrl && (!co.paths || Object.keys(co.paths).length === 0))) {
cache.set(key, null);
return null;
}
const baseUrl = (0, path_1.resolve)(key, co.baseUrl || '.');
const paths = co.paths || {};
const result = { baseUrl, paths };
cache.set(key, result);
return result;
}
function candidateExists(base) {
if ((0, fs_1.existsSync)(base))
return true;
for (const ext of CANDIDATE_EXTENSIONS) {
if ((0, fs_1.existsSync)(`${base}${ext}`))
return true;
}
for (const idx of INDEX_FILES) {
if ((0, fs_1.existsSync)((0, path_1.join)(base, idx)))
return true;
}
return false;
}
/**
* Returns true if `specifier` resolves to a real file on disk via the
* project's tsconfig `paths` / `baseUrl`. Used to suppress VIBE001 false
* positives on alias imports like `@/lib/db` or `~/components/Foo`.
*/
function resolvesViaTsconfigPaths(specifier, directory) {
const cfg = loadTsconfigPaths(directory);
if (!cfg)
return false;
// paths can have wildcard patterns like "@/*": ["./src/*"]
for (const [pattern, targets] of Object.entries(cfg.paths)) {
const wildcardIdx = pattern.indexOf('*');
if (wildcardIdx === -1) {
if (specifier !== pattern)
continue;
for (const target of targets) {
const base = (0, path_1.isAbsolute)(target) ? target : (0, path_1.resolve)(cfg.baseUrl, target);
if (candidateExists(base))
return true;
}
}
else {
const prefix = pattern.slice(0, wildcardIdx);
const suffix = pattern.slice(wildcardIdx + 1);
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix))
continue;
const captured = specifier.slice(prefix.length, specifier.length - suffix.length);
for (const target of targets) {
const resolvedTarget = target.replace('*', captured);
const base = (0, path_1.isAbsolute)(resolvedTarget) ? resolvedTarget : (0, path_1.resolve)(cfg.baseUrl, resolvedTarget);
if (candidateExists(base))
return true;
}
}
}
// ubon-disable-next-line VIBE001 example import specifier in resolver docs
// Fall back to bare baseUrl resolution: `import X from "lib/db"` where
// baseUrl is `src/` and `src/lib/db.ts` exists.
if (cfg.baseUrl) {
const base = (0, path_1.resolve)(cfg.baseUrl, specifier);
if (candidateExists(base))
return true;
}
return false;
}
//# sourceMappingURL=tsconfig-resolver.js.map