133 lines
8.5 KiB
TypeScript
133 lines
8.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { buildWorkViewExport, renderWorkViewCsv, renderWorkViewHtml, renderWorkViewMarkdown, workViewCsvRows } from '../../src/exports/work-view-export';
|
|
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] };
|
|
}
|
|
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 csv = renderWorkViewCsv(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', 'Result Status\nFail', 'kein technischer Behebungsnachweis', 'Synthetic plan']) expect(csv).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(renderWorkViewCsv(report)).toContain('Score-neutral:');
|
|
expect(renderWorkViewCsv(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 [renderWorkViewCsv(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('<result>');
|
|
expect(html).not.toMatch(/<(?:img|script|iframe)\b/i);
|
|
expect(html).toContain("default-src 'none'");
|
|
const csv = renderWorkViewCsv(report);
|
|
expect(csv.startsWith('\ufeffSoll-Zustand;Abweichungen\n')).toBe(true);
|
|
expect(csv).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 csv = renderWorkViewCsv(project(f));
|
|
expect(csv).toContain('Referenzierte Results nicht geladen');
|
|
expect(csv).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 = renderWorkViewCsv(report);
|
|
expect(JSON.stringify(f)).toBe(original);
|
|
f.record.fields.find(field => field.label === 'Actual')!.value = 'changed later';
|
|
expect(renderWorkViewCsv(report)).toBe(before);
|
|
expect(buildWorkViewExport({ ...f, records: [], generatedAt: 'now' }).entries).toEqual([]);
|
|
});
|
|
|
|
it('preserves oversized evidence in Excel-safe two-column continuation rows without cutting Unicode or data', () => {
|
|
const long = 'Synthetic evidence =1; "value" | \\path 😀\n'.repeat(2300);
|
|
const f = fixture('generic', { Expected: '1', Evidence: long });
|
|
// Input deliberately bypasses sheet whitespace normalization to check exact source text.
|
|
f.record.fields.find(field => field.label === 'Evidence')!.value = long;
|
|
const report = project(f);
|
|
const rows = workViewCsvRows(report);
|
|
expect(rows.length).toBeGreaterThan(3);
|
|
expect(rows.every(row => row.length === 2)).toBe(true);
|
|
expect(rows.flat().every(cell => cell.length < 32767)).toBe(true);
|
|
const reassembled = rows.slice(1).map(row => row[1].replace(/^Finding ID: [^\n]+\nTeil \d+\/\d+ · Fortsetzungszeile desselben Findings\n\n/, '')).join('');
|
|
expect(reassembled).toContain(long);
|
|
expect(report.entries).toHaveLength(1);
|
|
});
|
|
|
|
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"');
|
|
});
|
|
});
|