80 lines
6.6 KiB
TypeScript
80 lines
6.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { FindingOccurrence, ScanRun } from '../../src/domain/canonical-model';
|
|
import { discloseResultDbProjectionWithTrace } from '../../src/domain/result-db-disclosure';
|
|
import { technicalExplanation, type UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
import { buildRegulatoryFindingQuery } from '../../src/compliance/regulatory-query';
|
|
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
|
|
|
|
// A scanner's technical explanation (PingCastle today) is public rule guidance. It enters the Result DB
|
|
// as its own field, redacted as strictly as a description, guarded against client names and traced;
|
|
// tools without one leave it empty, and unknown sources stay fail-closed.
|
|
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' };
|
|
const explanation = 'The AdminSDHolder ACL is copied to protected accounts every 60 minutes; Dummy Customer changed it on 2026-01-03.';
|
|
|
|
describe('technical explanation field', () => {
|
|
it('reads the extractor key or the source label and ignores other or empty values', () => {
|
|
expect(technicalExplanation([{ key: 'technical-explanation', label: 'Technical explanation', value: ' Native text. ' }])).toBe('Native text.');
|
|
expect(technicalExplanation([{ key: 'column-3', label: 'Technical Explanation', value: 'Table text.' }])).toBe('Table text.');
|
|
expect(technicalExplanation([{ key: 'description', label: 'Description', value: 'Other text.' }])).toBeUndefined();
|
|
expect(technicalExplanation([{ key: 'technical-explanation', label: 'Technical explanation', value: ' ' }])).toBeUndefined();
|
|
expect(technicalExplanation([{ key: 'technical-explanation', label: 'Technical explanation', value: null }])).toBeUndefined();
|
|
});
|
|
|
|
it('is redacted like a description, traced, and withheld for unknown or GPO sources', () => {
|
|
const row = { resultProjectionId: 'scanner-finding:dummy', sourceKind: 'scanner-finding', sourceResultId: 'dummy', projectId: 'dummy-project',
|
|
collectionRunId: 'dummy-run', sourceTool: 'pingcastle', sourceRuleId: 'P-AdminSDHolder', title: 'AdminSDHolder ACL modified',
|
|
technicalExplanation: explanation, severityOriginal: 'high', severityNormalized: 'high', outcomeOriginal: 'fail', outcomeNormalized: 'fail',
|
|
technicalExecutionState: 'completed', resultType: 'compliance_check', isIoe: false, isDeviation: true, lifecycleState: 'new',
|
|
mappingConfidence: 1, importedAt: stamp, createdAt: stamp, frameworkReferences: [], categories: [] } as unknown as UnifiedResultProjection;
|
|
const disclosed = discloseResultDbProjectionWithTrace(row);
|
|
expect(disclosed.technicalExplanation).toContain('AdminSDHolder ACL is copied to protected accounts');
|
|
expect(disclosed.technicalExplanation).not.toContain('60');
|
|
expect(disclosed.technicalExplanation).not.toContain('2026-01-03');
|
|
expect(disclosed.disclosureTrace?.redactions.some(entry => entry.field === 'technicalExplanation')).toBe(true);
|
|
expect(JSON.stringify(disclosed.disclosureTrace)).not.toContain('AdminSDHolder');
|
|
|
|
expect(discloseResultDbProjectionWithTrace({ ...row, sourceTool: 'unknown-scanner' }).technicalExplanation).toBeUndefined();
|
|
expect(discloseResultDbProjectionWithTrace({ ...row, sourceKind: 'policy-evaluation' } as UnifiedResultProjection).technicalExplanation).toBeUndefined();
|
|
});
|
|
|
|
it('reaches the stored Result DB row of a PingCastle finding with the client name withheld, and the ISO query', async () => {
|
|
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: 'pingcastle', name: 'PingCastle' }, importedAt: stamp };
|
|
const occurrence: FindingOccurrence = { id: 'dummy-occurrence', definitionId: 'dummy-definition', scanRunId: scanRun.id, importFileId: 'dummy-file',
|
|
sourceIdentity: { tool: scanRun.tool, originalRuleId: 'P-AdminSDHolder', canonicalRuleId: 'P-AdminSDHolder', originalTitle: 'AdminSDHolder ACL modified' },
|
|
sourcePosition: { rowIndex: 1 },
|
|
originalFields: [{ key: 'technical-explanation', label: 'Technical explanation', value: explanation }],
|
|
fieldMappings: [], severity: { originalValue: 'High', normalizedValue: 'high', provenance },
|
|
status: { originalValue: 'Matched', normalizedValue: 'failed', provenance } };
|
|
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.html', format: 'HTML', sizeBytes: 10, importedAt: stamp,
|
|
detectedTool: scanRun.tool, detection: confidence,
|
|
processedWith: { parserId: 'html', parserVersion: '1.0.0', profileId: 'pingcastle', profileVersion: '1.0.0', mappingVersion: '1.0.0' },
|
|
sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }],
|
|
findingDefinitions: [{ id: 'dummy-definition', canonicalKey: 'pingcastle:P-AdminSDHolder',
|
|
identity: { version: 1, strategy: 'tool-rule-id', durable: true, evidence: ['synthetic'] },
|
|
title: 'AdminSDHolder ACL modified', category: 'Privileged Accounts', frameworkReferences: [], createdAt: stamp, updatedAt: stamp }],
|
|
findingOccurrences: [occurrence]
|
|
});
|
|
await session.commit();
|
|
|
|
const stored = await repository.getUnifiedResultById('scanner-finding:dummy-occurrence', 'dummy-project');
|
|
expect(stored?.technicalExplanation).toContain('AdminSDHolder ACL is copied to protected accounts');
|
|
expect(stored?.technicalExplanation).not.toContain('Dummy Customer');
|
|
expect(stored?.technicalExplanation).not.toContain('2026-01-03');
|
|
// Internal analysis keeps the complete original.
|
|
const internal = await repository.queryUnifiedResults({ audience: 'internal-analysis', projectId: 'dummy-project' });
|
|
expect(internal.items[0].technicalExplanation).toBe(explanation);
|
|
// The ISO query reads the disclosed row, so it carries the redacted technical explanation.
|
|
const query = buildRegulatoryFindingQuery(stored!)!.query;
|
|
expect(query).toContain('Technical explanation: The AdminSDHolder ACL is copied to protected accounts');
|
|
expect(query).not.toContain('Dummy Customer');
|
|
});
|
|
});
|