import { NextRequest, NextResponse } from 'next/server' import { Pool } from 'pg' // Disable SSL rejection for self-signed certs process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' const dbUrl = process.env.COMPLIANCE_DATABASE_URL || process.env.DATABASE_URL || 'postgresql://breakpilot:breakpilot123@bp-core-postgres:5432/breakpilot_db' const pool = new Pool({ connectionString: dbUrl }) // handleMeta returns global (filter-independent) counts incl. a ~2s member-join // facet. It is refetched on every filter change, so cache it briefly. let metaCache: { at: number; data: unknown } | null = null const META_TTL_MS = 120_000 // The use-case mapping tables (mc_use_case_mappings/mc_verification/mc_regulations) // are seeded per-environment and may not exist yet on a fresh/unseeded DB. Guard // every mapping query so the route degrades to empty filters instead of a 500. // Cached with a short TTL so it picks up the tables once that DB gets seeded. let mappingTablesCache: { at: number; present: boolean } | null = null async function hasMappingTables(): Promise { if (mappingTablesCache && Date.now() - mappingTablesCache.at < 300_000) { return mappingTablesCache.present } let present = false try { const r = await pool.query( "SELECT to_regclass('compliance.mc_use_case_mappings') IS NOT NULL AS present") present = !!r.rows[0]?.present } catch { present = false } mappingTablesCache = { at: Date.now(), present } return present } type MCListRow = { id: string; control_id: string; title: string; objective: string severity: string; category: string; total_controls: number phases_covered: string[] | null; created_at: string verification_method: string | null; use_cases: string[] | null primary_regulation: string | null } /** * 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 }) } } // Shared WHERE builder so list + count stay in lock-step (incl. the // use_case / verification_method / source_regulation mapping filters). function buildControlsWhere(params: URLSearchParams, hasMapping: boolean): { where: string; args: unknown[]; idx: number } { let where = "WHERE 1=1" const args: unknown[] = [] let idx = 1 const search = params.get('search') || '' if (search) { where += ` AND mc.canonical_name ILIKE $${idx}` args.push(`%${search}%`) idx++ } const severity = params.get('severity') || '' if (severity === 'high') { where += ` AND mc.total_controls > 100` } else if (severity === 'medium') { where += ` AND mc.total_controls BETWEEN 20 AND 100` } else if (severity === 'low') { where += ` AND mc.total_controls < 20` } const domain = params.get('domain') || '' if (domain) { where += ` AND mc.canonical_name LIKE $${idx}` args.push(`${domain}%`) idx++ } // Mapping-based filters only apply when the mapping tables exist (seeded DB). if (hasMapping) { const useCase = params.get('use_case') || '' const primaryOnly = params.get('primary') === '1' if (useCase) { where += ` AND EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m WHERE m.master_control_uuid = mc.id AND m.use_case = $${idx}${primaryOnly ? ' AND m.is_primary' : ''})` args.push(useCase) idx++ } const verification = params.get('verification_method') || '' if (verification === '__none__') { where += ` AND NOT EXISTS (SELECT 1 FROM compliance.mc_verification v WHERE v.master_control_uuid = mc.id)` } else if (verification) { where += ` AND EXISTS (SELECT 1 FROM compliance.mc_verification v WHERE v.master_control_uuid = mc.id AND v.verification_method = $${idx})` args.push(verification) idx++ } const regulation = params.get('source_regulation') || '' if (regulation) { where += ` AND EXISTS (SELECT 1 FROM compliance.mc_regulations r WHERE r.master_control_uuid = mc.id AND r.source_regulation = $${idx})` args.push(regulation) idx++ } const mapped = params.get('mapped') || '' if (mapped === 'mapped') { where += ` AND EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m WHERE m.master_control_uuid = mc.id)` } else if (mapped === 'unmapped') { where += ` AND NOT EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m WHERE m.master_control_uuid = mc.id)` } } // Member-based filter: an MC matches if ANY of its atomic members has the // category. Only category/severity/release_state are populated on the // deduplicated members; evidence_type, target_audience and source_citation // are 100% NULL there, so those canonical filters cannot apply to MCs // without an upstream backfill (wiring them would just return 0). const category = params.get('category') || '' if (category) { where += ` AND EXISTS (SELECT 1 FROM compliance.master_control_members mcm JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid WHERE mcm.master_control_uuid = mc.id AND cc.category = $${idx})` args.push(category); idx++ } return { where, args, idx } } async function handleControls(params: URLSearchParams) { 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' const hasMapping = await hasMappingTables() const { where, args, idx } = buildControlsWhere(params, hasMapping) const sortCol = sort === 'control_id' ? 'mc.master_control_id' : sort === 'created_at' ? 'mc.created_at' : sort === 'source' ? 'mc.canonical_name' : 'mc.master_control_id' const mapCols = hasMapping ? `, (SELECT v.verification_method FROM compliance.mc_verification v WHERE v.master_control_uuid = mc.id) as verification_method, (SELECT array_agg(m.use_case ORDER BY m.is_primary DESC, m.use_case) FROM compliance.mc_use_case_mappings m WHERE m.master_control_uuid = mc.id) as use_cases, (SELECT r.source_regulation FROM compliance.mc_regulations r WHERE r.master_control_uuid = mc.id AND r.is_primary LIMIT 1) as primary_regulation` : `, NULL as verification_method, NULL::text[] as use_cases, NULL as primary_regulation` 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${mapCols} 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: MCListRow) => ({ 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: r.primary_regulation ? { source: r.primary_regulation } : null, verification_method: r.verification_method, evidence_type: null, target_audience: [], use_cases: r.use_cases || [], requirements: [], test_procedure: [], evidence: [], open_anchors: [], total_controls: r.total_controls, phases_covered: r.phases_covered, created_at: r.created_at, scope: { platforms: [], components: [], data_classes: [] }, risk_score: null, implementation_effort: null, })) return NextResponse.json(controls) } async function handleCount(params: URLSearchParams) { const hasMapping = await hasMappingTables() const { where, args } = buildControlsWhere(params, hasMapping) 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) { if (metaCache && Date.now() - metaCache.at < META_TTL_MS) { return NextResponse.json(metaCache.data) } 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 `) // category facet is member-based (those tables always exist); the mapping // facets only when the mapping tables are present (seeded DB). const hasMapping = await hasMappingTables() const catRes = await pool.query(`SELECT cc.category v, count(DISTINCT mcm.master_control_uuid) c FROM compliance.master_control_members mcm JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid WHERE cc.category IS NOT NULL GROUP BY 1 ORDER BY 2 DESC`) const emptyRows = { rows: [] as Array> } const [ucRes, vRes, regRes, mappedRes] = hasMapping ? await Promise.all([ pool.query(`SELECT use_case, count(DISTINCT master_control_uuid) c FROM compliance.mc_use_case_mappings GROUP BY 1 ORDER BY 2 DESC`), pool.query(`SELECT verification_method, count(*) c FROM compliance.mc_verification GROUP BY 1 ORDER BY 2 DESC`), pool.query(`SELECT source_regulation, count(DISTINCT master_control_uuid) c FROM compliance.mc_regulations GROUP BY 1 ORDER BY 2 DESC LIMIT 200`), pool.query(`SELECT count(DISTINCT master_control_uuid) c FROM compliance.mc_use_case_mappings`), ]) : [emptyRows, emptyRows, emptyRows, { rows: [{ c: '0' }] }] const facet = (rows: Array<{ v: string; c: string }>) => Object.fromEntries(rows.filter(x => x.v).map(x => [x.v, parseInt(x.c)])) const total = parseInt(r.total) const mappedTotal = parseInt(mappedRes.rows[0].c) const payload = { total, severity_counts: { high: parseInt(r.high_count), medium: parseInt(r.medium_count), low: parseInt(r.low_count), }, domains: domainRes.rows.map((d: { domain: string; count: string }) => ({ domain: d.domain, count: parseInt(d.count) })), sources: [], no_source_count: 0, release_state_counts: { active: total }, verification_method_counts: Object.fromEntries( vRes.rows.map((x: { verification_method: string; c: string }) => [x.verification_method, parseInt(x.c)])), category_counts: facet(catRes.rows), evidence_type_counts: {}, use_case_counts: Object.fromEntries( ucRes.rows .filter((x: { use_case: string | null }) => x.use_case) .map((x: { use_case: string; c: string }) => [x.use_case, parseInt(x.c)])), regulations: regRes.rows .filter((x: { source_regulation: string | null }) => x.source_regulation) .map((x: { source_regulation: string; c: string }) => ({ source_regulation: x.source_regulation, count: parseInt(x.c) })), mapped_total: mappedTotal, unmapped_count: total - mappedTotal, } metaCache = { at: Date.now(), data: payload } return NextResponse.json(payload) } 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]) // Use-case / verification / regulation mapping (only when the tables exist). const mapping: Record = (await hasMappingTables()) ? ((await pool.query(` SELECT (SELECT json_agg(json_build_object('use_case', m.use_case, 'is_primary', m.is_primary) ORDER BY m.is_primary DESC, m.use_case) FROM compliance.mc_use_case_mappings m WHERE m.master_control_uuid = $1) as use_cases, (SELECT v.verification_method FROM compliance.mc_verification v WHERE v.master_control_uuid = $1) as verification_method, (SELECT json_agg(json_build_object('source_regulation', r.source_regulation, 'is_primary', r.is_primary, 'member_count', r.member_count) ORDER BY r.is_primary DESC, r.member_count DESC) FROM compliance.mc_regulations r WHERE r.master_control_uuid = $1) as regulations `, [mc.id])).rows[0] || {}) : {} const regs = mapping.regulations || [] const primaryReg = regs.find((x: { is_primary: boolean }) => x.is_primary) || regs[0] 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: [], verification_method: mapping.verification_method || null, use_cases: mapping.use_cases || [], regulations: regs, source_citation: primaryReg ? { source: primaryReg.source_regulation } : null, scope: { platforms: [], components: [], data_classes: [] }, risk_score: null, implementation_effort: null, created_at: mc.created_at, }) }