// @vitest-environment jsdom import { describe, expect, it, vi } from 'vitest'; import pingCastleXml from '../../fixtures/pingcastle/healthcheck.synthetic.xml?raw'; import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow'; import { currentPolicyEvaluationsForWork, loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace'; import { createCanonicalImportBatch } from '../../src/importers/canonical-import'; import { importFile } from '../../src/importers/importer'; import { pingCastleProfile } from '../../src/profiles/pingcastle/pingcastle-profile'; import { importProfiles } from '../../src/profiles/profile-catalog'; import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model'; import { POLICY_EVALUATION_ENGINE, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation'; import { selectCurrentPolicyEvaluations } from '../../src/domain/unified-result'; describe('canonical project workspace loading', () => { it('uses one current evaluation per aggregation even when baseline coverage references more backups', () => { const evaluation = ( id: string, version: string, baselineCount: number, completedAt: string, observationId = 'observation-one' ) => ({ run: { id, aggregationRunId: 'aggregation-one', completedAt, baselineAnalysisIds: Array.from({ length: baselineCount }, (_, index) => `baseline-${index}`), evaluator: { ...POLICY_EVALUATION_ENGINE, version } }, results: [{ observationIds: [observationId], backupInstanceIds: ['backup-one'] }] }) as unknown as StoredPolicyEvaluation; const complete = evaluation('complete', POLICY_EVALUATION_ENGINE.version, 11, '2026-09-06T10:00:00Z'); complete.results[0].backupInstanceIds = ['backup-one', 'backup-two', 'backup-three']; const earlier = evaluation('earlier', POLICY_EVALUATION_ENGINE.version, 11, '2026-09-06T09:00:00Z'); earlier.results[0].backupInstanceIds = ['backup-one', 'backup-two']; const laterPartial = evaluation('later-partial', POLICY_EVALUATION_ENGINE.version, 1, '2026-09-06T11:00:00Z'); const obsolete = evaluation('obsolete', '1.3.0', 12, '2026-09-06T12:00:00Z'); const unrelated = evaluation('unrelated', POLICY_EVALUATION_ENGINE.version, 12, '2026-09-06T13:00:00Z', 'other'); expect(currentPolicyEvaluationsForWork( [laterPartial, obsolete, unrelated, earlier, complete], new Set(['observation-one']) )).toEqual([complete]); const previous = evaluation('previous-version', '1.3.3', 12, '2026-09-06T12:00:00Z'); expect(currentPolicyEvaluationsForWork([previous], new Set(['observation-one']), new Set([previous.run.id]))) .toEqual([previous]); expect(currentPolicyEvaluationsForWork([previous, complete], new Set(['observation-one']), new Set([previous.run.id]))) .toEqual([complete]); expect(currentPolicyEvaluationsForWork([complete, previous], new Set(['observation-one']), new Set([previous.run.id]))) .toEqual([complete]); expect(currentPolicyEvaluationsForWork([previous], new Set(['observation-one']), new Set())).toEqual([]); expect(selectCurrentPolicyEvaluations([previous])).toEqual([]); }); it('honors authoritative supersession, preserves separate imports and excludes mixed timeslots', () => { const evaluation = (id: string, aggregationRunId: string, observationIds: string[]) => ({ run: { id, aggregationRunId, completedAt: '2026-09-06T10:00:00Z', baselineAnalysisIds: ['baseline'], evaluator: POLICY_EVALUATION_ENGINE }, results: [{ observationIds, backupInstanceIds: ['same-backup'] }] }) as StoredPolicyEvaluation; const history = evaluation('history', 'superseded-aggregation', ['one']); const current = evaluation('current', 'current-aggregation', ['one']); const separateImport = evaluation('separate', 'separate-aggregation', ['two']); const otherSlot = evaluation('other-slot', 'other-aggregation', ['elsewhere']); const mixed = evaluation('mixed', 'mixed-aggregation', ['one', 'elsewhere']); const records = [history, current, separateImport, otherSlot, mixed]; const before = JSON.stringify(records); expect(currentPolicyEvaluationsForWork(records, new Set(['one', 'two']), new Set(['current', 'separate', 'other-slot', 'mixed']))).toEqual([current, separateImport]); expect(currentPolicyEvaluationsForWork(records, new Set(['one']), new Set())).toEqual([]); expect(JSON.stringify(records)).toBe(before); }); it('opens a GPO-only assessment without loading another timeslot or requiring scanner results', async () => { const stamp = '2026-09-06T00:00:00Z'; const model: CanonicalAssessmentModel = { schemaVersion: 1, project: { id: 'synthetic-client', name: 'Synthetic', description: '', attributes: {}, createdAt: stamp, updatedAt: stamp }, assessment: { id: 'gpo-slot', projectId: 'synthetic-client', name: 'Synthetic GPO slot', createdAt: stamp, updatedAt: stamp }, scanRuns: [], importFiles: [], sourceTables: [], findingDefinitions: [], findingOccurrences: [], evidence: [], reviews: [], diagnostics: [] }; const reader = { listProjectAssessments: vi.fn().mockResolvedValue([model.assessment, { ...model.assessment, id: 'other-slot' }]), loadAssessmentModel: vi.fn().mockResolvedValue(model), queryUnifiedResults: vi.fn().mockResolvedValue({ items: [], total: 0, limit: 500, offset: 0 }) }; const workspace = await loadCanonicalProjectWorkspace(reader, model.project.id, [], ['gpo-slot']); expect(reader.loadAssessmentModel).toHaveBeenCalledTimes(1); expect(reader.loadAssessmentModel).toHaveBeenCalledWith('gpo-slot'); expect(workspace.assessmentIds).toEqual(['gpo-slot']); expect(workspace.models).toEqual([model]); expect(workspace.dataset).toBeNull(); expect(reader.queryUnifiedResults).toHaveBeenCalledWith(expect.objectContaining({ assessmentIds: ['gpo-slot'] })); }); it('retains GPO results from an explicitly selected mixed timeslot when scanner runs live in another assessment', async () => { const stamp = '2026-09-06T00:00:00Z'; const base: CanonicalAssessmentModel = { schemaVersion: 1, project: { id: 'synthetic-client', name: 'Synthetic', description: '', attributes: {}, createdAt: stamp, updatedAt: stamp }, assessment: { id: 'scanner-slot', projectId: 'synthetic-client', name: 'Scanner slot', createdAt: stamp, updatedAt: stamp }, scanRuns: [{ id: 'scanner-run', assessmentId: 'scanner-slot', sequence: 1, label: 'Synthetic scanner', tool: { id: 'synthetic', name: 'Synthetic' }, importedAt: stamp }], importFiles: [], sourceTables: [], findingDefinitions: [], findingOccurrences: [], evidence: [], reviews: [], diagnostics: [] }; const gpoModel = { ...base, assessment: { ...base.assessment, id: 'gpo-slot', name: 'GPO slot' }, scanRuns: [] }; const observation = { sourceKind: 'policy-observation', sourceResultId: 'policy-one', resultProjectionId: 'policy-observation:policy-one', projectId: base.project.id, assessmentId: 'gpo-slot', importArtifactId: 'gpo-artifact', title: 'Synthetic policy', importedAt: stamp, policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyState: 'configured', policyIdentityStatus: 'resolved', policyIdentityKey: 'synthetic-policy', policyBackupInstanceId: 'backup-one', policyValues: [{ kind: 'registry-policy', key: 'Software\\Policies\\Synthetic', valueName: 'Mode', operation: 'set-value', typeCode: 4, data: { kind: 'uint32', value: 1 } }] }; const secondObservation = { ...observation, sourceResultId: 'policy-two', resultProjectionId: 'policy-observation:policy-two', policyBackupInstanceId: 'backup-two' }; const reader = { listProjectAssessments: vi.fn().mockResolvedValue([base.assessment, gpoModel.assessment]), loadAssessmentModel: vi.fn(async (id: string) => id === 'gpo-slot' ? gpoModel : base), queryUnifiedResults: vi.fn().mockResolvedValue({ items: [observation, secondObservation], total: 2, limit: 500, offset: 0 }), listPolicyEvaluations: vi.fn().mockResolvedValue({ records: [] }) }; const workspace = await loadCanonicalProjectWorkspace(reader, base.project.id, ['scanner-run'], ['scanner-slot', 'gpo-slot']); expect(reader.queryUnifiedResults).toHaveBeenCalledWith(expect.objectContaining({ assessmentIds: ['scanner-slot', 'gpo-slot'], sourceKind: ['policy-observation'] })); expect(workspace.assessmentIds).toEqual(['scanner-slot', 'gpo-slot']); expect(workspace.dataset?.sheets[0].analysis.records.some(record => record.fields.some(field => field.label === 'Source: Scan' && field.value.startsWith('GPOs')))).toBe(true); }); it('projects persisted PingCastle XML RiskRules instead of generic XML nodes', async () => { const imported = await importFile( new File([pingCastleXml], 'ad_hc_synthetic.example.xml', { type: 'application/xml' }), { profiles: importProfiles } ); const preview = previewEntryFromDataset(imported, { sha256: 'c'.repeat(64), fileName: imported.fileName, sizeBytes: pingCastleXml.length, lastModified: 0 }); const entry = confirmEntryMappings(preview, importProfiles); const model = createCanonicalImportBatch( 'pingcastle-workspace', [entry], '2026-07-22T12:00:00.000Z' ).model; const reader = { listProjectAssessments: vi.fn().mockResolvedValue([model.assessment]), loadAssessmentModel: vi.fn().mockResolvedValue(model) }; const workspace = await loadCanonicalProjectWorkspace(reader, model.project.id); expect(reader.listProjectAssessments).toHaveBeenCalledWith(model.project.id); expect(reader.loadAssessmentModel).toHaveBeenCalledWith(model.assessment.id); expect(workspace.dataset?.profileId).toBe('pingcastle'); expect(workspace.dataset?.sheets[1].analysis.headers).toEqual(expect.arrayContaining(['Source Rule ID', 'Title', 'Status'])); expect(workspace.dataset?.sheets[1].analysis.headers).not.toContain('DomainSid'); expect(workspace.dataset?.sheets[1].analysis.records[0].title).toBe('Synthetic inactive privileged account'); expect(pingCastleProfile.defaults(workspace.dataset?.sheets[1]).titleColumn).toBe('Title'); }); it('opens a selected PingCastle HTML finding with the Result DB title from its paired XML report', async () => { const imported = await importFile( new File([pingCastleXml], 'ad_hc_synthetic.example.xml', { type: 'application/xml' }), { profiles: importProfiles } ); const preview = previewEntryFromDataset(imported, { sha256: 'd'.repeat(64), fileName: imported.fileName, sizeBytes: pingCastleXml.length, lastModified: 0 }); const source = createCanonicalImportBatch('pingcastle-paired-report', [ confirmEntryMappings(preview, importProfiles) ], '2026-09-23T12:00:00.000Z').model; const resultTitle = 'The synthetic LAN Manager Authentication Level allows NTLMv1 or LM.'; const checkTitle = 'Ensure that synthetic NTLMv1 and LM protocols are banned'; const xml = structuredClone(source); xml.assessment.id = 'assessment-xml'; xml.scanRuns[0].assessmentId = 'assessment-xml'; xml.findingDefinitions[0].title = resultTitle; xml.findingOccurrences[0].sourceIdentity.originalRuleId = 'S-oldNtlm'; xml.findingOccurrences[0].sourceIdentity.canonicalRuleId = 'S-oldNtlm'; xml.findingOccurrences[0].sourceIdentity.originalTitle = resultTitle; const html = structuredClone(xml); html.assessment.id = 'assessment-html'; html.scanRuns[0].id = 'run-html'; html.scanRuns[0].assessmentId = 'assessment-html'; html.importFiles[0].id = 'file-html'; html.importFiles[0].scanRunId = 'run-html'; html.importFiles[0].fileName = 'ad_hc_synthetic.example.html'; html.importFiles[0].format = 'HTML'; html.findingDefinitions[0].title = checkTitle; html.findingOccurrences[0].id = 'occurrence-html'; html.findingOccurrences[0].scanRunId = 'run-html'; html.findingOccurrences[0].importFileId = 'file-html'; html.findingOccurrences[0].sourceIdentity.originalTitle = checkTitle; html.findingOccurrences[0].originalFields = [ { key: 'risk-id', label: 'Rule ID', value: 'S-oldNtlm' }, { key: 'title', label: 'Title', value: checkTitle }, { key: 'description', label: 'Description', value: 'Synthetic scanner explanation.' } ]; const models = new Map([[html.assessment.id, html], [xml.assessment.id, xml]]); const reader = { listProjectAssessments: vi.fn().mockResolvedValue([html.assessment, xml.assessment]), loadAssessmentModel: vi.fn(async (id: string) => models.get(id)!) }; const workspace = await loadCanonicalProjectWorkspace(reader, html.project.id, ['run-html'], ['assessment-html', 'assessment-xml']); const records = workspace.dataset?.sheets[0].analysis.records ?? []; expect(workspace.models).toHaveLength(1); expect(records).toHaveLength(1); expect(records[0].title).toBe(resultTitle); expect(records[0].fields.find(field => field.label === 'Source Rule ID')?.value).toBe('S-oldNtlm'); expect(records[0].fields.find(field => field.label === 'Native source heading')?.value).toBe(checkTitle); expect(html.findingOccurrences[0].sourceIdentity.originalTitle).toBe(checkTitle); }); });