import { NextRequest, NextResponse } from 'next/server' import { requireRole } from '@/lib/auth' import { logger } from '@/lib/logger' import { runSecurityScan, FIX_SAFETY, type CheckSeverity, type FixSafety, type Check } from '@/lib/security-scan' type FixScope = 'safe' | 'safe+restart' | 'all' interface AgentScanFixRequest { action: 'scan' | 'fix' | 'scan-and-fix' fixScope?: FixScope ids?: string[] force?: boolean dryRun?: boolean } function isFixableInScope(checkId: string, scope: FixScope, force: boolean): boolean { const safety = FIX_SAFETY[checkId] if (!safety) return false if (safety === 'safe') return true if (safety === 'requires-restart' && (scope === 'safe+restart' || scope === 'all')) return true if (safety === 'requires-review' && scope === 'all' && force) return true return false } export async function POST(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) let body: AgentScanFixRequest try { body = await request.json() } catch { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) } const { action, fixScope = 'safe+restart', ids, force = false, dryRun = false } = body if (!action || !['scan', 'fix', 'scan-and-fix'].includes(action)) { return NextResponse.json({ error: 'action must be "scan", "fix", or "scan-and-fix"' }, { status: 400 }) } try { // Always scan first const scanResult = runSecurityScan() const allChecks = Object.values(scanResult.categories).flatMap(c => c.checks) const failingChecks = allChecks.filter(c => c.status !== 'pass') const scanResponse = { overall: scanResult.overall, score: scanResult.score, failingChecks: failingChecks.map(c => ({ id: c.id, name: c.name, status: c.status, severity: c.severity ?? 'medium' as CheckSeverity, detail: c.detail, fix: c.fix, fixSafety: FIX_SAFETY[c.id] ?? c.fixSafety ?? ('manual-only' as FixSafety), autoFixable: isFixableInScope(c.id, fixScope, force), })), passingCount: allChecks.length - failingChecks.length, totalCount: allChecks.length, categories: Object.fromEntries( Object.entries(scanResult.categories).map(([key, cat]) => [ key, { score: cat.score, failCount: cat.checks.filter(c => c.status !== 'pass').length }, ]) ), } if (action === 'scan') { const criticalCount = failingChecks.filter(c => c.severity === 'critical').length const highCount = failingChecks.filter(c => c.severity === 'high').length return NextResponse.json({ scan: scanResponse, summary: `Security score: ${scanResult.score}/100 (${scanResult.overall}). ${failingChecks.length} issue(s): ${criticalCount} critical, ${highCount} high.`, }) } // Fix or scan-and-fix const targetIds = ids ? new Set(ids) : null const checksToFix = failingChecks.filter(c => { if (targetIds && !targetIds.has(c.id)) return false return isFixableInScope(c.id, fixScope, force) }) const skipped: Array<{ id: string; reason: string }> = [] const requiresManual: Array<{ id: string; name: string; instructions: string }> = [] // Identify skipped and manual checks for (const c of failingChecks) { if (targetIds && !targetIds.has(c.id)) continue const safety = FIX_SAFETY[c.id] ?? c.fixSafety if (!safety || safety === 'manual-only') { requiresManual.push({ id: c.id, name: c.name, instructions: c.fix }) } else if (!isFixableInScope(c.id, fixScope, force)) { const reason = safety === 'requires-review' && !force ? 'requires-review: set force=true to apply' : safety === 'requires-restart' && fixScope === 'safe' ? 'requires-restart: use fixScope "safe+restart" or "all"' : `fix safety level "${safety}" not in scope "${fixScope}"` skipped.push({ id: c.id, reason }) } } if (dryRun) { return NextResponse.json({ scan: scanResponse, fixes: { applied: checksToFix.map(c => ({ id: c.id, name: c.name, fixed: false, detail: `[dry-run] Would apply fix: ${c.fix}`, fixSafety: FIX_SAFETY[c.id], })), skipped, requiresRestart: checksToFix.some(c => FIX_SAFETY[c.id] === 'requires-restart'), requiresManual, }, summary: `Dry run: ${checksToFix.length} fix(es) would be applied, ${skipped.length} skipped, ${requiresManual.length} require manual action.`, }) } // Actually apply fixes by calling the fix endpoint logic const fixIds = checksToFix.map(c => c.id) let fixResponse: any = { fixed: 0, failed: 0, results: [] } if (fixIds.length > 0) { // Import and call the fix route handler internally const fixUrl = new URL('/api/security-scan/fix', request.url) const fixReq = new NextRequest(fixUrl, { method: 'POST', headers: request.headers, body: JSON.stringify({ ids: fixIds }), }) // Dynamically import to avoid circular deps const { POST: fixHandler } = await import('../fix/route') const fixRes = await fixHandler(fixReq) fixResponse = await fixRes.json() } const applied = (fixResponse.results || []).map((r: any) => ({ ...r, fixSafety: FIX_SAFETY[r.id], })) const requiresRestart = applied.some((r: any) => r.fixed && FIX_SAFETY[r.id] === 'requires-restart') logger.info({ action, fixScope, force, dryRun, applied: applied.length, skipped: skipped.length }, 'Agent security scan+fix') // Re-scan after fixes to get updated score const postFixScan = fixIds.length > 0 ? runSecurityScan() : scanResult return NextResponse.json({ scan: { ...scanResponse, score: postFixScan.score, overall: postFixScan.overall, }, fixes: { applied, skipped, requiresRestart, requiresManual, }, summary: buildSummary(applied, skipped, requiresManual, requiresRestart, postFixScan.score, postFixScan.overall), }) } catch (error) { logger.error({ err: error }, 'Agent security scan error') return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } function buildSummary( applied: any[], skipped: any[], requiresManual: any[], requiresRestart: boolean, score: number, overall: string, ): string { const parts: string[] = [] const fixedCount = applied.filter((r: any) => r.fixed).length if (fixedCount > 0) parts.push(`${fixedCount} issue(s) fixed`) if (skipped.length > 0) parts.push(`${skipped.length} skipped`) if (requiresManual.length > 0) parts.push(`${requiresManual.length} require manual action`) if (requiresRestart) parts.push('server restart recommended') parts.push(`score: ${score}/100 (${overall})`) return parts.join('. ') + '.' }