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', 'Not configured', 'No baseline rule', 'Not comparable', 'Conflicts', 'Duplicates']) { expect(html).toContain(verdict); } expect(html).toContain('effektive Endpunktzustand ist nicht verifiziert'); 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="scan"] 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="scan"] 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="scan"] [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('groups GPO Findings into collapsible policy sections', async () => { const { JSDOM } = await import('jsdom'); const failing = (id: string, title: string) => ({ id, verdict: 'Fail' as const, conflict: false, duplicate: false, title, scope: 'computer', identity: id, backupInstanceIds: [] }); const { html } = await render({ policyEntries: [ failing('dummy-a', 'System Access · DummyLength'), failing('dummy-b', 'Privilege Rights · SeDummyPrivilege'), failing('dummy-c', 'System Access · DummyLockout'), failing('dummy-d', 'Software\\Policies\\Dummy\\Key') ] }); const { document, Event } = new JSDOM(html, { runScripts: 'dangerously' }).window; const groups = [...document.querySelectorAll('section[aria-labelledby="gpo"] details.group')]; expect(groups.map(group => group.querySelector('summary')!.textContent!.replace(/\s+/g, ' ').trim())) .toEqual(['Privilege Rights 1', 'Registry 1', 'System Access 2']); expect(groups.every(group => group.open)).toBe(true); groups[2].open = false; const search = document.querySelector('section[aria-labelledby="gpo"] [data-filter-search]')!; search.value = 'lockout'; search.dispatchEvent(new Event('input')); expect(groups[2].open).toBe(true); expect(groups.filter(group => !group.hidden)).toHaveLength(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="scan"] .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="gpo"] 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'); // The register keeps every other verdict; long score-neutral groups start collapsed and stay out of print. const group = document.querySelector('section[aria-labelledby="register"] details.group')!; expect(group.querySelector('summary')!.textContent).toContain('Not configured'); expect(group.open).toBe(false); expect(group.hasAttribute('data-print-closed')).toBe(true); expect(group.querySelectorAll('[data-filter-item]')).toHaveLength(3); }); });