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.
102 lines • 4.14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseScanner = void 0;
const glob_1 = require("glob");
const fs_1 = require("fs");
const path_1 = require("path");
const result_cache_1 = require("../utils/result-cache");
const file_source_cache_1 = require("../utils/file-source-cache");
class BaseScanner {
resultCache = null;
initCache(options, signature) {
this.resultCache = options.noResultCache ? null : new result_cache_1.ResultCache(options.directory, signature);
}
getCached(file, contentHash) {
return this.resultCache?.get(file, contentHash) ?? null;
}
setCached(file, contentHash, results) {
this.resultCache?.set(file, contentHash, results);
}
saveCache() {
this.resultCache?.save();
}
async *iterateFiles(options, pattern, ignore) {
const files = await (0, glob_1.glob)(pattern, {
cwd: options.directory,
// dot:true so files like .cursor/mcp.json or .env.local are reachable;
// we still ignore noisy hidden dirs (.git, .next, etc.) explicitly.
dot: true,
ignore: [
'.git/**',
'.next/**',
'.svelte-kit/**',
'.turbo/**',
'.cache/**',
'.parcel-cache/**',
'.nuxt/**',
'.output/**',
'.vercel/**',
'.netlify/**',
// Build / coverage artifacts — HTML, .js.map, compiled output. These
// aren't source and produce massive A11Y / noise if scanned.
'coverage/**',
'dist/**',
'build/**',
'out/**',
'.ubon/**',
...ignore,
...(options.exclude || [])
]
});
const maxSize = options.maxFileSize || file_source_cache_1.DEFAULT_MAX_FILE_SIZE;
const cache = file_source_cache_1.FileSourceCache.forDirectory(options.directory);
for (const file of files) {
const absolute = (0, path_1.join)(options.directory, file);
try {
const stat = (0, fs_1.statSync)(absolute);
if (stat.size > maxSize) {
if (options.verbose) {
console.error(`🪷 ${this.name}: skipping ${file} (size ${stat.size} > ${maxSize} bytes)`);
}
continue;
}
const content = cache.read(absolute) ?? (0, fs_1.readFileSync)(absolute, 'utf-8');
const contentHash = result_cache_1.ResultCache.hashContent(content);
yield { file, content, lines: content.split('\n'), contentHash };
}
catch (error) {
if (options.verbose) {
console.error(`🪷 ${this.name}: failed to read ${file}:`, error);
}
}
}
}
hasFileSuppression(lines) {
return lines.some((line) => /ubon-disable-file/.test(line));
}
isSuppressed(lines, lineIndex, ruleId) {
const disableNext = /ubon-disable-next-line\s+([A-Z0-9_,\s-]+)/.exec(lines[lineIndex] || '');
const prevDisable = lineIndex > 0 ? /ubon-disable-next-line\s+([A-Z0-9_,\s-]+)/.exec(lines[lineIndex - 1]) : null;
const disabledList = new Set([
...(disableNext && disableNext[1] ? disableNext[1].split(/[,\s]+/).filter(Boolean) : []),
...(prevDisable && prevDisable[1] ? prevDisable[1].split(/[,\s]+/).filter(Boolean) : [])
]);
return disabledList.has(ruleId);
}
createResult(partial, lineText) {
const range = partial.range || (partial.line
? {
startLine: partial.line,
startColumn: 1,
endLine: partial.line,
endColumn: Math.max(1, (lineText || '').length)
}
: undefined);
return {
...partial,
range
};
}
}
exports.BaseScanner = BaseScanner;
//# sourceMappingURL=base-scanner.js.map