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.

156 lines 5.71 kB
"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