Assessor_2.0/tests/compliance/bsi-grundschutz.test.ts

145 lines
8.8 KiB
TypeScript

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<string, unknown> = {}) => ({
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']);
});
});