155 lines
12 KiB
TypeScript
155 lines
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, expect, it } from 'vitest';
|
|
import * as XLSX from 'xlsx';
|
|
import literalFixture from '../../fixtures/maester/object-literal.synthetic.html?raw';
|
|
import { importFile } from '../../src/importers/importer';
|
|
import { importSource } from '../../src/importers/engine';
|
|
import { importProfiles } from '../../src/profiles/profile-catalog';
|
|
import { nodeImportRuntimeAdapters } from '../../server/node-import-runtime-adapters';
|
|
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 { canonicalModelToDataset } from '../../src/features/project-import/canonical-model-to-dataset';
|
|
import { resultMessageFor, resultStatusFieldFor } from '../../src/features/linked-results/result-message';
|
|
import { processingReadiness } from '../../src/domain/processing-compatibility';
|
|
import { mapFindingCandidates } from '../../src/importers/candidate-mapping';
|
|
import { maesterProfile } from '../../src/profiles/maester/maester-profile';
|
|
import type { SourceExtractionResult } from '../../src/domain/source-extraction';
|
|
|
|
const htmlImport = (html: string) => importFile(new File([html], 'ReportTemplate.html', { type: 'text/html' }), { profiles: importProfiles });
|
|
function canonical(dataset: Awaited<ReturnType<typeof htmlImport>>) {
|
|
const entry = previewEntryFromDataset(dataset, { sha256: 'a'.repeat(64), fileName: dataset.fileName, sizeBytes: 1000, lastModified: 0 });
|
|
const model = createCanonicalImportBatch('maester-coverage', [confirmEntryMappings(entry, importProfiles)], '2026-09-09T08:00:00Z').model;
|
|
assertCanonicalAssessmentModel(model);
|
|
return model;
|
|
}
|
|
|
|
describe('Maester variant and context coverage', () => {
|
|
it('imports all literal tests through the browser and server without running the report', async () => {
|
|
delete (globalThis as Record<string, unknown>).__maesterActiveContentExecuted;
|
|
const bytes = new TextEncoder().encode(literalFixture);
|
|
const results = [await htmlImport(literalFixture), await importSource({ metadata: { name: 'ReportTemplate.html', mediaType: 'text/html', lastModified: 0, sizeBytes: bytes.length }, bytes }, { profiles: importProfiles }, nodeImportRuntimeAdapters)];
|
|
for (const imported of results) {
|
|
expect(imported.profileId).toBe('maester');
|
|
expect(imported.recognition?.source.requiresConfirmation).toBe(false);
|
|
expect(imported.extraction?.candidates).toHaveLength(6);
|
|
expect(imported.extraction?.processing.status).toBe('partial');
|
|
expect(imported.extraction?.diagnostics.filter(item => item.code === 'maester-count-mismatch').map(item => item.details)).toEqual([
|
|
{ objectPath: '$.TotalCount', counter: 'TotalCount', declared: 5, actual: 6 },
|
|
{ objectPath: '$.FailedCount', counter: 'FailedCount', declared: 0, actual: 1 }
|
|
]);
|
|
const model = canonical(imported);
|
|
expect(model.findingOccurrences.map(item => item.status.normalizedValue)).toEqual(['failed', 'investigate', 'passed', 'not-run', 'not-run', 'error']);
|
|
expect(model.findingOccurrences.map(item => item.status.originalValue)).toEqual(['Failed', 'Investigate', 'Passed', 'Skipped', 'NotRun', 'Error']);
|
|
expect(model.findingOccurrences[0].originalFields).toContainEqual(expect.objectContaining({ label: 'FutureField', value: 0, path: '$.Tests[0].FutureField' }));
|
|
expect(model.findingOccurrences[0].originalFields).toContainEqual(expect.objectContaining({ label: 'ResultDetail.FutureDetail', value: { retained: true }, path: '$.Tests[0].ResultDetail.FutureDetail' }));
|
|
expect(model.evidence.filter(item => item.sourceFieldKeys.includes('TestResult'))).toHaveLength(3);
|
|
expect(model.environmentObservations).toContainEqual(expect.objectContaining({ property: 'entra.tenant_id', normalizedValue: 'synthetic-tenant-id', source: expect.objectContaining({ objectPath: '$.TenantId' }) }));
|
|
expect(model.environmentObservations).toContainEqual(expect.objectContaining({ property: 'maester.source.SystemInfo', originalValue: { OS: 'Synthetic OS', Available: true, Count: 0, Nullable: null } }));
|
|
expect(model.environmentObservations).toContainEqual(expect.objectContaining({ property: 'maester.tests.declared_total', originalValue: 5 }));
|
|
expect(model.environmentObservations).toContainEqual(expect.objectContaining({ property: 'maester.tests.actual_total', normalizedValue: 6, attributes: expect.objectContaining({ derived: true }) }));
|
|
expect(model.environmentObservations).toContainEqual(expect.objectContaining({ property: 'maester.source.LoadedModules', originalValue: [], state: 'empty' }));
|
|
const projection = canonicalModelToDataset(model)!;
|
|
const failed = projection.sheets.flatMap(sheet => sheet.analysis.records).find(record => record.fields.some(field => field.label === 'Source Rule ID' && field.value === 'MT.9901'))!;
|
|
expect(resultMessageFor(failed, 'maester')).toBe('<p>Synthetic evidence</p>');
|
|
expect(resultStatusFieldFor(failed, 'maester')?.value).toBe('Failed');
|
|
}
|
|
expect((globalThis as Record<string, unknown>).__maesterActiveContentExecuted).toBeUndefined();
|
|
});
|
|
|
|
it('recognizes a validated report beyond the raw-text detection sample', async () => {
|
|
const source = literalFixture.replace('<script>', '<script>/*' + 'x'.repeat(300_000) + '*/');
|
|
const imported = await htmlImport(source.replace('<title>Maester Security Tests</title>', '<title>Report</title>'));
|
|
expect(imported.profileId).toBe('maester');
|
|
expect(imported.recognition?.source.selected.evidence).toContainEqual(expect.objectContaining({ signal: 'source-extractor' }));
|
|
});
|
|
|
|
it('maps Excel Result as evidence regardless of column order and preserves every cell', async () => {
|
|
const rows = [['ID', 'Result', 'Title', 'Severity', 'Status', 'Notes'],
|
|
['MT.9910', 'Synthetic result text', 'Synthetic Excel test', 'High', 'Failed', 'Synthetic note'],
|
|
['MT.9911', 'Passed', 'Synthetic second test', 'Low', 'Investigate', '']];
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(rows), 'Tests');
|
|
const imported = await importFile(new File([XLSX.write(workbook, { type: 'array', bookType: 'xlsx' })], 'Maester.xlsx'), { profiles: importProfiles });
|
|
expect(imported.recognition?.fields.find(field => field.columnIndex === 1)?.appliedRole).toBe('evidence');
|
|
const model = canonical(imported);
|
|
expect(model.findingOccurrences.map(item => item.status.normalizedValue)).toEqual(['failed', 'investigate']);
|
|
expect(model.findingOccurrences.map(item => item.originalFields.map(field => field.value))).toEqual(rows.slice(1));
|
|
expect(model.evidence.map(item => item.value)).toEqual(['Synthetic result text', 'Passed']);
|
|
const record = imported.sheets[0].analysis.records[0];
|
|
expect(resultMessageFor(record, 'maester')).toBe('Synthetic result text');
|
|
expect(resultStatusFieldFor(record, 'maester')?.value).toBe('Failed');
|
|
const investigate = imported.sheets[0].analysis.records[1];
|
|
expect(maesterProfile.matchesStatus(investigate, 'investigate')).toBe(true);
|
|
expect(maesterProfile.matchesStatus(investigate, 'passed')).toBe(false);
|
|
});
|
|
|
|
it('keeps Maester error text with Unicode nulls as an explicit PostgreSQL-safe source escape', async () => {
|
|
const rows = [['ID', 'Title', 'Severity', 'Status', 'Result'],
|
|
['MT.9912', 'Synthetic Graph error', 'High', 'Error', 'HttpStatusCode=401 ErrorMessage=\u0000Synthetic denial']];
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(rows), 'Tests');
|
|
const imported = await importFile(new File([XLSX.write(workbook, { type: 'array', bookType: 'xlsx' })], 'Maester.xlsx'), { profiles: importProfiles });
|
|
const model = canonical(imported);
|
|
|
|
expect(model.evidence[0].value).toBe('HttpStatusCode=401 ErrorMessage=\\u0000Synthetic denial');
|
|
expect(JSON.stringify(model)).not.toContain('\\u0000Synthetic denial'.replace('\\u0000', '\u0000'));
|
|
expect(model.diagnostics).toContainEqual(expect.objectContaining({
|
|
code: 'unicode-null-escaped',
|
|
details: { replacements: expect.any(Number), representation: '\\u0000' }
|
|
}));
|
|
});
|
|
|
|
it('keeps native CSV Result as status when there is no separate Status column', async () => {
|
|
const imported = await importFile(new File(['ID,Title,Result,ResultDetail\nMT.9910,Synthetic,Failed,Synthetic proof'], 'Maester-TestResults.csv'), { profiles: importProfiles });
|
|
const model = canonical(imported);
|
|
expect(model.findingOccurrences[0].status.normalizedValue).toBe('failed');
|
|
expect(model.evidence[0].value).toBe('Synthetic proof');
|
|
const record = imported.sheets[0].analysis.records[0];
|
|
expect(resultMessageFor(record, 'maester')).toBe('Synthetic proof');
|
|
expect(resultStatusFieldFor(record, 'maester')?.value).toBe('Failed');
|
|
});
|
|
|
|
it.each([
|
|
'const testResults={Result:"Failed",Tests:loadResults(),EndOfJson:true};',
|
|
'const testResults={Result:"Failed",Tests:[{Id:"MT.1",Title:`${execute()}`}],EndOfJson:true};',
|
|
'const testResults={Result:"Failed",Tests:[{Id:"MT.1"}],get TenantId(){return execute()}};',
|
|
'const testResults=loadResults();',
|
|
'const testResults={Result:"Failed",Tests:[null],EndOfJson:true};',
|
|
'const testResults={Result:"Failed",Tests:[{Id:"MT.1"}],EndOfJson:true}.Tests;',
|
|
'const testResults={Result:"Failed",Tests:[{Id:"MT.1"}]};const data={Result:"Failed",Tests:[{Id:"MT.2"}]};'
|
|
])('rejects unsupported or invalid reports instead of importing zero/partial findings', async script => {
|
|
await expect(htmlImport(`<html><title>Maester Security Tests</title><body>Report<script>${script}</script></body></html>`)).rejects.toThrow(/Maester/);
|
|
});
|
|
|
|
it('keeps multi-tenant findings and report metadata source-scoped', async () => {
|
|
const payload = { WrapperVersion: 'synthetic', Tenants: [
|
|
{ TenantId: 'tenant-a', Result: 'Failed', Tests: [{ Id: 'MT.1', Title: 'Synthetic one', Result: 'Failed' }] },
|
|
{ TenantId: 'tenant-b', Result: 'Passed', Tests: [{ Id: 'MT.1', Title: 'Synthetic two', Result: 'Passed' }] }
|
|
] };
|
|
const imported = await htmlImport(`<html><script type="application/json">${JSON.stringify({ Tenants: payload.Tenants, WrapperVersion: payload.WrapperVersion })}</script></html>`);
|
|
const model = canonical(imported);
|
|
expect(model.findingOccurrences).toHaveLength(2);
|
|
expect(model.environmentObservations?.filter(item => item.property === 'entra.tenant_id').map(item => item.subject.identity)).toEqual(['tenant-a', 'tenant-b']);
|
|
expect(model.environmentObservations).toContainEqual(expect.objectContaining({ property: 'maester.source.WrapperVersion', originalValue: 'synthetic' }));
|
|
expect(model.findingOccurrences.map(item => item.sourcePosition?.objectPath)).toEqual(['$.Tenants[0].Tests[0]', '$.Tenants[1].Tests[0]']);
|
|
});
|
|
|
|
it('marks earlier imports for reimport without breaking their comparison key', () => {
|
|
expect(processingReadiness('maester', { profileVersion: '1.0.0' } as never)).toBe('reimport-recommended');
|
|
expect(processingReadiness('maester', { profileVersion: '1.1.0' } as never)).toBe('reimport-recommended');
|
|
expect(processingReadiness('maester', { profileVersion: '1.2.0' } as never)).toBe('current');
|
|
});
|
|
|
|
it('keeps conditional decisions separate for different native candidate shapes', () => {
|
|
const native = (id: string, entries: string[][]) => ({ version: 1 as const, id, source: { fileName: 'synthetic' },
|
|
fields: entries.map(([label, value]) => ({ key: label, label, originalValue: value, decodedValue: value, source: { fileName: 'synthetic', objectPath: `${id}.${label}` } })) });
|
|
const extraction = { candidates: [native('a', [['Result', 'Failed']]), native('b', [['Result', 'Synthetic text'], ['Status', 'Passed']])],
|
|
extractor: { kind: 'source-extractor', id: 'synthetic', version: '1.0.0' } } as SourceExtractionResult;
|
|
const mapped = mapFindingCandidates(extraction, maesterProfile);
|
|
expect(mapped.candidates[0].fields[0].role).toBe('status');
|
|
expect(mapped.candidates[1].fields[0].role).toBe('evidence');
|
|
expect(mapped.candidates[0].fields[0].mappingDecisionId).not.toBe(mapped.candidates[1].fields[0].mappingDecisionId);
|
|
});
|
|
});
|