import type { CliPlugin, PluginConfigValue, PluginContext } from '../types.js' /** * Security Plugin * Provides comprehensive security middleware including headers, input sanitization, and audit logging */ export const securityPlugin: CliPlugin = { id: 'security', category: 'middleware', description: 'Comprehensive security middleware with headers, sanitization, and audit logging', config: { enableSecurityHeaders: { type: 'boolean', description: 'Enable enhanced security headers (CSP, HSTS, etc.)', default: true, }, enableInputSanitization: { type: 'boolean', description: 'Enable input sanitization for XSS protection', default: true, }, enableAuditLogging: { type: 'boolean', description: 'Enable security audit logging', default: true, }, cspPolicy: { type: 'select', description: 'Content Security Policy strictness level', options: [ { name: 'Strict (API only)', value: 'strict' }, { name: 'Permissive (HTML content)', value: 'permissive' }, { name: 'Custom', value: 'custom' }, ], default: 'strict', }, customCsp: { type: 'string', description: 'Custom CSP policy (when cspPolicy is "custom")', default: '', }, enableHsts: { type: 'boolean', description: 'Enable HTTP Strict Transport Security (HTTPS only)', default: true, }, hstsMaxAge: { type: 'number', description: 'HSTS max age in seconds', default: 31536000, }, apiVersion: { type: 'string', description: 'API version header value', default: 'v1', }, enableFrameProtection: { type: 'boolean', description: 'Enable X-Frame-Options protection', default: true, }, frameOptions: { type: 'select', description: 'X-Frame-Options policy', options: [ { name: 'DENY', value: 'DENY' }, { name: 'SAMEORIGIN', value: 'SAMEORIGIN' }, ], default: 'DENY', }, }, prepare(_ctx: PluginContext, _config: Record) { // No additional dependencies needed - uses built-in Hono middleware }, async apply(ctx: PluginContext, config: Record): Promise { const enableSecurityHeaders = config.enableSecurityHeaders !== false const enableInputSanitization = config.enableInputSanitization !== false const enableAuditLogging = config.enableAuditLogging !== false const cspPolicy = config.cspPolicy || 'strict' const customCsp = config.customCsp || '' const enableHsts = config.enableHsts !== false const hstsMaxAge = config.hstsMaxAge || 31536000 const apiVersion = config.apiVersion || 'v1' const enableFrameProtection = config.enableFrameProtection !== false const frameOptions = config.frameOptions || 'DENY' // Generate security middleware file const securityMiddlewareContent = `import type { Context, MiddlewareHandler } from 'hono' import { createMiddleware } from 'hono/factory' import { HTTPException } from 'hono/http-exception' import type { AppBindings } from './types.js' /** * Security middleware generated by Security Plugin */ ${ enableSecurityHeaders ? ` /** * Enhanced security headers with CSP */ export function enhancedSecurityHeaders(): MiddlewareHandler { return createMiddleware(async (c, next) => { await next() // Core security headers c.header('X-Content-Type-Options', 'nosniff') ${enableFrameProtection ? `c.header('X-Frame-Options', '${frameOptions}')` : ''} c.header('X-XSS-Protection', '1; mode=block') c.header('Referrer-Policy', 'strict-origin-when-cross-origin') // Enhanced permissions policy c.header( 'Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), serial=(), bluetooth=()' ) // Content Security Policy const path = new URL(c.req.url).pathname const contentType = c.res.headers.get('content-type') || '' ${ cspPolicy === 'permissive' ? ` // Permissive CSP for HTML content (dashboards, documentation) if ( path === '/reference' || path === '/' || contentType.includes('text/html') ) { c.header( 'Content-Security-Policy', "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://cdn.jsdelivr.net; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://unpkg.com https://cdn.jsdelivr.net; " + "font-src 'self' https://fonts.gstatic.com; " + "img-src 'self' data: https:; " + "connect-src 'self'; " + "frame-ancestors 'none'; " + "base-uri 'none';" ) } else { // Strict CSP for JSON API endpoints c.header( 'Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'; base-uri 'none';" ) } ` : cspPolicy === 'custom' && customCsp ? ` c.header('Content-Security-Policy', '${customCsp}') ` : ` // Strict CSP for JSON API endpoints c.header( 'Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'; base-uri 'none';" ) ` } ${ enableHsts ? ` // Only add HSTS for HTTPS if (c.req.url.startsWith('https://')) { c.header('Strict-Transport-Security', 'max-age=${hstsMaxAge}; includeSubDomains; preload') } ` : '' } // API-specific headers c.header('X-API-Version', '${apiVersion}') c.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate') c.header('Pragma', 'no-cache') c.header('Expires', '0') }) } ` : '' } ${ enableInputSanitization ? ` /** * Input sanitization middleware */ function sanitizeString(str: string): string { return str .replace(/]*>.*?<\\/script>/gi, '') .replace(/<[^>]*>/g, '') .replace(/javascript:/gi, '') .replace(/on\\w+\\s*=/gi, '') } function sanitizeObject(obj: any): any { if (typeof obj === 'string') { return sanitizeString(obj) } if (Array.isArray(obj)) { return obj.map(sanitizeObject) } if (obj && typeof obj === 'object') { const sanitized: any = {} for (const [key, value] of Object.entries(obj)) { sanitized[sanitizeString(key)] = sanitizeObject(value) } return sanitized } return obj } export function inputSanitization(): MiddlewareHandler { return createMiddleware(async (c, next) => { const method = c.req.method if (['POST', 'PUT', 'PATCH'].includes(method)) { try { const body = await c.req.json().catch(() => null) if (body && typeof body === 'object') { // Sanitize common XSS patterns const _sanitized = sanitizeObject(body) // Note: In practice, you might want to store sanitized body for later use } } catch { // Continue if body parsing fails - let route handler deal with it } } await next() }) } ` : '' } ${ enableAuditLogging ? ` /** * Comprehensive request logging for security monitoring */ export function securityAuditLog(): MiddlewareHandler { return createMiddleware(async (c, next) => { const startTime = Date.now() const method = c.req.method const url = c.req.url const userAgent = c.req.header('user-agent') || 'unknown' const ip = c.req.header('cf-connecting-ip') || c.req.header('x-forwarded-for') || 'unknown' const cfCountry = c.req.header('cf-ipcountry') const user = c.get('user') const requestId = c.get('requestId') // Log security-relevant request details const securityContext = { type: 'security_audit', timestamp: new Date().toISOString(), request: { id: requestId, method, url: new URL(url).pathname, // Don't log query params (may contain sensitive data) ip, country: cfCountry, userAgent, contentLength: c.req.header('content-length'), contentType: c.req.header('content-type'), }, user: user ? { id: user.id, email: user.email } : null, } let statusCode: number | undefined let error: unknown = null try { await next() statusCode = c.res.status } catch (err) { error = err statusCode = err instanceof HTTPException ? err.status : 500 throw err } finally { const duration = Date.now() - startTime // Only log if statusCode is available if (statusCode !== undefined) { // Log completion with security context console.warn( JSON.stringify({ ...securityContext, response: { statusCode, duration, error: error ? { message: (error as Error).message, type: error.constructor.name, } : null, }, }) ) // Alert on suspicious activities if (statusCode === 401 || statusCode === 403) { console.warn( JSON.stringify({ type: 'security_alert', level: 'warning', message: \`Authentication/Authorization failure: \${method} \${url}\`, ...securityContext.request, statusCode, }) ) } // Alert on potential attacks if (statusCode === 429) { console.warn( JSON.stringify({ type: 'security_alert', level: 'warning', message: \`Rate limit exceeded: \${ip}\`, ...securityContext.request, }) ) } } } }) } ` : '' } ` await ctx.addFile('src/lib/security.middleware.ts', securityMiddlewareContent) // Update main app file to include security middleware if (await ctx.fileExists('src/index.ts')) { await ctx.modifyFile('src/index.ts', (content: string) => { // Add import if not already present if (!content.includes('security.middleware')) { const imports = [] if (enableSecurityHeaders) imports.push('enhancedSecurityHeaders') if (enableInputSanitization) imports.push('inputSanitization') if (enableAuditLogging) imports.push('securityAuditLog') if (imports.length > 0) { const importLine = `import { ${imports.join(', ')} } from './lib/security.middleware.js'` // Find the last import statement const lines = content.split('\\n') let lastImportIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('import ')) { lastImportIndex = i } } if (lastImportIndex >= 0) { lines.splice(lastImportIndex + 1, 0, importLine) } else { lines.unshift(importLine) } content = lines.join('\\n') } } // Add middleware usage if not already present const middlewareToAdd = [] if (enableAuditLogging && !content.includes('securityAuditLog()')) { middlewareToAdd.push("app.use('*', securityAuditLog())") } if (enableInputSanitization && !content.includes('inputSanitization()')) { middlewareToAdd.push("app.use('*', inputSanitization())") } if (enableSecurityHeaders && !content.includes('enhancedSecurityHeaders()')) { middlewareToAdd.push("app.use('*', enhancedSecurityHeaders())") } if (middlewareToAdd.length > 0) { // Find where to insert middleware calls if (content.includes('const app = ') || content.includes('export const app = ')) { const lines = content.split('\\n') let insertIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].includes('const app = ') || lines[i].includes('export const app = ')) { insertIndex = i + 1 break } } if (insertIndex >= 0) { lines.splice(insertIndex, 0, '', '// Security middleware', ...middlewareToAdd) content = lines.join('\\n') } } } return content }) } }, }