import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { BSI_KERNEL_CLASS } from '../../src/compliance/bsi-grundschutz'; import { BSI_CATALOG_SOURCE_URL, BSI_CROSSWALK_SOURCE_URL, BsiCatalogService } from '../../server/compliance/bsi-catalog-service'; import { bundledBsiCatalogs } from '../../server/compliance/bsi-snapshots'; // `Update from BSI` (Phase 13F, user decision 2026-10-08): synthetic OSCAL in the published shape; ids and prose are invented. const sourceCatalog = (version: string, ids = ['KONF.1.1', 'KONF.2.1']) => JSON.stringify({ catalog: { metadata: { title: 'Synthetischer Katalog', version }, groups: [{ id: 'KONF', title: 'Konfiguration', controls: ids.map(id => ({ id, class: BSI_KERNEL_CLASS, title: `Titel ${id}`, parts: [{ name: 'statement', prose: `Konfiguration MUSS ${id} umsetzen.` }] })) }] } }); const sourceCrosswalk = (version = '9.9-synthetic', targets = ['KONF.1.1', 'KONF.3.1']) => JSON.stringify({ 'mapping-collection': { metadata: { title: 'Synthetische Zuordnung', version }, mappings: [{ 'target-resource': { href: 'BSI-Stand-der-Technik-Kernel-catalog.json' }, maps: [{ uuid: 'm1', relationship: 'subset-of', sources: [{ 'id-ref': '8.9' }], targets: targets.map(target => ({ 'id-ref': target })) }] }] } }); const NEWER = '2099-01-01T00:00:00+00:00'; let directory: string; let storage: string; beforeEach(async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-bsi-')); storage = join(directory, 'bsi', 'active-catalog.json'); }); afterEach(async () => { await rm(directory, { recursive: true, force: true }); }); function fetcher(files: Record) { return vi.fn(async (url: string | URL | Request) => { const body = files[String(url)]; return body instanceof Response ? body : new Response(body, { status: 200 }); }) as unknown as typeof fetch & ReturnType; } describe('BSI catalog service', () => { it('starts with the bundled snapshots', async () => { const service = new BsiCatalogService(storage, fetcher({})); expect(await service.catalogs()).toBe(bundledBsiCatalogs); expect(await service.current()).toMatchObject({ version: 1, origin: 'bundled', catalogVersion: bundledBsiCatalogs.catalog.version, crosswalkVersion: '1.0.1-qa', requirementCount: 901, missingTargets: ['BER.5.1.2', 'BER.6.8', 'DET.3.1.13', 'GEB.5.4'] }); }); it('stores a newer official state privately, activates it at once and reports it unchanged on the next update', async () => { const fetch = fetcher({ [BSI_CATALOG_SOURCE_URL]: sourceCatalog(NEWER), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() }); const service = new BsiCatalogService(storage, fetch, () => new Date('2026-10-08T12:00:00Z')); const result = await service.refresh(); expect(result).toMatchObject({ updated: true, origin: 'bsi', updatedAt: '2026-10-08T12:00:00.000Z', catalogVersion: NEWER, crosswalkVersion: '9.9-synthetic', requirementCount: 2, crosswalkEntryCount: 1, missingTargets: ['KONF.3.1'] }); // Only the fixed official URLs, without following a redirect. expect(fetch.mock.calls.map(call => [String(call[0]), (call[1] as RequestInit).redirect])).toEqual([ [BSI_CATALOG_SOURCE_URL, 'error'], [BSI_CROSSWALK_SOURCE_URL, 'error']]); expect((await service.catalogs()).catalog.requirements.map(item => item.id)).toEqual(['KONF.1.1', 'KONF.2.1']); expect((await service.catalogs()).catalog.sourceUrl).toBe(BSI_CATALOG_SOURCE_URL); if (process.platform !== 'win32') expect((await stat(storage)).mode & 0o777).toBe(0o600); // A restarted server reads the stored state. expect(await new BsiCatalogService(storage, fetcher({})).current()).toMatchObject({ origin: 'bsi', catalogVersion: NEWER }); expect(await service.refresh()).toMatchObject({ updated: false, catalogVersion: NEWER }); }); it('never steps back behind the active catalog', async () => { const service = new BsiCatalogService(storage, fetcher({ [BSI_CATALOG_SOURCE_URL]: sourceCatalog('2000-01-01T00:00:00+00:00'), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() })); expect(await service.refresh()).toMatchObject({ updated: false, origin: 'bundled' }); await expect(stat(storage)).rejects.toMatchObject({ code: 'ENOENT' }); }); it('fails explicitly and keeps the active catalog when BSI is unreachable, unavailable, too large or invalid', async () => { const cases: Array<[Record | null, string]> = [ [null, 'bsi_unreachable'], [{ [BSI_CATALOG_SOURCE_URL]: new Response('', { status: 404 }), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() }, 'bsi_unavailable'], [{ [BSI_CATALOG_SOURCE_URL]: new Response('{}', { headers: { 'content-length': String(64 * 1024 * 1024) } }), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() }, 'bsi_source_too_large'], [{ [BSI_CATALOG_SOURCE_URL]: sourceCatalog(NEWER, []), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() }, 'bsi_source_invalid'], [{ [BSI_CATALOG_SOURCE_URL]: sourceCatalog(NEWER), [BSI_CROSSWALK_SOURCE_URL]: '{"mapping-collection":{}}' }, 'bsi_source_invalid'] ]; for (const [files, code] of cases) { const download = files ? fetcher(files) : vi.fn(async () => { throw new TypeError("offline"); }) as unknown as typeof globalThis.fetch; const service = new BsiCatalogService(storage, download); await expect(service.refresh()).rejects.toMatchObject({ code }); expect(await service.current()).toMatchObject({ origin: 'bundled' }); } await expect(stat(storage)).rejects.toMatchObject({ code: 'ENOENT' }); }); it('returns to the bundled snapshots on reset', async () => { const service = new BsiCatalogService(storage, fetcher({ [BSI_CATALOG_SOURCE_URL]: sourceCatalog(NEWER), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() })); await service.refresh(); expect(await service.reset()).toMatchObject({ origin: 'bundled', catalogVersion: bundledBsiCatalogs.catalog.version }); expect(await service.catalogs()).toBe(bundledBsiCatalogs); await expect(stat(storage)).rejects.toMatchObject({ code: 'ENOENT' }); }); it('refuses a damaged stored state and lets a stored state older than the bundled one give way to it', async () => { const service = new BsiCatalogService(storage, fetcher({ [BSI_CATALOG_SOURCE_URL]: sourceCatalog(NEWER), [BSI_CROSSWALK_SOURCE_URL]: sourceCrosswalk() })); await service.refresh(); const stored = JSON.parse(await readFile(storage, 'utf8')); await writeFile(storage, JSON.stringify({ ...stored, catalog: { ...stored.catalog, framework: 'other' } })); await expect(new BsiCatalogService(storage, fetcher({})).current()).rejects.toMatchObject({ code: 'bsi_storage_invalid' }); await writeFile(storage, JSON.stringify({ ...stored, catalog: { ...stored.catalog, version: '2000-01-01T00:00:00+00:00' } })); expect(await new BsiCatalogService(storage, fetcher({})).catalogs()).toBe(bundledBsiCatalogs); }); });