19d8a7e2b9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
206 lines
6.2 KiB
TypeScript
206 lines
6.2 KiB
TypeScript
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,
|
|
})
|
|
}
|