import { describe, expect, it, vi } from 'vitest'; import { AUTO_ACCEPT_COMMENT, RegulatoryMappingService } from '../../server/compliance/regulatory-mapping-service'; import { RegulatoryFindingRetrievalService } from '../../server/compliance/regulatory-finding-retrieval'; import { acceptedControlIds, REGULATORY_GPO_QUERY_VERSION, REGULATORY_MAPPING_PROMPT_VERSION, type RegulatoryMappingBatchRun, type RegulatoryMappingDecision, type RegulatoryMappingRun } from '../../src/compliance/regulatory-mapping'; import type { RegulatoryChunk, RegulatoryRetrievalRequest } from '../../src/compliance/regulatory-rag'; import { filterUnifiedResults, type UnifiedResultProjection } from '../../src/domain/unified-result'; import type { ComplianceReadPort, ComplianceStore } from '../../src/compliance/compliance-store'; type EngineRepository = ComplianceReadPort & ComplianceStore; import { removeResultDbScopeIdentifiers } from '../../src/domain/result-db-disclosure'; import { complianceWorkFacetIndex } from '../../src/compliance/ui/compliance-work-facets'; import type { FindingRecord } from '../../src/domain/finding'; const stamp = '2026-09-15T10:00:00.000Z'; // Result-table / occurrence data that must never reach the model or the query. const resultTableAsset = 'DC01.customer.local'; const customerPolicyValue = 'CUSTOMER\synthetic.account'; function finding(overrides: Partial = {}): UnifiedResultProjection { return { resultProjectionId: 'scanner-finding:occurrence-1', sourceKind: 'scanner-finding', sourceResultId: 'occurrence-1', projectId: 'project-1', assessmentId: 'assessment-1', collectionRunId: 'run-1', scanRunId: 'run-1', sourceTool: 'purple-knight', sourceRuleId: 'A-SmbSigning', title: 'SMB signing is not required on domain controllers', description: 'The SMB server permits unsigned sessions, allowing relay attacks.', affectedAsset: resultTableAsset, severityOriginal: 'high', severityNormalized: 'high', outcomeOriginal: 'fail', outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'compliance_check', isIoe: false, isDeviation: true, lifecycleState: 'new', mappingConfidence: 1, reviewStatus: 'unreviewed', importedAt: stamp, createdAt: stamp, frameworkReferences: [], categories: [], ...overrides }; } 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) }; } /** In-memory repository keyed like the global (rule-keyed) schema. */ function mockRepository(findings: UnifiedResultProjection[]) { const byProjection = new Map(findings.map(item => [item.resultProjectionId, item])); const runs = new Map(); const batches: RegulatoryMappingBatchRun[] = []; const decisions: RegulatoryMappingDecision[] = []; return { getProject: vi.fn(async (projectId: string) => findings.some(item => item.projectId === projectId) ? { id: projectId, name: 'Synthetic project' } : null), listProjects: vi.fn(async () => [...new Set(findings.map(item => item.projectId))].map(id => ({ id, name: 'Synthetic project' }))), listAssessments: vi.fn(async (projectId: string) => [...new Set(findings.filter(item => item.projectId === projectId).map(item => item.assessmentId).filter(Boolean))] .map(id => ({ id: id!, projectId, label: 'Synthetic assessment', createdAt: stamp }))), listProjectScanRuns: vi.fn(async (projectId: string) => [...new Map(findings.filter(item => item.projectId === projectId && item.scanRunId) .map(item => [item.scanRunId!, { id: item.scanRunId!, assessmentId: item.assessmentId!, sequence: 1, startedAt: stamp }])).values()]), queryUnifiedResults: vi.fn(async (query = {}) => { const filtered = filterUnifiedResults(findings, query); const offset = query.offset ?? 0; return { total: filtered.length, items: filtered.slice(offset, offset + (query.limit ?? filtered.length)) }; }), // Mirrors the real Result DB read: scoped to the project, returned without real scope ids. getUnifiedResultById: vi.fn(async (id: string, projectId?: string) => { const item = byProjection.get(id); return item && (projectId === undefined || item.projectId === projectId) ? removeResultDbScopeIdentifiers(item) : null; }), getRegulatoryMappingRun: vi.fn(async (id: string) => runs.get(id) ?? null), claimRegulatoryMappingRun: vi.fn(async (run: RegulatoryMappingRun, force: boolean) => { const existing = runs.get(run.id); if (existing && (['queued', 'running'].includes(existing.status) || existing.status === 'succeeded' && !force)) { return { run: existing, claimed: false }; } runs.set(run.id, run); return { run, claimed: true }; }), saveRegulatoryMappingRun: vi.fn(async (run: RegulatoryMappingRun) => { runs.set(run.id, run); return run; }), listRegulatoryMappingRuns: vi.fn(async () => [...runs.values()]), createRegulatoryMappingBatchRun: vi.fn(async (run: RegulatoryMappingBatchRun) => { const existing = batches.find(item => item.projectId === run.projectId && (item.idempotencyKey === run.idempotencyKey || item.scopeKey === run.scopeKey && ['queued', 'running'].includes(item.status))); if (existing) return existing; batches.push(run); return run; }), updateRegulatoryMappingBatchRun: vi.fn(async (run: RegulatoryMappingBatchRun) => { const index = batches.findIndex(item => item.id === run.id); if (index < 0) throw new Error('missing batch'); batches[index] = run; return run; }), listRegulatoryMappingBatchRuns: vi.fn(async (projectId: string, scopeKey?: string) => batches .filter(item => item.projectId === projectId && (!scopeKey || item.scopeKey === scopeKey)) .slice().reverse()), createRegulatoryMappingDecision: vi.fn(async (decision: RegulatoryMappingDecision, expected?: string) => { const run = runs.get(decision.mappingId); if (!run || run.ruleKey !== decision.ruleKey || run.status !== 'succeeded') throw new Error('missing mapping'); if (!run.candidates.some(c => c.controlId === decision.controlId)) throw new Error('ungrounded control'); const latest = decisions.filter(d => d.ruleKey === decision.ruleKey && d.controlId === decision.controlId) .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id))[0]; if ((expected ?? undefined) !== (latest?.id ?? undefined)) throw new Error('conflict'); const stored = { ...decision, ...(latest ? { previousDecisionId: latest.id } : {}) }; decisions.push(stored); return stored; }), listRegulatoryMappingDecisions: vi.fn(async (ruleKey: string) => decisions.filter(d => d.ruleKey === ruleKey) .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id))) } as unknown as EngineRepository; } function service(repository: EngineRepository, complete: (system: string, input: string) => Promise) { return new RegulatoryMappingService(repository, repository, { findingRetrieval: new RegulatoryFindingRetrievalService({ retrieve: vi.fn(async (_r: 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')]) }, client: { model: 'work-model', complete: complete as never }, corpusId: 'iso-test', corpusVersion: 'v2', embeddingModel: 'bge-m3', now: () => new Date(stamp) }); } describe('regulatory mapping service', () => { it('shares one global mapping and decision across projects and sends no result-table data', async () => { const findingB = finding({ resultProjectionId: 'scanner-finding:occurrence-2', sourceResultId: 'occurrence-2', projectId: 'project-2', affectedAsset: 'DC02.other.local' }); const repository = mockRepository([finding(), findingB]); const complete = vi.fn(async (_system: string, _input: string) => ({ value: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] }, usage: { totalTokens: 12 } })); const svc = service(repository, complete); await svc.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await svc.close(); // The same rule mapped once in project-1 is visible to project-2 (global sharing). const shared = await svc.get('project-2', 'scanner-finding:occurrence-2'); expect(shared).toMatchObject({ state: 'succeeded', run: { ruleKey: 'purple-knight:A-SmbSigning', sourceTool: 'purple-knight', embeddingModel: 'bge-m3', tokenUsage: { totalTokens: 12 } } }); expect(shared.run?.result?.mappings[0].controlId).toBe('5.1'); expect(shared.run?.promptVersion).toBe(REGULATORY_MAPPING_PROMPT_VERSION); expect(complete).toHaveBeenCalledOnce(); // The copyrighted ISO clause text is stripped from the response but still reaches the model. expect(shared.run?.candidates.every(c => c.evidence.every(item => item.text === undefined))).toBe(true); expect(complete.mock.calls[0][0]).toContain('governs the same security subject'); expect(complete.mock.calls[0][1]).toContain('Synthetic semantic evidence.'); expect(complete.mock.calls[0][1]).toContain('SMB signing is not required'); expect(complete.mock.calls[0][1]).not.toContain(resultTableAsset); expect(complete.mock.calls[0][1]).not.toContain('DC02.other.local'); expect(complete.mock.calls[0][1]).not.toContain('A.5.1'); // The automatic acceptance in project-1 is visible to project-2 (global decisions), and so is a later manual decision. expect(shared.decisions.map(d => ({ controlId: d.controlId, decision: d.decision }))).toEqual([{ controlId: '5.1', decision: 'accepted' }]); await svc.decide('project-1', { resultProjectionId: 'scanner-finding:occurrence-1', controlId: '5.1', decision: 'rejected', expectedLatestDecisionId: shared.decisions[0].id }); const afterDecision = await svc.get('project-2', 'scanner-finding:occurrence-2'); const rejected = afterDecision.decisions.find(d => d.decision === 'rejected'); expect(rejected).toMatchObject({ controlId: '5.1', previousDecisionId: shared.decisions[0].id }); expect(acceptedControlIds(afterDecision.decisions)).toEqual([]); }); it('maps a finding although the Result DB row carries no real project id, but never across projects', async () => { const repository = mockRepository([finding()]); const complete = vi.fn(async (_system: string, _input: string) => ({ value: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] } })); const svc = service(repository, complete); expect((await repository.getUnifiedResultById('scanner-finding:occurrence-1'))?.projectId).toBe('scoped-project'); await expect(svc.get('project-1', 'scanner-finding:occurrence-1')).resolves.toMatchObject({ generationEnabled: true }); await svc.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await svc.close(); const automatic = (await svc.get('project-1', 'scanner-finding:occurrence-1')).decisions[0]; await expect(svc.decide('project-1', { resultProjectionId: 'scanner-finding:occurrence-1', controlId: '5.1', decision: 'rejected', expectedLatestDecisionId: automatic.id })) .resolves.toMatchObject({ state: 'succeeded' }); await expect(svc.get('project-2', 'scanner-finding:occurrence-1')).rejects.toThrow('does not exist in this project'); await expect(svc.start('project-2', { resultProjectionId: 'scanner-finding:occurrence-1' })).rejects.toThrow('does not exist in this project'); await expect(svc.decide('project-2', { resultProjectionId: 'scanner-finding:occurrence-1', controlId: '5.1', decision: 'rejected' })) .rejects.toThrow('does not exist in this project'); }); it('counts accepted controls once per finding for the timeslot deviations of a client', async () => { const gpo = (id: string) => finding({ resultProjectionId: `policy-evaluation-comparison:${id}`, sourceResultId: `comparison-${id}`, sourceKind: 'policy-evaluation', sourceTool: 'GPOs', sourceRuleId: 'registry:machine:software\policies\synthetic:mode', policyScope: 'computer', title: 'Synthetic policy mode' }); const findings = [ finding(), finding({ resultProjectionId: 'scanner-finding:occurrence-2', sourceResultId: 'occurrence-2' }), finding({ resultProjectionId: 'scanner-finding:not-deviation', sourceResultId: 'not-deviation', isDeviation: false }), finding({ resultProjectionId: 'scanner-finding:rejected', sourceResultId: 'rejected', sourceRuleId: 'A-Rejected', title: 'SMB signing is not enforced' }), // Two baseline packages of one GPO setting are one Work record and count once. gpo('one'), gpo('two') ]; const repository = mockRepository(findings); const svc = service(repository, 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'] }] } }))); for (const resultProjectionId of ['scanner-finding:occurrence-1', 'scanner-finding:rejected', 'policy-evaluation-comparison:one']) { await svc.start('project-1', { resultProjectionId }); await svc.close(); } const accepted = (await svc.get('project-1', 'scanner-finding:rejected')).decisions[0]; await svc.decide('project-1', { resultProjectionId: 'scanner-finding:rejected', controlId: '5.1', decision: 'rejected', expectedLatestDecisionId: accepted.id }); const overview = await svc.controlOverview('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'] }); expect(overview).toMatchObject({ version: 1, projectId: 'project-1', findingCount: 3, controls: [{ controlId: '5.1', annexAControlId: 'A.5.1', title: 'Synthetic control', theme: 'Organizational' }] }); expect(overview.controls[0].findings.map(item => item.resultProjectionId)) .toEqual(['scanner-finding:occurrence-1', 'scanner-finding:occurrence-2', 'policy-evaluation-comparison:one']); // Only the timeslot deviations are read, and only through the read-only port. expect(vi.mocked(repository.queryUnifiedResults).mock.calls.at(-1)![0]).toMatchObject({ audience: 'result-db', deviation: true, projectId: 'project-1' }); // The Work filters read every mappable finding, so the status filter in Work decides what is shown. const assignments = await svc.controlAssignments('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'] }); expect(assignments.findingCount).toBe(4); expect(assignments.controls[0].findings.map(item => item.resultProjectionId)).toContain('scanner-finding:not-deviation'); expect(vi.mocked(repository.queryUnifiedResults).mock.calls.at(-1)?.[0]?.deviation).toBeUndefined(); await expect(svc.controlOverview('project-1', { assessmentIds: [], scanRunIds: [] })).rejects.toThrow('timeslot scope is required'); await expect(svc.controlOverview('project-1', { assessmentIds: ['foreign-assessment'], scanRunIds: [] })).rejects.toThrow('outside this project'); }); it('counts a GPO setting once and gives Work its setting ID although the Result DB withholds it', async () => { // Internal comparisons carry the setting ID; the Result DB rows of the same comparisons do not. const comparison = (id: string, settingId: string, title: string, policyScope: 'computer' | 'user') => finding({ resultProjectionId: `policy-evaluation-comparison:${id}`, sourceResultId: `comparison-${id}`, sourceKind: 'policy-evaluation', sourceTool: 'GPOs', sourceRuleId: settingId, policyScope, title }); const findings = [ // Three baseline packages of one setting, a second setting, the same title in the other scope and // a different setting that shares the first setting's title. comparison('a1', 'gpo-policy-identity-aaaa', 'Synthetic · Mode', 'computer'), comparison('a2', 'gpo-policy-identity-aaaa', 'Synthetic · Mode', 'computer'), comparison('a3', 'gpo-policy-identity-aaaa', 'Synthetic · Mode', 'computer'), comparison('b1', 'gpo-policy-identity-bbbb', 'Security Options · SyntheticKey', 'computer'), comparison('b2', 'gpo-policy-identity-bbbb', 'Security Options · SyntheticKey', 'computer'), comparison('c1', 'gpo-policy-identity-cccc', 'Synthetic · Mode', 'user'), comparison('d1', 'gpo-policy-identity-dddd', 'Synthetic · Mode', 'computer') ]; const repository = mockRepository(findings); const internal = vi.mocked(repository.queryUnifiedResults).getMockImplementation()!; vi.mocked(repository.queryUnifiedResults).mockImplementation(async (query = {}) => { const page = await internal(query); return query.audience === 'internal-analysis' ? page : { ...page, items: page.items.map(item => ({ ...item, sourceRuleId: undefined })) }; }); const svc = service(repository, 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'] }] } }))); const scope = { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true } }; await svc.startBatch('project-1', { ...scope, idempotencyKey: 'batch-gpo-settings' }); await svc.close(); // Mapping stays one global rule per disclosed title; counting follows the Work settings. expect((await svc.getBatch('project-1', scope)).run?.counts) .toMatchObject({ filteredResultCount: 7, eligibleFindingCount: 4, distinctRuleCount: 2 }); const overview = await svc.controlOverview('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'] }); expect(overview.findingCount).toBe(4); expect(overview.controls[0].findings.map(item => `${item.policyScope}:${item.sourceRuleId}`).sort()).toEqual([ 'computer:gpo-policy-identity-aaaa', 'computer:gpo-policy-identity-bbbb', 'computer:gpo-policy-identity-dddd', 'user:gpo-policy-identity-cccc']); // The internal read is limited to GPO comparisons of the same timeslot. const internalQueries = vi.mocked(repository.queryUnifiedResults).mock.calls.map(call => call[0] ?? {}) .filter(query => query.audience === 'internal-analysis'); expect(internalQueries.length).toBeGreaterThan(0); expect(internalQueries.every(query => query.projectId === 'project-1' && query.sourceKind?.join() === 'policy-evaluation' && query.assessmentIds?.join() === 'assessment-1' && query.scanRunIds?.join() === 'run-1')).toBe(true); // The Work ISO filter finds the rolled-up GPO setting by scope and setting ID. const workSetting = { id: `gpo-work-setting:${encodeURIComponent(JSON.stringify(['project-1', 'aggregation-1', 'computer', 'gpo-policy-identity-aaaa']))}` } as FindingRecord; expect(complianceWorkFacetIndex(overview).facets[0].valuesFor!(workSetting)).toEqual(['5.1']); }); it('accepts the first two proposals automatically, keeps existing decisions and accepts nothing without a proposal', async () => { const semantic = (controlId: string): RegulatoryChunk => ({ ...chunk('ISO/IEC 27002'), controlId, annexAControlId: `A.${controlId}`, documentId: `27002-${controlId}`, chunkId: `semantic-${controlId}` }); const proposal = (controlId: string) => ({ controlId, relationship: 'supporting', confidence: 'medium', rationale: 'Synthetic rationale.', evidenceChunkIds: [`semantic-${controlId}`] }); const build = (repository: EngineRepository, value: unknown) => new RegulatoryMappingService(repository, repository, { findingRetrieval: new RegulatoryFindingRetrievalService({ retrieve: vi.fn(async (_r: RegulatoryRetrievalRequest) => ['8.20', '5.15', '5.1'].map((controlId, index) => ({ score: 0.9 - index / 10, chunk: semantic(controlId) }))) }), index: { ensureCollection: vi.fn(), replaceDocuments: vi.fn(), searchDense: vi.fn(), list: vi.fn(async () => [chunk('ISO/IEC 27001')]) }, client: { model: 'work-model', complete: vi.fn(async () => ({ value })) as never }, corpusId: 'iso-test', corpusVersion: 'v2', embeddingModel: 'bge-m3', now: () => new Date(stamp) }); const accepted = async (svc: RegulatoryMappingService) => acceptedControlIds((await svc.get('project-1', 'scanner-finding:occurrence-1')).decisions).sort(); const mapped = (...controls: string[]) => ({ schemaVersion: 1, outcome: 'mapped', mappings: controls.map(proposal) }); // Three proposals: the first two in the model's order are accepted, the third stays undecided. const three = build(mockRepository([finding()]), mapped('5.15', '8.20', '5.1')); await three.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await three.close(); expect(await accepted(three)).toEqual(['5.15', '8.20']); expect((await three.get('project-1', 'scanner-finding:occurrence-1')).decisions.every(item => item.comment === AUTO_ACCEPT_COMMENT)).toBe(true); // A single proposal is accepted alone. const one = build(mockRepository([finding()]), mapped('8.20')); await one.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await one.close(); expect(await accepted(one)).toEqual(['8.20']); // Regenerating never overrides a manual reject; a still undecided first proposal is accepted. const automatic = (await three.get('project-1', 'scanner-finding:occurrence-1')).decisions.find(item => item.controlId === '5.15')!; await three.decide('project-1', { resultProjectionId: 'scanner-finding:occurrence-1', controlId: '5.15', decision: 'rejected', expectedLatestDecisionId: automatic.id }); await three.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await three.close(); expect(await accepted(three)).toEqual(['8.20']); // Without a proposal nothing is accepted. const none = build(mockRepository([finding()]), { schemaVersion: 1, outcome: 'no_relevant_control', mappings: [], reason: 'Synthetic refusal.' }); await none.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await none.close(); expect((await none.get('project-1', 'scanner-finding:occurrence-1')).decisions).toEqual([]); }); it('keeps a control decided by an earlier run reviewable after a new pipeline version stops retrieving it', async () => { const semantic = (controlId: string): RegulatoryChunk => ({ ...chunk('ISO/IEC 27002'), controlId, annexAControlId: `A.${controlId}`, documentId: `27002-${controlId}`, chunkId: `semantic-${controlId}` }); const repository = mockRepository([finding()]); const build = (corpusVersion: string, controlId: string) => new RegulatoryMappingService(repository, repository, { findingRetrieval: new RegulatoryFindingRetrievalService({ retrieve: vi.fn(async (_r: RegulatoryRetrievalRequest) => [{ score: 0.9, chunk: semantic(controlId) }]) }), index: { ensureCollection: vi.fn(), replaceDocuments: vi.fn(), searchDense: vi.fn(), list: vi.fn(async () => [chunk('ISO/IEC 27001')]) }, client: { model: 'work-model', complete: vi.fn(async () => ({ value: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId, relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: [`semantic-${controlId}`] }] } })) as never }, corpusId: 'iso-test', corpusVersion, embeddingModel: 'bge-m3', now: () => new Date(stamp) }); const older = build('v1', '8.3'); await older.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await older.close(); const current = build('v2', '8.2'); await current.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await current.close(); const response = await current.get('project-1', 'scanner-finding:occurrence-1'); expect(acceptedControlIds(response.decisions).sort()).toEqual(['8.2', '8.3']); expect(response.run?.candidates.map(item => item.controlId)).toEqual(['8.2']); expect(response.earlierControls).toEqual([{ controlId: '8.3', annexAControlId: 'A.8.3', title: semantic('8.3').title }]); // Rejecting the earlier control is grounded by the older run that retrieved it. const earlier = response.decisions.find(item => item.controlId === '8.3')!; const rejected = await current.decide('project-1', { resultProjectionId: 'scanner-finding:occurrence-1', controlId: '8.3', decision: 'rejected', expectedLatestDecisionId: earlier.id }); expect(acceptedControlIds(rejected.decisions)).toEqual(['8.2']); expect(rejected.decisions.find(item => item.controlId === '8.3' && item.previousDecisionId === earlier.id)).toMatchObject({ decision: 'rejected', mappingId: earlier.mappingId }); }); it('keeps a saved mapping readable when the retrieval index is unavailable', async () => { const repository = mockRepository([finding()]); const complete = vi.fn(async (_system: string, _input: string) => ({ value: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] } })); const online = service(repository, complete); await online.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await online.close(); const offline = new RegulatoryMappingService(repository, repository, { findingRetrieval: {} as never, index: { list: vi.fn(async () => { throw new Error('Index unavailable'); }) } as never, client: { model: 'work-model', complete } as never, corpusId: 'iso-test', corpusVersion: 'v2', embeddingModel: 'bge-m3' }); const response = await offline.get('project-1', 'scanner-finding:occurrence-1'); expect(response).toMatchObject({ generationEnabled: false, state: 'succeeded', run: { status: 'succeeded' } }); expect(response.run?.result?.mappings[0].controlId).toBe('5.1'); expect(response.run?.candidates.every(candidate => candidate.evidence.every(item => item.text === undefined))).toBe(true); }); it('drops one ungrounded evidence id but keeps the otherwise-valid mapping', async () => { const { svc } = await runWith({ schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'not-a-supplied-chunk'] }] }); const completed = await svc.get('project-1', 'scanner-finding:occurrence-1'); expect(completed.run?.status).toBe('succeeded'); expect(completed.run?.result?.mappings[0].evidenceChunkIds).toEqual(['semantic-5.1']); expect(completed.run?.errorCode).toBeUndefined(); }); it('fails safely with a structural detail when the model maps to an unknown control', async () => { const { svc } = await runWith({ schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: 'A.5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1'] }] }); const completed = await svc.get('project-1', 'scanner-finding:occurrence-1'); expect(completed.run?.status).toBe('failed'); expect(completed.run?.errorCode).toBe('regulatory_mapping_invalid_output'); expect(completed.run?.errorDetail).toContain('unknown candidate control'); expect(completed.run?.errorDetail).not.toContain(resultTableAsset); }); it('maps the complete filtered timeslot including GPO comparisons, excludes context rows and deduplicates global rules', async () => { const findings = [ finding(), finding({ resultProjectionId: 'scanner-finding:occurrence-2', sourceResultId: 'occurrence-2' }), finding({ resultProjectionId: 'scanner-finding:occurrence-3', sourceResultId: 'occurrence-3', sourceRuleId: 'A-Second', title: 'Second synthetic check' }), finding({ resultProjectionId: 'policy-observation:one', sourceResultId: 'policy-one', sourceKind: 'policy-observation', sourceRuleId: undefined }), // Two baseline packages of one GPO setting share one global rule. ...['one', 'two'].map(id => finding({ resultProjectionId: `policy-evaluation-comparison:${id}`, sourceResultId: `comparison-${id}`, sourceKind: 'policy-evaluation', sourceTool: 'GPOs', sourceRuleId: 'registry:machine:software\policies\synthetic:mode', title: 'Synthetic policy mode', policyActualValue: { principals: [customerPolicyValue] } })), finding({ resultProjectionId: 'scanner-finding:unsupported', sourceResultId: 'unsupported', sourceRuleId: 'A-Unsupported', title: '' }), finding({ resultProjectionId: 'scanner-finding:not-deviation', sourceResultId: 'not-deviation', sourceRuleId: 'A-Ignored', isDeviation: false }) ]; const repository = mockRepository(findings); const complete = vi.fn(async (_system: string, _input: string) => ({ value: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] } })); const svc = service(repository, complete); await svc.startBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true, affectedAsset: resultTableAsset }, idempotencyKey: 'batch-filtered-1' }); await svc.close(); const response = await svc.getBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true, affectedAsset: resultTableAsset } }); expect(response.run).toMatchObject({ status: 'completed', counts: { filteredResultCount: 7, excludedNonScannerCount: 1, // The two baseline comparisons of one GPO setting are one eligible finding, like its Work record. eligibleFindingCount: 4, unsupportedFindingCount: 1, distinctRuleCount: 3, scheduledRuleCount: 3, succeededRuleCount: 3, failedRuleCount: 0 } }); expect(complete).toHaveBeenCalledTimes(3); expect(complete.mock.calls.every(call => !String(call[1]).includes(resultTableAsset) && !String(call[1]).includes('synthetic.account'))).toBe(true); }); it('maps a single GPO baseline comparison under its technical setting and refuses a raw observation', async () => { const gpo = finding({ resultProjectionId: 'policy-evaluation-comparison:one', sourceResultId: 'comparison-one', sourceKind: 'policy-evaluation', sourceTool: 'GPOs', sourceRuleId: 'registry:machine:software\policies\synthetic:mode', title: 'Synthetic policy mode', policyActualValue: { principals: [customerPolicyValue] } }); const observation = finding({ resultProjectionId: 'policy-observation:one', sourceResultId: 'observation-one', sourceKind: 'policy-observation' }); const complete = vi.fn(async (_system: string, _input: string) => ({ value: { schemaVersion: 1, outcome: 'mapped', mappings: [{ controlId: '5.1', relationship: 'primary', confidence: 'high', rationale: 'Synthetic rationale.', evidenceChunkIds: ['semantic-5.1', 'reference-5.1'] }] } })); const svc = service(mockRepository([gpo, observation]), complete); await svc.start('project-1', { resultProjectionId: gpo.resultProjectionId }); await svc.close(); const mapped = await svc.get('project-1', gpo.resultProjectionId); expect(mapped).toMatchObject({ generationEnabled: true, state: 'succeeded', run: { ruleKey: 'GPOs:registry:machine:software\policies\synthetic:mode' } }); // The stored identity tuple must differ per GPO query version, or a version bump collides with the older row. expect(mapped.run?.promptVersion).toBe(`${REGULATORY_MAPPING_PROMPT_VERSION}+${REGULATORY_GPO_QUERY_VERSION}`); expect(complete).toHaveBeenCalledTimes(1); expect(String(complete.mock.calls[0][1])).not.toContain('synthetic.account'); expect(await svc.get('project-1', observation.resultProjectionId)).toMatchObject({ generationEnabled: false, state: 'unsupported' }); await expect(svc.start('project-1', { resultProjectionId: observation.resultProjectionId })).rejects.toThrow('GPO baseline comparisons'); }); it('bounds concurrent mapping completions across a batch', async () => { const findings = Array.from({ length: 5 }, (_, index) => finding({ resultProjectionId: `scanner-finding:occurrence-${index}`, sourceResultId: `occurrence-${index}`, sourceRuleId: `A-Rule-${index}`, title: `Synthetic check ${index}` })); let active = 0; let maximum = 0; const complete = vi.fn(async () => { active += 1; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 5)); active -= 1; return { value: { schemaVersion: 1, outcome: 'no_relevant_control', reason: 'Synthetic.', mappings: [] } }; }); const svc = service(mockRepository(findings), complete); await svc.startBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true }, idempotencyKey: 'batch-concurrency-1' }); await svc.close(); expect(complete).toHaveBeenCalledTimes(5); expect(maximum).toBe(2); }); it('reads every server page independently of loaded rows', async () => { const findings = Array.from({ length: 501 }, (_, index) => finding({ resultProjectionId: `scanner-finding:occurrence-${index}`, sourceResultId: `occurrence-${index}` })); const repository = mockRepository(findings); const complete = vi.fn(async () => ({ value: { schemaVersion: 1, outcome: 'no_relevant_control', reason: 'Synthetic.', mappings: [] } })); const svc = service(repository, complete); await svc.startBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true }, idempotencyKey: 'batch-many-pages-1' }); await svc.close(); const response = await svc.getBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true } }); expect(response.run?.counts).toMatchObject({ filteredResultCount: 501, eligibleFindingCount: 501, distinctRuleCount: 1 }); expect(repository.queryUnifiedResults).toHaveBeenCalledTimes(2); expect(complete).toHaveBeenCalledOnce(); }); it('fails the batch safely when the filtered total changes between pages', async () => { const findings = Array.from({ length: 501 }, (_, index) => finding({ resultProjectionId: `scanner-finding:occurrence-${index}`, sourceResultId: `occurrence-${index}` })); const repository = mockRepository(findings); let page = 0; vi.mocked(repository.queryUnifiedResults).mockImplementation(async (query = {}) => { page += 1; const offset = query.offset ?? 0; return { total: page === 1 ? 501 : 500, items: findings.slice(offset, offset + (query.limit ?? findings.length)) }; }); const complete = vi.fn(); const svc = service(repository, complete); await svc.startBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true }, idempotencyKey: 'batch-changing-total-1' }); await svc.close(); const response = await svc.getBatch('project-1', { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true } }); expect(response.run).toMatchObject({ status: 'failed', errorCode: 'regulatory_mapping_batch_scope_changed' }); expect(complete).not.toHaveBeenCalled(); }); it('completes with partial failures, then reuses success and retries only the failed rule', async () => { const findings = [finding(), finding({ resultProjectionId: 'scanner-finding:occurrence-2', sourceResultId: 'occurrence-2', sourceRuleId: 'A-Second', title: 'Second synthetic check' })]; let secondAttempts = 0; const complete = vi.fn(async (_system: string, input: string) => { if (input.includes('Second synthetic check') && ++secondAttempts === 1) throw new Error('synthetic provider failure'); return { value: { schemaVersion: 1, outcome: 'no_relevant_control', reason: 'Synthetic.', mappings: [] } }; }); const svc = service(mockRepository(findings), complete); const scope = { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true } }; await svc.startBatch('project-1', { ...scope, idempotencyKey: 'batch-partial-1' }); await svc.close(); expect((await svc.getBatch('project-1', scope)).run).toMatchObject({ status: 'completed', counts: { scheduledRuleCount: 2, succeededRuleCount: 1, failedRuleCount: 1 } }); await svc.startBatch('project-1', { ...scope, idempotencyKey: 'batch-partial-2' }); await svc.close(); expect((await svc.getBatch('project-1', scope)).run).toMatchObject({ status: 'completed', counts: { distinctRuleCount: 2, reusedSucceededRuleCount: 1, scheduledRuleCount: 1, succeededRuleCount: 1, failedRuleCount: 0 } }); expect(complete).toHaveBeenCalledTimes(3); }); it('reuses an active single-finding mapping without a duplicate model call', async () => { let release!: () => void; const gate = new Promise(resolve => { release = resolve; }); const complete = vi.fn(async () => { await gate; return { value: { schemaVersion: 1, outcome: 'no_relevant_control', reason: 'Synthetic.', mappings: [] } }; }); const svc = service(mockRepository([finding()]), complete); await svc.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); const scope = { assessmentIds: ['assessment-1'], scanRunIds: ['run-1'], filters: { deviation: true } }; await svc.startBatch('project-1', { ...scope, idempotencyKey: 'batch-active-reuse-1' }); release(); await svc.close(); expect((await svc.getBatch('project-1', scope)).run?.counts).toMatchObject({ distinctRuleCount: 1, reusedActiveRuleCount: 1, scheduledRuleCount: 0 }); expect(complete).toHaveBeenCalledOnce(); }); }); async function runWith(value: unknown): Promise<{ svc: RegulatoryMappingService }> { const repository = mockRepository([finding()]); const svc = service(repository, vi.fn(async () => ({ value }))); await svc.start('project-1', { resultProjectionId: 'scanner-finding:occurrence-1' }); await svc.close(); return { svc }; }