import { NextRequest, NextResponse } from 'next/server' import { Pool } from 'pg' const pool = new Pool({ connectionString: process.env.COMPLIANCE_DATABASE_URL || process.env.DATABASE_URL || 'postgresql://breakpilot:breakpilot123@bp-core-postgres:5432/breakpilot_db', }) /** * MC API that returns data in the same format as the canonical controls * endpoint. This allows the MC page to reuse ControlListView components. */ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const endpoint = searchParams.get('endpoint') || 'controls' switch (endpoint) { case 'frameworks': return NextResponse.json([]) case 'controls': return handleControls(searchParams) case 'controls-count': return handleCount(searchParams) case 'controls-meta': return handleMeta(searchParams) case 'control': return handleDetail(searchParams) default: return NextResponse.json({ error: 'unknown' }, { status: 400 }) } } catch (e) { return NextResponse.json({ error: String(e) }, { status: 500 }) } } async function handleControls(params: URLSearchParams) { const search = params.get('search') || '' const limit = Math.min(parseInt(params.get('limit') || '50'), 200) const offset = parseInt(params.get('offset') || '0') const sort = params.get('sort') || 'control_id' const order = params.get('order') === 'desc' ? 'DESC' : 'ASC' let where = "WHERE 1=1" const args: unknown[] = [] let idx = 1 if (search) { where += ` AND mc.canonical_name ILIKE $${idx}` args.push(`%${search}%`) idx++ } const sortCol = sort === 'control_id' ? 'mc.master_control_id' : sort === 'created_at' ? 'mc.created_at' : 'mc.master_control_id' args.push(limit, offset) const res = await pool.query(` SELECT mc.master_control_id as control_id, mc.canonical_name as title, 'Master Control mit ' || mc.total_controls || ' Atomic Controls' as objective, CASE WHEN mc.total_controls > 100 THEN 'high' WHEN mc.total_controls > 20 THEN 'medium' ELSE 'low' END as severity, 'master_control' as category, mc.total_controls, mc.phases_covered, mc.id, mc.created_at FROM compliance.master_controls mc ${where} ORDER BY ${sortCol} ${order} LIMIT $${idx} OFFSET $${idx + 1} `, args) // Map to canonical control format const controls = res.rows.map(r => ({ id: r.id, control_id: r.control_id, title: r.title, objective: r.objective, severity: r.severity, category: r.category, release_state: 'active', source_citation: null, verification_method: null, evidence_type: null, target_audience: [], requirements: [], test_procedure: [], evidence: [], open_anchors: [], total_controls: r.total_controls, phases_covered: r.phases_covered, created_at: r.created_at, })) return NextResponse.json(controls) } async function handleCount(params: URLSearchParams) { const search = params.get('search') || '' let where = "WHERE 1=1" const args: unknown[] = [] if (search) { where += ` AND mc.canonical_name ILIKE $1` args.push(`%${search}%`) } const res = await pool.query( `SELECT count(*) FROM compliance.master_controls mc ${where}`, args ) return NextResponse.json({ total: parseInt(res.rows[0].count) }) } async function handleMeta(params: URLSearchParams) { const res = await pool.query(` SELECT count(*) as total, count(CASE WHEN total_controls > 100 THEN 1 END) as high_count, count(CASE WHEN total_controls BETWEEN 20 AND 100 THEN 1 END) as medium_count, count(CASE WHEN total_controls < 20 THEN 1 END) as low_count FROM compliance.master_controls `) const r = res.rows[0] // Get top L1 tokens as "domains" const domainRes = await pool.query(` SELECT split_part(canonical_name, '_', 1) as domain, count(*) as count FROM compliance.master_controls GROUP BY 1 ORDER BY 2 DESC LIMIT 30 `) return NextResponse.json({ total: parseInt(r.total), severity_counts: { high: parseInt(r.high_count), medium: parseInt(r.medium_count), low: parseInt(r.low_count), }, domains: domainRes.rows.map(d => ({ domain: d.domain, count: parseInt(d.count) })), sources: [], no_source_count: 0, release_state_counts: { active: parseInt(r.total) }, verification_method_counts: {}, category_counts: {}, evidence_type_counts: {}, }) } async function handleDetail(params: URLSearchParams) { const id = params.get('id') || '' const res = await pool.query(` SELECT mc.id, mc.master_control_id as control_id, mc.canonical_name as title, 'Master Control mit ' || mc.total_controls || ' Atomic Controls' as objective, mc.total_controls, mc.phases_covered, mc.phase_control_count, mc.created_at FROM compliance.master_controls mc WHERE mc.master_control_id = $1 OR mc.id::text = $1 `, [id]) if (res.rows.length === 0) { return NextResponse.json({ error: 'not found' }, { status: 404 }) } const mc = res.rows[0] // Load members const membersRes = await pool.query(` SELECT cc.control_id, cc.title, cc.severity, mcm.phase, mcm.action FROM compliance.master_control_members mcm JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid WHERE mcm.master_control_uuid = $1 ORDER BY mcm.phase, cc.control_id LIMIT 100 `, [mc.id]) return NextResponse.json({ id: mc.id, control_id: mc.control_id, title: mc.title, objective: mc.objective, severity: mc.total_controls > 100 ? 'high' : mc.total_controls > 20 ? 'medium' : 'low', category: 'master_control', release_state: 'active', total_controls: mc.total_controls, phases_covered: mc.phases_covered, phase_control_count: mc.phase_control_count, members: membersRes.rows, requirements: membersRes.rows.map((m: { control_id: string; title: string; phase: string }) => `[${m.phase}] ${m.control_id}: ${m.title}` ), test_procedure: [], evidence: [], open_anchors: [], target_audience: [], source_citation: null, created_at: mc.created_at, }) }