// @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');
});
});