Assessor_2.0/tests/reporting/technical-report.test.ts

358 lines
26 KiB
TypeScript

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 = /<script\b|<link\b|\son[a-z]+=|https?:\/\//i;
const DOMAINS = [
{ domainId: 'identity', domainLabel: 'Identity & Authentication' },
{ domainId: 'hardening', domainLabel: 'System Hardening' }
];
function record(id: string, toolId: string, state: string, domainId: string, extra: Partial<ReportWorkRecord> = {}): 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 <Client>' },
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('<html lang="de" data-theme="light">');
expect(html).toContain('--tile-surface');
expect(html).toContain('font-family:"Inter Variable"');
expect(html).toContain('Synthetic &lt;Client&gt;');
expect(html).not.toContain('Synthetic <Client>');
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)) <script>x</script>');
const document = new JSDOM(`<div>${html}</div>`).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) <script>x</script>']);
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(`<div>${reportRichText('1.1.1 (L1) Ensure synthetic setting [Link](https://example.test/a?b=" # "/c)')}</div>`).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<typeof item.referenceRecognition>;
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');
});
});