UNPKG

ubon

Version:

Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.

90 lines (89 loc) 3.82 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LinkScanner = void 0; const rules_1 = require("../types/rules"); const glob_1 = require("glob"); const fs_1 = require("fs"); const https_1 = __importDefault(require("https")); const http_1 = __importDefault(require("http")); class LinkScanner { constructor() { this.name = 'Broken Links Scanner'; } async scan(options) { const results = []; // Find links in source files and check reachability const files = await (0, glob_1.glob)('**/*.{md,mdx,js,jsx,ts,tsx,html}', { cwd: options.directory, ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', 'examples/**'] }); const urlRegex = /(https?:\/\/[^\s)"'<>]+)/g; const uniqueUrls = new Map(); for (const file of files) { let content = ''; try { content = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8'); } catch { } const lines = content.split('\n'); lines.forEach((line, idx) => { let m; const re = new RegExp(urlRegex); while ((m = re.exec(line)) !== null) { const url = m[1]; const arr = uniqueUrls.get(url) || []; arr.push({ file, line: idx + 1 }); uniqueUrls.set(url, arr); } }); } const checkUrl = (url) => { return new Promise(resolve => { const lib = url.startsWith('https') ? https_1.default : http_1.default; const req = lib.request(url, { method: 'HEAD', timeout: 7000 }, (res) => { resolve(res.statusCode || 0); res.resume(); }); req.on('timeout', () => { req.destroy(new Error('timeout')); }); req.on('error', () => resolve(0)); req.end(); }); }; const entries = Array.from(uniqueUrls.entries()); const concurrency = 8; for (let i = 0; i < entries.length; i += concurrency) { const batch = entries.slice(i, i + concurrency); const statuses = await Promise.all(batch.map(([u]) => checkUrl(u))); for (let j = 0; j < batch.length; j++) { const [url, locs] = batch[j]; const status = statuses[j]; if (status === 0 || status >= 400) { const meta = rules_1.RULES.LINK002; for (const loc of locs) { results.push({ type: meta.severity === 'high' ? 'error' : 'warning', category: meta.category, message: `${meta.message}: ${url} (${status || 'timeout'})`, file: loc.file, line: loc.line, severity: meta.severity, ruleId: meta.id, confidence: 0.8, fix: meta.fix }); } } } } if (results.length === 0) { // keep the informative suggestion about puppeteer for internal crawling const meta = rules_1.RULES.LINK001; results.push({ type: 'info', category: meta.category, message: meta.message, ruleId: meta.id, severity: meta.severity, fix: meta.fix, confidence: 0.6 }); } return results; } } exports.LinkScanner = LinkScanner;