Assessor_2.0/tests/features/gpo-source-evidence-interac...

166 lines
12 KiB
TypeScript

// @vitest-environment jsdom
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { AppProvider, useApp } from '../../src/app/AppProvider';
import { AssessorApiClient } from '../../src/api/assessor-api-client';
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
import type { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
import type { ImportedDataset } from '../../src/domain/dataset';
import { POLICY_EVALUATION_ENGINE, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation';
import { appendPolicyObservationsToDataset } from '../../src/features/project-import/policy-observations-to-dataset';
import { loadCanonicalProjectWorkspace } from '../../src/features/project-import/load-canonical-project-workspace';
import { SearchFilterBar, SearchTextControl, SheetAndSearchControls } from '../../src/features/search-filter/SearchFilterBar';
import { useFilteredRecords } from '../../src/features/search-filter/use-filtered-records';
import { FindingReader } from '../../src/features/finding-reader/FindingReader';
import { inlineResultTableSheets } from '../../src/features/linked-results/inline-result-tables';
import { makeSheet } from '../../src/importers/sheet-factory';
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); localStorage.clear(); });
const nativeId = (index: number) => `{00000000-0000-0000-0000-${String(index).padStart(12, '0')}}`;
function source(index: number, value: number | 'delete', setting = 'Alpha mode'): UnifiedResultProjection {
return {
sourceKind: 'policy-observation', sourceResultId: `observation-${index}`, projectId: 'synthetic', assessmentId: 'slot',
policyBackupInstanceId: `backup-${index}`, policyGpoId: nativeId(index), policyNativeBackupId: nativeId(index + 100),
policySourceSequence: index, title: setting, importedAt: '2026-09-17T00:00:00Z',
policyScope: 'computer', policyDeliveryMechanism: 'registry-policy', policyIdentityStatus: 'resolved',
policyIdentityKey: setting, policyState: value === 'delete' ? 'instruction' : 'configured',
sourceLocator: { objectPath: `C:/SyntheticPrivate/${nativeId(index + 100)}/DomainSysvol/GPO/Machine/registry.pol#records=${index}` },
policyValues: [{ kind: 'registry-policy', operation: value === 'delete' ? 'delete-value' : 'set-value',
key: 'Software\\Policies\\Synthetic', valueName: setting,
...(value === 'delete' ? {} : { typeCode: 4, data: { kind: 'uint32', value } }) }]
} as unknown as UnifiedResultProjection;
}
function fixture(mixed: boolean) {
const sources = [source(1, 1), source(2, 'delete'), source(3, 1), source(4, 1), source(5, 0, 'Beta switch')];
const comparison = (observations: UnifiedResultProjection[], conflicted: boolean): PolicyEvaluationResult => ({
id: `comparison-${observations[0].policyIdentityKey}`, observationIds: observations.map(item => item.sourceResultId),
backupInstanceIds: observations.map(item => item.policyBackupInstanceId), baselineId: 'baseline', baselineLabel: 'Synthetic baseline',
identity: { id: observations[0].policyIdentityKey, scope: 'computer', target: { kind: 'registry-value',
hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic', valueName: observations[0].title } },
baselineState: 'explicit-recommendation', customerState: conflicted ? 'conflicted' : 'configured',
classification: 'confirmed_configured_deviation', diagnosticCodes: conflicted ? ['policy-evaluation-customer-conflict'] : [],
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different', baseline: { kind: 'integer', value: '1' }, diagnosticCodes: [] },
baselineVariantAssessment: { version: 1, scope: 'backup-configuration', decision: 'all-fail',
actualValues: observations.flatMap(item => item.policyValues!), variants: [{
settingId: 'baseline-setting', baselineValues: [{ kind: 'registry-policy', operation: 'set-value',
key: 'Software\\Policies\\Synthetic', valueName: observations[0].title, typeCode: 4,
data: { kind: 'uint32', value: 1 } }],
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different',
baseline: { kind: 'integer', value: '1' }, diagnosticCodes: [] }
}], diagnosticCodes: [] }
} as unknown as PolicyEvaluationResult);
const evaluation = { run: { id: 'evaluation', projectId: 'synthetic', aggregationRunId: 'aggregation',
completedAt: '2026-09-17T00:01:00Z', evaluator: POLICY_EVALUATION_ENGINE },
results: [comparison(sources.slice(0, 4), true), comparison(sources.slice(4), false)] } as StoredPolicyEvaluation;
const scanner = mixed ? { sheets: [makeSheet('All Records', [['Title', 'Status', 'Source: Scan'],
['Synthetic scanner', 'Fail', 'Maester']], '', 'Synthetic', { isProjectSheet: true })],
profileId: 'generic', scanTool: 'Multiple Tools' } as ImportedDataset : null;
return { dataset: appendPolicyObservationsToDataset(scanner, 'Synthetic', sources, [evaluation])!, sources, evaluation };
}
function Harness({ onResults }: { onResults(): void }) {
const app = useApp();
const records = useFilteredRecords();
const selected = records.find(record => record.id === app.selectedRecordId) ?? records[0];
const position = selected ? records.indexOf(selected) : 0;
return <>
<button data-testid="open" onClick={() => void app.openProject('synthetic', [], ['slot'])}>Open synthetic</button>
<SheetAndSearchControls/><SearchTextControl/><SearchFilterBar/>
<ul data-testid="findings">{records.map(record => <li key={record.id}>
<button onClick={() => app.setSelectedRecord(record.id)}>{record.title}</button>
</li>)}</ul>
{selected && <FindingReader record={selected} title={selected.title} linkedSheets={inlineResultTableSheets(selected)}
resultReferences={[]} search={app.search} position={position} recordCount={records.length}
toolId={app.activeSheet?.toolId ?? app.activeProfile.id}
onPrevious={() => app.setSelectedRecord(records[Math.max(0, position - 1)].id)}
onNext={() => app.setSelectedRecord(records[Math.min(records.length - 1, position + 1)].id)}
onReview={() => {}} onResults={onResults}/>}
</>;
}
describe('Work shows GPO source attribution immediately', () => {
it.each([false, true])('preserves source attribution through Deviation, search, Conflicts and record navigation (mixed=%s)', async mixed => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
const { dataset, sources, evaluation } = fixture(mixed);
const unchanged = JSON.stringify([sources, evaluation]);
vi.mocked(loadCanonicalProjectWorkspace).mockResolvedValue({ dataset, assessmentIds: ['slot'], models: [{
project: { id: 'synthetic', name: 'Synthetic' }, assessment: { id: 'slot' }, reviews: []
} as unknown as CanonicalAssessmentModel] });
const container = document.createElement('div');
const root = createRoot(container);
const onResults = vi.fn();
const select = async (label: string, value: string) => act(async () => {
const control = container.querySelector<HTMLSelectElement>(`select[aria-label="${label}"]`)!;
control.value = value;
control.dispatchEvent(new Event('change', { bubbles: true }));
});
const click = async (selector: string) => act(async () => container.querySelector<HTMLButtonElement>(selector)!.click());
const titles = () => [...container.querySelectorAll('[data-testid="findings"] li')].map(item => item.textContent);
const groups = () => [...container.querySelectorAll<HTMLElement>('.policy-source-group')];
const attribution = () => groups().map(group => ({
label: group.dataset.sourceLabel,
relation: group.dataset.sourceRelation,
operation: group.textContent?.includes('Operation: delete-value') ? 'delete-value' : 'set-value',
data: group.textContent?.includes('Data: 0') ? '0' : group.textContent?.includes('Data: 1') ? '1' : '—'
}));
const expectedAttribution = [1, 2, 3, 4].map(index => ({ label: `GPO ${nativeId(index)}`,
relation: index === 2 ? 'conflicting' : 'aligned', operation: index === 2 ? 'delete-value' : 'set-value',
data: index === 2 ? '—' : '1' }));
try {
await act(async () => root.render(<AppProvider><Harness onResults={onResults}/></AppProvider>));
await click('[data-testid="open"]');
if (mixed) await select('Source: Scan', 'gpos · 2026-09-17t00:00:00z');
expect(container.querySelector<HTMLSelectElement>('select[aria-label="Result Status"]')?.value).toBe('deviation');
expect(titles()).toEqual(['Alpha mode', 'Beta switch']);
await act(async () => [...container.querySelectorAll<HTMLButtonElement>('[data-testid="findings"] button')]
.find(button => button.textContent === 'Alpha mode')!.click());
expect(attribution()).toEqual(expectedAttribution);
expect(container.querySelector('.policy-source-evidence > h4')?.textContent).toBe('4 GPO backup sources');
expect(container.querySelector('.finding-reader .outcome-mark')?.textContent).toBe('Fail');
expect(container.querySelector('[data-policy-diagnostic="conflict"]')).not.toBeNull();
expect(container.querySelector('[data-policy-diagnostic="duplicate"]')).toBeNull();
expect(container.querySelector('.policy-source-notice')?.textContent).toContain('not an execution order between GPOs');
expect(container.querySelector('.policy-source-notice')?.textContent).toContain('does not establish the effective endpoint value');
expect(container.querySelector('.policy-source-table')).toBeNull();
expect(container.querySelectorAll('.policy-source-group__badge')).toHaveLength(0);
expect(groups()[1].classList.contains('is-conflicting')).toBe(true);
expect(container.querySelectorAll('.policy-source-deviation-value')).toHaveLength(1);
expect(container.querySelector('.policy-source-deviation-value')?.textContent).toBe('Operation: delete-value');
expect(groups()[1].textContent).toContain('Source sequence: 2');
expect(groups()[1].textContent).toContain('Key: Software\\Policies\\Synthetic');
expect(container.querySelector('.policy-value-block[data-value="configured"]')).toBeNull();
expect(container.querySelector('.finding-reader')?.textContent).not.toContain('C:/SyntheticPrivate');
expect(onResults).not.toHaveBeenCalled();
await act(async () => {
const input = container.querySelector<HTMLInputElement>('input[placeholder="Search"]')!;
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(input, 'Alpha mode');
input.dispatchEvent(new Event('input', { bubbles: true }));
});
expect(titles()).toEqual(['Alpha mode']);
expect(attribution()).toEqual(expectedAttribution);
expect(container.querySelector('.finding-reader mark')).not.toBeNull();
await select('Result Status', 'conflicts');
expect(titles()).toEqual(['Alpha mode']);
expect(attribution()).toEqual(expectedAttribution);
await click('[aria-label="Clear search"]');
await select('Result Status', 'deviation');
await click('[aria-label="Next record"]');
expect(container.querySelector('.reader-heading h2')?.textContent).toBe('Beta switch');
expect(attribution()).toEqual([{ label: `GPO ${nativeId(5)}`, relation: 'conflicting', operation: 'set-value', data: '0' }]);
expect(container.querySelectorAll('.policy-source-deviation-value')).toHaveLength(1);
expect(container.querySelector('.policy-source-deviation-value')?.textContent).toBe('Data: 0');
expect(container.querySelector('[data-policy-diagnostic="conflict"]')).toBeNull();
await click('[aria-label="Previous record"]');
expect(attribution()).toEqual(expectedAttribution);
expect(onResults).not.toHaveBeenCalled();
expect(JSON.stringify([sources, evaluation])).toBe(unchanged);
} finally { await act(async () => root.unmount()); }
});
});