import { NextRequest, NextResponse } from 'next/server' import { getDatabase } from '@/lib/db' import { requireRole } from '@/lib/auth' import { readLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' import { getSecurityPosture } from '@/lib/security-events' import { getMcpCallStats } from '@/lib/mcp-audit' import { runSecurityScan } from '@/lib/security-scan' type Timeframe = 'hour' | 'day' | 'week' | 'month' const TIMEFRAME_SECONDS: Record = { hour: 3600, day: 86400, week: 7 * 86400, month: 30 * 86400, } export async function GET(request: NextRequest) { const auth = requireRole(request, 'admin') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) const rateCheck = readLimiter(request) if (rateCheck) return rateCheck try { const { searchParams } = new URL(request.url) const timeframe = (searchParams.get('timeframe') || 'day') as Timeframe const eventTypeFilter = searchParams.get('event_type') const severityFilter = searchParams.get('severity') const agentFilter = searchParams.get('agent') const workspaceId = auth.user.workspace_id ?? 1 const seconds = TIMEFRAME_SECONDS[timeframe] || TIMEFRAME_SECONDS.day const since = Math.floor(Date.now() / 1000) - seconds const db = getDatabase() // Infrastructure scan (same as onboarding security scan) const scan = runSecurityScan() // Event-based posture (incidents, trust scores) const eventPosture = getSecurityPosture(workspaceId) // Blend: weighted average — 70% infrastructure config, 30% event history const blendedScore = Math.round(scan.score * 0.7 + eventPosture.score * 0.3) const level = blendedScore >= 90 ? 'hardened' : blendedScore >= 70 ? 'secure' : blendedScore >= 40 ? 'needs-attention' : 'at-risk' // Auth events const authEventsQuery = db.prepare(` SELECT event_type, severity, agent_name, detail, ip_address, created_at FROM security_events WHERE workspace_id = ? AND created_at > ? AND event_type IN ('auth.failure', 'auth.token_rotation', 'auth.access_denied') ORDER BY created_at DESC LIMIT 50 `).all(workspaceId, since) as any[] const loginFailures = authEventsQuery.filter(e => e.event_type === 'auth.failure').length const tokenRotations = authEventsQuery.filter(e => e.event_type === 'auth.token_rotation').length const accessDenials = authEventsQuery.filter(e => e.event_type === 'auth.access_denied').length // Agent trust const agents = db.prepare(` SELECT agent_name, trust_score, last_anomaly_at, auth_failures + injection_attempts + rate_limit_hits + secret_exposures as anomalies FROM agent_trust_scores WHERE workspace_id = ? ORDER BY trust_score ASC `).all(workspaceId) as any[] const flaggedCount = agents.filter((a: any) => a.trust_score < 0.8).length // Secret exposures const secretEvents = db.prepare(` SELECT event_type, severity, agent_name, detail, created_at FROM security_events WHERE workspace_id = ? AND created_at > ? AND event_type = 'secret.exposure' ORDER BY created_at DESC LIMIT 20 `).all(workspaceId, since) as any[] // MCP audit summary const mcpTotals = db.prepare(` SELECT COUNT(*) as total_calls, COUNT(DISTINCT tool_name) as unique_tools, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failures FROM mcp_call_log WHERE workspace_id = ? AND created_at > ? `).get(workspaceId, since) as any const topTools = db.prepare(` SELECT tool_name, COUNT(*) as count FROM mcp_call_log WHERE workspace_id = ? AND created_at > ? GROUP BY tool_name ORDER BY count DESC LIMIT 10 `).all(workspaceId, since) as any[] const totalCalls = mcpTotals?.total_calls ?? 0 const failureRate = totalCalls > 0 ? Math.round(((mcpTotals?.failures ?? 0) / totalCalls) * 10000) / 100 : 0 // Rate limit hits const rateLimitEvents = db.prepare(` SELECT COUNT(*) as total FROM security_events WHERE workspace_id = ? AND created_at > ? AND event_type = 'rate_limit.hit' `).get(workspaceId, since) as any const rateLimitByIp = db.prepare(` SELECT ip_address, COUNT(*) as count FROM security_events WHERE workspace_id = ? AND created_at > ? AND event_type = 'rate_limit.hit' AND ip_address IS NOT NULL GROUP BY ip_address ORDER BY count DESC LIMIT 10 `).all(workspaceId, since) as any[] // Injection attempts const injectionEvents = db.prepare(` SELECT event_type, severity, agent_name, detail, ip_address, created_at FROM security_events WHERE workspace_id = ? AND created_at > ? AND event_type = 'injection.attempt' ORDER BY created_at DESC LIMIT 20 `).all(workspaceId, since) as any[] // Timeline (bucketed by hour) const bucketSize = timeframe === 'hour' ? 300 : 3600 let timelineQuery = ` SELECT (created_at / ${bucketSize}) * ${bucketSize} as bucket, COUNT(*) as event_count, MAX(CASE WHEN severity = 'critical' THEN 3 WHEN severity = 'warning' THEN 2 ELSE 1 END) as max_severity FROM security_events WHERE workspace_id = ? AND created_at > ? ` const timelineParams: any[] = [workspaceId, since] if (eventTypeFilter) { timelineQuery += ' AND event_type = ?' timelineParams.push(eventTypeFilter) } if (severityFilter) { timelineQuery += ' AND severity = ?' timelineParams.push(severityFilter) } if (agentFilter) { timelineQuery += ' AND agent_name = ?' timelineParams.push(agentFilter) } timelineQuery += ' GROUP BY bucket ORDER BY bucket ASC' const timeline = db.prepare(timelineQuery).all(...timelineParams) as any[] const severityMap: Record = { 3: 'critical', 2: 'warning', 1: 'info' } return NextResponse.json({ posture: { score: blendedScore, level }, scan: { score: scan.score, overall: scan.overall, categories: scan.categories, }, authEvents: { loginFailures, tokenRotations, accessDenials, recentEvents: authEventsQuery.slice(0, 10), }, agentTrust: { agents: agents.map((a: any) => ({ name: a.agent_name, score: Math.round(a.trust_score * 100) / 100, anomalies: a.anomalies, })), flaggedCount, }, secretExposures: { total: secretEvents.length, recent: secretEvents.slice(0, 5), }, mcpAudit: { totalCalls, uniqueTools: mcpTotals?.unique_tools ?? 0, failureRate, topTools: topTools.map((t: any) => ({ name: t.tool_name, count: t.count })), }, rateLimits: { totalHits: rateLimitEvents?.total ?? 0, byIp: rateLimitByIp.map((r: any) => ({ ip: r.ip_address, count: r.count })), }, injectionAttempts: { total: injectionEvents.length, recent: injectionEvents.slice(0, 5), }, timeline: timeline.map((t: any) => ({ timestamp: t.bucket, eventCount: t.event_count, severity: severityMap[t.max_severity] || 'info', })), }) } catch (error) { logger.error({ err: error }, 'GET /api/security-audit error') return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } }