// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import * as XLSX from 'xlsx'; import type { JsonValue } from '../../src/domain/canonical-model'; import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation'; import { importFile } from '../../src/importers/importer'; import { importSource } from '../../src/importers/engine'; import { nodeImportRuntimeAdapters } from '../../server/node-import-runtime-adapters'; import { importProfiles } from '../../src/profiles/profile-catalog'; import { createCanonicalImportBatch } from '../../src/importers/canonical-import'; import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow'; import { canonicalModelToDataset } from '../../src/features/project-import/canonical-model-to-dataset'; import { linkResultSheets } from '../../src/profiles/purple-knight/result-linker'; import { processingReadiness } from '../../src/domain/processing-compatibility'; import { ImportError } from '../../src/shared/errors'; const names = ['SI900001', 'SI900002']; const indicators = names.map((ShortName, index) => ({ ResIndicator: { ShortName, UUID: `id-${index}`, Name: `Synthetic check ${index}`, Version: '1', Severity: 'High', Weight: 10, CategoryID: 1, Selected: true, IsEvaluated: true, IsFailed: index === 0, ExposureType: index === 0 ? 'IOE' : 'None', Targets: ['AD'], Description: '

Synthetic description.

', StartTime: '2026-01-15T09:30:00.0000000Z', CompletionTime: '2026-01-15T09:30:01.2300000Z', SecurityFrameworks: [{ Name: 'MITRE ATT&CK', Tags: ['T1', 'T2', 'T3', 'T4', 'T5'] }], Future: { Empty: '', Zero: 0, Flag: false } }, ExecutionResult: { Status: index === 0 ? 'Failed' : 'Pass', Score: index === 0 ? 10 : 0, ResultMessage: 'Synthetic result', Remediation: '

Review it.

' }, IndicatorReportObjects: index === 0 ? [['Object: synthetic', 'EventTimestamp: 1/15/2026 9:30:57 AM', 'Ignored: False']] : [], TotalResultsCount: index === 0 ? 1 : 0 })); const root = { AppVersion: 'synthetic', Edition: 'Community', GeneralConfig: { Categories: [{ ID: 1, Name: 'Synthetic' }] }, FutureMetadata: { unknown: [false, 0, ''] }, IndicatorsTotalCount: { 'id-0': 1 }, reportResultsList: [{ ADType: 'AD', ForestName: 'synthetic.example', SelectedDomains: ['synthetic.example'], UserName: 'synthetic runner', ReportResults: { TotalScore: 50, TotalCount: 2, TotalEvaluatedCount: 2, PassedCount: 1, IOEsCount: 1, IOCsCount: 0, NotRelevant: 0, UnselectedCount: 0, GeneratedOn: '2026-01-15 09:30', TotalSelectedCount: 2 }, ReportCategories: [{ Category: { ID: 1, Name: 'Synthetic', Score: 75 }, Indicators: ['id-0', 'id-1'] }] }] }; const header = ['ShortName', 'Name', 'SI version', 'Status', 'Severity', 'Weight', 'Number of results', 'Result']; const rows = indicators.map((item, index) => [item.ResIndicator.ShortName, item.ResIndicator.Name, '1', index === 0 ? 'IOE Found' : 'Pass', 'High', 10, item.TotalResultsCount, index === 0 ? names[0] : '']); const summary = [['Tool edition', 'Community'], ['Tool version', 'synthetic'], ['AD: Security posture score', 50], ['AD: Forest name', 'synthetic.example'], ['AD: Number of domains', 1], ['Selected indicators', 2], ['AD: Passed', 1], ['AD: IOEs found', 1], ['AD category score: Synthetic', 75]]; function html(data: unknown = root, items: unknown[] = indicators): string { return ``; } async function parse(content: string, name = 'report.html') { return importFile(new File([content], name), { profiles: importProfiles }); } function canonical(dataset: Awaited>) { const preview = previewEntryFromDataset(dataset, { fileName: dataset.fileName, sha256: 'a'.repeat(64), sizeBytes: 1, lastModified: 0 }); const result = createCanonicalImportBatch('synthetic-pk', [confirmEntryMappings(preview, importProfiles)], '2026-09-09T00:00:00Z').model; assertCanonicalAssessmentModel(result); return result; } async function expectDiagnostic(content: string, code: string) { let caught: unknown; try { await parse(content); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(ImportError); expect((caught as ImportError).diagnostics.some(item => item.code === code)).toBe(true); } describe('PurpleKnight full-report integrity', () => { it('imports workbook findings alongside its summary and resolves retained detail sheets', async () => { const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(summary), 'Assessment summary'); XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([header, ...rows]), 'Indicators results'); XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([['Object', 'EventTimestamp'], ['synthetic', '2026-01-15 09:30:57']]), names[0]); const bytes = new Uint8Array(XLSX.write(workbook, {type: 'array', bookType: 'xlsx'})); const browser = await importFile(new File([bytes], 'report.xlsx'), {profiles:importProfiles}); const server = await importSource({metadata:{name:'report.xlsx',sizeBytes:bytes.length,lastModified:0},bytes}, {profiles:importProfiles}, nodeImportRuntimeAdapters); expect(browser.extraction).toEqual(server.extraction); const model = canonical(browser); expect(model.findingOccurrences).toHaveLength(2); expect(model.environmentObservations).toHaveLength(summary.length); const projected = canonicalModelToDataset(model)!; const main = projected.sheets.find(sheet => sheet.sourceSheet === 'Indicators results')!; expect(linkResultSheets(main.analysis.records[0], projected.sheets, main.id)).toHaveLength(1); expect(model.findingOccurrences.map(item => item.status.normalizedValue)).toEqual(['failed', 'passed']); }); it('extracts HTML metadata and the same semantic summary values as CSV', async () => { const htmlModel = canonical(await parse(html())); const csvModel = canonical(await parse(summary.map(row => row.join(';')).join('\n'), 'Assessment_summary.csv')); for (const metric of csvModel.environmentObservations ?? []) { expect(htmlModel.environmentObservations?.find(item => item.property === metric.property)?.normalizedValue, metric.property).toEqual(metric.normalizedValue); } expect(htmlModel.environmentObservations?.find(item => item.property === 'purple_knight.source.FutureMetadata')?.originalValue).toEqual(root.FutureMetadata); expect(htmlModel.reportSections).toHaveLength(1); expect((window as Window & { __pkExecuted?: boolean }).__pkExecuted).toBeUndefined(); }); it('preserves exact native field paths, original values, unknown fields and framework overflow', async () => { const imported = await parse(html()); const fields = imported.extraction!.candidates[0].fields; const resolve = (path: string): JsonValue | undefined => { const parts = [...path.matchAll(/\.([\w]+)|\["([^"]+)"\]|\[(\d+)\]/g)].map(match => match[1] ?? match[2] ?? match[3]); let current: unknown = { Category_1: { 'id-0': indicators[0], 'id-1': indicators[1] } }; for (const key of parts) current = (current as Record)?.[key]; return current as JsonValue | undefined; }; for (const field of fields) if (field.source.objectPath) expect(field.originalValue, field.label).toEqual(resolve(field.source.objectPath)); expect(fields.find(field => field.label === 'Source ResIndicator.Future')?.originalValue).toEqual(indicators[0].ResIndicator.Future); expect(fields.find(field => field.label === 'Source ResIndicator.SecurityFrameworks')?.originalValue).toEqual(indicators[0].ResIndicator.SecurityFrameworks); expect(fields.find(field => field.label === 'Runtime')?.decodedValue).toBe('00:00:01:23'); expect(fields.find(field => field.label === 'Status')?.decodedValue).toBe('IOE Found'); expect(fields.find(field => field.label === 'Category')?.decodedValue).toBe('Synthetic'); expect(imported.extraction!.supportingTables![0].rows[0][1]).toBe('1/15/2026 9:30:57 AM'); const model = canonical(imported); expect(model.findingOccurrences[0].originalFields.find(field => field.label === 'Source ResIndicator.Future')?.value).toEqual(indicators[0].ResIndicator.Future); const work = canonicalModelToDataset(model)!; const record = work.sheets.find(sheet => !sheet.isProjectSheet && sheet.analysis.records.some(record => record.fields.some(field => field.label === 'Source Rule ID')))?.analysis.records[0]; expect(record?.fields.find(field => field.label === 'Status')?.value).toBe('IOE Found'); expect(record?.fields.find(field => field.label === 'Category')?.value).toBe('Synthetic'); }); it('uses structural HTML recognition even behind the raw-text detection limit', async () => { const imported = await parse(html().replace('', ``), 'neutral.html'); expect(imported.profileId).toBe('purple-knight'); expect(imported.recognition?.source.requiresConfirmation).toBe(false); }); it('ignores report-like text in comments and strings without executing anything', async () => { const decoy = ``; expect((await parse(html().replace('', `${decoy}`))).extraction?.candidates).toHaveLength(2); }); it('ignores fake script elements in attributes, HTML comments and raw-text containers', async () => { const decoy = ''; const content = html().replace('', `
`) .replace('`), 'purple-knight-binding-duplicate'], ['malformed details', html(root, [{...indicators[0], IndicatorReportObjects: [42]}, indicators[1]]), 'purple-knight-detail-invalid'], ['unreadable nonempty report', html(root, []), 'purple-knight-results-missing'], ['missing report summary', html().replace('window.reportJSON', 'window.unrelated'), 'purple-knight-summary-missing'] ])('rejects %s explicitly', async (_case, content, code) => expectDiagnostic(content, code)); it.each(['TotalCount', 'PassedCount', 'IOEsCount', 'NotRelevant', 'TotalEvaluatedCount'])('diagnoses a wrong %s without dropping results', async key => { const modified = structuredClone(root); Object.assign(modified.reportResultsList[0].ReportResults, {[key]:99}); const imported = await parse(html(modified)); expect(imported.extraction?.processing.status).toBe('partial'); expect(imported.extraction?.diagnostics.some(item => item.code === 'purple-knight-count-mismatch')).toBe(true); expect(canonical(imported).findingOccurrences).toHaveLength(2); }); it('reports detail-count mismatches and duplicate identities while retaining original evidence', async () => { const duplicate = {...indicators[1],ResIndicator:{...indicators[1].ResIndicator,ShortName:names[0]}}; const imported = await parse(html(root,[{...indicators[0],TotalResultsCount:5},duplicate])); expect(imported.extraction?.diagnostics.map(item => item.code)).toEqual(expect.arrayContaining(['purple-knight-id-duplicate','purple-knight-count-mismatch'])); expect(canonical(imported).findingOccurrences).toHaveLength(2); }); it('treats prototype-like unknown status text as source data, not a counter key', async () => { const unknown = {...indicators[1],ExecutionResult:{...indicators[1].ExecutionResult,Status:'constructor'}}; const imported = await parse(html(root,[indicators[0],unknown])); expect(canonical(imported).findingOccurrences).toHaveLength(2); expect(imported.extraction?.diagnostics.map(item => item.code)).toContain('purple-knight-count-mismatch'); expect(JSON.stringify(imported.extraction)).not.toContain('function Object'); }); it('preserves a declared empty report and its zero metrics', async () => { const empty = {...root,reportResultsList:[{ADType:'AD',ReportResults:{TotalCount:0,PassedCount:0,TotalScore:0},ReportCategories:[]}]}; const model = canonical(await parse(html(empty,[]))); expect(model.findingOccurrences).toHaveLength(0); expect(model.environmentObservations?.find(item => item.property === 'purple_knight.ad.security_posture_score')?.normalizedValue).toBe(0); }); it('accepts explicit null details for an indicator with no results', async () => { const imported = await parse(html(root,[indicators[0],{...indicators[1],IndicatorReportObjects:null}])); expect(canonical(imported).findingOccurrences).toHaveLength(2); expect(imported.extraction?.supportingTables).toHaveLength(1); expect(imported.extraction?.diagnostics).toEqual([]); expect(imported.extraction?.candidates[1].fields.find(field => field.label === 'Result')).toMatchObject({ originalValue:null, source:{objectPath:'$.Category_1["id-1"].IndicatorReportObjects'} }); }); it('recognizes a native all-passed CSV by name and schema, but not filename alone', async () => { const name = 'Security_Assessment_Report_synthetic_2026_01_15_09_30_00_(Indicators_results).csv'; const imported = await parse([header,...rows.map(row => row.map((cell,index) => index === 3 ? 'Pass' : cell))].map(row => row.join(';')).join('\n'), name); expect(imported.profileId).toBe('purple-knight'); expect(imported.recognition?.source.requiresConfirmation).toBe(false); const unrelated = await parse('Title;Value\nSynthetic;1',name); expect(unrelated.profileId).toBe('generic'); }); it('keeps old imports compatible and recommends an explicit reimport', () => { expect(processingReadiness('purple-knight',{parserId:'synthetic',parserVersion:'1',profileId:'purple-knight',profileVersion:'1.2.0',mappingVersion:'1.1.0'})).toBe('reimport-recommended'); expect(processingReadiness('purple-knight',{parserId:'synthetic',parserVersion:'1',profileId:'purple-knight',profileVersion:'1.3.0',mappingVersion:'1.1.0'})).toBe('reimport-recommended'); expect(processingReadiness('purple-knight',{parserId:'synthetic',parserVersion:'1',profileId:'purple-knight',profileVersion:'1.4.0',mappingVersion:'1.1.0'})).toBe('current'); }); });