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.

111 lines (110 loc) 5.51 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AstSecurityScanner = void 0; const glob_1 = require("glob"); const fs_1 = require("fs"); const rules_1 = require("../types/rules"); class AstSecurityScanner { constructor() { this.name = 'AST Security Scanner'; } async scan(options) { // Lazily require TypeScript at runtime; if unavailable (global install without dev deps), skip AST checks gracefully let tsReal; try { // eslint-disable-next-line @typescript-eslint/no-var-requires tsReal = require('typescript'); } catch { return []; } const results = []; const files = await (0, glob_1.glob)('**/*.{js,jsx,ts,tsx}', { cwd: options.directory, ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', 'examples/**'] }); for (const file of files) { let sourceText = ''; try { sourceText = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8'); } catch { continue; } const sf = tsReal.createSourceFile(file, sourceText, tsReal.ScriptTarget.ES2020, true, file.endsWith('.tsx') ? tsReal.ScriptKind.TSX : file.endsWith('.ts') ? tsReal.ScriptKind.TS : tsReal.ScriptKind.JS); const add = (metaKey, node, message) => { const meta = rules_1.RULES[metaKey]; const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart()); const lineStart = sf.getPositionOfLineAndCharacter(line, 0); const lineEnd = sf.getLineEndOfPosition(node.getStart()); const lineText = sourceText.slice(lineStart, lineEnd).trim(); results.push({ type: meta.severity === 'high' ? 'error' : 'warning', category: meta.category, message: message || meta.message, file, line: line + 1, range: { startLine: line + 1, startColumn: character + 1, endLine: line + 1, endColumn: character + 1 + Math.max(1, node.getWidth()) }, severity: meta.severity, ruleId: meta.id, match: lineText.slice(0, 200), confidence: 0.9, fix: meta.fix }); }; const visit = (node) => { // eval(...) if (tsReal.isCallExpression(node)) { const expr = node.expression; if (tsReal.isIdentifier(expr) && expr.text === 'eval') { add('SEC016', node); } // React.createElement(userInput) if (tsReal.isPropertyAccessExpression(expr)) { if (tsReal.isIdentifier(expr.expression) && expr.expression.text === 'React' && expr.name.text === 'createElement') { const first = node.arguments[0]; if (first && !tsReal.isStringLiteralLike(first)) { add('SEC019', node); } } } // dynamic import(userControlled) if (tsReal.isCallExpression(node) && node.expression.kind === tsReal.SyntaxKind.ImportKeyword) { const arg = node.arguments[0]; if (arg && !tsReal.isStringLiteralLike(arg)) { add('NEXT004', node); } } // fetch(...) if (tsReal.isIdentifier(expr) && expr.text === 'fetch') { const second = node.arguments[1]; let hasSignal = false; if (second && tsReal.isObjectLiteralExpression(second)) { hasSignal = second.properties.some(p => tsReal.isPropertyAssignment(p) && tsReal.isIdentifier(p.name) && p.name.text === 'signal'); } if (!hasSignal) add('JSNET001', node, rules_1.RULES.JSNET001.message); } } // dangerouslySetInnerHTML if (tsReal.isIdentifier(node) && node.text === 'dangerouslySetInnerHTML') { add('SEC017', node); } // process.env.X || 'fallback' if (tsReal.isBinaryExpression(node) && node.operatorToken.kind === tsReal.SyntaxKind.BarBarToken) { const left = node.left; const right = node.right; const isEnv = tsReal.isPropertyAccessExpression(left) && tsReal.isPropertyAccessExpression(left.expression) && tsReal.isIdentifier(left.expression.expression) && left.expression.expression.text === 'process' && tsReal.isIdentifier(left.expression.name) && left.expression.name.text === 'env'; const isLiteral = tsReal.isStringLiteralLike(right); if (isEnv && isLiteral) { add('SEC008', node); } } tsReal.forEachChild(node, visit); }; visit(sf); } return results; } } exports.AstSecurityScanner = AstSecurityScanner;