import { describe, expect, it } from 'vitest'; import type { Assessment, AssessmentProject, FindingDefinition, FindingOccurrence, ImportFileRecord, ScanRun } from '../../src/domain/canonical-model'; import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository'; const timestamp = '2026-07-22T10: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 project: AssessmentProject = { id: 'project-results', name: 'Synthetic project', createdAt: timestamp, updatedAt: timestamp, attributes: {} }; const assessment: Assessment = { id: 'assessment-results', projectId: project.id, name: 'Synthetic assessment', createdAt: timestamp, updatedAt: timestamp }; function run(id: string, sequence: number, toolId: string): ScanRun { return { id, assessmentId: assessment.id, sequence, label: id, tool: { id: toolId, name: toolId }, importedAt: timestamp }; } function file(scanRun: ScanRun): ImportFileRecord { return { id: `file-${scanRun.id}`, scanRunId: scanRun.id, fileName: `${scanRun.id}.csv`, format: 'CSV', sizeBytes: 10, importedAt: timestamp, detectedTool: scanRun.tool, detection: confidence, processedWith: { parserId: 'csv', parserVersion: '1.0.0', profileId: scanRun.tool.id, profileVersion: '1.0.0', mappingVersion: '1.0.0' }, sourceArtifact: { kind: 'not-retained', reason: 'synthetic' } }; } function definition(id: string, key: string, category: string): FindingDefinition { return { id, canonicalKey: key, identity: { version: 1, strategy: key.includes('unresolved') ? 'unresolved' : 'tool-rule-id', durable: !key.includes('unresolved'), evidence: ['synthetic'] }, title: 'Same visible title', category, frameworkReferences: [], createdAt: timestamp, updatedAt: timestamp }; } function occurrence( id: string, scanRun: ScanRun, definitionId: string, status: 'failed' | 'error', originalStatus: string, ruleId?: string ): FindingOccurrence { return { id, definitionId, scanRunId: scanRun.id, importFileId: `file-${scanRun.id}`, sourceIdentity: { tool: scanRun.tool, originalRuleId: ruleId, canonicalRuleId: ruleId, originalTitle: 'Same visible title' }, sourcePosition: { rowIndex: 1 }, originalFields: [ { key: 'Status', label: 'Status', value: originalStatus, columnIndex: 0 }, { key: 'Category', label: 'Category', value: scanRun.tool.id === 'pingcastle' ? 'Privileged Accounts' : 'Account Security', columnIndex: 1 } ], fieldMappings: [{ targetRole: 'category', sourceFieldKeys: ['Category'], normalizedValue: scanRun.tool.id === 'pingcastle' ? 'Privileged Accounts' : 'Account Security', provenance }], severity: { originalValue: 'High', normalizedValue: 'high', provenance }, status: { originalValue: originalStatus, normalizedValue: status, provenance } }; } async function commit( repository: LocalAssessorRepository, scanRun: ScanRun, definitions: FindingDefinition[], occurrences: FindingOccurrence[] ): Promise { const session = await repository.beginImport({ sessionId: `session-${scanRun.id}`, projectId: project.id, assessmentId: assessment.id, scanRun }); await session.stage({ importFiles: [file(scanRun)], findingDefinitions: definitions, findingOccurrences: occurrences }); await session.commit(); } describe('Unified result read model', () => { it('keeps occurrences separate, splits status dimensions and exposes namespaced categories and reviews', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const firstRun = run('run-pingcastle', 1, 'pingcastle'); const secondRun = run('run-purple-knight', 2, 'purple-knight'); const stable = definition('definition-stable', 'pingcastle:pc-1', 'Privileged Accounts'); const unresolved = definition('definition-unresolved', 'unresolved:run-purple-knight:1', 'Account Security'); await commit(repository, firstRun, [stable], [ occurrence('occurrence-1', firstRun, stable.id, 'failed', 'FAILED', 'PC-1') ]); await commit(repository, secondRun, [unresolved], [ occurrence('occurrence-2', secondRun, unresolved.id, 'error', 'Failed to run') ]); await repository.saveReview({ id: 'review-result-1', target: { kind: 'occurrence', occurrenceId: 'occurrence-1' }, workflowStatus: 'accepted', priority: 'high', includeInActionPlan: false, owner: 'synthetic-owner', markers: [], createdAt: timestamp, updatedAt: timestamp, revision: 1 }, 0); const all = await repository.queryUnifiedResults({ projectId: project.id }); expect(all.total).toBe(2); expect(all.items.map(item => item.sourceResultId)).toEqual(['occurrence-1', 'occurrence-2']); const currentTimeslot = await repository.queryUnifiedResults({ projectId: project.id, scanRunIds: [secondRun.id] }); expect(currentTimeslot.items.map(item => item.sourceResultId)).toEqual(['occurrence-2']); const first = await repository.getUnifiedResultById('scanner-finding:occurrence-1'); expect(first).toMatchObject({ sourceKind: 'scanner-finding', severityNormalized: 'high', outcomeOriginal: 'FAILED', outcomeNormalized: 'fail', technicalExecutionState: 'completed', resultType: 'exposure_finding', isIoe: true, exposureState: 'detected', lifecycleState: 'unknown', reviewStatus: 'resolved', reviewDisposition: 'accepted', reviewOwner: 'synthetic-owner', categories: [{ namespace: 'pingcastle', categoryLabel: 'Privileged Accounts' }] }); const executionError = await repository.queryUnifiedResults({ outcome: ['execution_error'], technicalExecutionState: ['failed'] }); expect(executionError.items.map(item => item.sourceResultId)).toEqual(['occurrence-2']); expect(executionError.items[0].outcomeOriginal).toBe('Failed to run'); // Ein technischer Ausführungsfehler ist per RESULT_CONTRACT nie is_ioe=true, // auch wenn das Werkzeug ein exposure_finding-Werkzeug ist. expect(executionError.items[0]).toMatchObject({ resultType: 'exposure_finding', isIoe: false, exposureState: 'execution_error' }); const ioeOnly = await repository.queryUnifiedResults({ isIoe: true }); expect(ioeOnly.items.map(item => item.sourceResultId)).toEqual(['occurrence-1']); const complianceOnly = await repository.queryUnifiedResults({ resultType: ['compliance_check'] }); expect(complianceOnly.total).toBe(0); const categories = await repository.listResultCategories(); expect(categories.map(category => [category.namespace, category.categoryLabel])).toEqual([ ['pingcastle', 'Privileged Accounts'], ['purple-knight', 'Account Security'] ]); const facets = await repository.queryUnifiedResultFacets({ severity: ['high'] }); expect(facets.total).toBe(2); expect(facets.facets.find(facet => facet.id === 'outcome')?.values).toEqual([ { value: 'execution_error', label: 'execution_error', count: 1 }, { value: 'fail', label: 'fail', count: 1 } ]); expect(facets.facets.find(facet => facet.id === 'resultType')?.values).toEqual([ { value: 'exposure_finding', label: 'exposure_finding', count: 2 } ]); expect(facets.facets.find(facet => facet.id === 'isIoe')?.values).toEqual([ { value: 'false', label: 'false', count: 1 }, { value: 'true', label: 'true', count: 1 } ]); }); it('marks a failed CIS-CAT compliance check as deviation without treating it as IOE', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const scanRun = run('run-cis-cat', 1, 'cis-cat'); const finding = { ...definition('definition-cis-cat', 'cis-cat:2.2.2', 'CIS'), title: "2.2.2 Ensure 'Access this computer from the network' is configured" }; await commit(repository, scanRun, [finding], [ occurrence('occurrence-cis-cat', scanRun, finding.id, 'failed', 'Fail', '2.2.2') ]); const result = await repository.getUnifiedResultById('scanner-finding:occurrence-cis-cat'); expect(result).toMatchObject({ title: finding.title, outcomeNormalized: 'fail', resultType: 'compliance_check', isIoe: false, isDeviation: true }); await expect(repository.queryUnifiedResults({ sourceTool: ['cis-cat'], deviation: true })).resolves.toMatchObject({ total: 1, items: [expect.objectContaining({ sourceResultId: 'occurrence-cis-cat' })] }); }); it('exposes only privacy-approved PingCastle environment observations in Result DB', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const scanRun = run('run-pingcastle-environment', 1, 'pingcastle'); const importFile: ImportFileRecord = { ...file(scanRun), format: 'HTML', fileName: 'synthetic-pingcastle.html', processedWith: { parserId: 'html-report', parserVersion: '1.1.0', profileId: 'pingcastle', profileVersion: '1.6.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1' } }; const session = await repository.beginImport({ sessionId: 'session-pingcastle-environment', projectId: project.id, assessmentId: assessment.id, scanRun }); await session.stage({ importFiles: [importFile], environmentObservations: [{ version: 1, id: 'environment-dfl', scanRunId: scanRun.id, importFileId: importFile.id, tool: scanRun.tool, subject: { type: 'domain-information', identity: 'synthetic.example' }, property: 'domain.functional_level', label: 'Domain functional level', originalValue: 'Windows Server 2016', normalizedValue: 2016, valueType: 'number', state: 'observed', sectionKey: 'sectionDomainInformation', source: { objectPath: 'html/sections/domain/table/0' }, mappingVersion: '1.2.0+context-1.0.0', attributes: { extractionMethod: 'synthetic-fixture' } }, { version: 1, id: 'environment-domain-name', scanRunId: scanRun.id, importFileId: importFile.id, tool: scanRun.tool, subject: { type: 'domain-information', identity: 'private.synthetic.example' }, property: 'domain.fqdn', label: 'Domain', originalValue: 'private.synthetic.example', normalizedValue: 'private.synthetic.example', valueType: 'string', state: 'observed', sectionKey: 'sectionDomainInformation', source: { objectPath: 'html/sections/domain/table/1' }, mappingVersion: '1.2.0+context-1.0.0', attributes: { customerMarker: 'private-synthetic' } }] }); await session.commit(); const results = await repository.queryUnifiedResults({ projectId: project.id, sourceKind: ['environment-observation'], environmentProperty: ['domain.functional_level'], environmentSubjectType: ['active-directory-environment'], environmentState: ['observed'], search: '2016' }); expect(results).toMatchObject({ total: 1, items: [expect.objectContaining({ resultProjectionId: 'environment-observation:environment-dfl', sourceKind: 'environment-observation', sourceTool: 'pingcastle', sourceProfileVersion: '1.6.0', mappingRuleVersion: '1.2.0+context-1.0.0', processingStatus: 'current', importArtifactId: importFile.id, environmentProperty: 'domain.functional_level', environmentSubjectType: 'active-directory-environment', environmentNormalizedValue: 2016, environmentValueType: 'number', environmentState: 'observed', supplementalDisclosurePolicyVersion: '1.0.0', supplementalDisclosureClass: 'technical-metadata', resultType: 'informational', isIoe: false, isDeviation: false })] }); await expect(repository.getUnifiedResultById('environment-observation:environment-dfl')) .resolves.toMatchObject({ sourceResultId: 'environment-dfl', environmentNormalizedValue: 2016 }); await expect(repository.getUnifiedResultById('environment-observation:environment-domain-name')) .resolves.toBeNull(); expect(JSON.stringify(results)).not.toContain('synthetic.example'); expect(JSON.stringify(results)).not.toContain('private-synthetic'); expect(results.items[0]).not.toHaveProperty('sourceLocator'); expect(results.items[0]).not.toHaveProperty('environmentOriginalValue'); expect(results.items[0]).not.toHaveProperty('environmentSubjectIdentity'); expect(results.items[0]).not.toHaveProperty('environmentAttributes'); await expect(repository.queryUnifiedResults({ sourceKind: ['environment-observation'], deviation: true })) .resolves.toMatchObject({ total: 0 }); const facets = await repository.queryUnifiedResultFacets({ projectId: project.id }); expect(facets.facets.find(facet => facet.id === 'sourceKind')?.values) .toContainEqual(expect.objectContaining({ value: 'environment-observation', count: 1 })); expect(facets.facets.find(facet => facet.id === 'environmentProperty')?.values) .toContainEqual(expect.objectContaining({ value: 'domain.functional_level', count: 1 })); expect(facets.facets.find(facet => facet.id === 'environmentState')?.values) .toContainEqual(expect.objectContaining({ value: 'observed', count: 1 })); expect(facets.facets.find(facet => facet.id === 'environmentSubjectType')?.values) .toContainEqual(expect.objectContaining({ value: 'active-directory-environment', count: 1 })); const categories = await repository.listResultCategories({ sourceKind: ['environment-observation'] }); expect(categories).toEqual(expect.arrayContaining([ expect.objectContaining({ namespace: 'environment-subject', categoryCode: 'active-directory-environment' }), expect.objectContaining({ namespace: 'environment-property', categoryCode: 'domain' }) ])); }); it('does not merge equal titles without a durable identity', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const scanRun = run('run-unresolved', 1, 'generic'); const left = definition('definition-left', 'unresolved:left', 'Synthetic'); const right = definition('definition-right', 'unresolved:right', 'Synthetic'); await commit(repository, scanRun, [left, right], [ occurrence('occurrence-left', scanRun, left.id, 'failed', 'Fail'), occurrence('occurrence-right', scanRun, right.id, 'failed', 'Fail') ]); const results = await repository.queryUnifiedResults({ search: 'Same visible title' }); expect(results.total).toBe(2); expect(new Set(results.items.map(item => item.canonicalDefinitionId))).toEqual( new Set(['definition-left', 'definition-right']) ); }); it('filters and facets by mapping rule version and source profile version', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const firstRun = run('run-mapping-a', 1, 'pingcastle'); const secondRun = run('run-mapping-b', 2, 'purple-knight'); const definitionA = definition('definition-mapping-a', 'pingcastle:mapping-a', 'Privileged Accounts'); const definitionB = definition('definition-mapping-b', 'purple-knight:mapping-b', 'Account Security'); const fileA: ImportFileRecord = { ...file(firstRun), processedWith: { ...file(firstRun).processedWith, profileVersion: '1.0.0', mappingVersion: '1.0.0' } }; const fileB: ImportFileRecord = { ...file(secondRun), processedWith: { ...file(secondRun).processedWith, profileVersion: '2.0.0', mappingVersion: '2.1.0' } }; async function commitWithFile(scanRun: ScanRun, importFile: ImportFileRecord, defs: FindingDefinition[], occurrences: FindingOccurrence[]): Promise { const session = await repository.beginImport({ sessionId: `session-${scanRun.id}`, projectId: project.id, assessmentId: assessment.id, scanRun }); await session.stage({ importFiles: [importFile], findingDefinitions: defs, findingOccurrences: occurrences }); await session.commit(); } await commitWithFile(firstRun, fileA, [definitionA], [occurrence('occurrence-mapping-a', firstRun, definitionA.id, 'failed', 'FAILED', 'PC-M1')]); await commitWithFile(secondRun, fileB, [definitionB], [occurrence('occurrence-mapping-b', secondRun, definitionB.id, 'failed', 'FAILED', 'PK-M1')]); const onlyNewMapping = await repository.queryUnifiedResults({ mappingRuleVersion: ['2.1.0'] }); expect(onlyNewMapping.items.map(item => item.sourceResultId)).toEqual(['occurrence-mapping-b']); const onlyOldProfile = await repository.queryUnifiedResults({ sourceProfileVersion: ['1.0.0'] }); expect(onlyOldProfile.items.map(item => item.sourceResultId)).toEqual(['occurrence-mapping-a']); const facets = await repository.queryUnifiedResultFacets({}); expect(facets.facets.find(facet => facet.id === 'mappingRuleVersion')?.values).toEqual( expect.arrayContaining([ expect.objectContaining({ value: '1.0.0', count: 1 }), expect.objectContaining({ value: '2.1.0', count: 1 }) ]) ); expect(facets.facets.find(facet => facet.id === 'sourceProfileVersion')?.values).toEqual( expect.arrayContaining([ expect.objectContaining({ value: '1.0.0', count: 1 }), expect.objectContaining({ value: '2.0.0', count: 1 }) ]) ); }); it('keeps all options of a facet visible even when filtering by that same facet', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const scanRun = run('run-severity-visibility', 1, 'pingcastle'); const definitionCritical = definition('definition-severity-critical', 'pingcastle:severity-critical', 'Privileged Accounts'); const definitionHigh = definition('definition-severity-high', 'pingcastle:severity-high', 'Privileged Accounts'); const criticalOccurrence: FindingOccurrence = { ...occurrence('occurrence-severity-critical', scanRun, definitionCritical.id, 'failed', 'FAILED', 'SEV-C'), severity: { originalValue: 'Critical', normalizedValue: 'critical', provenance } }; const highOccurrence = occurrence('occurrence-severity-high', scanRun, definitionHigh.id, 'failed', 'FAILED', 'SEV-H'); await commit(repository, scanRun, [definitionCritical, definitionHigh], [criticalOccurrence, highOccurrence]); const facetsFilteredToCritical = await repository.queryUnifiedResultFacets({ severity: ['critical'] }); expect(facetsFilteredToCritical.total).toBe(1); const severityFacet = facetsFilteredToCritical.facets.find(facet => facet.id === 'severity'); expect(severityFacet?.values.map(value => value.value).sort()).toEqual(['critical', 'high']); expect(severityFacet?.values.find(value => value.value === 'critical')?.count).toBe(1); expect(severityFacet?.values.find(value => value.value === 'high')?.count).toBe(1); }); // Ohne diese Reihenfolge muesste je Werkzeug nach einer eigenen Punktespalte // sortiert werden (Purple Knight: Weight, PingCastle: Points). Ueber die // kanonische Severity steht die Kritikalitaet fuer alle Berichte gleich oben. it('returns the most critical findings first, across tools', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const scanRun = run('run-severity-order', 1, 'purple-knight'); const severities: Array<[string, 'critical' | 'low' | 'high' | 'medium']> = [ ['low', 'low'], ['critical', 'critical'], ['medium', 'medium'], ['high', 'high'] ]; const definitions = severities.map(([name]) => definition(`definition-order-${name}`, `purple-knight:order-${name}`, 'Account Security')); const occurrences = severities.map(([name, severity], index) => ({ ...occurrence(`occurrence-order-${name}`, scanRun, definitions[index].id, 'failed', 'IOEs found', `ORD-${name}`), severity: { originalValue: severity, normalizedValue: severity, provenance } })); await commit(repository, scanRun, definitions, occurrences); const results = await repository.queryUnifiedResults({ projectId: project.id }); expect(results.items.map(item => item.severityNormalized)).toEqual(['critical', 'high', 'medium', 'low']); }); it('removes unavailable options of other facets after applying active filters', async () => { const repository = new LocalAssessorRepository(); await repository.saveProject(project); await repository.saveAssessment(assessment); const scanRun = run('run-cross-facet-visibility', 1, 'pingcastle'); const definitionCriticalFail = definition('definition-cross-critical-fail', 'pingcastle:cross-critical-fail', 'Privileged Accounts'); const definitionHighError = definition('definition-cross-high-error', 'pingcastle:cross-high-error', 'Privileged Accounts'); const criticalFailOccurrence: FindingOccurrence = { ...occurrence('occurrence-cross-critical-fail', scanRun, definitionCriticalFail.id, 'failed', 'FAILED', 'CROSS-1'), severity: { originalValue: 'Critical', normalizedValue: 'critical', provenance } }; const highErrorOccurrence = occurrence('occurrence-cross-high-error', scanRun, definitionHighError.id, 'error', 'Failed to run', 'CROSS-2'); await commit(repository, scanRun, [definitionCriticalFail, definitionHighError], [criticalFailOccurrence, highErrorOccurrence]); // Nur nach severity=critical gefiltert; die zweite Occurrence (severity high, // outcome execution_error) ist damit auch keine erreichbare Filteroption. const facets = await repository.queryUnifiedResultFacets({ severity: ['critical'] }); expect(facets.total).toBe(1); const outcomeFacet = facets.facets.find(facet => facet.id === 'outcome'); expect(outcomeFacet?.values.map(value => value.value)).toEqual(['fail']); expect(outcomeFacet?.values.find(value => value.value === 'fail')?.count).toBe(1); expect(outcomeFacet?.values.find(value => value.value === 'execution_error')).toBeUndefined(); }); });