Assessor_2.0/tests/features/gpo-problem-summary-reader....

178 lines
13 KiB
TypeScript

// @vitest-environment jsdom
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import type { FindingRecord, PolicySourceEvidence } from '../../src/domain/finding';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
/** Synthetic Registry.pol contribution of one GPO backup. */
function source(backup: string, key: string): PolicySourceEvidence {
return { id: backup, backupKey: backup, backupInstanceId: `gpo-backup-${backup}`, sequence: 3,
sourceReference: `{${backup}}/DomainSysvol/GPO/Machine/registry.pol#records=3`, baselineRelation: 'conflicting', deviationFields: [['data']],
values: [{ kind: 'registry-policy', operation: 'set-value', key, valueName: 'SyntheticEnable', data: { kind: 'uint32', value: 0 }, typeCode: 4 }] };
}
function record(): FindingRecord {
const fields = [['Title', 'SyntheticEnable'], ['Status', 'Fail'], ['Scope', 'computer'], ['Conflict', 'Conflicting customer states; see original GPO source instructions.'],
['Technical Identity', 'synthetic-identity'], ['Registry path', 'HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic'], ['Remediation', 'repeated per package']]
.map(([label, value], index) => ({ label, value, index }));
return { id: 'synthetic', index: 0, title: 'SyntheticEnable', fields, filled: fields, meta: [],
policySourceEvidence: [source('a', 'SOFTWARE\\Policies\\Synthetic'), source('b', 'Software\\Policies\\Synthetic'), source('c', 'Software\\Policies\\Synthetic')],
policyBaselineComparisons: [1, 2].map(index => ({ baseline: `Synthetic baseline ${index}`, status: 'Fail', summary: '',
context: 'Synthetic role context.', scopeNotice: 'Synthetic scope notice.', explanation: '',
remediation: 'Computer policy: HKEY_LOCAL_MACHINE\\Software\\Policies\\Synthetic\\SyntheticEnable\nExpected: 1',
recommendations: [{ template: `Synthetic template ${index}`, settingId: `setting-${index}`, comparison: 'Different', expected: '1', registryType: '4', additional: [], missing: [] }] })) };
}
function render(item = record()) {
const container = document.createElement('div');
container.innerHTML = renderToStaticMarkup(<FindingReader record={item} title={item.title} linkedSheets={[]} resultReferences={[]}
search="" position={0} recordCount={1} toolId="group-policy" onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>);
return container;
}
describe('GPO first-read problem summary', () => {
it('states configured versus expected, the contributing GPOs, the flag and the next step before the evidence', () => {
const container = render();
const summary = container.querySelector('.reader-summary')!;
const problem = summary.querySelector('.policy-problem')!;
// The problem is the first content block after the setting heading.
expect(summary.querySelector('h3 + .policy-problem')).toBe(problem);
expect(problem.getAttribute('data-outcome')).toBe('fail');
const values = Object.fromEntries([...problem.querySelectorAll('.policy-problem__values > div')]
.map(row => [row.querySelector('dt')!.textContent, row.querySelector('dd')!.textContent]));
expect(values).toEqual({ Configured: '0', Expected: '1, registry type 4 (REG_DWORD)' });
expect(problem.querySelector('.policy-problem__context')?.textContent)
.toBe('3 GPO backup sources with the same value · differs from all 2 assessed baseline variants in 2 baseline packages');
expect([...problem.querySelectorAll('.policy-problem__flag')].map(item => item.textContent)).toEqual([
'Conflict: Conflicting customer states; see original GPO source instructions.',
'The registry key letter case differs between sources; Windows treats registry key names case-insensitively.'
]);
// Problem and next step are separate blocks with their own marker.
const next = problem.nextElementSibling!;
expect(next.matches('section.policy-next-step')).toBe(true);
expect(next.querySelector('h4')?.textContent).toBe('Next step');
expect(problem.querySelector('.policy-problem__sources')).toBeNull();
expect(next.querySelector('.policy-problem__sources')?.textContent)
.toBe('In: GPO backup gpo-backup-a, GPO backup gpo-backup-b, GPO backup gpo-backup-c');
// Identical instructions of several backups are stated above, so their complete sections are on demand.
const evidence = next.nextElementSibling!;
expect(evidence.classList.contains('policy-source-evidence')).toBe(true);
const collapsed = evidence.querySelector('details.policy-source-collapse')!;
expect(collapsed.hasAttribute('open')).toBe(false);
expect(collapsed.querySelector('summary')?.textContent).toBe('Show the 3 identical source instructions');
expect(collapsed.querySelectorAll('.policy-source-group')).toHaveLength(3);
expect(container.querySelectorAll('.policy-source-deviation-value')).toHaveLength(3);
// Not repeated as separate report fields; technical provenance moves to Additional Information.
const headings = [...summary.querySelectorAll(':scope > div > h4')].map(item => item.textContent);
expect(headings).not.toContain('Conflict');
expect(headings).not.toContain('Remediation');
expect(headings).not.toContain('Technical Identity');
expect(headings).toContain('Scope');
expect(container.querySelector('.all-fields')?.textContent).toContain('synthetic-identity');
});
it('keeps work tracking available but collapsed below the finding', () => {
const container = render();
const tracking = container.querySelector('details.reader-tracking')!;
expect(tracking.hasAttribute('open')).toBe(false);
expect(tracking.querySelector('summary')?.textContent).toBe('Work trackingStatus: Open · Priority: Medium');
expect(tracking.querySelectorAll('.dashboard-grid select, .review-grid textarea')).toHaveLength(7);
expect(container.querySelector('.reader-summary')!.compareDocumentPosition(tracking) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(container.querySelector('.finding-reader > .dashboard-grid')).toBeNull();
});
it('adds no problem block without current structured comparisons', () => {
const item = record();
delete item.policyBaselineComparisons;
expect(render(item).querySelector('.policy-problem')).toBeNull();
});
});
describe('GPO source sections with differing instructions', () => {
it('summarizes differing values per backup and keeps their complete sections on demand', () => {
const item = record();
const differing = item.policySourceEvidence![1];
differing.values = [{ ...(differing.values[0] as object), data: { kind: 'uint32', value: 1 } }] as never;
const container = render(item);
const collapsed = container.querySelector('details.policy-source-collapse')!;
expect(collapsed.hasAttribute('open')).toBe(false);
expect(collapsed.querySelector('summary')?.textContent).toBe('Show the 3 source instructions');
expect(collapsed.querySelectorAll('.policy-source-group')).toHaveLength(3);
// The value-to-backup overview stays visible outside the disclosure.
const overview = container.querySelector('.policy-source-overview')!;
expect(overview.closest('details')).toBeNull();
expect(overview.querySelector('.policy-source-overview__summary')?.textContent).toBe('2 different instructions across 3 sources');
});
});
describe('GPO problem and next-step markers', () => {
it('use the shared marker width, the fail color for the problem and the orange next-step role', async () => {
const { readFileSync } = await import('node:fs');
const styles = readFileSync('src/styles/components.css', 'utf8');
const tokens = readFileSync('src/styles/tokens.css', 'utf8');
expect(styles).toMatch(/\.reader-summary > \.policy-problem, \.reader-summary > \.policy-next-step \{[^}]*border-left: var\(--marker-width\) solid/);
expect(styles).toMatch(/\.reader-summary > \.policy-problem\[data-outcome="fail"\] \{ border-left-color: var\(--outcome-fail\); \}/);
expect(styles).toMatch(/\.reader-summary > \.policy-next-step \{[^}]*border-left-color: var\(--next-step-accent\);/);
expect(tokens).toContain('--next-step-accent: var(--sev-high);');
});
});
describe('GPO problem summary with differing package results', () => {
it('explains the rolled-up verdict, names the templates that match and states the shared intro once', () => {
const principal = (key: string) => ({ id: key, backupKey: key, backupInstanceId: `gpo-backup-${key}`, sourceReference: `{${key}}/GptTmpl.inf#lines=1`,
baselineRelation: 'review' as const, values: [{ kind: 'principal-set', principals: ['*S-1-5-32-544'] }] });
const variant = (template: string, match: boolean) => ({ template, settingId: template, comparison: match ? 'Match' : 'Different',
expected: match ? 'Values: *S-1-5-32-544' : 'Empty account list', additional: match ? [] : ['*S-1-5-32-544'], missing: [] });
const comparison = (baseline: string, status: string, remediation: string, recommendations: ReturnType<typeof variant>[]) => ({
baseline, status, summary: '', context: 'Synthetic role context.', scopeNotice: 'Synthetic scope notice.', explanation: '', remediation, recommendations });
const mixed = 'Establish the applicable baseline template before changing this setting. Compare the individual recommendations below; do not combine their values into a new baseline.';
const item = record();
item.fields = [{ label: 'Title', value: 'SyntheticRight', index: 0 }, { label: 'Status', value: 'Fail', index: 1 }];
item.policySourceEvidence = [principal('a')];
item.policyBaselineComparisons = [
comparison('Synthetic client baseline', 'Fail', 'Computer policy: SyntheticRight\nExpected: Values: Not set', [variant('Synthetic client', false)]),
comparison('Synthetic server baseline 1', 'Review required', mixed, [variant('Synthetic server 1 - Member Server', false), variant('Synthetic server 1 - Domain Controller', true)]),
comparison('Synthetic server baseline 2', 'Review required', mixed, [variant('Synthetic server 2 - Domain Controller', true), variant('Synthetic server 2 - Member Server', false)])
];
const problem = render(item).querySelector('.policy-problem')!;
expect([...problem.querySelectorAll('.policy-problem__context')].map(line => line.textContent)).toEqual([
'1 GPO backup source',
'Verdict Fail: the highest result of the individual baseline packages. Fail in 1 of 3 packages, Review required in 2 of 3 packages; in 2 of them the templates of the same package disagree.'
]);
const expected = [...problem.querySelectorAll('.policy-problem__values > div')].find(row => row.querySelector('dt')?.textContent === 'Expected')!;
expect([...expected.querySelectorAll('.policy-problem__entry')].map(entry => entry.textContent)).toEqual([
'Empty account list — Different · 3 baseline templates',
'Values: *S-1-5-32-544 (Administrators) — Match · Synthetic server 1 - Domain Controller; Synthetic server 2 - Domain Controller'
]);
const next = problem.nextElementSibling!;
expect(next.textContent!.split('Establish the applicable baseline template').length - 1).toBe(1);
expect([...next.querySelectorAll('.policy-problem__step')].map(step => step.querySelector('.policy-problem__packages')?.textContent))
.toEqual(['Synthetic client baseline']);
});
});
describe('GPO backups without a link', () => {
it('states "No effect" in the problem summary and marks every unlinked source, without changing the verdict', () => {
const item = record();
item.policySourceEvidence = item.policySourceEvidence!.map(source => ({ ...source, linkStatus: 'not-linked' as const }));
const container = render(item);
const problem = container.querySelector('.policy-problem')!;
expect(container.querySelector('.outcome-mark')?.textContent).toBe('Fail');
expect([...problem.querySelectorAll('.policy-problem__flag')].map(flag => flag.textContent)).toContain(
'No effect: every contributing GPO is not linked in its backup report, so this value applies to no device.');
expect(container.querySelectorAll('.policy-source-group__badge.is-not-linked')).toHaveLength(3);
expect(container.querySelectorAll('.policy-source-group__link-note')).toHaveLength(3);
});
it('counts unlinked contributors when only some GPOs are unlinked and stays silent without retained link facts', () => {
const partial = record();
partial.policySourceEvidence = partial.policySourceEvidence!.map((source, index) => index === 0
? { ...source, linkStatus: 'not-linked' as const } : { ...source, linkStatus: 'linked' as const });
const flags = [...render(partial).querySelectorAll('.policy-problem__flag')].map(flag => flag.textContent);
expect(flags).toContain('1 of 3 contributing GPOs are not linked in their backup report and apply to no device.');
const unknown = render(record());
expect(unknown.querySelector('.policy-source-group__badge.is-not-linked')).toBeNull();
expect(unknown.textContent).not.toContain('not linked');
});
});