// @vitest-environment jsdom import { readFileSync, readdirSync } from 'node:fs'; import { createHash } from 'node:crypto'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import * as XLSX from 'xlsx'; import contracts from './import-contracts.json'; 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 { previewEntryFromDataset, confirmEntryMappings } from '../../src/features/import-workflow/import-workflow'; import { createCanonicalImportBatch } from '../../src/importers/canonical-import'; import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation'; import { canonicalSecurityScores } from '../../src/scoring/canonical-security-score'; import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model'; // Keep all source data intact. Only generated record links, processing versions and // import timestamps are excluded: a compatible implementation may change these. function semanticRecord(record: object) { const excluded = new Set(['id', 'definitionId', 'occurrenceId', 'scanRunId', 'importFileId', 'sourceTableId', 'createdAt', 'updatedAt', 'mappingRuleVersion', 'mappingVersion', 'extractorVersion']); return Object.fromEntries(Object.entries(record).filter(([key]) => !excluded.has(key))); } function mapped(value: { originalValue: unknown; normalizedValue: unknown } | undefined) { return value && { originalValue: value.originalValue, normalizedValue: value.normalizedValue }; } function result(model: CanonicalAssessmentModel) { return { definitions: model.findingDefinitions.map(semanticRecord), findings: model.findingOccurrences.map(item => ({ ...semanticRecord(item), status: mapped(item.status), severity: mapped(item.severity), outcome: mapped(item.outcome), technicalExecutionState: mapped(item.technicalExecutionState), score: mapped(item.score), affectedAsset: mapped(item.affectedAsset), fieldMappings: item.fieldMappings.map(({ provenance, ...mapping }) => ({ ...mapping, decisionSource: provenance.decisionSource })), })), evidence: model.evidence.map(item => ({ ...semanticRecord(item), finding: model.findingOccurrences.find(finding => finding.id === item.occurrenceId)?.sourceIdentity })), observations: model.environmentObservations?.map(semanticRecord), reportSections: model.reportSections?.map(semanticRecord), scores: canonicalSecurityScores([model]).map(semanticRecord), tables: model.sourceTables.map(table => ({ name: table.name, index: table.index, hidden: table.hidden, rows: table.rows, rowCount: table.rowCount, columnCount: table.columnCount })), }; } function input(contract: typeof contracts[number]) { const text = readFileSync(resolve(contract.fixture), 'utf8').replaceAll('\r\n', '\n'); expect(createHash('sha256').update(text).digest('hex'), `${contract.id}: protected fixture changed`).toBe(contract.sha256); if (contract.variant === 'xlsx') { const sheet = XLSX.read(text, { type: 'string', raw: true }).Sheets.Sheet1; const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, sheet, 'Tests'); return new Uint8Array(XLSX.write(workbook, { type: 'array', bookType: 'xlsx' })); } const transformed = contract.variant === 'bare-json' ? text.replace(/const testResults\s*=\s*/, '') : contract.variant === 'let' || contract.variant === 'var' ? text.replace('const testResults=', `${contract.variant} testResults=`) : text; return new TextEncoder().encode(transformed); } describe('protected scanner import contracts (synthetic data only)', () => { it('covers every registered tool and keeps contract identifiers unique', () => { expect(new Set(contracts.map(item => item.id)).size).toBe(contracts.length); expect([...new Set(contracts.map(item => item.tool))].sort()).toEqual(importProfiles.map(item => item.id).sort()); expect(contracts.every(item => item.fixture.includes('.synthetic.'))).toBe(true); const originalContracts = ['pingcastle-html', 'pingcastle-xml', 'cis-cat-html', 'cis-cat-future-controls', 'cis-cat-summary', 'maester-csv', 'maester-legacy-html', 'maester-literal-html', 'maester-current-html', 'purple-knight-html', 'purple-knight-json', 'purple-knight-summary', 'generic-csv', 'maester-bare-json', 'maester-let', 'maester-var', 'maester-xlsx', 'maester-multi-tenant']; expect(contracts.map(item => item.id)).toEqual(expect.arrayContaining(originalContracts)); const fixtures = readdirSync('fixtures', { recursive: true }).map(String) .filter(path => path.includes('.synthetic.')).map(path => `fixtures/${path.replaceAll('\\', '/')}`); expect([...new Set(contracts.map(item => item.fixture))].sort()).toEqual(fixtures.sort()); }); it('requires the fail-closed compatibility gate before normal tests and production builds', () => { const pkg = JSON.parse(readFileSync('package.json', 'utf8')); expect(pkg.scripts.test).toMatch(/^npm run test:imports && /); expect(pkg.scripts.prebuild).toBe('npm run test:imports'); expect(pkg.scripts['test:imports']).toBe('node scripts/check-import-contracts.mjs'); }); it.each(contracts)('$id retains recognition, preview and approved canonical results in browser and backend', async contract => { const bytes = input(contract); const metadata = { name: contract.fileName, sizeBytes: bytes.length, lastModified: 0, mediaType: contract.mediaType }; const browser = await importFile(new File([bytes], contract.fileName, { type: contract.mediaType }), { profiles: importProfiles }); const backend = await importSource({ bytes, metadata }, { profiles: importProfiles }, nodeImportRuntimeAdapters); const outputs = []; for (const dataset of [browser, backend]) { expect(dataset.profileId).toBe(contract.tool); expect(dataset.format).toBe(contract.format); const preview = previewEntryFromDataset(dataset, { ...metadata, fileName: contract.fileName, sha256: createHash('sha256').update(bytes).digest('hex') }); expect(preview.status).not.toBe('error'); const approved = confirmEntryMappings(preview, importProfiles); const model = createCanonicalImportBatch('protected-dummy-import', [approved], '2026-10-09T00:00:00Z').model; assertCanonicalAssessmentModel(model); expect(model.findingOccurrences).toHaveLength(contract.findings); outputs.push({ recognition: dataset.recognition?.source.selected.toolId, preview: { selected: preview.selected, recordCount: preview.recordCount, mappingsToConfirm: preview.mappingsToConfirm }, approved: { selected: approved.selected, recordCount: approved.recordCount }, ...result(model) }); } expect(outputs[1]).toEqual(outputs[0]); expect(outputs[0]).toMatchSnapshot(); }); });