Assessor_2.0/tests/exports/work-view-export.test.ts

123 lines
7.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import * as XLSX from 'xlsx';
import { buildWorkViewExport, renderWorkViewHtml, renderWorkViewMarkdown, type WorkViewExport } from '../../src/exports/work-view-export';
import { renderWorkViewXlsx } from '../../src/exports/work-view-xlsx';
import type { ImportedDataset } from '../../src/domain/dataset';
import { makeSheet } from '../../src/importers/sheet-factory';
import { defaultReview } from '../../src/domain/review';
function fixture(tool = 'generic', fields: Record<string, string> = {}) {
const values = { Title: 'Synthetic setting', Status: 'Fail', ...fields };
const sheet = makeSheet('Synthetic', [Object.keys(values), Object.values(values)], '', 'synthetic', { toolId: tool });
const dataset = { fileName: 'Synthetic', profileId: tool, sheets: [sheet] } as ImportedDataset;
return { dataset, sheet, record: sheet.analysis.records[0] };
}
/** Text of every cell in every sheet of the Excel export. */
function xlsxText(report: WorkViewExport): string {
const book = XLSX.read(renderWorkViewXlsx(report), { type: 'array' });
return book.SheetNames.flatMap(name => XLSX.utils.sheet_to_json<string[]>(book.Sheets[name], { header: 1, raw: true, defval: '' }).flat()).join('\n');
}
function project(f: ReturnType<typeof fixture>) {
return buildWorkViewExport({ ...f, records: [f.record], generatedAt: '2026-09-18T12:00:00.000Z' });
}
describe('two-column Work export contract', () => {
it('keeps a rolled-up GPO once, every source operation and baseline variant; review done never changes Fail', () => {
const f = fixture('group-policy', { Category: 'GPOs', 'Baseline value': JSON.stringify([
{ baseline: 'Synthetic baseline A', expected: 1 }, { baseline: 'Synthetic baseline B', expected: 2 }
]), Remediation: 'Confirm the applicable baseline before changing the setting.', Scope: 'computer' });
f.record.policySourceEvidence = ['backup-a', 'backup-b'].map((id, i) => ({ id, backupKey: id, backupInstanceId: id,
sourceReference: `Synthetic/${id}/registry.pol#records=${i + 1}`, sequence: i + 1,
values: [{ kind: 'registry-policy', operation: i ? 'delete-value' : 'set-value', key: 'Software\\Synthetic', valueName: 'Setting', ...(i ? {} : { data: 0, typeCode: 4 }) }] }));
const report = buildWorkViewExport({ ...f, records: [f.record], generatedAt: 'now',
reviews: { [f.record.id]: { ...defaultReview(), status: 'Done', nextStep: 'Synthetic plan', owner: 'Synthetic owner' } } });
expect(report.entries).toHaveLength(1);
const text = xlsxText(report);
for (const value of ['backup-a', 'backup-b', 'delete-value', 'Data: 0', 'Type code: 4', 'Source sequence: 2', 'Synthetic baseline A', 'Synthetic baseline B', 'Expected: 1', 'Expected: 2', 'Status: Done','kein technischer Behebungsnachweis', 'Synthetic plan']) expect(text).toContain(value);
expect(report.entries[0].status).toBe('Fail');
});
it.each(['Not configured', 'No baseline rule', 'Not comparable', 'Review required'])('does not invent a deviation or obligation for %s', status => {
const f = fixture('group-policy', { Category: 'GPOs', Status: status });
const report = project(f);
expect(report.entries[0].status).toBe(status);
expect(xlsxText(report)).toContain('Score-neutral:');
expect(xlsxText(report)).toContain('kein Nachweis der wirksamen Endpoint-Konfiguration');
});
it('includes complete inline Maester results and embedded remediation in the appropriate column', () => {
const f = fixture('maester', {
Description: 'Synthetic rule.\n\n#### Remediation action\n\n1. Change the synthetic setting.\n2. Rescan.',
Result: '| Account | State |\n| --- | --- |\n| Synthetic A | Fail |\n| Synthetic B | Pass |'
});
const report = project(f);
const [entry] = report.entries;
expect(entry.expected.some(block => block.value.includes('Change the synthetic setting'))).toBe(true);
expect(entry.observed.some(block => block.label === 'Results · Test result')).toBe(true);
for (const text of [xlsxText(report), renderWorkViewHtml(report), renderWorkViewMarkdown(report)]) {
expect(text).toContain('Synthetic A');
expect(text).toContain('Synthetic B');
expect(text).toContain('State: Pass');
expect(text).toContain('Change the synthetic setting');
}
});
it('retains every linked row, zero, false, empty cells and XML, without active imported markup', () => {
const f = fixture('cis-cat', { Results: 'source-table:42 "Evidence"', 'Rule Result XML': '<result><value>0</value></result>',
Description: '<img src="https://example.invalid/track" onerror="alert(1)">', Remediation: '=HYPERLINK("bad")' });
f.record.sourceFile = 'synthetic.html';
const evidence = makeSheet('Evidence', [['Asset', 'Value', 'Flag', 'Empty'], ...Array.from({ length: 160 }, (_, index) => [`Synthetic ${index}`, '0', 'false', ''])], '', 'synthetic.html', { sourceFile: 'synthetic.html', sourceTableIndex: 42, toolId: 'cis-cat' });
f.dataset.sheets.push(evidence);
const report = project(f);
expect(report.entries[0].resultRowCount).toBe(160);
const html = renderWorkViewHtml(report);
expect(html).toContain('Synthetic 159');
expect(html).toContain('Flag: false');
expect(html).toContain('Empty: ');
expect(html).toContain('&lt;result&gt;');
expect(html).not.toMatch(/<(?:img|script|iframe)\b/i);
expect(html).toContain("default-src 'none'");
expect(xlsxText(report)).toContain('Synthetic 159');
expect(renderWorkViewMarkdown(report)).not.toContain('<img');
});
it('names missing evidence instead of claiming a complete technical proof', () => {
const f = fixture('purple-knight', { Result: 'missing-synthetic-evidence.csv' });
const text = xlsxText(project(f));
expect(text).toContain('Referenzierte Results nicht geladen');
expect(text).toContain('missing-synthetic-evidence.csv');
});
it('is frozen and does not mutate source or select additional rows', () => {
const f = fixture('generic', { Expected: '1', Actual: '0' });
const original = JSON.stringify(f);
const report = project(f);
const before = xlsxText(report);
expect(JSON.stringify(f)).toBe(original);
f.record.fields.find(field => field.label === 'Actual')!.value = 'changed later';
expect(xlsxText(report)).toBe(before);
expect(buildWorkViewExport({ ...f, records: [], generatedAt: 'now' }).entries).toEqual([]);
});
it('reports a missing source table even when another linked table exists', () => {
const f = fixture('cis-cat', { Results: 'source-table:42 "Available"\nsource-table:43 "Missing"' });
f.record.sourceFile = 'synthetic.html';
f.dataset.sheets.push(makeSheet('Available', [['Asset'], ['Synthetic']], '', 'synthetic.html', { sourceFile: 'synthetic.html', sourceTableIndex: 42, toolId: 'cis-cat' }));
const report = project(f);
expect(report.entries[0].resultRowCount).toBe(1);
const warning = report.entries[0].observed.find(block => block.label === 'Results · fehlende Belege');
expect(warning?.value).toContain('source-table:43');
expect(warning?.value).not.toContain('source-table:42');
});
it('marks only source fields backed by retained deviation evidence, not the source section', () => {
const f = fixture('group-policy', { Category: 'GPOs' });
f.record.policySourceEvidence = [{ id: 'synthetic-observation', backupKey: 'synthetic-backup', sourceReference: 'Synthetic/registry.pol', sequence: 1,
values: [{ kind: 'registry-policy', operation: 'set-value', key: 'Software\\Synthetic', valueName: 'Value', data: 0, typeCode: 4 }], deviationFields: [['data']] }];
const html = renderWorkViewHtml(project(f));
expect(html).toContain('<span class="deviation-value">Data: 0</span>');
expect(html).not.toContain('<span class="deviation-value">Operation');
expect(html).not.toContain('<section class="deviation-value"');
});
});