// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import summaryFixture from '../../fixtures/purple-knight/assessment-summary.synthetic.csv?raw'; import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation'; import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow'; import { createCanonicalImportBatch } from '../../src/importers/canonical-import'; import { importFile } from '../../src/importers/importer'; import { importProfiles } from '../../src/profiles/profile-catalog'; import { canonicalSecurityScores } from '../../src/scoring/canonical-security-score'; const FILE_NAME = 'Security_Assessment_Report_synthetic.example_(Assessment_summary).csv'; const ENTRA_SUMMARY = `Generated date,2026-07-08 09:01 Selected indicators,56 Entra ID: scan duration,00:00:10 Entra ID: tenant,synthetic.onmicrosoft.com Entra ID: application ID,00000000-0000-0000-0000-000000000000 Entra ID: security posture score,64 Entra ID category score: Entra ID,61 Entra ID category score: Hybrid,70 Entra ID: Evaluated,56 Entra ID: IOEs found,19 Entra ID: Passed,37 `; async function importSummary(content = summaryFixture, fileName = FILE_NAME) { return importFile(new File([content], fileName, { type: 'text/csv' }), { profiles: importProfiles }); } function observationValue( observations: NonNullable>['extraction']>['environmentObservations'], property: string ) { return observations?.find(item => item.property === property); } describe('Purple Knight assessment summary source extraction', () => { it('turns the summary metrics into typed environment observations without creating findings', async () => { const imported = await importSummary(); expect(imported.extraction?.extractor.id).toBe('purple-knight-summary-source-extractor'); expect(imported.extraction?.candidates).toHaveLength(0); const observations = imported.extraction?.environmentObservations; expect(observationValue(observations, 'purple_knight.ad.security_posture_score')).toMatchObject({ normalizedValue: 71, valueType: 'number', state: 'observed' }); expect(observationValue(observations, 'purple_knight.ad.ioes_found')?.normalizedValue).toBe(17); expect(observationValue(observations, 'purple_knight.ad.evaluated')?.normalizedValue).toBe(117); expect(observationValue(observations, 'purple_knight.ad.passed')?.normalizedValue).toBe(100); expect(observationValue(observations, 'purple_knight.ad.not_relevant')?.normalizedValue).toBe(2); expect(observationValue(observations, 'purple_knight.tool_edition')?.originalValue).toBe('Community'); expect(observationValue(observations, 'purple_knight.ad.scan_duration')?.originalValue).toBe('00:00:21'); }); it('maps tool-neutral facts to the shared environment properties and keeps category scores separate', async () => { const observations = (await importSummary()).extraction?.environmentObservations; expect(observationValue(observations, 'forest.name')).toMatchObject({ originalValue: 'synthetic.example', subject: { type: 'active-directory-environment' } }); expect(observationValue(observations, 'domain.count')?.normalizedValue).toBe(2); expect(observationValue(observations, 'report.generated_at')).toMatchObject({ originalValue: '2026-01-15 09:30', valueType: 'datetime' }); expect(observationValue(observations, 'purple_knight.ad.category_score.kerberos_security')).toMatchObject({ label: 'Kerberos Security', normalizedValue: 95, subject: { type: 'security-domain', identity: 'kerberos_security' } }); expect(observations?.filter(item => item.property.startsWith('purple_knight.ad.category_score.'))).toHaveLength(6); }); it('recognizes the summary as a Purple Knight source and persists it as context without findings', async () => { const imported = await importSummary(); expect(imported.profileId).toBe('purple-knight'); expect(imported.recognition?.source.selected.toolId).toBe('purple-knight'); expect(imported.recognition?.source.requiresConfirmation).toBe(false); const preview = previewEntryFromDataset(imported, { sha256: FILE_NAME.padEnd(64, '0').slice(0, 64), fileName: FILE_NAME, sizeBytes: summaryFixture.length, lastModified: 0 }); const model = createCanonicalImportBatch('summary', [confirmEntryMappings(preview, importProfiles)], '2026-08-25T08:00:00.000Z').model; assertCanonicalAssessmentModel(model); expect(model.findingOccurrences).toHaveLength(0); expect(model.scanRuns[0].tool).toEqual({ id: 'purple-knight', name: 'Purple Knight for Active Directory' }); expect(model.scanRuns[0].tool.id).not.toBe('maester'); expect(model.scanRuns[0].tool.name).not.toMatch(/maester|entra/i); expect(model.environmentObservations?.some(item => item.tool.id === 'maester')).toBe(false); expect(model.environmentObservations?.some(item => item.property.startsWith('maester.') || item.property.startsWith('entra.'))).toBe(false); expect(model.environmentObservations?.every(item => item.tool.id === 'purple-knight')).toBe(true); expect(model.environmentObservations?.find(item => item.property === 'purple_knight.ad.security_posture_score')?.normalizedValue).toBe(71); expect(model.reportSections?.map(section => section.sectionKey)).toContain('purple-knight-assessment-summary'); }); it('recognizes Purple Knight for Entra ID, retains its tenant and labels the scan scope', async () => { const fileName = 'Security_Assessment_Report_synthetic.example_(Assessment_summary).csv'; const imported = await importSummary(ENTRA_SUMMARY, fileName); const observations = imported.extraction?.environmentObservations; expect(imported.profileId).toBe('purple-knight'); expect(observationValue(observations, 'entra.tenant_name')).toMatchObject({ normalizedValue: 'synthetic.onmicrosoft.com', subject: { type: 'entra-tenant', identity: 'synthetic.onmicrosoft.com' } }); expect(observationValue(observations, 'purple_knight.entra.security_posture_score')?.normalizedValue).toBe(64); expect(observationValue(observations, 'purple_knight.entra.category_score.entra_id')?.normalizedValue).toBe(61); const preview = previewEntryFromDataset(imported, { sha256: fileName.padEnd(64, '0').slice(0, 64), fileName, sizeBytes: ENTRA_SUMMARY.length, lastModified: 0 }); const model = createCanonicalImportBatch('entra-summary', [confirmEntryMappings(preview, importProfiles)], '2026-09-22T08:00:00.000Z').model; expect(model.scanRuns[0].tool).toMatchObject({ id: 'purple-knight', name: 'Purple Knight for Entra ID' }); expect(model.importFiles[0].detectedTool.name).toBe('Purple Knight for Entra ID'); expect(canonicalSecurityScores([model])[0]).toMatchObject({ toolId: 'purple-knight', toolName: 'Purple Knight for Entra ID', method: 'native', score: 64, nativeProperty: 'purple_knight.entra.security_posture_score' }); }); it('leaves an unrelated two-column table to the generic extractor', async () => { const imported = await importFile( new File(['Metric,Value\nUptime,12\nOwner,Team A\n'], 'inventory.csv', { type: 'text/csv' }), { profiles: importProfiles } ); expect(imported.extraction?.extractor.id).not.toBe('purple-knight-summary-source-extractor'); }); });