8a0097f5da
CI / dep-audit (push) Has been skipped
CI / test-python-backend (push) Successful in 27s
CI / test-python-document-crawler (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / build-sha-integrity (push) Successful in 14s
CI / validate-canonical-controls (push) Successful in 10s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / detect-changes (push) Successful in 19s
CI / python-lint (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m8s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
Die "Korpus-Dokumente"-Tabelle wird nach Dokument-Art geordnet (Gesetze & Verordnungen → Behörden-Leitfäden → Standards & Best Practice → Rechtsprechung) mit Zwischenüberschriften, und je Herausgeber-Familie zusammengefasst (alle DSK, alle EDPB, alle OWASP/NIST/ENISA gemeinsam). Deterministischer Kategorisierer (categorizeCorpusDoc) + Grouper (groupCorpusDocs), pure + unit-getestet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
294 lines
9.0 KiB
TypeScript
294 lines
9.0 KiB
TypeScript
// Pure helpers for the Coverage page (#74). Kept separate so they are unit-testable
|
|
// without rendering the server component.
|
|
|
|
export interface UseCaseRow {
|
|
key: string
|
|
label: string
|
|
group: string
|
|
regulations: string[]
|
|
verification_methods: string[]
|
|
mapped_controls: number
|
|
atom_total: number
|
|
atom_relevant: number
|
|
}
|
|
|
|
export interface CorpusDoc {
|
|
source_regulation: string
|
|
license_rule: number | null
|
|
license_tier: string
|
|
atom_count: number
|
|
use_case: string | null
|
|
}
|
|
|
|
export interface LicenseSummaryRow {
|
|
license_rule: number | null
|
|
label: string
|
|
atom_count: number
|
|
}
|
|
|
|
export interface LicenseCatalogEntry {
|
|
source_id: string
|
|
title: string
|
|
publisher: string | null
|
|
url: string | null
|
|
version: string | null
|
|
license_id: string | null
|
|
license_name: string | null
|
|
commercial_use: string | null
|
|
ship_in_product: boolean | null
|
|
terms_url: string | null
|
|
}
|
|
|
|
export interface CorpusOverview {
|
|
license_summary: LicenseSummaryRow[]
|
|
documents: CorpusDoc[]
|
|
license_catalog: LicenseCatalogEntry[]
|
|
totals: { documents: number; catalog_sources: number }
|
|
}
|
|
|
|
// --- Korpus-Dokumente: gruppieren nach Art (Gesetz/Leitfaden/Standard/Urteil)
|
|
// + Herausgeber-Familie (DSK, EDPB, OWASP, NIST …). Deterministisch, pure. ---
|
|
interface DocCat {
|
|
key: string
|
|
label: string
|
|
order: number
|
|
}
|
|
const CAT_LAW: DocCat = { key: 'law', label: 'Gesetze & Verordnungen', order: 1 }
|
|
const CAT_GUIDANCE: DocCat = {
|
|
key: 'guidance',
|
|
label: 'Behörden-Leitfäden & Orientierungshilfen',
|
|
order: 2,
|
|
}
|
|
const CAT_STANDARD: DocCat = {
|
|
key: 'standard',
|
|
label: 'Standards & Best Practice',
|
|
order: 3,
|
|
}
|
|
const CAT_COURT: DocCat = { key: 'court', label: 'Rechtsprechung', order: 4 }
|
|
|
|
export function categorizeCorpusDoc(src: string): { cat: DocCat; family: string } {
|
|
const u = (src || '').toUpperCase()
|
|
// Standards & Best Practice (technische Familien)
|
|
if (u.includes('OWASP')) return { cat: CAT_STANDARD, family: 'OWASP' }
|
|
if (u.includes('NIST')) return { cat: CAT_STANDARD, family: 'NIST' }
|
|
if (u.includes('CISA')) return { cat: CAT_STANDARD, family: 'CISA' }
|
|
if (u.includes('OECD')) return { cat: CAT_STANDARD, family: 'OECD' }
|
|
if (u.includes('ENISA')) return { cat: CAT_STANDARD, family: 'ENISA' }
|
|
// Behörden-Leitfäden (Datenschutz-Aufsicht + EU-Kommissions-Guides)
|
|
if (u.startsWith('DSK'))
|
|
return { cat: CAT_GUIDANCE, family: 'DSK (Datenschutzkonferenz)' }
|
|
if (u.includes('EDPB')) return { cat: CAT_GUIDANCE, family: 'EDPB' }
|
|
if (u.includes('EDPS')) return { cat: CAT_GUIDANCE, family: 'EDPS' }
|
|
if (u.includes('WP29'))
|
|
return { cat: CAT_GUIDANCE, family: 'WP29 (Art.-29-Gruppe)' }
|
|
if (u.includes('BFDI')) return { cat: CAT_GUIDANCE, family: 'BfDI' }
|
|
if (u.includes('EU MACHINERY GUIDE') || u.includes('EU BLUE GUIDE'))
|
|
return { cat: CAT_GUIDANCE, family: 'EU-Kommission (Guides)' }
|
|
// Rechtsprechung
|
|
if (u.startsWith('BGH') || u.startsWith('BVGER') || u.startsWith('EUGH'))
|
|
return { cat: CAT_COURT, family: 'Rechtsprechung' }
|
|
// Default: Gesetz/Verordnung/Richtlinie
|
|
return { cat: CAT_LAW, family: 'Gesetze & Verordnungen' }
|
|
}
|
|
|
|
export interface CorpusFamilyGroup {
|
|
family: string
|
|
total: number
|
|
docs: CorpusDoc[]
|
|
}
|
|
|
|
export interface CorpusCatGroup {
|
|
key: string
|
|
label: string
|
|
order: number
|
|
total: number
|
|
families: CorpusFamilyGroup[]
|
|
}
|
|
|
|
// Group corpus docs by category (ordered: laws → guidance → standards → court),
|
|
// families within each sorted by size, docs within a family by size. So all DSK
|
|
// sit together, all EDPB together, all OWASP/NIST together, under headings.
|
|
export function groupCorpusDocs(docs: CorpusDoc[]): CorpusCatGroup[] {
|
|
const cats = new Map<string, { cat: DocCat; fam: Map<string, CorpusDoc[]> }>()
|
|
for (const d of docs) {
|
|
const { cat, family } = categorizeCorpusDoc(d.source_regulation)
|
|
if (!cats.has(cat.key)) cats.set(cat.key, { cat, fam: new Map() })
|
|
const fam = cats.get(cat.key)!.fam
|
|
if (!fam.has(family)) fam.set(family, [])
|
|
fam.get(family)!.push(d)
|
|
}
|
|
return [...cats.values()]
|
|
.map(({ cat, fam }) => {
|
|
const families = [...fam.entries()]
|
|
.map(([family, ds]) => ({
|
|
family,
|
|
docs: [...ds].sort((a, b) => b.atom_count - a.atom_count),
|
|
total: ds.reduce((s, d) => s + d.atom_count, 0),
|
|
}))
|
|
.sort((a, b) => b.total - a.total)
|
|
return {
|
|
key: cat.key,
|
|
label: cat.label,
|
|
order: cat.order,
|
|
total: families.reduce((s, f) => s + f.total, 0),
|
|
families,
|
|
}
|
|
})
|
|
.sort((a, b) => a.order - b.order)
|
|
}
|
|
|
|
export const USE_CASE_GROUP_LABELS: Record<string, string> = {
|
|
document: 'Dokument-Compliance',
|
|
security: 'Security',
|
|
cross_cutting: 'Querschnitt',
|
|
product: 'Produkt / Sektor',
|
|
}
|
|
|
|
export function licenseTierBadgeClass(rule: number | null): string {
|
|
switch (rule) {
|
|
case 1:
|
|
return 'bg-green-100 text-green-800'
|
|
case 2:
|
|
return 'bg-blue-100 text-blue-800'
|
|
case 3:
|
|
return 'bg-amber-100 text-amber-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-700'
|
|
}
|
|
}
|
|
|
|
export function commercialBadgeClass(commercial: string | null): string {
|
|
switch ((commercial || '').toLowerCase()) {
|
|
case 'allowed':
|
|
return 'bg-green-100 text-green-800'
|
|
case 'restricted':
|
|
return 'bg-amber-100 text-amber-800'
|
|
case 'prohibited':
|
|
return 'bg-red-100 text-red-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-700'
|
|
}
|
|
}
|
|
|
|
// --- Controls drill-down (#80 Stufe-Flip + Provenance) ---
|
|
|
|
export interface ControlItem {
|
|
id: string
|
|
control_id?: string | null
|
|
title: string
|
|
objective?: string | null
|
|
severity?: string | null
|
|
sub_topic?: string | null
|
|
canonical_obligation?: string | null
|
|
source_regulation?: string | null
|
|
source_article?: string | null
|
|
relevant: boolean
|
|
tier: 'core' | 'review'
|
|
source_type: 'derived' | 'own_library'
|
|
addressee?: string | null
|
|
applicable: boolean
|
|
is_gov: boolean
|
|
}
|
|
|
|
export interface ControlsResponse {
|
|
use_case: string
|
|
label: string
|
|
group: string
|
|
granularity: string
|
|
tier: string
|
|
total: number
|
|
core_count: number
|
|
review_count: number
|
|
out_of_scope_count: number
|
|
include_out_of_scope: boolean
|
|
limit: number
|
|
offset: number
|
|
sub_topic: string | null
|
|
subtopic_counts: Record<string, number>
|
|
controls: ControlItem[]
|
|
}
|
|
|
|
// Addressee axis: who must fulfil an obligation. out-of-scope (authority power /
|
|
// member-state-EU / third party / meta) is advisory — hidden by default, never
|
|
// deleted. oeffentliche_stelle = additive GOV hint (public-sector customer).
|
|
export const ADDRESSEE_LABELS: Record<string, string> = {
|
|
unternehmen: 'Unternehmen',
|
|
oeffentliche_stelle: 'Öffentliche Stelle',
|
|
aufsichtsbefugnis: 'Aufsichtsbehörde',
|
|
staat_eu: 'Mitgliedstaat/EU',
|
|
dritter: 'Dritter',
|
|
meta: 'Meta',
|
|
}
|
|
|
|
export function addresseeLabel(a?: string | null): string {
|
|
return a ? ADDRESSEE_LABELS[a] || a : ''
|
|
}
|
|
|
|
// Provenance line: own library vs derived-from-document (with the document, and
|
|
// article when known). The user wants to see WHERE a derived control came from.
|
|
export function provenanceLabel(
|
|
c: Pick<ControlItem, 'source_type' | 'source_regulation' | 'source_article'>,
|
|
): string {
|
|
if (c.source_type === 'own_library') return 'Eigene Bibliothek'
|
|
const doc = c.source_regulation?.trim()
|
|
if (!doc) return 'Abgeleitet'
|
|
const art = c.source_article?.trim()
|
|
return art ? `Abgeleitet · ${doc} ${art}` : `Abgeleitet · ${doc}`
|
|
}
|
|
|
|
export function provenanceBadgeClass(sourceType: string): string {
|
|
return sourceType === 'own_library'
|
|
? 'bg-amber-100 text-amber-800'
|
|
: 'bg-blue-100 text-blue-800'
|
|
}
|
|
|
|
export function severityBadgeClass(sev: string | null | undefined): string {
|
|
switch ((sev || '').toLowerCase()) {
|
|
case 'critical':
|
|
return 'bg-red-100 text-red-800'
|
|
case 'high':
|
|
return 'bg-orange-100 text-orange-800'
|
|
case 'medium':
|
|
return 'bg-yellow-100 text-yellow-800'
|
|
default:
|
|
return 'bg-gray-100 text-gray-600'
|
|
}
|
|
}
|
|
|
|
// Split into the two display tiers: Kern-Pflichten (relevant) and the
|
|
// 'zur Prüfung' tier (shown but flagged) — never hidden.
|
|
export function splitByTier(controls: ControlItem[]): {
|
|
core: ControlItem[]
|
|
review: ControlItem[]
|
|
} {
|
|
const core: ControlItem[] = []
|
|
const review: ControlItem[] = []
|
|
for (const c of controls) (c.relevant ? core : review).push(c)
|
|
return { core, review }
|
|
}
|
|
|
|
export interface UseCaseGroup {
|
|
group: string
|
|
label: string
|
|
rows: UseCaseRow[]
|
|
}
|
|
|
|
// Group use-cases by their registry group (stable order), each group's rows
|
|
// sorted by how many relevant obligations it carries (desc).
|
|
export function groupUseCases(rows: UseCaseRow[]): UseCaseGroup[] {
|
|
const order = ['document', 'security', 'cross_cutting', 'product']
|
|
const by: Record<string, UseCaseRow[]> = {}
|
|
for (const r of rows) {
|
|
;(by[r.group] ||= []).push(r)
|
|
}
|
|
const groups = order.filter((g) => by[g]?.length)
|
|
for (const g of Object.keys(by)) {
|
|
if (!order.includes(g)) groups.push(g)
|
|
}
|
|
return groups.map((g) => ({
|
|
group: g,
|
|
label: USE_CASE_GROUP_LABELS[g] || g,
|
|
rows: [...by[g]].sort((a, b) => b.atom_relevant - a.atom_relevant),
|
|
}))
|
|
}
|