UNPKG

vibe-guard

Version:

🛡️ Vibe-Guard Security Scanner - Catch security issues before they catch you!

158 lines 6.93 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MissingSecurityHeadersRule = void 0; const types_1 = require("../types"); class MissingSecurityHeadersRule extends types_1.BaseRule { constructor() { super(...arguments); this.name = 'missing-security-headers'; this.description = 'Detects missing HTTP security headers'; this.severity = 'medium'; this.securityHeaders = [ 'Content-Security-Policy', 'X-Frame-Options', 'X-Content-Type-Options', 'X-XSS-Protection', 'Strict-Transport-Security', 'Referrer-Policy', 'Permissions-Policy', 'X-Permitted-Cross-Domain-Policies' ]; this.serverPatterns = [ // Express.js patterns { pattern: /app\.(?:get|post|put|delete|patch|use)\s*\(/gi, type: 'Express route handler' }, { pattern: /router\.(?:get|post|put|delete|patch|use)\s*\(/gi, type: 'Express router' }, { pattern: /app\.listen\s*\(/gi, type: 'Express server' }, // Next.js API routes { pattern: /export\s+(?:default\s+)?(?:async\s+)?function\s+handler/gi, type: 'Next.js API handler' }, { pattern: /export\s+(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?\([^)]*req[^)]*res[^)]*\)/gi, type: 'Next.js API function' }, // Node.js HTTP server { pattern: /createServer\s*\(\s*(?:async\s+)?\([^)]*req[^)]*res[^)]*\)/gi, type: 'Node.js HTTP server' }, { pattern: /http\.createServer/gi, type: 'HTTP server creation' }, // Framework response patterns { pattern: /res\.(?:send|json|render|redirect)/gi, type: 'Response method' }, { pattern: /response\.(?:send|json|render|redirect)/gi, type: 'Response method' }, // Flask patterns { pattern: /@app\.route/gi, type: 'Flask route' }, { pattern: /return\s+(?:render_template|jsonify|redirect)/gi, type: 'Flask response' }, // Django patterns { pattern: /def\s+\w+\s*\([^)]*request[^)]*\)/gi, type: 'Django view function' }, { pattern: /HttpResponse\s*\(/gi, type: 'Django HTTP response' }, // PHP patterns { pattern: /header\s*\(\s*['"`][^'"`]*['"`]/gi, type: 'PHP header function' } ]; } check(fileContent) { const issues = []; // Only check server/web application files if (!this.isWebApplicationFile(fileContent.path)) { return issues; } // Check if file contains server/route patterns const hasServerCode = this.hasServerCode(fileContent.content); if (!hasServerCode) { return issues; } // Check for missing security headers this.checkMissingHeaders(fileContent, issues); return issues; } isWebApplicationFile(filePath) { const webFiles = [ /\.js$/i, /\.ts$/i, /\.jsx$/i, /\.tsx$/i, /\.py$/i, /\.php$/i, /\.rb$/i, /\.go$/i, /\.java$/i, /\.cs$/i ]; // Skip test files const testPatterns = [ /test/i, /spec/i, /\.test\./i, /\.spec\./i, /__tests__/i ]; if (testPatterns.some(pattern => pattern.test(filePath))) { return false; } return webFiles.some(pattern => pattern.test(filePath)); } hasServerCode(content) { return this.serverPatterns.some(({ pattern }) => pattern.test(content)); } checkMissingHeaders(fileContent, issues) { const content = fileContent.content; const missingHeaders = []; for (const header of this.securityHeaders) { if (!this.hasSecurityHeader(content, header)) { missingHeaders.push(header); } } if (missingHeaders.length > 0) { // Find a good location to report the issue (first route handler or server setup) const location = this.findReportLocation(fileContent); if (location) { issues.push(this.createIssue(fileContent.path, location.line, location.column, location.lineContent, `Missing security headers: ${missingHeaders.join(', ')}`, `Add security headers to protect against common attacks. Consider using helmet.js for Express or implementing headers manually: ${this.getHeaderRecommendations(missingHeaders)}`)); } } } hasSecurityHeader(content, header) { const headerPatterns = [ // Express.js patterns new RegExp(`res\\.(?:set|header)\\s*\\(\\s*['"\`]${header}['"\`]`, 'gi'), new RegExp(`res\\.setHeader\\s*\\(\\s*['"\`]${header}['"\`]`, 'gi'), // Helmet.js patterns /helmet\s*\(\s*\)/gi, /helmet\./gi, // Manual header setting new RegExp(`['"\`]${header}['"\`]\\s*:\\s*['"\`]`, 'gi'), // PHP patterns new RegExp(`header\\s*\\(\\s*['"\`]${header}:`, 'gi'), // Python Flask patterns new RegExp(`response\\.headers\\[['"\`]${header}['"\`]\\]`, 'gi'), // Django patterns new RegExp(`response\\[['"\`]${header}['"\`]\\]`, 'gi') ]; return headerPatterns.some(pattern => pattern.test(content)); } findReportLocation(fileContent) { for (const { pattern } of this.serverPatterns) { const matches = this.findMatches(fileContent.content, pattern); if (matches.length > 0) { const firstMatch = matches[0]; if (firstMatch) { return { line: firstMatch.line, column: firstMatch.column, lineContent: firstMatch.lineContent }; } } } return null; } getHeaderRecommendations(missingHeaders) { const recommendations = []; if (missingHeaders.includes('Content-Security-Policy')) { recommendations.push("CSP: \"default-src 'self'\""); } if (missingHeaders.includes('X-Frame-Options')) { recommendations.push("X-Frame-Options: 'DENY'"); } if (missingHeaders.includes('X-Content-Type-Options')) { recommendations.push("X-Content-Type-Options: 'nosniff'"); } if (missingHeaders.includes('Strict-Transport-Security')) { recommendations.push("HSTS: 'max-age=31536000; includeSubDomains'"); } return recommendations.slice(0, 2).join(', ') + (recommendations.length > 2 ? '...' : ''); } } exports.MissingSecurityHeadersRule = MissingSecurityHeadersRule; //# sourceMappingURL=missing-security-headers.js.map