ubon
Version:
Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.
70 lines (69 loc) • 4.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PythonSecurityScanner = void 0;
const glob_1 = require("glob");
const fs_1 = require("fs");
class PythonSecurityScanner {
constructor() {
this.name = 'Python Security Scanner';
this.patterns = [
{ ruleId: 'PYSEC001', confidence: 0.9, pattern: /(['"])sk-[A-Za-z0-9_-]{16,}\1/g, message: 'Potential API key exposed', severity: 'high', fix: 'Move secrets to environment variables' },
{ ruleId: 'PYSEC002', confidence: 0.9, pattern: /exec\s*\(/g, message: 'Use of exec() detected', severity: 'high', fix: 'Avoid exec(); use safer alternatives' },
{ ruleId: 'PYSEC003', confidence: 0.9, pattern: /eval\s*\(/g, message: 'Use of eval() detected', severity: 'high', fix: 'Avoid eval(); use safer alternatives' },
{ ruleId: 'PYSEC004', confidence: 0.85, pattern: /subprocess\.[P\w]+\(.*shell\s*=\s*True/g, message: 'subprocess with shell=True', severity: 'high', fix: 'Avoid shell=True; pass args as list' },
{ ruleId: 'PYSEC005', confidence: 0.85, pattern: /yaml\.load\(/g, message: 'yaml.load() unsafe without Loader', severity: 'medium', fix: 'Use yaml.safe_load()' },
{ ruleId: 'PYSEC006', confidence: 0.8, pattern: /pickle\.(load|loads)\(/g, message: 'Insecure pickle usage', severity: 'medium', fix: 'Avoid pickle with untrusted data' },
{ ruleId: 'PYSEC007', confidence: 0.8, pattern: /requests\.[a-z]+\([^)]*verify\s*=\s*False/g, message: 'TLS verification disabled', severity: 'medium', fix: 'Remove verify=False' },
{ ruleId: 'PYSEC008', confidence: 0.7, pattern: /except\s*:\s*pass/g, message: 'Broad except with pass', severity: 'low', fix: 'Catch specific exceptions and handle properly' },
{ ruleId: 'PYSEC009', confidence: 0.8, pattern: /DEBUG\s*=\s*True/g, message: 'DEBUG=True in settings', severity: 'medium', fix: 'Disable DEBUG in production' },
{ ruleId: 'PYSEC010', confidence: 0.7, pattern: /ALLOWED_HOSTS\s*=\s*\[[^\]]*\*[^\]]*\]/g, message: 'ALLOWED_HOSTS includes *', severity: 'low', fix: 'Restrict ALLOWED_HOSTS' },
{ ruleId: 'PYNET001', confidence: 0.8, pattern: /requests\.[a-z]+\([^)]*\)/g, message: 'requests call without timeout', severity: 'medium', fix: 'Add timeout= to requests.* calls' },
];
}
async scan(options) {
const results = [];
const files = await (0, glob_1.glob)('**/*.py', { cwd: options.directory, ignore: ['.venv/**', 'venv/**', 'node_modules/**', 'dist/**', 'build/**', '.next/**', 'examples/**'] });
for (const file of files) {
try {
const content = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8');
const lines = content.split('\n');
let ubonDisableAll = false;
lines.forEach((line, index) => {
if (/ubon-disable-file/.test(line)) {
ubonDisableAll = true;
}
if (ubonDisableAll)
return;
const disableNext = /ubon-disable-next-line\s+([A-Z0-9_,\s-]+)/.exec(line);
for (const p of this.patterns) {
if (p.ruleId === 'PYNET001' && /timeout\s*=\s*\d+/.test(line)) {
continue;
}
if (p.pattern.test(line)) {
if (disableNext && disableNext[1] && disableNext[1].split(/[,\s]+/).filter(Boolean).includes(p.ruleId)) {
continue;
}
results.push({
type: p.severity === 'high' ? 'error' : 'warning',
category: 'security',
message: p.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: p.severity,
ruleId: p.ruleId,
confidence: p.confidence,
fix: p.fix
});
}
}
});
}
catch (e) {
// ignore
}
}
return results;
}
}
exports.PythonSecurityScanner = PythonSecurityScanner;