Assessor_2.0/tests/compliance/regulatory-mapping.integrat...

96 lines
7.3 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { Pool } from 'pg';
import { describe, expect, it } from 'vitest';
import { createLiteLlmClientFromEnvironment } from '../../server/ai/litellm-client';
import { runDatabaseMigrations } from '../../server/postgres/migration-runner';
import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository';
import { createRegulatoryRagRuntime } from '../../server/compliance/regulatory-rag-runtime';
import { RegulatoryMappingService } from '../../server/compliance/regulatory-mapping-service';
import { PostgresComplianceStore } from '../../server/compliance/postgres-compliance-store';
import { complianceReadPortFrom } from '../../src/compliance/compliance-store';
import type { Assessment, AssessmentProject, ScanRun } from '../../src/domain/canonical-model';
import { RepositoryConflictError } from '../../src/persistence/repository-errors';
const enabled = process.env.ASSESSOR_REGULATORY_MAPPING_INTEGRATION === '1' && Boolean(process.env.ASSESSOR_TEST_DATABASE_URL);
const describeIntegration = enabled ? describe : describe.skip;
describeIntegration('regulatory mapping end-to-end integration', () => {
it('maps one synthetic finding through BGE-M3, Qdrant, LiteLLM, and PostgreSQL', async () => {
const stamp = new Date().toISOString();
const suffix = randomUUID();
const project: AssessmentProject = { id: `synthetic-regulatory-${suffix}`, name: 'Synthetic regulatory integration', createdAt: stamp, updatedAt: stamp, attributes: {} };
const assessment: Assessment = { id: `synthetic-assessment-${suffix}`, projectId: project.id, name: 'Synthetic', createdAt: stamp, updatedAt: stamp };
const scanRun: ScanRun = { id: `synthetic-run-${suffix}`, assessmentId: assessment.id, sequence: 1, label: 'Synthetic', tool: { id: 'purple-knight', name: 'Purple Knight', version: 'synthetic' }, importedAt: stamp };
const occurrenceId = `synthetic-occurrence-${suffix}`;
const pool = new Pool({ connectionString: process.env.ASSESSOR_TEST_DATABASE_URL, max: 2, connectionTimeoutMillis: 5_000 });
await runDatabaseMigrations(pool);
const repository = await PostgresAssessorRepository.create(pool);
const rag = createRegulatoryRagRuntime();
const client = createLiteLlmClientFromEnvironment();
if (!client) throw new Error('Approved LiteLLM configuration is required.');
const store = new PostgresComplianceStore(pool);
const service = new RegulatoryMappingService(complianceReadPortFrom(repository), store, {
findingRetrieval: rag.findingRetrieval, index: rag.index, embeddingModel: rag.embeddingModel, client,
corpusId: process.env.ASSESSOR_RAG_ACTIVE_CORPUS_ID,
corpusVersion: process.env.ASSESSOR_RAG_ACTIVE_CORPUS_VERSION
});
let deleted = false;
try {
await repository.saveProject(project);
await repository.saveAssessment(assessment);
const session = await repository.beginImport({ sessionId: `synthetic-session-${suffix}`, projectId: project.id, assessmentId: assessment.id, scanRun });
await session.stage({
importFiles: [{ id: `synthetic-file-${suffix}`, scanRunId: scanRun.id, fileName: 'synthetic.csv', format: 'CSV', sizeBytes: 100,
importedAt: stamp, detectedTool: scanRun.tool, detection: { level: 'high', score: 1, reasons: ['synthetic'] },
processedWith: { parserId: 'csv', parserVersion: 'synthetic', profileId: 'purple-knight', profileVersion: 'synthetic', mappingVersion: 'synthetic' },
sourceArtifact: { kind: 'not-retained', reason: 'Synthetic integration fixture' } }],
sourceTables: [{ id: `synthetic-table-${suffix}`, importFileId: `synthetic-file-${suffix}`, name: 'Synthetic', index: 0, rowCount: 1, columnCount: 2, attributes: {} }],
findingDefinitions: [{ id: `synthetic-definition-${suffix}`, canonicalKey: `purple-knight:synthetic-${suffix}`, title: 'SMB signing disabled',
description: 'Synthetic public-topic fixture', category: 'Synthetic', frameworkReferences: [], createdAt: stamp, updatedAt: stamp }],
findingOccurrences: [{ id: occurrenceId, definitionId: `synthetic-definition-${suffix}`, scanRunId: scanRun.id,
importFileId: `synthetic-file-${suffix}`, sourceTableId: `synthetic-table-${suffix}`,
sourceIdentity: { tool: scanRun.tool, originalRuleId: 'SYNTHETIC-SMB', originalTitle: 'SMB signing disabled' },
sourcePosition: { rowIndex: 1 }, originalFields: [{ key: 'title', label: 'Title', value: 'SMB signing disabled', columnIndex: 0 }],
fieldMappings: [], severity: { originalValue: 'high', normalizedValue: 'high', provenance: { decisionSource: 'source-native',
confidence: { level: 'high', score: 1, reasons: ['synthetic'] }, mappingVersion: 'synthetic' } },
status: { originalValue: 'Open', normalizedValue: 'open', provenance: { decisionSource: 'source-native',
confidence: { level: 'high', score: 1, reasons: ['synthetic'] }, mappingVersion: 'synthetic' } } }]
});
await session.commit();
const resultProjectionId = `scanner-finding:${occurrenceId}`;
await service.start(project.id, { resultProjectionId });
await service.close();
const result = await service.get(project.id, resultProjectionId);
expect(result.run?.status, result.run?.errorCode).toBe('succeeded');
expect(result.run?.candidates).toHaveLength(8);
expect(result.run?.candidates.every(candidate => candidate.evidence.some(item => item.sourceStandard === 'ISO/IEC 27001'))).toBe(true);
expect(result.run?.result?.mappings.length).toBeLessThanOrEqual(5);
expect(result.run?.result?.mappings.every(mapping => {
const candidate = result.run?.candidates.find(item => item.controlId === mapping.controlId);
return Boolean(candidate && mapping.evidenceChunkIds.every(id => candidate.evidence.some(item => item.chunkId === id)));
})).toBe(true);
const candidate = result.run!.candidates[0];
const ruleKey = result.run!.ruleKey;
const reviewed = await service.decide(project.id, {
resultProjectionId, controlId: candidate.controlId, decision: 'accepted', comment: 'Synthetic review.'
});
expect(reviewed.decisions[0]).toMatchObject({ controlId: candidate.controlId, decision: 'accepted' });
// Omitting expectedLatestDecisionId conflicts with the existing global decision for this rule+control.
await expect(store.createRegulatoryMappingDecision({
version: 1, id: `synthetic-conflict-${suffix}`, ruleKey, sourceTool: 'purple-knight', sourceRuleId: 'SYNTHETIC-SMB',
mappingId: result.run!.id, controlId: candidate.controlId, decision: 'rejected', createdAt: new Date().toISOString()
})).rejects.toBeInstanceOf(RepositoryConflictError);
await repository.deleteProject(project.id, { projectId: project.id, cascade: true });
deleted = true;
// Regulatory mappings and decisions are global: they survive project deletion.
await expect(store.getRegulatoryMappingRun(result.run!.id)).resolves.not.toBeNull();
expect((await store.listRegulatoryMappingDecisions(ruleKey)).length).toBeGreaterThan(0);
} finally {
await service.close();
if (!deleted) await repository.deleteProject(project.id, { projectId: project.id, cascade: true }).catch(() => false);
await pool.end();
}
}, 120_000);
});