372e1fe9e9
CI / detect-changes (push) Successful in 14s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 7s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Failing after 15s
CI / go-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m23s
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 34s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Phase 2: Live-Filter an /sdk/master-controls (Use Case, Quell-Regulierung, Verifikations-Methode, Coverage, Primärzweck-Toggle, category via Member-EXISTS). API mit EXISTS-Filtern + gecachten Meta-Counts in master-controls/route.ts. Phase A: neue UseCase telekommunikation + Fix der Impressum-Fehlrouten im Register (TKG/AT-TKG->telekommunikation, telemedien->dse, GewO->handelsrecht); echte Impressum-Quellen (TMG/Mediengesetz) bleiben impressum. Deterministischer Seed aus source_regulation; Tests grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
360 lines
14 KiB
TypeScript
360 lines
14 KiB
TypeScript
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
|
|
|
|
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): { 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++
|
|
}
|
|
|
|
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 { where, args, idx } = buildControlsWhere(params)
|
|
|
|
const sortCol = sort === 'control_id' ? 'mc.master_control_id' :
|
|
sort === 'created_at' ? 'mc.created_at' :
|
|
sort === 'source' ? 'mc.canonical_name' : '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,
|
|
(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
|
|
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 { where, args } = buildControlsWhere(params)
|
|
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
|
|
`)
|
|
|
|
// Mapping distribution + coverage + member-derived category facet. Only
|
|
// category is populated on the deduplicated members (evidence_type /
|
|
// target_audience are NULL there), so it is the one canonical facet we surface.
|
|
const [ucRes, vRes, regRes, mappedRes, catRes] = 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`),
|
|
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 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 (the "regulation→code" lineage)
|
|
const mapRes = 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])
|
|
const mapping = mapRes.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,
|
|
})
|
|
}
|