ubon
Version:
Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.
132 lines (131 loc) • 5.21 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OSVScanner = void 0;
const fs_1 = require("fs");
const https_1 = __importDefault(require("https"));
const cache_1 = require("../utils/cache");
function postJson(url, payload) {
return new Promise((resolve, reject) => {
const { hostname, pathname } = new URL(url);
const data = JSON.stringify(payload);
const req = https_1.default.request({ hostname, path: pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } }, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(body));
}
catch (e) {
resolve({});
}
}
else {
reject(new Error(`OSV ${res.statusCode}`));
}
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
class OSVScanner {
constructor() {
this.name = 'Dependency Advisory Scanner';
this.cache = new cache_1.FileCache('osv');
}
async scan(options) {
const results = [];
// Clear cache if requested
if (options.clearCache) {
this.cache.clear();
}
const npmPath = `${options.directory}/package.json`;
const pyPath = `${options.directory}/requirements.txt`;
const npmDeps = [];
if ((0, fs_1.existsSync)(npmPath)) {
try {
const pkg = JSON.parse((0, fs_1.readFileSync)(npmPath, 'utf-8'));
const all = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
for (const [name, ver] of Object.entries(all)) {
const clean = String(ver).replace(/^[^0-9]*/, '');
if (clean)
npmDeps.push({ name, version: clean });
}
}
catch { }
}
const pyDeps = [];
if ((0, fs_1.existsSync)(pyPath)) {
try {
const text = (0, fs_1.readFileSync)(pyPath, 'utf-8');
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#'))
continue;
const [nameVer] = trimmed.split(/\s+/);
const [name, version] = nameVer.split('==');
if (name)
pyDeps.push({ name, version });
}
}
catch { }
}
const queries = [];
for (const d of npmDeps) {
queries.push({ package: { ecosystem: 'npm', name: d.name }, version: d.version });
}
for (const d of pyDeps) {
queries.push({ package: { ecosystem: 'PyPI', name: d.name }, version: d.version });
}
if (queries.length === 0)
return results;
// Create cache key for this set of queries
const cacheKey = (0, cache_1.createOSVCacheKey)(queries);
// Try to get cached results first (unless caching is disabled)
let data = options.noCache ? null : this.cache.get(cacheKey);
if (!data) {
// No cached data, make API call
try {
data = await postJson('https://api.osv.dev/v1/querybatch', { queries });
// Cache the results for 24 hours (unless caching is disabled)
if (!options.noCache) {
this.cache.set(cacheKey, data, cache_1.CACHE_TTL.OSV_VULNERABILITIES);
}
}
catch (error) {
// API call failed, return empty results
return results;
}
}
try {
const vulns = data.results || [];
vulns.forEach((entry, idx) => {
const q = queries[idx];
const pkgName = q.package.name;
const eco = q.package.ecosystem;
if (entry.vulns && entry.vulns.length) {
for (const v of entry.vulns) {
results.push({
type: 'error',
category: 'security',
message: `Vulnerability in ${eco}:${pkgName} (${v.id || v.aliases?.[0] || 'UNKNOWN'})`,
severity: 'high',
ruleId: 'OSV001',
fix: v.summary || 'Update to a patched version',
confidence: 0.9
});
}
}
});
}
catch { }
return results;
}
}
exports.OSVScanner = OSVScanner;