Assessor_2.0/tests/compliance/compliance-engine-invarianc...

149 lines
12 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { RegulatoryFindingRetrievalService } from '../../server/compliance/regulatory-finding-retrieval';
import { RegulatoryMappingService } from '../../server/compliance/regulatory-mapping-service';
import { complianceReadPortFrom } from '../../src/compliance/compliance-store';
import { LocalComplianceStore } from '../../src/compliance/local-compliance-store';
import type { RegulatoryChunk, RegulatoryRetrievalRequest } from '../../src/compliance/regulatory-rag';
import type { ScanRun } from '../../src/domain/canonical-model';
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
import { bundledBsiCatalogs } from '../../server/compliance/bsi-snapshots';
import { bsiCorpusVersion } from '../../server/compliance/bsi-corpus';
import { BSI_CORPUS_ID } from '../../src/compliance/regulatory-mapping';
// Permanent on/off invariance of the Compliance Engine (AGENTS.md, "Core Engine and engines"): the Core
// works unchanged with the engine switched off, and running the engine - a single mapping, a decision and
// a filtered batch - leaves every stored ASSESSOR value and the repository revision exactly as they were.
// The engine writes only its own store.
const stamp = '2026-10-05T08:00:00.000Z';
const confidence = { level: 'high' as const, score: 1, reasons: ['synthetic'] };
const provenance = { decisionSource: 'source-native' as const, confidence, mappingVersion: '1.0.0' };
function chunk(sourceStandard: 'ISO/IEC 27001' | 'ISO/IEC 27002'): RegulatoryChunk {
const reference = sourceStandard === 'ISO/IEC 27001';
return {
schemaVersion: 2, corpusId: 'iso-test', corpusVersion: 'v2', documentId: reference ? '27001-5.1' : '27002-5.1-purpose',
framework: 'ISO/IEC 27002', frameworkVersion: '2022', controlId: '5.1', annexAControlId: 'A.5.1',
controlTheme: 'organizational', sourceStandard, sourceEdition: '2022', nationalEdition: 'DIN 2024',
sectionKind: reference ? 'control' : 'purpose', sourcePageStart: 1, sourcePageEnd: 1,
retrievalRole: reference ? 'reference' : 'semantic', parserVersion: 'test', sourceSha256: reference ? '1'.repeat(64) : '2'.repeat(64),
chunkId: reference ? 'reference-5.1' : 'semantic-5.1', chunkIndex: 0, language: 'de', title: 'Synthetic control',
text: reference ? 'Synthetic normative statement.' : 'Synthetic semantic evidence.', textSha256: 'a'.repeat(64)
};
}
async function syntheticRepository(): Promise<LocalAssessorRepository> {
const repository = new LocalAssessorRepository();
await repository.saveProject({ id: 'dummy-project', name: 'Dummy Customer', createdAt: stamp, updatedAt: stamp, attributes: {} });
await repository.saveAssessment({ id: 'dummy-assessment', projectId: 'dummy-project', name: 'Dummy', createdAt: stamp, updatedAt: stamp });
const scanRun: ScanRun = { id: 'dummy-run', assessmentId: 'dummy-assessment', sequence: 1, label: 'Dummy',
tool: { id: 'purple-knight', name: 'Purple Knight' }, importedAt: stamp };
const session = await repository.beginImport({ sessionId: 'dummy-session', projectId: 'dummy-project', assessmentId: 'dummy-assessment', scanRun });
await session.stage({
importFiles: [{ id: 'dummy-file', scanRunId: scanRun.id, fileName: 'dummy.csv', format: 'CSV', sizeBytes: 10, importedAt: stamp,
detectedTool: scanRun.tool, detection: confidence,
processedWith: { parserId: 'csv', parserVersion: '1.0.0', profileId: 'purple-knight', profileVersion: '1.0.0', mappingVersion: '1.0.0' },
sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }],
findingDefinitions: [{ id: 'dummy-definition', canonicalKey: 'purple-knight:A-SmbSigning',
identity: { version: 1, strategy: 'tool-rule-id', durable: true, evidence: ['synthetic'] },
title: 'SMB signing is not required on domain controllers', category: 'Synthetic', frameworkReferences: [], createdAt: stamp, updatedAt: stamp }],
findingOccurrences: [{ id: 'dummy-occurrence', definitionId: 'dummy-definition', scanRunId: scanRun.id, importFileId: 'dummy-file',
sourceIdentity: { tool: scanRun.tool, originalRuleId: 'A-SmbSigning', canonicalRuleId: 'A-SmbSigning',
originalTitle: 'SMB signing is not required on domain controllers' },
sourcePosition: { rowIndex: 1 }, originalFields: [{ key: 'Status', label: 'Status', value: 'Fail', columnIndex: 0 }],
fieldMappings: [], severity: { originalValue: 'High', normalizedValue: 'high', provenance },
status: { originalValue: 'Fail', normalizedValue: 'failed', provenance } }]
});
await session.commit();
return repository;
}
describe('Compliance Engine on/off invariance', () => {
it('leaves every stored ASSESSOR value and the repository revision unchanged', async () => {
const repository = await syntheticRepository();
// Switched off: the Core alone, read before any engine work.
const off = { revision: repository.revision, data: JSON.stringify(await repository.exportProject('dummy-project')) };
const resultDbOff = JSON.stringify(await repository.queryUnifiedResults({ audience: 'result-db', projectId: 'dummy-project' }));
const store = new LocalComplianceStore();
const service = new RegulatoryMappingService(complianceReadPortFrom(repository), store, {
findingRetrieval: new RegulatoryFindingRetrievalService({ retrieve: vi.fn(async (_request: RegulatoryRetrievalRequest) => [{ score: 0.9, chunk: chunk('ISO/IEC 27002') }]) }),
index: { ensureCollection: vi.fn(), replaceDocuments: vi.fn(), searchDense: vi.fn(), list: vi.fn(async () => [chunk('ISO/IEC 27001')]) } as never,
client: { model: 'work-model', complete: vi.fn(async () => ({ value: {
schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high',
rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }]
} })) } as never,
corpusId: 'iso-test', corpusVersion: 'v2', embeddingModel: 'bge-m3', now: () => new Date(stamp)
});
const resultProjectionId = 'scanner-finding:dummy-occurrence';
await service.start('dummy-project', { resultProjectionId });
await service.close();
// The first proposal is accepted automatically; a manual decision then replaces it.
const automatic = (await service.get('dummy-project', resultProjectionId)).decisions[0];
await service.decide('dummy-project', { resultProjectionId, controlId: '5.1', decision: 'rejected', expectedLatestDecisionId: automatic.id });
await service.startBatch('dummy-project', { assessmentIds: ['dummy-assessment'], scanRunIds: [], filters: {}, idempotencyKey: 'dummy-batch-1' });
await service.close();
// The engine really ran and kept its results in its own store.
const engine = await store.exportProject('dummy-project');
expect(engine.regulatoryMappingRuns.map(run => run.status)).toEqual(['succeeded']);
expect(engine.regulatoryMappingDecisions.map(decision => decision.decision)).toEqual(['accepted', 'rejected']);
expect(engine.regulatoryMappingBatchRuns.map(batch => batch.status)).toEqual(['completed']);
// Switched on: the Core is untouched.
expect(repository.revision).toBe(off.revision);
expect(JSON.stringify(await repository.exportProject('dummy-project'))).toBe(off.data);
expect(JSON.stringify(await repository.queryUnifiedResults({ audience: 'result-db', projectId: 'dummy-project' }))).toBe(resultDbOff);
});
it('leaves the Core unchanged through a direct BSI mapping, its decisions, the ISO <-> BSI review and a BSI batch', async () => {
const repository = await syntheticRepository();
const off = { revision: repository.revision, data: JSON.stringify(await repository.exportProject('dummy-project')) };
const resultDbOff = JSON.stringify(await repository.queryUnifiedResults({ audience: 'result-db', projectId: 'dummy-project' }));
const requirementId = bundledBsiCatalogs.catalog.requirements[0].id;
const corpusVersion = bsiCorpusVersion(bundledBsiCatalogs.catalog);
const bsiChunk: RegulatoryChunk = { schemaVersion: 1, corpusId: BSI_CORPUS_ID, corpusVersion, documentId: `bsi-kernel:${requirementId}`,
framework: 'BSI Grundschutz++', frameworkVersion: bundledBsiCatalogs.catalog.version, controlId: requirementId, chunkId: 'bsi-chunk', chunkIndex: 0,
language: 'de', title: 'Synthetic requirement', text: 'Synthetic requirement text.', textSha256: 'b'.repeat(64) };
const isBsi = (filters?: { corpusId?: string }) => filters?.corpusId === BSI_CORPUS_ID;
const store = new LocalComplianceStore();
const service = new RegulatoryMappingService(complianceReadPortFrom(repository), store, {
findingRetrieval: new RegulatoryFindingRetrievalService({ retrieve: vi.fn(async (request: RegulatoryRetrievalRequest) =>
[{ score: 0.9, chunk: isBsi(request.filters) ? bsiChunk : chunk('ISO/IEC 27002') }]) }),
index: { ensureCollection: vi.fn(), replaceDocuments: vi.fn(), searchDense: vi.fn(),
list: vi.fn(async (filters: { corpusId?: string }) => [isBsi(filters) ? bsiChunk : chunk('ISO/IEC 27001')]) } as never,
client: { model: 'work-model', complete: vi.fn(async (system: string) => ({ value: system.includes('BSI Grundschutz++')
? { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: requirementId, relationship: 'primary', confidence: 'high',
rationale: 'Synthetic rationale.', evidenceChunkIds: ['bsi-chunk'] }] }
: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high',
rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] } })) } as never,
corpusId: 'iso-test', corpusVersion: 'v2', embeddingModel: 'bge-m3', now: () => new Date(stamp),
bsiCorpus: { configured: true, ensure: vi.fn(async () => corpusVersion) } as never
});
const resultProjectionId = 'scanner-finding:dummy-occurrence';
await service.start('dummy-project', { resultProjectionId });
await service.start('dummy-project', { resultProjectionId, framework: 'bsi-kernel' });
await service.close();
const automatic = (await service.get('dummy-project', resultProjectionId, 'bsi-kernel')).decisions[0];
await service.decide('dummy-project', { resultProjectionId, framework: 'bsi-kernel', controlId: requirementId, decision: 'accepted',
comment: 'Confirmed.', expectedLatestDecisionId: automatic.id });
const review = await service.consistency('dummy-project', { assessmentIds: ['dummy-assessment'], scanRunIds: [] });
expect(review.items).toHaveLength(1);
await service.markConsistencyReviewed('dummy-project', { resultProjectionId });
await service.startBatch('dummy-project', { assessmentIds: ['dummy-assessment'], scanRunIds: [], filters: {}, idempotencyKey: 'dummy-bsi-batch', framework: 'bsi-kernel' });
await service.close();
const engine = await store.exportProject('dummy-project');
expect(engine.regulatoryMappingRuns.map(run => run.framework ?? 'iso27001').sort()).toEqual(['bsi-kernel', 'iso27001']);
expect(engine.regulatoryConsistencyReviews).toHaveLength(1);
expect(engine.regulatoryMappingBatchRuns.map(batch => [batch.framework, batch.status])).toEqual([['bsi-kernel', 'completed']]);
expect(repository.revision).toBe(off.revision);
expect(JSON.stringify(await repository.exportProject('dummy-project'))).toBe(off.data);
expect(JSON.stringify(await repository.queryUnifiedResults({ audience: 'result-db', projectId: 'dummy-project' }))).toBe(resultDbOff);
});
it('gives the engine no Core write method', () => {
const port = complianceReadPortFrom(new LocalAssessorRepository());
expect(Object.keys(port).sort()).toEqual(['getProject', 'getUnifiedResultById', 'listAssessments', 'listProjectScanRuns',
'listProjects', 'queryUnifiedResults']);
});
});