import { describe, expect, it } from 'vitest'; import { buildReportProjection } from '../../src/domain/report-projection'; import type { UnifiedResultProjection } from '../../src/domain/unified-result'; import { buildTechnicalReportHtml } from '../../src/exports/technical-report'; describe('Technical Report', () => { it('renders source-backed actual/expected values, exact GPO instructions and all policy states without scripts', () => { const result = { resultProjectionId: 'dummy-result', sourceKind: 'policy-evaluation', sourceResultId: 'dummy-source', projectId: 'dummy-project', collectionRunId: 'dummy-run', sourceTool: 'GPOs', title: '', sourceRuleId: 'registry:dummy', isDeviation: true, severityNormalized: 'unknown', outcomeNormalized: 'fail', policyActualValue: 0, policyBaselineValue: 1, policyComparisonRelation: 'different', policyDiagnosticCodes: ['dummy-conflict'], frameworkReferences: [{ namespace: 'GPOs', value: 'MITRE T1110' }] } as unknown as UnifiedResultProjection; const projection = buildReportProjection({ generatedAt: '2026-09-23T10:00:00Z', project: { id: 'dummy-project', name: 'Dummy Customer' }, results: [result], domains: [{ id: 'gpo', label: 'GPO & Baseline' }], domainOf: () => 'gpo', policyEntries: [{ id: 'dummy-setting', verdict: 'Fail', conflict: true, duplicate: false, title: 'Dummy Registry Rule', scope: 'machine', identity: 'dummy-key', backupInstanceIds: ['dummy-backup'] }], technicalPolicySources: [{ settingId: 'dummy-setting', sources: [{ id: 'dummy-source', backupInstanceId: 'dummy-backup', gpoId: '{DUMMY-GUID}', nativeBackupId: '{DUMMY-BACKUP}', sourceReference: 'dummy/source', sequence: 2, values: [{ kind: 'registry-policy', operation: 'set-value', data: 0, typeCode: 'REG_DWORD' }], deviationFields: [['data']] }] }] }); const html = buildTechnicalReportHtml(projection); expect(html).toContain(''); expect(html).toContain('<Dummy Registry Rule>'); expect(html).not.toContain(''); expect(html).toContain('Konfiguriert'); expect(html).toContain('Erwartet'); expect(html).toContain('dummy/source'); expect(html).toContain('dummy-backup'); expect(html).toContain('MITRE T1110'); expect(html).toContain('
0
'); for (const verdict of ['Deviation', 'Passed', 'Review required', 'Conflicts', 'Duplicates']) { expect(html).toContain(verdict); } // No explanatory filler text in the Technical Report. for (const filler of ['effektive Endpunktzustand', 'betroffene GPO Backups', 'kein eigenes Verdict', 'importierten GPO-Backup-Satz', 'Ein Klick zeigt']) { expect(html).not.toContain(filler); } expect(html.match(/class="(?:note|section__lead)"/g)).toBeNull(); expect(html).not.toMatch(/]*\bsrc\s*=| = {}) => ({ resultProjectionId: 'dummy-scan', sourceKind: 'scanner-finding', sourceResultId: 'dummy-scan-source', projectId: 'dummy-project', collectionRunId: 'dummy-run', sourceTool: 'purple-knight', title: 'Dummy indicator', sourceRuleId: 'DUMMY-1', isDeviation: true, severityNormalized: 'high', outcomeNormalized: 'fail', description: 'Dummy description.', frameworkReferences: [], ...overrides }) as unknown as UnifiedResultProjection; const render = async (input: Partial[0]>) => { const { JSDOM } = await import('jsdom'); const html = buildTechnicalReportHtml(buildReportProjection({ generatedAt: '2026-09-24T10:00:00Z', project: { id: 'dummy-project', name: 'Dummy Customer' }, results: [], domains: [{ id: 'identity', label: 'Identity & Account Hygiene' }, { id: 'gpo', label: 'GPO & Baseline' }], domainOf: result => result.sourceKind === 'policy-evaluation' ? 'gpo' : 'identity', ...input })); return { html, document: new JSDOM(html).window.document }; }; it('shows a finding as description, result and solution and leaves empty source fields out', async () => { const { document } = await render({ results: [scanner({ recommendation: '

Restrict the dummy right.

' })], technicalFindingFields: [{ resultProjectionId: 'dummy-scan', fields: [ { label: 'Result message', value: 'Found 2 dummy objects.', index: 0 }, { label: 'MITRE D3FEND_1', value: '', index: 1 }, { label: 'MITRE ATT&CK_3', value: '—', index: 2 }, { label: 'Description', value: 'Dummy description.', index: 3 }, { label: 'Weight', value: '10', index: 4 } ] }] }); const row = document.querySelector('section[aria-labelledby="findings"] details.row')!; const labels = [...row.querySelectorAll('.block__label')].map(node => node.textContent); expect(labels.slice(0, 3)).toEqual(['Beschreibung', 'Befund', 'Lösung']); expect(row.querySelector('.solution strong')!.textContent).toBe('dummy'); expect(row.querySelector('img')).toBeNull(); expect(row.querySelector('.solution')!.textContent).toContain(''); const original = [...row.querySelectorAll('.more .kv dt')].map(node => node.textContent); expect(original).toContain('Weight'); expect(original).not.toContain('MITRE D3FEND_1'); expect(original).not.toContain('MITRE ATT&CK_3'); // Already shown as description, so not repeated in the technical details. expect(original).not.toContain('Description'); // Severity is supporting text, not a filled status badge. expect(row.querySelector('summary .sev')!.textContent).toBe('High'); expect(document.querySelectorAll('.badge, .pill')).toHaveLength(0); }); it('shows each Security Domain as a collapsible group that reopens for a matching search', async () => { const { JSDOM } = await import('jsdom'); const { html } = await render({ results: [scanner(), scanner({ resultProjectionId: 'dummy-scan-2', title: 'Dummy second indicator' })] }); const { document, Event } = new JSDOM(html, { runScripts: 'dangerously' }).window; const group = document.querySelector('section[aria-labelledby="findings"] details.group')!; expect(group.open).toBe(true); expect(group.querySelector('summary')!.textContent).toContain('Identity & Account Hygiene'); expect(group.querySelectorAll('details.row')).toHaveLength(2); group.open = false; const search = document.querySelector('section[aria-labelledby="findings"] [data-filter-search]')!; search.value = 'second'; search.dispatchEvent(new Event('input')); expect(group.open).toBe(true); expect([...group.querySelectorAll('details.row')].filter(row => !row.hidden)).toHaveLength(1); }); it('lists scanner and GPO deviations in one Findings list grouped by Security Domain', async () => { const { JSDOM } = await import('jsdom'); const failing = (id: string, title: string) => ({ id, verdict: 'Fail' as const, conflict: false, duplicate: id === 'dummy-b', title, scope: 'computer', identity: id, backupInstanceIds: [] }); const { html } = await render({ results: [scanner(), scanner({ resultProjectionId: 'dummy-scan-gpo', title: 'Dummy scanner GPO check', severityNormalized: 'unknown' })], domainOf: result => result.resultProjectionId === 'dummy-scan-gpo' || result.sourceKind === 'policy-evaluation' ? 'gpo' : 'identity', policyEntries: [failing('dummy-a', 'System Access · DummyLength'), failing('dummy-b', 'Privilege Rights · SeDummyPrivilege')] }); const { document, Event } = new JSDOM(html, { runScripts: 'dangerously' }).window; expect(document.querySelector('section[aria-labelledby="scan"], section[aria-labelledby="gpo"]')).toBeNull(); const section = document.querySelector('section[aria-labelledby="findings"]')!; const groups = [...section.querySelectorAll('details.group')]; const titles = (group: Element) => [...group.querySelectorAll('.row__title')].map(node => node.textContent); expect(groups.map(group => group.querySelector('summary')!.textContent!.replace(/\s+/g, ' ').trim())) .toEqual(['GPO & Baseline 3', 'Identity & Account Hygiene 1']); expect(titles(groups[0])).toEqual(['Dummy scanner GPO check', 'Privilege Rights · SeDummyPrivilege', 'System Access · DummyLength']); // One toolbar filters the whole list, by source and by GPO diagnostics. expect(section.querySelectorAll('[data-filter-toolbar]')).toHaveLength(1); section.querySelector('[data-filter-key="tool"][data-filter-value="group-policy"]')!.click(); expect([...section.querySelectorAll('details.row')].filter(row => !row.hidden).map(row => row.querySelector('.row__title')!.textContent)) .toEqual(['Privilege Rights · SeDummyPrivilege', 'System Access · DummyLength']); section.querySelector('[data-filter-key="flags"][data-filter-value="duplicate"]')!.click(); expect([...section.querySelectorAll('details.row')].filter(row => !row.hidden)).toHaveLength(1); const search = section.querySelector('[data-filter-search]')!; search.value = 'nothing matches'; search.dispatchEvent(new Event('input')); expect(groups.every(group => group.hidden)).toBe(true); }); it('shows environment counts and operating systems in the overview without turning unknown values into zero', async () => { const { document } = await render({ environment: [{ key: 'Domain', value: 'dummy.example' }, { key: 'Domain Controller', value: '2' }], technicalEnvironment: { counts: [{ key: 'Domain Controller', value: 2 }, { key: 'Computer objects', value: 1241 }, { key: 'User accounts', value: undefined }], operatingSystems: [{ label: 'Servers', total: 7, entries: [{ name: 'Windows Server 2022', count: 7 }] }, { label: 'Other / unclassified', entries: [{ name: 'Dummy OS' }] }] } }); const section = document.querySelector('section[aria-labelledby="overview"]')!; const facts = [...section.querySelectorAll('.env')].map(card => [...card.querySelectorAll('dl div')] .map(row => [row.querySelector('dt')!.textContent, row.querySelector('dd')!.textContent])); expect(facts[0]).toEqual([['Domain', 'dummy.example'], ['Domain Controller', '2'], ['Computer objects', '1.241']]); expect(facts[1]).toEqual([['Windows Server 2022', '7'], ['Dummy OS', '—']]); expect([...section.querySelectorAll('.os__group')].map(node => node.textContent!.replace(/\s+/g, ' ').trim())).toEqual(['Servers 7', 'Other / unclassified']); const empty = (await render({})).document.querySelector('section[aria-labelledby="overview"]')!.textContent; expect(empty).toContain('Keine Betriebssystemdaten übermittelt.'); }); it('shows the GPO card with Deviation, Passed, Review required, Conflicts and Duplicates only', async () => { const entry = (id: string, verdict: string, extra: Record = {}) => ({ id, verdict, conflict: false, duplicate: false, title: id, scope: 'computer', identity: id, backupInstanceIds: [], ...extra }) as never; const { document } = await render({ policyEntries: [ ...Array.from({ length: 25 }, (_, index) => entry('fail-' + index, 'Fail', index < 2 ? { duplicate: true } : {})), ...Array.from({ length: 25 }, (_, index) => entry('pass-' + index, 'Pass')), ...Array.from({ length: 30 }, (_, index) => entry('absent-' + index, 'Not configured')), entry('no-rule', 'No baseline rule'), entry('preference', 'Not comparable') ] }); const card = [...document.querySelectorAll('section[aria-labelledby="overview"] .card')] .find(item => item.querySelector('.card__title')?.textContent === 'GPO Policy Settings')!; const bars = [...card.querySelectorAll('.bars li')].map(row => [row.querySelector('.bars__label')!.textContent, row.querySelector('.bars__value')!.textContent]); expect(bars).toEqual([['Deviation', '25'], ['Passed', '25'], ['Review required', '0'], ['Conflicts', '0'], ['Duplicates', '2']]); expect(card.querySelector('.card__meta')!.textContent).toBe('50 bewertete Settings'); expect(card.textContent).not.toMatch(/Not configured|No baseline rule|Not comparable/); }); it('keeps severity on the finding rows only, not in the overview', async () => { const { document } = await render({ results: [scanner()] }); expect(document.querySelector('#overview')!.closest('section')!.textContent).not.toMatch(/severity|critical|\bhigh\b/i); // Result inventory, convergence, scope limits and appendix are removed from the Technical Report. expect([...document.querySelectorAll('h2')].map(node => node.textContent)).toEqual(['Überblick', 'Findings']); const row = document.querySelector('section[aria-labelledby="findings"] details.row')!; expect(row.querySelector('summary .sev')!.textContent).toBe('High'); expect(document.querySelector('section[aria-labelledby="overview"] .kpi__figure')!.textContent).toBe('1'); }); it('renders pipe-delimited source rows as a table and keeps irregular rows as text', async () => { const details = [ 'The detail can be found in Dummy Groups', 'DN', 'CN=dummy.one,OU=Dummy,DC=dummy,DC=example', 'CN=dummy.two,CN=Users,DC=dummy,DC=example', 'This section explains the dummy groups.', 'Group Name | Nb Admins | Nb Enabled', 'Dummy Operators | 0 | 0', 'Dummy Admins | 3 | 2', '', 'Dummy Admins Close', '', 'User', 'dummy.admin', 'dummy-service', 'Administrator', 'Accounts are listed above.', '', 'Name | State', 'dummy.user | YES | extra' ].join('\n'); const { document } = await render({ results: [scanner()], technicalFindingFields: [{ resultProjectionId: 'dummy-scan', fields: [{ label: 'Details', value: details, index: 0 }] }] }); const found = [...document.querySelectorAll('section[aria-labelledby="findings"] .block')] .find(block => block.querySelector('.block__label')!.textContent === 'Befund')!; const tables = found.querySelectorAll('table'); expect(tables).toHaveLength(3); // A flattened one-column list of account names under its label. expect([...tables[2].querySelectorAll('th')].map(cell => cell.textContent)).toEqual(['User']); expect([...tables[2].querySelectorAll('td')].map(cell => cell.textContent)).toEqual(['dummy.admin', 'dummy-service', 'Administrator']); expect([...found.querySelectorAll('p')].some(node => node.textContent!.includes('Accounts are listed above.'))).toBe(true); // A flattened one-column table of distinguished names keeps its header. expect([...tables[0].querySelectorAll('th')].map(cell => cell.textContent)).toEqual(['DN']); expect([...tables[0].querySelectorAll('td')].map(cell => cell.textContent)) .toEqual(['CN=dummy.one,OU=Dummy,DC=dummy,DC=example', 'CN=dummy.two,CN=Users,DC=dummy,DC=example']); expect(found.textContent).toContain('This section explains the dummy groups.'); expect([...tables[1].querySelectorAll('th')].map(cell => cell.textContent)).toEqual(['Group Name', 'Nb Admins', 'Nb Enabled']); expect([...tables[1].querySelectorAll('tbody tr')].map(row => [...row.querySelectorAll('td')].map(cell => cell.textContent))) .toEqual([['Dummy Operators', '0', '0'], ['Dummy Admins', '3', '2']]); expect(found.textContent).toContain('The detail can be found in Dummy Groups'); expect(found.textContent).toContain('Dummy Admins Close'); // Differing column counts are not forced into a table. expect(found.textContent).toContain('Name | State'); expect(found.textContent).toContain('dummy.user | YES | extra'); }); it('rolls GPO baseline comparisons up to one finding per policy setting with expected and configured values', async () => { const comparison = (variant: number) => ({ resultProjectionId: `dummy-gpo-${variant}`, sourceKind: 'policy-evaluation', sourceResultId: `dummy-gpo-source-${variant}`, projectId: 'dummy-project', collectionRunId: 'dummy-run', sourceTool: 'GPOs', title: 'Dummy Rights · SeDummyPrivilege', sourceRuleId: 'dummy-identity', policyIdentityKey: 'dummy-identity', isDeviation: true, severityNormalized: 'unknown', outcomeNormalized: 'fail', policyBaselineLabel: `Dummy Baseline ${variant}`, policyComparisonRelation: 'different', frameworkReferences: [], policyBaselineVariantAssessment: { version: 1, scope: 'backup-configuration', decision: 'all-fail', diagnosticCodes: [], actualValues: [{ kind: 'principal-set', principals: ['*S-1-5-19', '*S-1-5-99'] }], variants: [{ settingId: `dummy-baseline-${variant}`, baselineValues: [{ kind: 'principal-set', principals: ['*S-1-5-19'] }], comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different', diagnosticCodes: [] } }] } }) as unknown as UnifiedResultProjection; const { document } = await render({ results: [comparison(1), comparison(2), comparison(3)], policyEntries: [ { id: 'dummy-setting', verdict: 'Fail', conflict: false, duplicate: false, title: 'Dummy Rights · SeDummyPrivilege', scope: 'computer', identity: 'dummy-identity', backupInstanceIds: ['dummy-backup'] }, ...Array.from({ length: 3 }, (_, index) => ({ id: `dummy-nc-${index}`, verdict: 'Not configured' as const, conflict: false, duplicate: false, title: `Dummy absent ${index}`, scope: 'user', identity: `dummy-nc-${index}`, backupInstanceIds: [] })) ], technicalPolicySources: [{ settingId: 'dummy-setting', sources: [{ id: 'dummy-source', backupInstanceId: 'dummy-backup', gpoId: '{DUMMY-GUID}', sourceReference: 'dummy/GptTmpl.inf', values: [{ kind: 'principal-set', principals: ['*S-1-5-19', '*S-1-5-99'] }], deviationItems: [['*S-1-5-99']] }] }] }); const rows = document.querySelectorAll('section[aria-labelledby="findings"] details.row'); expect(rows).toHaveLength(1); const [expected, configured] = [...rows[0].querySelectorAll('.compare__col')]; expect([...expected.querySelectorAll('li')].map(node => node.textContent)).toEqual(['*S-1-5-19']); expect(expected.textContent).toContain('Dummy Baseline 1 · Dummy Baseline 2 · Dummy Baseline 3'); // Only the configured principal absent from every baseline variant is marked. expect([...configured.querySelectorAll('li.different')].map(node => node.textContent)).toEqual(['*S-1-5-99']); expect(rows[0].querySelector('.compare')!.textContent).not.toContain('"kind"'); expect(rows[0].querySelector('.solution')!.textContent).toContain('auf den erwarteten Baseline-Wert setzen'); // Score-neutral settings are not listed anywhere: the GPO Policy Register is removed. expect(document.querySelector('section[aria-labelledby="register"]')).toBeNull(); expect(document.body.textContent).not.toContain('Dummy absent'); }); });