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.

176 lines (175 loc) 9.05 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RailsSecurityScanner = void 0; const glob_1 = require("glob"); const fs_1 = require("fs"); class RailsSecurityScanner { constructor() { this.name = 'Rails Security Scanner'; } async scan(options) { const results = []; const files = await (0, glob_1.glob)('**/*.{rb,erb}', { cwd: options.directory, ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', 'vendor/**'] }); for (const file of files) { try { const content = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8'); const lines = content.split('\n'); lines.forEach((line, index) => { // SQL injection: string interpolation in where if (line.includes('.where(') && line.includes('#{')) { results.push({ type: 'error', category: 'security', message: 'Potential SQL injection via string interpolation in where()', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'high', ruleId: 'RAILS001', confidence: 0.9, fix: 'Use parameterized queries: where("name = ?", params[:name])' }); } // SQL injection: find_by_sql with interpolation or concatenation if ((line.includes('.find_by_sql(') && line.includes('#{')) || /\.find_by_sql\(.*\+\s*params\[/.test(line)) { results.push({ type: 'error', category: 'security', message: 'Potential SQL injection in find_by_sql()', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'high', ruleId: 'RAILS001', confidence: 0.85, fix: 'Use sanitized SQL or parameterization' }); } // Command injection via system/backticks if (/(system\s*\(|`.+`)/.test(line)) { results.push({ type: 'error', category: 'security', message: 'Potential command injection (system/backticks)', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'high', ruleId: 'RAILS002', confidence: 0.8, fix: 'Avoid shell execution or sanitize inputs thoroughly' }); } // YAML.load without safe_load if (/YAML\.load\s*\(/.test(line)) { results.push({ type: 'warning', category: 'security', message: 'YAML.load is unsafe; use YAML.safe_load', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'medium', ruleId: 'RAILS003', confidence: 0.8, fix: 'Use YAML.safe_load with permitted classes' }); } // ERB html_safe XSS risk if (/<%=\s*.*\.html_safe\s*%>/.test(line)) { results.push({ type: 'warning', category: 'security', message: 'html_safe used in ERB (XSS risk)', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'medium', ruleId: 'RAILS004', confidence: 0.7, fix: 'Avoid html_safe; ensure content is sanitized' }); } // ERB raw output <%== %> if (/<%==\s*.*%>/.test(line)) { results.push({ type: 'warning', category: 'security', message: 'Raw ERB output (<%==) can lead to XSS', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'medium', ruleId: 'RAILS004', confidence: 0.7, fix: 'Use escaped output <%= %> and sanitize' }); } // Mass assignment if (/\b(create|update)\(\s*params\[[^\]]+\]\s*\)/.test(line)) { results.push({ type: 'warning', category: 'security', message: 'Potential mass assignment from params', file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: 'medium', ruleId: 'RAILS005', confidence: 0.7, fix: 'Use strong parameters (params.require(...).permit(...))' }); } }); // Whole-file SQLi checks (multi-line resilience) const sqlInterpPatterns = [ { re: /\.where\([\s\S]*?#\{[\s\S]*?\)/g, ruleId: 'RAILS001', msg: 'Potential SQL injection via string interpolation in where()' }, { re: /\.find_by_sql\([\s\S]*?#\{[\s\S]*?\)/g, ruleId: 'RAILS001', msg: 'Potential SQL injection in find_by_sql()' } ]; for (const pat of sqlInterpPatterns) { let m; while ((m = pat.re.exec(content)) !== null) { const pos = m.index; const before = content.slice(0, pos); const lineNum = before.split('\n').length; results.push({ type: 'error', category: 'security', message: pat.msg, file, line: lineNum, range: { startLine: lineNum, startColumn: 1, endLine: lineNum, endColumn: 200 }, severity: 'high', ruleId: 'RAILS001', confidence: 0.85, fix: 'Use parameterized queries instead of string interpolation' }); } } // Broad heuristic: same-file presence of interpolation and where/find_by_sql if ((content.includes('.where(') && content.includes('#{')) || (content.includes('find_by_sql(') && content.includes('#{'))) { results.push({ type: 'error', category: 'security', message: 'Potential SQL injection via string interpolation in SQL', file, line: 1, range: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1 }, severity: 'high', ruleId: 'RAILS001', confidence: 0.7, fix: 'Use parameterized queries' }); } } catch { // skip unreadable files } } return results; } } exports.RailsSecurityScanner = RailsSecurityScanner;