import { describe, expect, it } from 'vitest'; import { BSI_KERNEL_CLASS, bsiIsoCrosswalk, bsiKernelCatalog, bsiRequirementText, deriveBsiRequirements } from '../../src/compliance/bsi-grundschutz'; import pinnedCatalog from '../../server/compliance/data/bsi-grundschutz-kernel-2026-09-24.json'; import pinnedCrosswalk from '../../server/compliance/data/bsi-iso27001-crosswalk-1.0.1-qa.json'; import type { BsiIsoCrosswalk, BsiKernelCatalog } from '../../src/compliance/bsi-grundschutz'; // Synthetic OSCAL in the published shape; ids, titles and prose are invented. const control = (id: string, extra: Record = {}) => ({ id, class: BSI_KERNEL_CLASS, title: `Titel ${id}`, props: [{ name: 'sec_level', value: 'normal-SdT' }, { name: 'effort_level', value: '2' }], parts: [ { id: `${id}_stm`, name: 'statement', props: [{ name: 'modal_verb', value: 'MUSS' }], prose: `Praktik MUSS ${id} umsetzen.` }, { id: `${id}_gdn`, name: 'guidance', prose: `Erlaeuterung zu ${id}.` } ], ...extra }); const syntheticCatalog = (controls: unknown[], groups: unknown[] = []) => ({ catalog: { metadata: { title: 'Synthetischer Katalog', version: '2026-01-01T00:00:00+00:00' }, groups: [ { id: 'KONF', title: 'Konfiguration', controls }, { id: 'GC', title: 'Governance', controls: [{ ...control('GC.1.1'), class: 'BSI-Methodik-Grundschutz-plus-plus' }] }, ...groups ] } }); const syntheticMapping = (maps: unknown[]) => ({ 'mapping-collection': { metadata: { title: 'Synthetische Zuordnung', version: '0.1' }, mappings: [ { 'target-resource': { href: 'BSI-Methodik-Grundschutz++-catalog.json' }, maps: [ { uuid: 'm0', relationship: 'subset-of', sources: [{ 'id-ref': '5.1' }], targets: [{ 'id-ref': 'GC.1.1' }] }] }, { 'target-resource': { href: 'BSI-Stand-der-Technik-Kernel-catalog.json' }, maps } ] } }); const map = (uuid: string, iso: string, relationship: string, targets: string[]) => ({ uuid, relationship, sources: [{ 'id-ref': iso }], targets: targets.map(target => ({ 'id-ref': target })) }); describe('BSI Kernel catalog conversion', () => { it('keeps only Kernel requirements with practice, parent, statement and parameters as published', () => { const catalog = bsiKernelCatalog(syntheticCatalog([ control('KONF.1.1', { controls: [control('KONF.1.1.1', { params: [{ id: 'konf.1.1.1-prm1', label: 'regelmaessig' }], parts: [{ id: 's', name: 'statement', prose: 'Praktik MUSS {{ insert: param, konf.1.1.1-prm1 }} pruefen.' }] })] }) ]), 'https://example.test/catalog.json', 'a'.repeat(64)); expect(catalog.requirements.map(item => item.id)).toEqual(['KONF.1.1', 'KONF.1.1.1']); expect(catalog.practices).toEqual([{ id: 'KONF', title: 'Konfiguration' }]); expect(catalog.requirements[0]).toMatchObject({ practice: 'KONF', modalVerb: 'MUSS', guidance: 'Erlaeuterung zu KONF.1.1.', securityLevel: 'normal-SdT', effortLevel: '2' }); const child = catalog.requirements[1]; expect(child).toMatchObject({ parentId: 'KONF.1.1', statement: 'Praktik MUSS {{ insert: param, konf.1.1.1-prm1 }} pruefen.' }); expect(child.guidance).toBeUndefined(); expect(bsiRequirementText(child.statement, child.params)).toBe('Praktik MUSS regelmaessig pruefen.'); expect(catalog).toMatchObject({ version: '2026-01-01T00:00:00+00:00', sourceSha256: 'a'.repeat(64) }); expect(catalog.license).toContain('CC BY-SA 4.0'); }); it('prefers a single published parameter value over its label', () => { const catalog = bsiKernelCatalog(syntheticCatalog([control('KONF.2.1', { params: [{ id: 'p1', label: 'Bezeichnung', values: ['jaehrlich'] }], parts: [{ name: 'statement', prose: 'Praktik MUSS {{ insert: param, p1 }} pruefen.' }] })]), 'u', 's'); expect(bsiRequirementText(catalog.requirements[0].statement, catalog.requirements[0].params)).toBe('Praktik MUSS jaehrlich pruefen.'); }); it('fails explicitly on malformed input instead of producing a partial catalog', () => { expect(() => bsiKernelCatalog({}, 'u', 's')).toThrow(/OSCAL Grundschutz\+\+ catalog/); expect(() => bsiKernelCatalog(syntheticCatalog([]), 'u', 's')).toThrow(/no Kernel requirements/); expect(() => bsiKernelCatalog(syntheticCatalog([control('KONF.1.1'), control('KONF.1.1')]), 'u', 's')).toThrow(/Repeated Kernel requirement ids: KONF.1.1/); expect(() => bsiKernelCatalog(syntheticCatalog([control('konf-1')]), 'u', 's')).toThrow(/Invalid Kernel requirement id/); expect(() => bsiKernelCatalog(syntheticCatalog([control('KONF.1.1', { parts: [{ name: 'guidance', prose: 'x' }] })]), 'u', 's')).toThrow(/no statement/); expect(() => bsiKernelCatalog(syntheticCatalog([control('KONF.1.1', { parts: [{ name: 'statement', prose: 'MUSS {{ insert: param, fehlt }}.' }] })]), 'u', 's')).toThrow(/unknown parameter fehlt/); }); }); describe('BSI ISO 27001 crosswalk conversion', () => { it('keeps only the Kernel mapping with its relationships and targets as published', () => { const crosswalk = bsiIsoCrosswalk(syntheticMapping([map('m1', '8.9', 'subset-of', ['KONF.1.1', 'KONF.1.1.1'])]), 'u', 's'); expect(crosswalk.targetCatalog).toBe('BSI-Stand-der-Technik-Kernel-catalog.json'); expect(crosswalk.entries).toEqual([{ uuid: 'm1', isoControlId: '8.9', relationship: 'subset-of', targets: ['KONF.1.1', 'KONF.1.1.1'] }]); }); it('fails explicitly on unknown relationships, sources or targets', () => { expect(() => bsiIsoCrosswalk(syntheticMapping([map('m', '8.9', 'related-to', ['KONF.1.1'])]), 'u', 's')).toThrow(/Unknown mapping relationship/); expect(() => bsiIsoCrosswalk(syntheticMapping([map('m', 'A.8.9', 'subset-of', ['KONF.1.1'])]), 'u', 's')).toThrow(/ISO Annex A source/); expect(() => bsiIsoCrosswalk(syntheticMapping([map('m', '8.9', 'subset-of', [])]), 'u', 's')).toThrow(/Invalid mapping targets/); expect(() => bsiIsoCrosswalk({ 'mapping-collection': { metadata: { version: '1' }, mappings: [] } }, 'u', 's')).toThrow(/exactly one mapping to the Kernel catalog/); }); }); describe('BSI requirement derivation from accepted ISO controls', () => { const catalog = bsiKernelCatalog(syntheticCatalog([control('KONF.1.1'), control('KONF.2.1'), control('KONF.10.1')]), 'u', 's'); const crosswalk = bsiIsoCrosswalk(syntheticMapping([ map('m1', '8.9', 'subset-of', ['KONF.10.1', 'KONF.1.1']), map('m2', '8.20', 'intersects-with', ['KONF.1.1', 'KONF.9.9']), map('m3', '5.15', 'equivalent-to', ['KONF.2.1']) ]), 'u', 's'); it('derives Kernel requirements with every ISO route, reports missing targets and ignores other controls', () => { const derivation = deriveBsiRequirements(['8.20', '8.9'], crosswalk, catalog); expect(derivation.requirements).toEqual([ { requirementId: 'KONF.1.1', title: 'Titel KONF.1.1', practice: 'KONF', via: [ { isoControlId: '8.9', relationship: 'subset-of' }, { isoControlId: '8.20', relationship: 'intersects-with' }] }, { requirementId: 'KONF.10.1', title: 'Titel KONF.10.1', practice: 'KONF', via: [{ isoControlId: '8.9', relationship: 'subset-of' }] } ]); expect(derivation.missingTargets).toEqual([{ isoControlId: '8.20', requirementId: 'KONF.9.9' }]); expect(derivation).toMatchObject({ crosswalkVersion: '0.1', catalogVersion: '2026-01-01T00:00:00+00:00' }); }); it('is independent of input order and yields nothing without accepted controls', () => { expect(deriveBsiRequirements(['8.9', '8.20', '8.9'], crosswalk, catalog)).toEqual(deriveBsiRequirements(['8.20', '8.9'], crosswalk, catalog)); expect(deriveBsiRequirements([], crosswalk, catalog)).toMatchObject({ requirements: [], missingTargets: [] }); }); }); describe('pinned BSI snapshots', () => { const catalog = pinnedCatalog as BsiKernelCatalog; const crosswalk = pinnedCrosswalk as BsiIsoCrosswalk; it('carry provenance and the decided Kernel scope', () => { expect(catalog).toMatchObject({ framework: 'bsi-grundschutz-plus-plus', catalogClass: BSI_KERNEL_CLASS, version: '2026-09-24T07:12:39.131983+00:00' }); expect(catalog.requirements).toHaveLength(901); expect(new Set(catalog.requirements.map(item => item.id)).size).toBe(901); expect(catalog.sourceUrl).toMatch(/^https:\/\/raw\.githubusercontent\.com\/BSI-Bund\/Stand-der-Technik-Bibliothek\/[0-9a-f]{40}\//); expect(catalog.sourceSha256).toMatch(/^[0-9a-f]{64}$/); expect(crosswalk).toMatchObject({ version: '1.0.1-qa', targetCatalog: 'BSI-Stand-der-Technik-Kernel-catalog.json' }); expect(crosswalk.sourceSha256).toMatch(/^[0-9a-f]{64}$/); }); it('report exactly the mapping targets the pinned catalog no longer contains', () => { const allIso = [...new Set(crosswalk.entries.map(entry => entry.isoControlId))]; const missing = deriveBsiRequirements(allIso, crosswalk, catalog).missingTargets.map(item => item.requirementId); expect([...new Set(missing)].sort()).toEqual(['BER.5.1.2', 'BER.6.8', 'DET.3.1.13', 'GEB.5.4']); }); });