import { describe, expect, it } from 'vitest'; import * as XLSX from 'xlsx'; import type { ImportedDataset } from '../../src/domain/dataset'; import { defaultReview, type Review } from '../../src/domain/review'; import { buildWorkViewExport } from '../../src/exports/work-view-export'; import { renderWorkViewXlsx } from '../../src/exports/work-view-xlsx'; import { makeSheet } from '../../src/importers/sheet-factory'; /** Synthetic mixed Work sheet; `Source: Scan` assigns each record to its tool as in the project sheet. */ function mixed(rows: Array>, extraSheets: ImportedDataset['sheets'] = []) { const labels = [...new Set(rows.flatMap(row => Object.keys(row)))]; const sheet = makeSheet('All Records', [labels, ...rows.map(row => labels.map(label => row[label] ?? ''))], '', 'synthetic.html', { isProjectSheet: true, toolId: 'generic' }); const dataset = { fileName: 'Synthetic', profileId: 'generic', sheets: [sheet, ...extraSheets] } as ImportedDataset; return { dataset, sheet, records: sheet.analysis.records }; } function workbook(input: ReturnType, options: { isDeviation?: (title: string) => boolean; reviews?: Record } = {}) { const report = buildWorkViewExport({ ...input, generatedAt: '2026-09-24T12:00:00.000Z', reviews: options.reviews, isDeviation: options.isDeviation && (record => options.isDeviation!(record.title)) }); const bytes = renderWorkViewXlsx(report); const book = XLSX.read(bytes, { type: 'array', cellFormula: true }); const rows = (name: string) => XLSX.utils.sheet_to_json(book.Sheets[name], { header: 1, raw: true, defval: '' }); /** Finding rows of a tool sheet as header → value objects. */ const findings = (name: string) => { const [header, ...body] = rows(name); return body.map(row => Object.fromEntries(header.map((key, index) => [key, row[index]]))); }; const zip = XLSX.CFB.read(bytes, { type: 'array' }); const part = (path: string) => new TextDecoder().decode(XLSX.CFB.find(zip, path).content); return { report, book, rows, findings, part }; } describe('Work Excel finding list', () => { it('writes an overview and one sheet per tool whose columns follow the finding reader', () => { const input = mixed([ { Title: 'Synthetic PC finding', Status: 'Matched', Severity: 'High', Category: 'Anomalies', Description: 'Synthetic description', 'Technical explanation': 'Synthetic explanation', 'Advised solution': 'Synthetic fix', 'Source Rule ID': 'A-Synthetic', 'Source: Scan': 'PingCastle · 2026-09-01' }, { Title: 'Synthetic Maester test', Status: 'Failed', Severity: 'Medium', Description: 'Synthetic Maester description', 'Source: Scan': 'Maester · 2026-09-01' }, { Title: 'Synthetic PC second', Status: 'Matched', Severity: 'Low', Description: 'Second description', 'Source: Scan': 'PingCastle · 2026-09-01' } ]); const { book, rows, findings } = workbook(input, { isDeviation: title => title !== 'Synthetic PC second' }); expect(book.SheetNames).toEqual(['Übersicht', 'PingCastle', 'Maester']); const header = rows('PingCastle')[0]; expect(header.slice(0, 6)).toEqual(['Nr', 'Finding', 'Status', 'Severity', 'Badges', 'Source Rule ID']); // Reader order for PingCastle: Description, Technical explanation, Advised solution. expect(header.indexOf('Description')).toBeLessThan(header.indexOf('Technical explanation')); expect(header.indexOf('Technical explanation')).toBeLessThan(header.indexOf('Advised solution')); expect(header.slice(-3)).toEqual(['Results', 'Additional Information', 'Record ID']); expect(header).not.toContain('Soll-Zustand'); expect(header).not.toContain('Ist-Zustand / Abweichung'); const [first, second] = findings('PingCastle'); expect(first).toMatchObject({ Nr: '1', Finding: 'Synthetic PC finding', Status: 'Matched', Severity: 'High', Badges: 'Category: Anomalies', 'Source Rule ID': 'A-Synthetic', Description: 'Synthetic description', 'Technical explanation': 'Synthetic explanation', 'Advised solution': 'Synthetic fix' }); expect(second).toMatchObject({ Nr: '2', Finding: 'Synthetic PC second', Description: 'Second description', 'Advised solution': '' }); expect(findings('Maester').map(row => row.Finding)).toEqual(['Synthetic Maester test']); const overview = rows('Übersicht').map(row => row.join('|')).join('\n'); expect(overview).toContain('Treffer|3'); expect(overview).toContain('PingCastle|2|1'); expect(overview).toContain('Maester|1|1'); }); it('puts the Results window content and the review into their own columns', () => { const evidence = makeSheet('Synthetic evidence', [['Host', 'State'], ['Synthetic host A', '0'], ['Synthetic host B', 'false']], '', 'synthetic.html', { sourceFile: 'synthetic.html', sourceTableIndex: 42, toolId: 'cis-cat' }); const input = mixed([{ Title: 'Synthetic rule', Status: 'Fail', Description: 'Synthetic CIS description', Results: 'source-table:42 "Synthetic evidence"', 'Source: Scan': 'CIS-CAT Pro · 2026-09-01' }], [evidence]); input.records[0].sourceFile = 'synthetic.html'; const { findings } = workbook(input, { reviews: { [input.records[0].id]: { ...defaultReview(), status: 'Done', owner: 'Synthetic team', markerQuickwin: true } } }); const [finding] = findings('CIS-CAT Pro'); expect(finding.Results).toContain('Synthetic evidence'); expect(finding.Results).toContain('Host: Synthetic host A\nState: 0'); expect(finding.Results).toContain('State: false'); expect(finding.Review).toBe('Status: Done\nPriority: Medium\nOwner / Team: Synthetic team\nMarkers: Quick Win'); expect(finding.Status).toBe('Fail'); // a done review never changes the result status }); it('shows GPO findings like the reader: customer and baseline values, sources, neutral note and scope boundary', () => { const input = mixed([ { Title: 'Synthetic policy', Status: 'Deviation', Category: 'GPOs', 'Configured Value': '1', 'Baseline value': '0', Description: 'Synthetic policy description', 'Source: Scan': 'GPOs · 2026-09-01' }, { Title: 'Synthetic absent policy', Status: 'Not configured', Category: 'GPOs', 'Baseline value': '1', 'Source: Scan': 'GPOs · 2026-09-01' } ]); input.records[0].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: 1, typeCode: 4 }] }]; const { book, findings, rows } = workbook(input); expect(book.SheetNames).toEqual(['Übersicht', 'GPOs']); const [configured, absent] = findings('GPOs'); expect(configured['GPO sources']).toContain('Synthetic/registry.pol'); expect(configured['Expected baseline value']).toBeTruthy(); expect(configured.Description).toBe('Synthetic policy description'); expect(configured.Hinweise).toBe(''); expect(absent.Hinweise).toContain('Score-neutral'); expect(rows('Übersicht').flat().join('\n')).toContain('kein Nachweis der wirksamen Endpoint-Konfiguration'); }); it('formats headers, deviations, wrapping, filters and frozen identity columns', () => { const { part, rows } = workbook(mixed([ { Title: 'Synthetic deviation', Status: 'Matched', Description: 'x', 'Source: Scan': 'PingCastle · 2026-09-01' }, { Title: 'Synthetic passed', Status: 'Not matched', Description: 'y', 'Source: Scan': 'PingCastle · 2026-09-01' } ]), { isDeviation: title => title === 'Synthetic deviation' }); const last = String.fromCharCode(64 + rows('PingCastle')[0].length); const sheet = part('/xl/worksheets/sheet2.xml'); expect(part('/xl/styles.xml')).toContain(''); expect(sheet).toContain(''); expect(sheet).toContain(``); expect(sheet).toMatch(/