import { describe, expect, it, vi } from 'vitest'; import { readFileSync } from 'node:fs'; import { JSDOM } from 'jsdom'; import type { ImportedDataset } from '../../src/domain/dataset'; import { makeSheet } from '../../src/importers/sheet-factory'; import { buildReportProjection, type ReportProjection, type ReportWorkRecord } from '../../src/reporting/report-projection'; import { buildReportWorkspaceView, resultStatement } from '../../src/reporting/report-workspace-view'; import { buildTechnicalReportHtml } from '../../src/reporting/technical-report'; import { reportRichText } from '../../src/reporting/report-rich-text'; import { buildTechnicalReportModel } from '../../src/reporting/technical-report-model'; import { reportContext } from './context-report-fixture'; // Vitest stubs CSS imports; the production build embeds the token files verbatim (`?raw`). vi.mock('../../src/styles/ui-tokens.css?raw', () => ({ default: readFileSync('src/styles/ui-tokens.css', 'utf8') })); vi.mock('../../src/styles/tokens.css?raw', () => ({ default: readFileSync('src/styles/tokens.css', 'utf8') })); // Synthetic data only. const NO_SCRIPT = / = {}): ReportWorkRecord { return { id, title: `Synthetic check ${id}`, toolId, toolLabel: toolId, state, severity: 'unknown', domainId, ruleId: `RULE-${id}`, countingKey: toolId === 'group-policy' ? `gpo:computer\u0000identity-${id}` : `scanner:occ-${id}`, diagnostics: [], attack: [], defenses: [], ...extra }; } /** Synthetic Work cut: 12 deviations (9 scanner, 3 GPO), 5 passed, 3 unassessed; Client Info totals derived from the same records. */ function projection(): ReportProjection { const records: ReportWorkRecord[] = [ ...Array.from({ length: 6 }, (_, index) => record(`pk-${index}`, 'purple-knight', 'deviation', 'identity', { severity: index < 2 ? 'critical' : 'medium', area: 'Account Security', attack: index < 3 ? [{ kind: 'tactic', id: 'tactic:Credential Access', title: 'Credential Access', tactics: ['Credential Access'], evidence: 'declared' }] : [], defenses: index === 0 ? [{ id: 'D3-SPP', title: 'D3-SPP · Strong Password Policy', evidence: 'declared' }] : [] })), ...Array.from({ length: 3 }, (_, index) => record(`cis-${index}`, 'cis-cat', 'deviation', 'hardening', { area: 'Local Policies > User Rights Assignment', attack: index === 0 ? [{ kind: 'technique', id: 'T1110', title: 'Brute Force', tactics: ['Credential Access'], evidence: 'mentioned' }] : [] })), ...Array.from({ length: 3 }, (_, index) => record(`gpo-${index}`, 'group-policy', 'deviation', 'hardening', { area: 'Privilege Rights', policy: { type: 'security-template', backups: 2, unlinkedBackups: index === 0 ? 1 : 0 } })), ...Array.from({ length: 4 }, (_, index) => record(`pass-${index}`, 'purple-knight', 'passed', 'identity', { area: 'Account Security' })), record('gpo-pass', 'group-policy', 'passed', 'hardening', { policy: { type: 'registry-value', backups: 1, unlinkedBackups: 0 } }), record('not-run', 'maester', 'not-run', 'identity'), record('not-configured', 'group-policy', 'not-configured', 'hardening', { policy: { type: 'registry-value', backups: 0, unlinkedBackups: 0 } }), record('na', 'purple-knight', 'not-applicable', 'identity') ]; const projection = buildReportProjection({ generatedAt: '2026-10-07T10:00:00Z', project: { id: 'synthetic-project', name: 'Synthetic ' }, results: [], domains: DOMAINS.map(item => ({ id: item.domainId, label: item.domainLabel })), domainOf: () => 'identity', overallScore: 42, scoreSources: [ { toolId: 'purple-knight', score: 60, method: 'native', weight: 1, nativeValue: 60, nativeProperty: 'purple_knight.ad.security_posture_score', passed: 4, deviations: 6, assessed: 10, issues: [] }, { toolId: 'cis-cat', score: 0, method: 'pass-rate', weight: 1, passed: 0, deviations: 3, assessed: 3, issues: ['Incomplete assessment'] }, { toolId: 'group-policy', score: 25, method: 'pass-rate', weight: 1, passed: 1, deviations: 3, assessed: 4, issues: [] } ], workspaceCounts: { deviations: 12, assessed: 17, notRun: 1, notApplicable: 1, notConfigured: 1 }, workspaceDomains: DOMAINS.map(domain => { const inDomain = records.filter(item => item.domainId === domain.domainId); const passed = inDomain.filter(item => item.state === 'passed').length; const deviations = inDomain.filter(item => item.state === 'deviation').length; return { ...domain, passed, deviations, assessed: passed + deviations }; }), workspaceView: { version: 'synthetic', attackCatalogVersion: '19.2', totals: { passed: 5, deviations: 12, notConfigured: 1, conflicts: 0, duplicates: 0, assessed: 17, eligible: 19, unassessed: 2 }, records, environment: { directory: [{ label: 'Domain', value: 'synthetic.example' }], resilience: [{ label: 'AD Recycle Bin', value: '—' }], operatingSystems: [], restricted: [] } }, policyEntries: [ ...Array.from({ length: 3 }, (_, index) => ({ id: `gpo-${index}`, verdict: 'Fail' as const, conflict: false, duplicate: false, title: 'Synthetic GPO', scope: 'computer', identity: `identity-gpo-${index}`, backupInstanceIds: ['backup-a', 'backup-b'] })), { id: 'gpo-pass', verdict: 'Pass', conflict: false, duplicate: false, title: 'Synthetic GPO', scope: 'computer', identity: 'identity-gpo-pass', backupInstanceIds: ['backup-a'] }, { id: 'not-configured', verdict: 'Not configured', conflict: false, duplicate: false, title: 'Synthetic GPO', scope: 'computer', identity: 'identity-nc', backupInstanceIds: [] } ], riskRegister: { recorded: 2, current: 1 } }); const context = reportContext(); const pk = records.filter(item => item.toolId === 'purple-knight' && item.state === 'deviation'); const cis = records.filter(item => item.toolId === 'cis-cat'); const gpo = records.filter(item => item.toolId === 'group-policy' && item.state === 'deviation'); // Exclusive partition of 10 of the 12 deviations; two remain without a problem category. context.clusters = [ { ...context.clusters[0], id: 'ad-privileged-account-exposure', label: 'AD Privileged Account Exposure', severity: 'critical', scanner: 6, gpo: 1, findingKeys: [...pk.map(item => item.countingKey), gpo[0].countingKey], resultIds: ['secret-result-one'] }, { ...context.clusters[0], id: 'lateral-movement-exposure', label: 'Lateral Movement Exposure', severity: 'high', scanner: 2, gpo: 1, findingKeys: [cis[0].countingKey, cis[1].countingKey, gpo[1].countingKey], resultIds: ['secret-result-two'] }, { ...context.clusters[0], id: 'email-spoofing-phishing-exposure', label: 'Email Spoofing & Phishing Exposure', severity: 'high', scanner: 0, gpo: 0, hasEvidence: false, findingKeys: [], resultIds: [] } ]; projection.context = context; return projection; } const render = (source: ReportProjection) => new JSDOM(buildTechnicalReportHtml(source)).window.document; const sectionText = (document: Document, id: string) => (document.getElementById(id)?.textContent ?? '').replace(/\s+/g, ' '); describe('Technical Report', () => { it('keeps the A4 report at its shared readable sizes when opened in a wide browser window', () => { const document = new JSDOM(buildTechnicalReportHtml(projection())).window.document; const css = [...document.querySelectorAll('style')].map(style => style.textContent).join('\n'); // The desktop path previously added zoom:1.2 above 1200px, enlarging every // font, row and chart. Browser zoom remains the reader's own choice. expect(css).not.toMatch(/zoom\s*:\s*(?!1\s*[;}])[^;}]+/); expect(css).not.toMatch(/transform\s*:\s*scale\(/); expect(css).toContain('width:210mm;max-width:100%'); expect(css).toContain('--type-body-size: 16px'); expect(css).toContain('--type-table-body-size: 14px'); expect(css).toContain('font-size:var(--type-body-size)'); expect(css).toContain('font-size:var(--type-table-body-size)'); expect(document.querySelector('meta[name="viewport"]')?.getAttribute('content')).toBe('width=device-width,initial-scale=1'); }); it('is a static German report in the light ASSESSOR token language that loads and executes nothing', () => { const source = projection(); const before = JSON.stringify(source); const html = buildTechnicalReportHtml(source); expect(html).toContain(''); expect(html).toContain('--tile-surface'); expect(html).toContain('font-family:"Inter Variable"'); expect(html).toContain('Synthetic <Client>'); expect(html).not.toContain('Synthetic '); expect(html).not.toMatch(NO_SCRIPT); expect(JSON.stringify(source)).toBe(before); const document = new JSDOM(html).window.document; expect([...document.querySelectorAll('section.report-section h2')].map(item => item.textContent)).toEqual([ 'Lagebild', 'Beziehungen (Pathfinder)', 'Problemfelder', 'Datenbasis und Datenstand']); expect(document.querySelectorAll('nav .toc a')).toHaveLength(4); expect(html).toContain('@page{size:A4'); }); it('treats ASSESSOR as the single source and never names or compares scan tools', () => { const html = buildTechnicalReportHtml(projection()); const document = new JSDOM(html).window.document; const text = document.body.textContent ?? ''; expect(text).not.toMatch(/Purple Knight|PingCastle|CIS-CAT|Maester|purple-knight|cis-cat|maester|Scanner|Quelle\b|Quellen\b/); // A rule ID appears only inside the Finding Reader card head of its own finding. document.querySelectorAll('.finding-card').forEach(card => card.remove()); expect(document.body.textContent).not.toContain('RULE-'); }); it('reports the Client Info figures: headline and Security Domains come from the same Work records', () => { const source = projection(); const model = buildTechnicalReportModel(source); expect(model.totals).toMatchObject({ deviations: source.workspaceCounts!.deviations, assessed: source.workspaceCounts!.assessed, passed: 5 }); for (const domain of source.workspaceDomains!) { expect(model.domains.find(item => item.id === domain.domainId)).toMatchObject({ passed: domain.passed, deviations: domain.deviations, assessed: domain.assessed }); } expect(model.domains.reduce((sum, item) => sum + item.deviations, 0)).toBe(12); const document = render(source); expect(sectionText(document, 'lagebild')).toContain('Von 17 bewerteten Prüfungen weichen 12 vom Soll ab (71 %)'); expect(document.querySelector('#lagebild .donut-tile strong')?.textContent).toBe('42'); const basis = Object.fromEntries([...document.querySelectorAll('#datenbasis dl div')].map(item => [item.querySelector('dt')?.textContent, item.querySelector('dd')?.textContent])); expect(basis).toMatchObject({ 'Records gesamt': '20', Bewertet: '17', 'Nicht bewertet': '3' }); }); it('partitions every Client Info deviation exactly once, in the exact order and counts of the ASSESSOR heatmap', () => { const source = projection(); // ASSESSOR is authoritative: the published heatmap order is kept even when counts would sort differently. source.context!.clusters.reverse(); const model = buildTechnicalReportModel(source); expect(model.context!.clusters.map(item => [item.id, item.count])).toEqual([ ['email-spoofing-phishing-exposure', 0], ['lateral-movement-exposure', 3], ['ad-privileged-account-exposure', 7]]); expect(model.context!.clusters.map(item => item.id)).toEqual(source.context!.clusters.map(item => item.id)); for (const cluster of source.context!.clusters) { expect(model.context!.clusters.find(item => item.id === cluster.id)!.count).toBe(cluster.findingKeys!.length); } expect(model.context!.partitionTotal).toBe(10); expect(model.context!.empty).toEqual(['Email Spoofing & Phishing Exposure']); const document = render(source); const rows = [...document.querySelectorAll('#problemfelder .pf-list > .pf:not(.pf--sum)')]; const counts = rows.map(row => Number(row.querySelector('.pf-num')?.textContent)); expect(counts).toEqual([0, 3, 7, 2]); expect(counts.reduce((sum, value) => sum + value, 0)).toBe(12); expect(rows.at(-1)?.querySelector('.pf-title')?.textContent).toBe('Ohne Problemkategorie'); // A problem field without deviations is a plain row; populated fields open as an ASSESSOR disclosure row. expect(rows.map(row => row.tagName)).toEqual(['DIV', 'DETAILS', 'DETAILS', 'DIV']); expect(document.querySelector('#problemfelder .pf--sum .pf-num')?.textContent).toBe('12'); }); it('opens a finding line to its Finding Reader card: head, MITRE badges, Problem, Description and catalog references', () => { const source = projection(); const keys = source.context!.clusters.find(cluster => cluster.id === 'ad-privileged-account-exposure')!.findingKeys!; const record = source.workspaceView!.records.find(item => item.countingKey === keys[0])!; record.ruleId = 'SYN-001'; record.card = { status: { value: 'Failed', outcome: 'fail' }, badges: [{ text: 'Weight: 8' }, { text: 'Critical', severity: 'critical' }, { text: 'Category: Synthetic Area' }], problem: { heading: 'Problem', rows: [{ label: 'Result', value: 'Found 1 synthetic object.' }, { label: 'Affected', value: '1' }] }, mitre: ['MITRE ATT&CK: Credential Access'], description: 'First synthetic paragraph.\n\nSecond synthetic paragraph.', references: [{ label: 'ATT&CK TA0006 · Credential Access · Declared · Catalog match', text: 'Credential Access', url: 'https://attack.mitre.org/tactics/TA0006/' }] }; const document = render(source); const line = [...document.querySelectorAll('#pf-ad-privileged-account-exposure details.finding')] .find(item => item.querySelector('summary')?.textContent === record.title)!; expect(line.hasAttribute('open')).toBe(false); const card = line.querySelector('.finding-card')!; expect(card.querySelector('h4')?.textContent).toBe(record.title); expect(card.querySelector('.fc-status')?.textContent).toBe('Failed'); expect([...card.querySelector('.fc-badges')!.querySelectorAll('.meta-badge, .sev-badge')].map(item => item.textContent)) .toEqual(['Weight: 8', 'Critical', 'Category: Synthetic Area']); expect([...card.querySelectorAll('.fc-head .fc-badges')[1].querySelectorAll('.meta-badge')].map(item => item.textContent)) .toEqual(['MITRE ATT&CK: Credential Access']); expect(card.querySelector('.context-badge b')?.textContent).toBe('AD Privileged Account Exposure'); expect(card.querySelector('.fc-rule code')?.textContent).toBe('SYN-001'); expect([...card.querySelectorAll('.fc-problem dt')].map(item => item.textContent)).toEqual(['Result', 'Affected']); // Description and each catalog reference are collapsed disclosures below the Problem block, as in the Finding Reader. const more = [...card.querySelectorAll('details.fc-more')]; expect(more.map(item => item.querySelector('summary')?.textContent)).toEqual(['Description', 'ATT&CK TA0006 · Credential Access · Declared · Catalog match']); expect(more.every(item => !item.hasAttribute('open'))).toBe(true); expect([...more[0].querySelectorAll('.rich-text p')].map(item => item.textContent)).toEqual(['First synthetic paragraph.', 'Second synthetic paragraph.']); expect(more[1].querySelector('a')?.getAttribute('href')).toBe('https://attack.mitre.org/tactics/TA0006/'); }); it('opens a problem field row to its situation, technical lever, Security Domains and slim finding lines', () => { const source = projection(); const document = render(source); const row = document.getElementById('pf-ad-privileged-account-exposure')!; expect(row.tagName).toBe('DETAILS'); expect(row.hasAttribute('open')).toBe(false); expect(row.querySelector('summary .pf-title b')?.textContent).toBe('AD Privileged Account Exposure'); expect(row.querySelector('summary .pf-num')?.textContent).toBe('7'); expect(row.textContent).toContain('Technischer Ansatzpunkt'); expect(row.querySelector('.cluster-domains')?.textContent).toBe('Security Domains: Identity & Authentication 6 · System Hardening 1'); // Every Work record of the field appears once, reduced to severity hexagon and title. const keys = source.context!.clusters.find(cluster => cluster.id === 'ad-privileged-account-exposure')!.findingKeys!; const titles = source.workspaceView!.records.filter(record => keys.includes(record.countingKey)).map(record => record.title).sort(); const lines = [...row.querySelectorAll('.pf-findings li')]; expect(lines.map(line => line.querySelector('summary')?.textContent).sort()).toEqual(titles); expect(lines.every(line => line.querySelector('summary i.hex'))).toBe(true); expect(row.querySelector('table')).toBeNull(); const html = buildTechnicalReportHtml(source); expect(html).not.toMatch(/secret-result|occ-pk-0/); }); it('aggregates Pathfinder relations as Security Domain to problem field edges without a separate MITRE block', () => { const source = projection(); const model = buildTechnicalReportModel(source); expect(model.context!.domainLinks).toEqual([ { domainId: 'identity', clusterId: 'ad-privileged-account-exposure', count: 6 }, { domainId: 'hardening', clusterId: 'ad-privileged-account-exposure', count: 1 }, { domainId: 'hardening', clusterId: 'lateral-movement-exposure', count: 3 }]); const document = render(source); const relations = sectionText(document, 'beziehungen'); expect(document.querySelectorAll('#beziehungen svg.flow path.flow-link')).toHaveLength(3); // Every heatmap problem field appears as a node, a field without deviations included. expect(document.querySelectorAll('#beziehungen svg.flow rect.flow-node')).toHaveLength(2 + 3); expect([...document.querySelectorAll('#beziehungen svg.flow text')].map(item => item.textContent?.split(' ')[0])) .toEqual(expect.arrayContaining(['Identity', 'System'])); expect(relations).not.toMatch(/ATT&CK|D3FEND|MITRE/); expect(document.querySelectorAll('#beziehungen table')).toHaveLength(0); }); it('keeps every other section when no Context result of the same state is available', () => { const source = projection(); delete source.context; const document = render(source); expect(sectionText(document, 'problemfelder')).toContain('Für diesen Datenstand liegen keine Problemfelder vor.'); expect(document.querySelectorAll('section.report-section')).toHaveLength(4); expect(sectionText(document, 'lagebild')).toContain('Von 17 bewerteten Prüfungen weichen 12'); }); it('opens with the supplied Client Info overview facts as section Information', () => { const source = projection(); source.headerFacts = { data: [{ label: 'AD Domains', value: '1' }, { label: 'GPOs', value: '3' }], domainInformation: [{ label: 'Domain', value: 'synthetic.example' }, { label: 'Forest', value: 'synthetic.example' }], groups: [{ title: 'Resilience', facts: [{ label: 'AD Recycle Bin', value: 'Disabled' }] }] }; const document = render(source); const header = document.getElementById('information')!; expect([...document.querySelectorAll('section.report-section h2')].map(item => item.textContent)).toEqual([ 'Information', 'Lagebild', 'Beziehungen (Pathfinder)', 'Problemfelder', 'Datenbasis und Datenstand']); expect([...header.querySelectorAll('.hf-strip > div')].map(cell => cell.textContent)).toEqual(['1AD Domains', '3GPOs']); const groups = [...header.querySelectorAll('.hf-groups .hf-group')]; expect(groups.map(group => group.querySelector('h3')?.textContent)).toEqual(['Domain Information', 'Resilience']); expect([...groups[0].querySelectorAll('.hf-rows > div')].map(cell => cell.textContent)).toEqual(['Domainsynthetic.example', 'Forestsynthetic.example']); expect(header.querySelector('.tile')).toBeNull(); }); it('shows Overall Score and the deviations as donuts next to the six Security Domains; all checks by result in the data basis', () => { const source = projection(); const document = render(source); const tiles = [...document.querySelectorAll('#lagebild .donut-tile')]; expect(tiles.map(tile => tile.querySelector('figcaption')?.textContent)).toEqual(['Overall Score', 'Abweichungen']); // The deviation ring is one plain arc again: no severity split, no legend. expect(tiles[1].querySelectorAll('circle.ring-value')).toHaveLength(1); expect(tiles[1].querySelector('.donut-legend')).toBeNull(); expect(tiles[1].querySelector('.ring-readout strong')?.textContent).toBe(String(source.workspaceCounts!.deviations)); // Security Domains: every Client Info domain in catalog order with its Client Info deviation count. const rows = [...document.querySelectorAll('#lagebild figure.domains .domain-row')]; expect(rows.map(row => [row.querySelector('dt')?.textContent, Number(row.querySelector('.domain-num')?.textContent)])) .toEqual(source.workspaceDomains!.map(domain => [domain.domainLabel, domain.deviations])); // All checks by result state sit only in the data basis and sum to every Work record. expect(document.querySelector('#lagebild figure.checks')).toBeNull(); const model = buildTechnicalReportModel(source); const counts = [...document.querySelectorAll('#datenbasis .legend b')].map(item => Number(item.textContent)); expect(counts.reduce((sum, value) => sum + value, 0)).toBe(model.totals.records); }); it('restores flattened Markdown in a description as headings, steps, lists, emphasis and safe links without losing text', () => { const html = reportRichText('Synthetic intro text. #### Remediation action: Do this: 1. Open the **Synthetic** portal. 2. Click [Next](https://example.test/next). 3. Finish. ' + '#### Related links * [Synthetic Guide - Page 2](https://example.test/guide) * [Unsafe](javascript:alert(1)) '); const document = new JSDOM(`
${html}
`).window.document; expect([...document.querySelectorAll('h5')].map(item => item.textContent)).toEqual(['Remediation action', 'Related links']); expect([...document.querySelectorAll('ol li')].map(item => item.textContent)).toEqual(['Open the Synthetic portal.', 'Click Next.', 'Finish.']); expect(document.querySelector('ol b')?.textContent).toBe('Synthetic'); expect([...document.querySelectorAll('ul li')].map(item => item.textContent)).toEqual(['Synthetic Guide - Page 2', 'Unsafe) ']); expect([...document.querySelectorAll('a')].map(item => item.getAttribute('href'))).toEqual(['https://example.test/next', 'https://example.test/guide']); expect(document.querySelector('script')).toBeNull(); expect(document.body.textContent).not.toMatch(/\*\*|####|\]\(/); // A CIS rule number and a single `#` inside a URL stay text. const plain = new JSDOM(`
${reportRichText('1.1.1 (L1) Ensure synthetic setting [Link](https://example.test/a?b=" # "/c)')}
`).window.document; expect(plain.querySelectorAll('h5, ol')).toHaveLength(0); expect(plain.body.textContent).toBe('1.1.1 (L1) Ensure synthetic setting Link'); }); it('keeps only the statement of a result text in the card, without appended evidence or URLs', () => { expect(resultStatement('Your tenant has 3 resource(s) with access to all subscriptions in this tenant. - [Synthetic A](https://example.test/a) - [Synthetic B](https://example.test/b)')) .toBe('Your tenant has 3 resource(s) with access to all subscriptions in this tenant.'); expect(resultStatement('You have disabled a synthetic setting. ➡️ Open [Recommendation](https://example.test/r) in the portal.')) .toBe('You have disabled a synthetic setting.'); expect(resultStatement('Your tenant does not have [diagnostic settings](https://example.test/d) configured for all logs:\n| Log | State |')) .toBe('Your tenant does not have diagnostic settings configured for all logs'); expect(resultStatement('8 user(s) are inactive - Synthetic One with Role A - Synthetic Two with Role B')).toBe('8 user(s) are inactive'); expect(resultStatement('Found 1 synthetic endpoint that may support NTLM authentication.')).toBe('Found 1 synthetic endpoint that may support NTLM authentication.'); }); it('builds the lean Work cut with native area, counting key and the frozen Pathfinder references', () => { const sheet = makeSheet('Project', [['Title', 'Category', 'Status', 'Benchmark section', 'Source Rule ID'], ['Synthetic rule', 'CIS', 'Fail', 'Local Policies > User Rights Assignment', '2.2.2']], '', 'synthetic.csv', { isProjectSheet: true }); const item = sheet.analysis.records[0]; item.findingOccurrenceId = 'synthetic-occurrence'; item.referenceRecognition = { references: [ { framework: 'mitre-attack', id: 'tactic:Credential Access', title: 'Credential Access', kind: 'tactic', tactics: ['Credential Access'], status: 'active', evidence: 'declared', matchedBy: 'exact-title', catalogVersion: '19.2', catalogSha256: 'x', source: { fieldKey: 'k', fieldLabel: 'l', valuePath: '', representation: 'value', start: 0, end: 1, offsetUnit: 'utf16', text: 'C' } }, { framework: 'mitre-attack', id: 'T1003', title: 'Retired', kind: 'technique', tactics: [], status: 'revoked', evidence: 'declared', matchedBy: 'id', catalogVersion: '19.2', catalogSha256: 'x', source: { fieldKey: 'k', fieldLabel: 'l', valuePath: '', representation: 'value', start: 0, end: 1, offsetUnit: 'utf16', text: 'T' } } ] } as unknown as NonNullable; const view = buildReportWorkspaceView({ sheets: [sheet] } as unknown as ImportedDataset)!; expect(view.records[0]).toMatchObject({ area: 'Local Policies > User Rights Assignment', ruleId: '2.2.2', countingKey: 'scanner:synthetic-occurrence', attack: [{ kind: 'tactic', title: 'Credential Access', evidence: 'declared' }], defenses: [] }); expect(view.records[0]).not.toHaveProperty('fields'); }); });