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
JavaScript
;
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;