123 lines
6.5 KiB
TypeScript
123 lines
6.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { RegulatoryFindingRetrievalService } from '../../server/compliance/regulatory-finding-retrieval';
|
|
import type { RegulatoryRetrievalCandidate, RegulatoryRetrievalRequest } from '../../src/compliance/regulatory-rag';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { buildRegulatoryFindingQuery } from '../../src/compliance/regulatory-query';
|
|
|
|
// Result-table / occurrence data and raw addresses that must never reach the query.
|
|
const resultTableAsset = 'DC01.customer.local';
|
|
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: 'purple-knight',
|
|
title: 'SMB signing is not required on domain controllers',
|
|
description: `The SMB server permits unsigned sessions, allowing relay attacks. Reported by ${reporterEmail}.`,
|
|
recommendation: 'Require SMB signing through Group Policy on all domain controllers.',
|
|
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', frameworkReferences: [], categories: [],
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
const candidate: RegulatoryRetrievalCandidate = {
|
|
score: 0.8,
|
|
chunk: {
|
|
schemaVersion: 1, corpusId: 'synthetic', corpusVersion: '1', documentId: 'control-de',
|
|
framework: 'Synthetic Framework', frameworkVersion: '1', controlId: 'CTRL-1', chunkId: 'chunk-1',
|
|
chunkIndex: 0, language: 'de', title: 'Synthetic control', text: 'Synthetic regulatory text.',
|
|
textSha256: 'a'.repeat(64)
|
|
}
|
|
};
|
|
|
|
describe('regulatory finding retrieval boundary', () => {
|
|
it('builds the query from the non-personal check text and never from the result table', () => {
|
|
const query = buildRegulatoryFindingQuery(finding());
|
|
|
|
expect(query).toMatchObject({
|
|
schemaVersion: 1,
|
|
specificity: 'topic',
|
|
publicTool: 'Semperis Purple Knight',
|
|
publicTopics: ['SMB signing requirements']
|
|
});
|
|
// The rule-level check text is the intended source.
|
|
expect(query?.query).toContain('SMB signing is not required on domain controllers');
|
|
expect(query?.query).toContain('unsigned sessions');
|
|
// Result-table / occurrence data and raw email addresses never appear.
|
|
expect(query?.query).not.toContain(resultTableAsset);
|
|
expect(query?.query).not.toContain(reporterEmail);
|
|
expect(query?.query).not.toContain(ownerEmail);
|
|
expect(query?.query).toContain('[redacted-email]');
|
|
});
|
|
|
|
it('maps every scanner finding, sharpening with a catalog rule ID when available', () => {
|
|
expect(buildRegulatoryFindingQuery(finding({
|
|
sourceTool: 'pingcastle', sourceRuleId: 'a-krbtgt'
|
|
}))).toMatchObject({ specificity: 'rule', publicRuleId: 'A-Krbtgt' });
|
|
|
|
// Unknown tool, non-catalog rule ID, no topic match: still mapped from the check title.
|
|
const generic = buildRegulatoryFindingQuery(finding({
|
|
sourceTool: 'customer-private-scanner', sourceRuleId: 'A-Custom',
|
|
title: 'Weak service account configuration', description: undefined, recommendation: undefined
|
|
}));
|
|
expect(generic).toMatchObject({ specificity: 'topic' });
|
|
expect(generic?.query).toContain('Weak service account configuration');
|
|
expect(generic?.publicRuleId).toBeUndefined();
|
|
});
|
|
|
|
it('maps a GPO baseline comparison from its disclosed setting text and never from the customer value', () => {
|
|
const query = buildRegulatoryFindingQuery(finding({
|
|
resultProjectionId: 'policy-evaluation-comparison:synthetic', sourceKind: 'policy-evaluation', sourceTool: 'GPOs',
|
|
sourceRuleId: 'registry:machine:software\policies\synthetic:mode', title: 'Synthetic policy mode',
|
|
description: 'Controls the synthetic SMB signing mode.', recommendation: 'Enable the synthetic policy mode.',
|
|
policyBaselineLabel: 'DUMMY Microsoft Security Baseline Windows Server 2022 v1',
|
|
policyBaselineValue: { operation: 'set-value', data: { kind: 'dword', value: 1 } },
|
|
policyActualValue: { operation: 'set-value', data: { kind: 'dword', value: 0 }, principals: ['DUMMY\named.user'] }
|
|
}));
|
|
|
|
expect(query).toMatchObject({ specificity: 'topic', publicTool: 'Microsoft Group Policy' });
|
|
expect(query?.query).toContain('Synthetic policy mode');
|
|
expect(query?.publicRuleId).toBeUndefined();
|
|
expect(query?.query).not.toMatch(/named.user|DUMMY|policyActualValue/);
|
|
});
|
|
|
|
it('fails closed only for context records or findings without a check title', () => {
|
|
expect(buildRegulatoryFindingQuery(finding({ sourceKind: 'policy-observation' }))).toBeUndefined();
|
|
expect(buildRegulatoryFindingQuery(finding({ sourceKind: 'environment-observation' }))).toBeUndefined();
|
|
expect(buildRegulatoryFindingQuery(finding({ title: ' ' }))).toBeUndefined();
|
|
});
|
|
|
|
it('passes only the prepared query and explicit filters to the retriever', async () => {
|
|
const retrieve = vi.fn(async (_request: RegulatoryRetrievalRequest) => [candidate]);
|
|
const service = new RegulatoryFindingRetrievalService({ retrieve });
|
|
|
|
const result = await service.retrieve(finding(), 5, { framework: 'Synthetic Framework' });
|
|
|
|
expect(result).toMatchObject({ sourceResultId: 'source-1', status: 'candidates', candidates: [candidate] });
|
|
expect(retrieve).toHaveBeenCalledOnce();
|
|
expect(retrieve.mock.calls[0][0]).toEqual({
|
|
query: result.preparedQuery?.query,
|
|
limit: 5,
|
|
filters: { framework: 'Synthetic Framework' }
|
|
});
|
|
for (const marker of [resultTableAsset, reporterEmail, ownerEmail]) {
|
|
expect(JSON.stringify(retrieve.mock.calls[0][0])).not.toContain(marker);
|
|
}
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|