191 lines
9.3 KiB
TypeScript
191 lines
9.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
RegulatoryFindingRetrievalService,
|
|
mergeFindingQueryRankings,
|
|
reciprocalRankFuseByControl
|
|
} from '../../server/regulatory/regulatory-finding-retrieval';
|
|
import type { RegulatoryRetrievalCandidate, RegulatoryRetrievalRequest } from '../../src/domain/regulatory-rag';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { buildRegulatoryFindingQuery } from '../../src/profiles/public-search/regulatory-query';
|
|
|
|
// Result-table / occurrence data and raw addresses that must never reach the query.
|
|
const resultTableAsset = 'host.customer.invalid';
|
|
const reporterEmail = 'ops@acme.invalid';
|
|
const ownerEmail = 'analyst@acme.invalid';
|
|
|
|
function finding(overrides: Partial<UnifiedResultProjection> = {}): UnifiedResultProjection {
|
|
return {
|
|
resultProjectionId: 'projection-1', sourceKind: 'scanner-finding', sourceResultId: 'source-1',
|
|
projectId: 'tenant-1', assessmentId: 'assessment-1', collectionRunId: 'run-1',
|
|
sourceTool: 'scanner-one', sourceRuleId: 'RULE-1',
|
|
title: 'Unsigned protocol sessions are permitted',
|
|
description: `The service permits unsigned sessions. Reported by ${reporterEmail}.`,
|
|
recommendation: 'Require signed sessions through the platform policy.',
|
|
affectedAsset: resultTableAsset,
|
|
severityOriginal: 'high', severityNormalized: 'high', outcomeOriginal: 'fail', outcomeNormalized: 'fail',
|
|
technicalExecutionState: 'completed', resultType: 'compliance_check', isIoe: false, isDeviation: true,
|
|
lifecycleState: 'new', mappingConfidence: 1, reviewStatus: 'open', reviewOwner: ownerEmail,
|
|
importedAt: '2026-09-15T10:00:00.000Z', createdAt: '2026-09-15T10:00:00.000Z', categories: [],
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function hit(
|
|
controlId: string,
|
|
chunkId: string,
|
|
score: number,
|
|
sectionKind?: RegulatoryRetrievalCandidate['chunk']['sectionKind']
|
|
): RegulatoryRetrievalCandidate {
|
|
return {
|
|
score,
|
|
chunk: {
|
|
schemaVersion: 1, corpusId: 'synthetic', corpusVersion: '1', documentId: `${controlId}-document`,
|
|
framework: 'Synthetic Framework', frameworkVersion: '1', controlId, chunkId,
|
|
chunkIndex: 0, language: 'de', title: 'Synthetic control', text: 'Synthetic regulatory text.',
|
|
textSha256: 'a'.repeat(64),
|
|
...(sectionKind ? { sectionKind } : {})
|
|
}
|
|
};
|
|
}
|
|
|
|
describe('regulatory finding retrieval boundary', () => {
|
|
it('builds problem and remediation queries only from privacy-safe finding text', () => {
|
|
const query = buildRegulatoryFindingQuery(finding());
|
|
|
|
expect(query).toEqual({
|
|
schemaVersion: 4,
|
|
problemQuery: 'Security check: Unsigned protocol sessions are permitted. Check detail: The service permits unsigned sessions. Reported by [redacted-email].',
|
|
remediationQuery: 'Security check: Unsigned protocol sessions are permitted. Recommended action: Require signed sessions through the platform policy.',
|
|
retrievalQuery: 'Security check: Unsigned protocol sessions are permitted. Check detail: The service permits unsigned sessions. Reported by [redacted-email]. Recommended action: Require signed sessions through the platform policy.',
|
|
query: 'Retrieve regulatory controls whose normative requirements directly address this security finding. Security check: Unsigned protocol sessions are permitted. Check detail: The service permits unsigned sessions. Reported by [redacted-email]. Recommended action: Require signed sessions through the platform policy. Prefer controls with direct requirement text over merely related guidance. Do not infer customer scope, assets, configuration values, or compliance status.'
|
|
});
|
|
for (const marker of [resultTableAsset, reporterEmail, ownerEmail, 'scanner-one', 'RULE-1']) {
|
|
expect(JSON.stringify(query)).not.toContain(marker);
|
|
}
|
|
});
|
|
|
|
it('produces identical retrieval input for identical findings from unknown or known scanners', () => {
|
|
const first = buildRegulatoryFindingQuery(finding({ sourceTool: 'scanner-one', sourceRuleId: 'RULE-1' }));
|
|
const second = buildRegulatoryFindingQuery(finding({ sourceTool: 'brand-new-scanner', sourceRuleId: 'NEW-999' }));
|
|
|
|
expect(second).toEqual(first);
|
|
});
|
|
|
|
it('uses one problem query when no recommendation is available', () => {
|
|
const query = buildRegulatoryFindingQuery(finding({
|
|
title: 'Excessive service privileges',
|
|
description: undefined,
|
|
recommendation: undefined
|
|
}));
|
|
|
|
expect(query).toMatchObject({
|
|
schemaVersion: 4,
|
|
problemQuery: 'Security check: Excessive service privileges.',
|
|
retrievalQuery: 'Security check: Excessive service privileges.'
|
|
});
|
|
expect(query?.remediationQuery).toBeUndefined();
|
|
});
|
|
|
|
it('fails closed only for non-scanner records or findings without a check title', () => {
|
|
expect(buildRegulatoryFindingQuery(finding({ sourceKind: 'environment-observation' }))).toBeUndefined();
|
|
expect(buildRegulatoryFindingQuery(finding({ title: ' ' }))).toBeUndefined();
|
|
});
|
|
|
|
it('runs problem, remediation and section-specific full-context queries', async () => {
|
|
const retrieve = vi.fn(async (request: RegulatoryRetrievalRequest) => [hit(
|
|
'C-1',
|
|
`chunk-${request.filters?.sectionKind ?? (request.query.includes('Recommended action') ? 'remediation' : 'problem')}`,
|
|
0.8,
|
|
request.filters?.sectionKind
|
|
)]);
|
|
const service = new RegulatoryFindingRetrievalService({ retrieve });
|
|
|
|
const result = await service.retrieve(finding(), 12, { framework: 'Synthetic Framework' });
|
|
|
|
expect(result).toMatchObject({ sourceResultId: 'source-1', status: 'candidates' });
|
|
expect(retrieve.mock.calls.map(call => call[0])).toEqual([
|
|
{ query: result.preparedQuery?.problemQuery, limit: 64, filters: { framework: 'Synthetic Framework' } },
|
|
{ query: result.preparedQuery?.remediationQuery, limit: 64, filters: { framework: 'Synthetic Framework' } },
|
|
...(['control', 'purpose', 'guidance'] as const).map(sectionKind => ({
|
|
query: result.preparedQuery?.retrievalQuery,
|
|
limit: 32,
|
|
filters: { framework: 'Synthetic Framework', sectionKind }
|
|
}))
|
|
]);
|
|
expect(result.candidates.map(item => item.chunk.sectionKind).filter(Boolean))
|
|
.toEqual(['control', 'purpose', 'guidance']);
|
|
for (const marker of [resultTableAsset, reporterEmail, ownerEmail, 'scanner-one', 'RULE-1']) {
|
|
expect(JSON.stringify(retrieve.mock.calls)).not.toContain(marker);
|
|
}
|
|
});
|
|
|
|
it('does not duplicate the broad search when no remediation text is present', async () => {
|
|
const retrieve = vi.fn(async () => [hit('C-1', 'chunk-1', 0.8)]);
|
|
const service = new RegulatoryFindingRetrievalService({ retrieve });
|
|
|
|
await service.retrieve(finding({ recommendation: undefined }), 12);
|
|
|
|
expect(retrieve).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
it('fuses each control once per independent finding query', () => {
|
|
const fused = reciprocalRankFuseByControl([
|
|
[hit('C-1', 'problem-c1-a', 0.9), hit('C-1', 'problem-c1-b', 0.89), hit('C-2', 'problem-c2', 0.8)],
|
|
[hit('C-2', 'remediation-c2', 0.85), hit('C-3', 'remediation-c3', 0.75), hit('C-1', 'remediation-c1', 0.65)]
|
|
], 2);
|
|
|
|
expect(fused.map(item => item.chunk.chunkId)).toEqual([
|
|
'problem-c2', 'remediation-c2', 'problem-c1-a', 'remediation-c1'
|
|
]);
|
|
expect(new Set(fused.map(item => item.chunk.controlId))).toEqual(new Set(['C-1', 'C-2']));
|
|
expect(fused[0].score).toBeGreaterThan(fused[2].score);
|
|
});
|
|
|
|
it('lets the collapsed full-context section family contribute a shortlist control', () => {
|
|
const selected = mergeFindingQueryRankings(
|
|
[
|
|
[hit('C-1', 'problem-c1', 0.9), hit('C-2', 'problem-c2', 0.8)],
|
|
[hit('C-1', 'remediation-c1', 0.9)]
|
|
],
|
|
[
|
|
[hit('C-3', 'control-c3', 1, 'control'), hit('C-1', 'control-c1', 0.7, 'control')],
|
|
[hit('C-3', 'purpose-c3', 1, 'purpose'), hit('C-1', 'purpose-c1', 0.7, 'purpose')],
|
|
[hit('C-3', 'guidance-c3', 1, 'guidance'), hit('C-1', 'guidance-c1', 0.7, 'guidance')]
|
|
],
|
|
2
|
|
);
|
|
|
|
expect(new Set(selected.map(item => item.chunk.controlId))).toEqual(new Set(['C-1', 'C-3']));
|
|
expect(selected.some(item => item.chunk.controlId === 'C-2')).toBe(false);
|
|
expect(selected.filter(item => item.chunk.controlId === 'C-1').map(item => item.chunk.chunkId))
|
|
.toEqual(['control-c1', 'purpose-c1', 'guidance-c1', 'problem-c1', 'remediation-c1']);
|
|
});
|
|
|
|
it('counts the three full-context sections as one final ranking vote', () => {
|
|
const selected = mergeFindingQueryRankings(
|
|
[
|
|
[hit('C-1', 'problem-c1', 0.9), hit('C-2', 'problem-c2', 0.8)],
|
|
[hit('C-2', 'remediation-c2', 0.9)]
|
|
],
|
|
[
|
|
[hit('C-3', 'control-c3', 1, 'control')],
|
|
[hit('C-3', 'purpose-c3', 1, 'purpose')],
|
|
[hit('C-3', 'guidance-c3', 1, 'guidance')]
|
|
],
|
|
2
|
|
);
|
|
|
|
expect(new Set(selected.map(item => item.chunk.controlId))).toEqual(new Set(['C-1', 'C-2']));
|
|
expect(selected.some(item => item.chunk.controlId === 'C-3')).toBe(false);
|
|
});
|
|
|
|
it('does not call retrieval for a finding without a check title', async () => {
|
|
const retrieve = vi.fn();
|
|
const service = new RegulatoryFindingRetrievalService({ retrieve });
|
|
|
|
await expect(service.retrieve(finding({ title: '' }), 5))
|
|
.resolves.toEqual({ sourceResultId: 'source-1', status: 'insufficient-public-context', candidates: [] });
|
|
expect(retrieve).not.toHaveBeenCalled();
|
|
});
|
|
});
|