ubon
Version:
Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.
684 lines (683 loc) • 37.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecurityScanner = void 0;
const glob_1 = require("glob");
const fs_1 = require("fs");
const rules_1 = require("../types/rules");
const entropy_1 = require("../utils/entropy");
class SecurityScanner {
constructor() {
this.name = 'Security Scanner';
this.patterns = [
// API Keys and Secrets
{
ruleId: 'SEC001',
confidence: 0.9,
pattern: /(['"`])(?:sk-|pk_test_|pk_live_|rk_live_|rk_test_).+?\1/gi,
message: 'Potential API key or secret token exposed',
severity: 'high',
fix: 'Move sensitive keys to environment variables'
},
// Supabase specific patterns
{
ruleId: 'SEC002',
confidence: 0.8,
pattern: /(['"`])https:\/\/[a-zA-Z0-9]+\.supabase\.co\1/gi,
message: 'Supabase URL hardcoded (should use env var)',
severity: 'medium',
fix: 'Use NEXT_PUBLIC_SUPABASE_URL environment variable'
},
{
ruleId: 'SEC003',
confidence: 0.95,
pattern: /(['"`])eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\1/gi,
message: 'Supabase anon key hardcoded (JWT token pattern)',
severity: 'high',
fix: 'Use NEXT_PUBLIC_SUPABASE_ANON_KEY environment variable'
},
{
ruleId: 'SEC004',
confidence: 0.8,
pattern: /supabaseUrl\s*[:=]\s*['"`][^'"`]+['"`]/gi,
message: 'Supabase URL hardcoded in variable',
severity: 'medium',
fix: 'Use process.env.NEXT_PUBLIC_SUPABASE_URL'
},
{
ruleId: 'SEC005',
confidence: 0.9,
pattern: /supabaseKey\s*[:=]\s*['"`][^'"`]+['"`]/gi,
message: 'Supabase key hardcoded in variable',
severity: 'high',
fix: 'Use process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY'
},
// Database credentials
{
ruleId: 'SEC006',
confidence: 0.85,
pattern: /password\s*[:=]\s*['"`][^'"`]+['"`]/gi,
message: 'Hardcoded password detected',
severity: 'high',
fix: 'Use environment variables for passwords'
},
{
ruleId: 'SEC007',
confidence: 0.85,
pattern: /DATABASE_URL\s*[:=]\s*['"`][^'"`]+['"`]/gi,
message: 'Database URL hardcoded',
severity: 'high',
fix: 'Use environment variable for database connection'
},
// Environment variable fallbacks
{
ruleId: 'SEC008',
confidence: 0.75,
pattern: /process\.env\.\w+\s*\|\|\s*['"`][^'"`]+['"`]/gi,
message: 'Environment variable with hardcoded fallback',
severity: 'medium',
fix: 'Remove hardcoded fallback values'
},
// Common service keys
{
ruleId: 'SEC009',
confidence: 0.95,
pattern: /(['"`])(?:AKIA[0-9A-Z]{16})\1/gi,
message: 'AWS Access Key ID exposed',
severity: 'high',
fix: 'Move AWS credentials to environment variables'
},
{
ruleId: 'SEC010',
confidence: 0.9,
pattern: /(['"`])(?:ya29\.|1\/\/[0-9A-Za-z_-]+)\1/gi,
message: 'Google OAuth token exposed',
severity: 'high',
fix: 'Use secure token storage'
},
{
ruleId: 'SEC011',
confidence: 0.95,
pattern: /(['"`])(?:gh[pousr]_[A-Za-z0-9_]{36,})\1/gi,
message: 'GitHub token exposed',
severity: 'high',
fix: 'Use environment variables for GitHub tokens'
},
// Stripe keys
{
ruleId: 'SEC012',
confidence: 0.95,
pattern: /(['"`])(?:sk_live_[a-zA-Z0-9]{99})\1/gi,
message: 'Stripe live secret key exposed',
severity: 'high',
fix: 'CRITICAL: Move Stripe live keys to secure environment'
},
{
ruleId: 'SEC013',
confidence: 0.9,
pattern: /(['"`])(?:pk_live_[a-zA-Z0-9]{99})\1/gi,
message: 'Stripe live publishable key exposed',
severity: 'medium',
fix: 'Use environment variable for Stripe keys'
},
// OpenAI / AI service keys
{
ruleId: 'SEC014',
confidence: 0.95,
pattern: /(['"`])(?:sk-[a-zA-Z0-9]{48})\1/gi,
message: 'OpenAI API key exposed',
severity: 'high',
fix: 'Use OPENAI_API_KEY environment variable'
},
// Console logging
{
ruleId: 'SEC015',
confidence: 0.6,
pattern: /console\.(log|debug|info|warn|error)\(/gi,
message: 'Console statement found (may leak sensitive info)',
severity: 'low',
fix: 'Remove console statements before production'
},
// Security risks
{
ruleId: 'SEC016',
confidence: 0.9,
pattern: /eval\s*\(/gi,
message: 'Use of eval() detected (security risk)',
severity: 'high',
fix: 'Replace eval() with safer alternatives'
},
{
ruleId: 'SEC017',
confidence: 0.8,
pattern: /dangerouslySetInnerHTML/gi,
message: 'dangerouslySetInnerHTML usage (XSS risk)',
severity: 'medium',
fix: 'Sanitize HTML content or use safer alternatives'
}
];
}
async scan(options) {
const results = [];
const files = await (0, glob_1.glob)('**/*.{js,jsx,ts,tsx,vue,env}', {
cwd: options.directory,
ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', 'examples/**']
});
let processed = 0;
// Project-level Next.js structure presence flags (for P5 experimental rules)
const hasAppDir = files.some(f => /^app\//.test(f));
const hasPagesDir = files.some(f => /^pages\//.test(f));
const hasNotFoundApp = files.some(f => /^app\/not-found\.(js|jsx|ts|tsx)$/.test(f));
const has404Pages = files.some(f => /^pages\/404\.(js|jsx|ts|tsx)$/.test(f));
const hasErrorApp = files.some(f => /^app\/error\.(js|jsx|ts|tsx)$/.test(f));
const hasErrorPages = files.some(f => /^pages\/_error\.(js|jsx|ts|tsx)$/.test(f));
const hasDocumentPages = files.some(f => /^pages\/_document\.(js|jsx|ts|tsx)$/.test(f));
// Emit-once sentinels
let emittedP5_404 = false;
let emittedP5_error = false;
let emittedP5_document = false;
for (const file of files) {
try {
const content = (0, fs_1.readFileSync)(`${options.directory}/${file}`, 'utf-8');
const lines = content.split('\n');
processed++;
if (options.verbose && processed % 25 === 0) {
// Periodic progress indicator for large repos
console.log('🪷', `Scanning... (${processed}/${files.length} files)`);
}
const isAppRouter = /(^|\/)app\//.test(file) || hasAppDir;
let ubonDisableAll = false;
lines.forEach((line, index) => {
// Inline suppressions
if (/ubon-disable-file/.test(line)) {
ubonDisableAll = true;
}
if (ubonDisableAll)
return;
const disableNext = /ubon-disable-next-line\s+([A-Z0-9_,\s-]+)/.exec(line);
const prevDisable = index > 0 ? /ubon-disable-next-line\s+([A-Z0-9_,\s-]+)/.exec(lines[index - 1]) : null;
this.patterns.forEach((patternDef, patternIndex) => {
const { pattern, message, severity, fix } = patternDef;
const defaultConfidenceBySeverity = {
high: 0.9,
medium: 0.8,
low: 0.6
};
const ruleId = patternDef.ruleId || `SEC${String(patternIndex + 1).padStart(3, '0')}`;
const sev = severity;
const confidence = patternDef.confidence ?? defaultConfidenceBySeverity[sev];
// Skip lines that are comments or inside pattern definitions
if (line.trim().startsWith('//') ||
line.trim().startsWith('*') ||
line.includes('pattern:') ||
line.includes('message:') ||
line.includes('severity:') ||
line.includes('fix:') ||
file.includes('security-scanner.ts')) {
return;
}
// Skip console logs in logger files (intentional)
if (message.includes('Console statement') && file.includes('logger')) {
return;
}
const m = line.match(pattern);
if (m) {
const disabledList = new Set([
...(disableNext && disableNext[1] ? disableNext[1].split(/[,\s]+/).filter(Boolean) : []),
...(prevDisable && prevDisable[1] ? prevDisable[1].split(/[,\s]+/).filter(Boolean) : [])
]);
if (disabledList.has(ruleId)) {
return;
}
const fixEdits = [];
if (ruleId === 'SEC008') {
// Remove hardcoded fallback: process.env.X || 'fallback' -> process.env.X
const replacement = line.replace(/(process\.env\.\w+)\s*\|\|\s*['"][^'"`]+['"]/g, '$1');
if (replacement !== line) {
fixEdits.push({
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement
});
}
}
if (ruleId === 'SEC015') {
fixEdits.push({
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement: ''
});
}
results.push({
type: severity === 'high' ? 'error' : 'warning',
category: 'security',
message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
match: m[0]?.slice(0, 200),
severity,
ruleId,
confidence,
fix,
...(fixEdits.length ? { fixEdits } : {})
});
}
});
});
// Project-level Next.js structure checks (emit once per project)
if (/^(pages|app)\//.test(file)) {
// Missing not-found/404
if (!emittedP5_404) {
if (hasAppDir && !hasNotFoundApp) {
const meta = rules_1.RULES.NEXT201;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.5, fix: meta.fix });
emittedP5_404 = true;
}
else if (hasPagesDir && !has404Pages) {
const meta = rules_1.RULES.NEXT201;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.5, fix: meta.fix });
emittedP5_404 = true;
}
}
// Missing error boundary
if (!emittedP5_error) {
if (hasAppDir && !hasErrorApp) {
const meta = rules_1.RULES.NEXT202;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.5, fix: meta.fix });
emittedP5_error = true;
}
else if (hasPagesDir && !hasErrorPages) {
const meta = rules_1.RULES.NEXT202;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.5, fix: meta.fix });
emittedP5_error = true;
}
}
// _document for Pages Router only when code hints custom head/script usage
if (!emittedP5_document && hasPagesDir && /from\s+['"]next\/(head|script)['"]/.test(content) && !hasDocumentPages) {
const meta = rules_1.RULES.NEXT203;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.5, fix: meta.fix });
emittedP5_document = true;
}
}
// JS HTTP timeout/retry policy checks
lines.forEach((line, index) => {
// axios without timeout
if (/axios\.(get|post|put|delete|patch)\(/i.test(line) && !/timeout\s*:/i.test(line)) {
const meta = rules_1.RULES.JSNET001;
results.push({
type: meta.severity === 'high' ? 'error' : 'warning',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
match: line.slice(0, 200),
confidence: 0.7,
fix: meta.fix
});
}
// fetch without AbortController/timeout wrappers (heuristic)
if (/\bfetch\s*\(/.test(line) && !/AbortController|signal\s*:/.test(line)) {
const meta = rules_1.RULES.JSNET001;
const suggested = line.replace(/fetch\(([^)]*)\)/, 'fetch($1, { signal })');
const fixEdits = [{
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement: suggested
}];
results.push({
type: meta.severity === 'high' ? 'error' : 'warning',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
match: line.slice(0, 200),
confidence: 0.6,
fix: 'Use AbortController with a timeout to cancel long fetches',
fixEdits
});
}
// Set-Cookie missing attributes (with fix edits)
if (/setHeader\(\s*['"][Ss]et-[Cc]ookie['"],\s*['"][^'"]+['"]\s*\)/.test(line) || /Set-Cookie:/i.test(line)) {
const cookieStrMatch = line.match(/Set-Cookie:\s*([^;]+(?:;[^;]+)*)/i);
const cookieStr = cookieStrMatch ? cookieStrMatch[1] : line;
const hasHttpOnly = /HttpOnly/i.test(cookieStr);
const hasSecure = /Secure/i.test(cookieStr);
const hasSameSite = /SameSite/i.test(cookieStr);
if (!(hasHttpOnly && hasSecure && hasSameSite)) {
const meta = rules_1.RULES.COOKIE001;
const needed = `${hasHttpOnly ? '' : '; HttpOnly'}${hasSecure ? '' : '; Secure'}${hasSameSite ? '' : '; SameSite=Lax'}`;
const fixedLine = line.replace(/(['"])\s*\)\s*;?$/, `${needed}$1)`);
const fixEdits = [{
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement: fixedLine
}];
results.push({
type: meta.severity === 'high' ? 'error' : 'warning',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
match: line.slice(0, 200),
confidence: 0.8,
fix: meta.fix,
fixEdits
});
}
}
});
// Entropy-based secret detection (context-aware, reduced noise)
lines.forEach((line, index) => {
const toks = (0, entropy_1.extractQuotedLiterals)(line).filter(s => s.length >= 16);
for (const tok of toks) {
const ent = (0, entropy_1.shannonEntropy)(tok);
if (ent < 3.8 || !/[A-Za-z0-9]/.test(tok))
continue;
// File/context-based ignores: CSS/Tailwind, configs, globs
const lowerFile = file.toLowerCase();
const isCssContext = lowerFile.endsWith('.css') || lowerFile.endsWith('.scss') || lowerFile.endsWith('.sass') || lowerFile.endsWith('.less') || lowerFile.includes('tailwind.config');
if (isCssContext)
continue;
// Token-based ignores: hex colors, tailwind classes, data URIs, globs, UUIDs
const isHexColor = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(tok);
const isTailwind = /(bg|text|border|from|to|via)-[a-zA-Z]+-\d{2,3}/.test(tok);
const isDataUri = /^data:image\//.test(tok);
const isGlobLike = /\*\*?|\{.*\}|\*\.[a-zA-Z0-9]+/.test(tok);
const isUuid = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/.test(tok);
if (isHexColor || isTailwind || isDataUri || isGlobLike || isUuid)
continue;
// Suspicious indicators increase confidence
const looksLikeSecret = /\b(sk-|pk_live_|rk_(live|test)_|eyJ[A-Za-z0-9._-]{10,}|AKIA[0-9A-Z]{16}|password=|secret=|api_key=|token=|postgres(ql)?:\/\/|mongodb:\/\/)/.test(tok);
const isDotEnvFile = /(^|\/)\.env(\.|$)/.test(lowerFile);
if (!looksLikeSecret && !isDotEnvFile)
continue;
const meta = rules_1.RULES.SEC018;
results.push({
type: meta.severity === 'high' ? 'error' : 'warning',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
match: tok.slice(0, 200),
confidence: looksLikeSecret ? 0.9 : 0.8,
fix: meta.fix
});
}
});
// Secret logging detection (console + secret-like token)
lines.forEach((line, index) => {
if (/console\.(log|debug|info|warn|error)\(/.test(line) && /(sk-[A-Za-z0-9_-]{8,}|eyJ[A-Za-z0-9._-]{20,}|AKIA[0-9A-Z]{16})/.test(line)) {
const meta = rules_1.RULES.LOG001;
const fixEdits = [];
// Replace logged secret literal with a redacted placeholder
const redacted = line
.replace(/sk-[A-Za-z0-9_-]{8,}/g, 'sk-********')
.replace(/eyJ[A-Za-z0-9._-]{20,}/g, 'eyJ********')
.replace(/AKIA[0-9A-Z]{16}/g, 'AKIA**************');
fixEdits.push({
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement: redacted
});
results.push({
type: 'warning',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.8,
fix: meta.fix,
fixEdits
});
}
});
// Vue v-html binding (XSS risk)
if (file.endsWith('.vue')) {
lines.forEach((line, index) => {
if (/v-html\s*=\s*\"|v-html\s*=\s*\'/.test(line)) {
const meta = rules_1.RULES.VUE001;
results.push({
type: 'error',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.9,
fix: meta.fix
});
}
});
}
// Next.js SSR secrets heuristic
if (/get(ServerSideProps|StaticProps)\s*\(/.test(content) && /process\.env\./.test(content)) {
const meta = rules_1.RULES.NEXT006;
results.push({
type: 'error',
category: meta.category,
message: meta.message,
file,
severity: meta.severity,
ruleId: meta.id,
confidence: 0.6,
fix: meta.fix
});
}
// Next.js API route basic input validation heuristic
if (/\b(pages|app)\/api\//.test(file)) {
const hasValidatorImport = /from\s+['\"](zod|yup|ajv|valibot|superstruct|class-validator)['\"]/.test(content) || /import\s+\{?\s*(z|yup|Ajv|object|safeParse)/.test(content);
let usesRequestParams = false;
for (let i = 0; i < lines.length; i++) {
const l = lines[i];
if (/(req|request)\.(body|query|params)/.test(l)) {
usesRequestParams = true;
break;
}
}
if (usesRequestParams && !hasValidatorImport) {
const meta = rules_1.RULES.NEXT003;
results.push({
type: 'warning',
category: meta.category,
message: meta.message,
file,
severity: meta.severity,
ruleId: meta.id,
confidence: 0.6,
fix: meta.fix
});
}
}
// Next.js specific JWT/Cookie security checks
if (/\b(pages|app)\/api\//.test(file) || file.includes('middleware.ts')) {
lines.forEach((line, index) => {
// Check for JWT tokens in JSON responses (NEXT007)
if (/res\.json\s*\(\s*\{[^}]*\b(token|jwt|accessToken|authToken)\s*:/.test(line)) {
const meta = rules_1.RULES.NEXT007;
results.push({
type: 'error',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.9,
fix: meta.fix
});
}
// Check for unsafe redirects (NEXT009)
if (/res\.redirect\s*\(\s*(req\.query\.|req\.body\.|req\.params\.)/.test(line)) {
const meta = rules_1.RULES.NEXT009;
results.push({
type: 'error',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.8,
fix: meta.fix
});
}
// Check for permissive CORS (NEXT010)
if (/Access-Control-Allow-Origin.*\*/.test(line) || /cors\s*\(\s*\{\s*origin:\s*['"`]\*['"`]/.test(line)) {
const meta = rules_1.RULES.NEXT010;
results.push({
type: 'warning',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.7,
fix: meta.fix
});
}
// Check for environment variables leaked to client (NEXT011)
if (/process\.env\.(?!NEXT_PUBLIC_)\w+/.test(line) && (file.includes('components/') || file.includes('pages/') && !file.includes('/api/') || file.includes('app/') && !file.includes('/api/'))) {
const meta = rules_1.RULES.NEXT011;
results.push({
type: 'error',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.8,
fix: meta.fix
});
}
});
// Check for missing security headers (NEXT008)
const hasSecurityHeaders = /X-Content-Type-Options|X-Frame-Options|X-XSS-Protection|Strict-Transport-Security/.test(content);
const setsHeaders = /res\.setHeader|headers\s*:\s*\{/.test(content);
if (setsHeaders && !hasSecurityHeaders && !file.includes('_app.') && !file.includes('middleware.')) {
const meta = rules_1.RULES.NEXT008;
results.push({
type: 'warning',
category: meta.category,
message: meta.message,
file,
severity: meta.severity,
ruleId: meta.id,
confidence: 0.6,
fix: meta.fix
});
}
}
// API route structure: missing method validation (NEXT209) and unauthenticated sensitive responses (NEXT205)
if (/\b(pages|app)\/api\//.test(file)) {
const isPagesAPI = /\bpages\/api\//.test(file);
const isAppRoute = /\bapp\/api\//.test(file);
if (isPagesAPI) {
const mentionsReqMethodCheck = /req\.method/.test(content) && /(GET|POST|PUT|DELETE|PATCH)/.test(content);
if (!mentionsReqMethodCheck) {
const meta = rules_1.RULES.NEXT209;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.6, fix: meta.fix });
}
}
if (isAppRoute) {
const hasMethodExports = /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/.test(content);
if (!hasMethodExports) {
const meta = rules_1.RULES.NEXT209;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.6, fix: meta.fix });
}
}
// Unauthenticated API access heuristic: returns user/secrets without typical auth artifacts
const returnsSensitive = /res\.(json|send)\(\s*\{[^}]*\b(user|email|token|secret|apiKey)\b/.test(content);
const hasAuthSignals = /(getServerSession|next-auth|Authorization|getToken|cookies\(|jwt)/.test(content);
if (returnsSensitive && !hasAuthSignals) {
const meta = rules_1.RULES.NEXT205;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, severity: meta.severity, ruleId: meta.id, confidence: 0.6, fix: meta.fix });
}
}
// Navigation: router.push with external URL (NEXT208)
if (/useRouter\(\)/.test(content) || /from\s+['"]next\/navigation['"]/.test(content)) {
lines.forEach((line, index) => {
if (/router\.push\(\s*['"][a-z]+:\/\//i.test(line)) {
const meta = rules_1.RULES.NEXT208;
results.push({ type: 'warning', category: meta.category, message: meta.message, file, line: index + 1, range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) }, severity: meta.severity, ruleId: meta.id, confidence: 0.7, fix: meta.fix });
}
});
}
// Check for insecure JWT cookies (COOKIE002) and propose fix edits
lines.forEach((line, index) => {
const isJwtCookie = /(Set-Cookie|setHeader\(\s*['"][Ss]et-[Cc]ookie['"])|setCookie\s*\(/.test(line) && /(jwt|token)=/i.test(line);
const missingHttpOnly = !/HttpOnly/i.test(line);
const missingSecure = !/Secure/i.test(line);
if (isJwtCookie && (missingHttpOnly || missingSecure)) {
const meta = rules_1.RULES.COOKIE002;
const addition = `${missingHttpOnly ? '; HttpOnly' : ''}${missingSecure ? '; Secure' : ''}`;
const fixed = line.replace(/(['"])\s*\)\s*;?$/, `${addition}$1)`);
const fixEdits = [{
file,
startLine: index + 1,
startColumn: 1,
endLine: index + 1,
endColumn: Math.max(1, line.length),
replacement: fixed
}];
results.push({
type: 'error',
category: meta.category,
message: meta.message,
file,
line: index + 1,
range: { startLine: index + 1, startColumn: 1, endLine: index + 1, endColumn: Math.max(1, line.length) },
severity: meta.severity,
ruleId: meta.id,
confidence: 0.9,
fix: meta.fix,
fixEdits
});
}
});
}
catch (error) {
// Skip files that can't be read
}
}
return results;
}
}
exports.SecurityScanner = SecurityScanner;