267 lines
18 KiB
TypeScript
267 lines
18 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
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 { CanonicalAssessmentModel } from '../../src/domain/canonical-model';
|
|
import type { ImportedDataset } from '../../src/domain/dataset';
|
|
import type { UnifiedResultProjection } from '../../src/domain/unified-result';
|
|
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, 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 { makeSheet } from '../../src/importers/sheet-factory';
|
|
import { buildPolicyWorkResults, countPolicyWorkResults } from '../../src/evaluation/policy-work-results';
|
|
import { hydrateWorkspaceReviews, previousPolicyReviews } from '../../src/features/review-workspace/database-review';
|
|
import { defaultReview } from '../../src/domain/review';
|
|
|
|
vi.mock('../../src/features/project-import/load-canonical-project-workspace', () => ({ loadCanonicalProjectWorkspace: vi.fn() }));
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); localStorage.clear(); });
|
|
|
|
function fixture(count = 9) {
|
|
const actual = { kind: 'registry-policy', operation: 'set-value', typeCode: 4,
|
|
key: 'Software\\Policies\\Synthetic', valueName: 'SyntheticMode', data: { kind: 'uint32', value: 0 } } as const;
|
|
const observation = { sourceKind: 'policy-observation', sourceResultId: 'synthetic-observation', projectId: 'synthetic',
|
|
assessmentId: 'slot', policyBackupInstanceId: 'synthetic-backup', policyIdentityKey: 'synthetic-mode',
|
|
policyIdentityStatus: 'resolved', policyScope: 'computer', policyState: 'configured', policyDeliveryMechanism: 'registry-policy',
|
|
importedAt: '2026-09-01T00:00:00Z', title: 'SyntheticMode', policyValues: [actual],
|
|
sourceLocator: { objectPath: 'Synthetic/DomainSysvol/GPO/Machine/registry.pol#records=1' }
|
|
} as unknown as UnifiedResultProjection;
|
|
const comparisons = Array.from({ length: count }, (_, index) => ({
|
|
id: `comparison-${index}`, baselineId: `baseline-${index}`, baselineLabel: `Synthetic baseline ${index}`,
|
|
baselineAnalysisId: `analysis-${index}`, observationIds: [observation.sourceResultId],
|
|
baselineState: 'explicit-recommendation', customerState: 'configured', classification: 'confirmed_configured_deviation',
|
|
identity: { id: 'synthetic-mode', scope: 'computer', target: { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE',
|
|
key: actual.key, valueName: actual.valueName } }, diagnosticCodes: [],
|
|
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different', diagnosticCodes: [],
|
|
actual: { kind: 'integer', value: '0' }, baseline: { kind: 'integer', value: '1' } },
|
|
baselineVariantAssessment: { version: 1, scope: 'backup-configuration', decision: 'all-fail', diagnosticCodes: [],
|
|
actualValues: [actual], variants: [{ settingId: `setting-${index}`,
|
|
baselineValues: [{ ...actual, data: { kind: 'uint32', value: 1 } }],
|
|
comparator: { version: 1, comparator: { kind: 'equality' }, relation: 'different', diagnosticCodes: [],
|
|
actual: { kind: 'integer', value: '0' }, baseline: { kind: 'integer', value: '1' } } }] }
|
|
})) as unknown as PolicyEvaluationResult[];
|
|
const evaluation = { run: { id: 'evaluation', projectId: 'synthetic', aggregationRunId: 'aggregation',
|
|
evaluator: POLICY_EVALUATION_ENGINE, completedAt: '2026-09-02T00:00:00Z' }, results: comparisons } as StoredPolicyEvaluation;
|
|
return { observation, evaluation };
|
|
}
|
|
|
|
function project(observation: UnifiedResultProjection, evaluations: StoredPolicyEvaluation[], mixed = false) {
|
|
const scanner = mixed ? { sheets: [makeSheet('All Records', [['Title', 'Status', 'Source: Scan'],
|
|
['Scanner rule', 'Fail', 'Maester']], '', 'Synthetic', { isProjectSheet: true })], profileId: 'generic', scanTool: 'Multiple Tools' } as ImportedDataset : null;
|
|
return appendPolicyObservationsToDataset(scanner, 'Synthetic', [observation], evaluations)!;
|
|
}
|
|
const field = (dataset: ImportedDataset, label: string) => dataset.sheets[0].analysis.records[0].fields.find(item => item.label === label)!.value;
|
|
|
|
function Harness() {
|
|
const app = useApp();
|
|
const records = useFilteredRecords();
|
|
const record = records[0];
|
|
return <>
|
|
<button data-testid="open" onClick={() => void app.openProject('synthetic', [], ['slot'])}>Open synthetic</button>
|
|
<SheetAndSearchControls/><SearchFilterBar/>
|
|
<ul data-testid="findings">{records.map(item => <li key={item.id}>{item.title}</li>)}</ul>
|
|
{record && <FindingReader record={record} title={record.title} position={0} recordCount={records.length}
|
|
linkedSheets={[]} resultReferences={[]} search="" toolId="group-policy" onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>}
|
|
</>;
|
|
}
|
|
|
|
describe('one Work finding per technical setting across baseline packages', () => {
|
|
it.each([false, true])('selects missing required registry presence by its Fail verdict in both GPO entry paths (mixed=%s)', async mixed => {
|
|
const { observation, evaluation } = fixture(1);
|
|
observation.importedAt = '';
|
|
const missing = (id: string, baseline: PolicyEvaluationResult['comparator']['baseline']) => {
|
|
const result = structuredClone(evaluation.results[0]);
|
|
result.id = id;
|
|
result.identity.id = id;
|
|
result.identity.target = { kind: 'registry-value', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Policies\\Synthetic', valueName: id };
|
|
result.observationIds = [];
|
|
result.customerState = 'missing';
|
|
result.comparator = { ...result.comparator, actual: undefined, baseline };
|
|
result.baselineVariantAssessment!.actualValues = [];
|
|
result.baselineVariantAssessment!.variants[0].comparator = result.comparator;
|
|
return result;
|
|
};
|
|
evaluation.results.push(
|
|
missing('RequiredKey', { kind: 'registry-state', state: 'key-present' }),
|
|
missing('RequiredEmptyString', { kind: 'text', value: '' }),
|
|
missing('OrdinaryMissingRecommendation', { kind: 'integer', value: '1' })
|
|
);
|
|
const history = structuredClone(evaluation);
|
|
history.run = { ...history.run, id: 'historical', aggregationRunId: 'historical-snapshot',
|
|
evaluator: { ...POLICY_EVALUATION_ENGINE, version: '1.8.0' } };
|
|
history.results = [missing('HistoricalRequiredKey', { kind: 'registry-state', state: 'key-present' })];
|
|
const before = JSON.stringify([observation, evaluation, history]);
|
|
const dataset = project(observation, [evaluation, history], mixed);
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
|
|
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 select = async (label: string, value: string) => act(async () => {
|
|
const control = container.querySelector<HTMLSelectElement>(`select[aria-label="${label}"]`)!;
|
|
control.dispatchEvent(new Event('focusin', { bubbles: true }));
|
|
control.value = value;
|
|
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
const titles = () => [...container.querySelectorAll('[data-testid="findings"] li')].map(item => item.textContent);
|
|
try {
|
|
await act(async () => root.render(<AppProvider><Harness/></AppProvider>));
|
|
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="open"]')!.click());
|
|
if (mixed) await select('Source: Scan', 'gpos');
|
|
await select('Result Status', 'deviation');
|
|
expect(titles()).toHaveLength(3);
|
|
expect(titles().join(' ')).toContain('RequiredKey');
|
|
expect(titles().join(' ')).toContain('RequiredEmptyString');
|
|
const status = container.querySelector<HTMLSelectElement>('select[aria-label="Result Status"]')!;
|
|
expect(status.querySelector('option[value="deviation"]')?.textContent).toBe('Deviation · 3');
|
|
expect(status.querySelector('option[value="not-configured"]')?.textContent).toBe('Not configured · 1');
|
|
await select('Result Status', 'not-configured');
|
|
expect(titles()).toHaveLength(1);
|
|
expect(titles()[0]).toContain('OrdinaryMissingRecommendation');
|
|
await select('Result Status', 'passed');
|
|
expect(titles()).toEqual([]);
|
|
await select('Result Status', 'all');
|
|
expect(titles()).toHaveLength(5);
|
|
expect(titles().join(' ')).toContain('HistoricalRequiredKey');
|
|
await select('Result Status', 'deviation');
|
|
expect(titles()).toHaveLength(3);
|
|
expect(JSON.stringify([observation, evaluation, history])).toBe(before);
|
|
} finally { await act(async () => root.unmount()); }
|
|
});
|
|
|
|
it.each([false, true])('opens nine comparisons as one finding and filters each baseline separately (mixed=%s)', async mixed => {
|
|
const { observation, evaluation } = fixture();
|
|
const before = JSON.stringify([observation, evaluation]);
|
|
expect(buildPolicyWorkResults([observation], [evaluation])).toHaveLength(9);
|
|
const dataset = project(observation, [evaluation], mixed);
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
vi.spyOn(AssessorApiClient.prototype, 'health').mockResolvedValue({ status: 'ok' } as never);
|
|
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 select = async (label: string, value: string) => act(async () => {
|
|
const control = container.querySelector<HTMLSelectElement>(`select[aria-label="${label}"]`)!;
|
|
control.dispatchEvent(new Event('focusin', { bubbles: true }));
|
|
control.value = value;
|
|
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
});
|
|
try {
|
|
await act(async () => root.render(<AppProvider><Harness/></AppProvider>));
|
|
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="open"]')!.click());
|
|
if (mixed) await select('Source: Scan', 'gpos · 2026-09-01t00:00:00z');
|
|
await select('Result Status', 'deviation');
|
|
expect(container.querySelectorAll('[data-testid="findings"] li')).toHaveLength(1);
|
|
expect(container.querySelectorAll('.policy-source-group')).toHaveLength(1);
|
|
expect(container.querySelector('.outcome-mark')?.textContent).toBe('Fail');
|
|
expect(container.querySelector('[data-policy-diagnostic="duplicate"]')).toBeNull();
|
|
expect(container.querySelectorAll('.policy-source-deviation-value')).toHaveLength(1);
|
|
for (let i = 0; i < 9; i++) {
|
|
expect(container.querySelector('.finding-reader')?.textContent).toContain(`Setting reference: setting-${i}`);
|
|
await select('Baseline', `synthetic baseline ${i}`);
|
|
expect(container.querySelectorAll('[data-testid="findings"] li')).toHaveLength(1);
|
|
expect(container.querySelector('.outcome-mark')?.textContent).toBe('Fail');
|
|
}
|
|
await select('Result Status', 'duplicates');
|
|
expect(container.querySelectorAll('[data-testid="findings"] li')).toHaveLength(0);
|
|
expect(countPolicyWorkResults(buildPolicyWorkResults([observation], [evaluation])).Fail).toBe(1);
|
|
expect(JSON.stringify([observation, evaluation])).toBe(before);
|
|
} finally { await act(async () => root.unmount()); }
|
|
});
|
|
|
|
it('retains differing expected values and uses the existing Fail-before-Pass rollup without false red markings', () => {
|
|
const { observation, evaluation } = fixture(2);
|
|
const pass = evaluation.results[1];
|
|
pass.classification = 'configured_match';
|
|
pass.comparator = { ...pass.comparator, relation: 'match', baseline: { kind: 'integer', value: '0' } };
|
|
pass.baselineVariantAssessment!.decision = 'all-pass';
|
|
pass.baselineVariantAssessment!.variants[0].comparator = pass.comparator;
|
|
pass.baselineVariantAssessment!.variants[0].baselineValues = observation.policyValues as never;
|
|
const dataset = project(observation, [evaluation]);
|
|
expect(dataset.sheets[0].analysis.records).toHaveLength(1);
|
|
expect(field(dataset, 'Status')).toBe('Fail');
|
|
const expected = JSON.parse(field(dataset, 'Baseline value'));
|
|
expect(expected.map((item: { expected: { value: string } }) => item.expected.value)).toEqual(['1', '0']);
|
|
expect(field(dataset, 'Remediation')).toContain('Establish the applicable baseline template');
|
|
expect(field(dataset, 'Comparison')).toContain('Synthetic baseline 1: Pass');
|
|
expect(dataset.sheets[0].analysis.records[0].policySourceEvidence?.[0].deviationFields).toBeUndefined();
|
|
expect(project(observation, [{ ...evaluation, results: [...evaluation.results].reverse() }]).sheets[0].analysis.records[0].id)
|
|
.toBe(dataset.sheets[0].analysis.records[0].id);
|
|
});
|
|
|
|
it('preserves historical readiness and never fabricates a current red source marker', () => {
|
|
const { observation, evaluation } = fixture(2);
|
|
evaluation.run.evaluator = { ...POLICY_EVALUATION_ENGINE, version: '1.8.0' };
|
|
const dataset = project(observation, [evaluation]);
|
|
expect(dataset.sheets[0].analysis.records).toHaveLength(1);
|
|
expect(field(dataset, 'Status')).toBe('Review required');
|
|
expect(field(dataset, 'Evaluation readiness')).toContain('Re-evaluation required');
|
|
expect(dataset.sheets[0].analysis.records[0].policySourceEvidence?.[0].baselineRelation).toBeUndefined();
|
|
expect(dataset.sheets[0].analysis.records[0].policyFailingBackupSources).toHaveLength(1);
|
|
});
|
|
|
|
it.each(['Pass', 'Review required', 'Not configured'] as const)('rolls up %s without manufacturing a Fail', status => {
|
|
const { observation, evaluation } = fixture(2);
|
|
for (const result of evaluation.results) {
|
|
result.classification = status === 'Pass' ? 'configured_match' : status === 'Review required' ? 'review_required' : 'confirmed_configured_deviation';
|
|
result.baselineVariantAssessment!.decision = status === 'Pass' ? 'all-pass' : status === 'Review required' ? 'unavailable' : 'all-fail';
|
|
if (status === 'Not configured') { result.customerState = 'missing'; result.observationIds = []; }
|
|
}
|
|
const dataset = status === 'Not configured'
|
|
? appendPolicyObservationsToDataset(null, 'Synthetic', [], [evaluation])! : project(observation, [evaluation]);
|
|
expect(dataset.sheets[0].analysis.records).toHaveLength(1);
|
|
expect(field(dataset, 'Status')).toBe(status);
|
|
expect(dataset.sheets[0].analysis.records[0].policyFailingBackupSources).toHaveLength(0);
|
|
});
|
|
|
|
it.each([false, true])('preserves earlier baseline notes and does not choose between different reviews (mixed=%s)', mixed => {
|
|
const { observation, evaluation } = fixture(2);
|
|
const originalRecords = evaluation.results.map(result => project(observation, [{ ...evaluation, results: [result] }], mixed)
|
|
.sheets[0].analysis.records.find(record => record.title === 'SyntheticMode')!);
|
|
const dataset = project(observation, [evaluation], mixed);
|
|
const record = dataset.sheets[0].analysis.records.find(record => record.title === 'SyntheticMode')!;
|
|
expect(record.policyReviewMembers?.map(item => item.id)).toEqual(expect.arrayContaining(originalRecords.map(item => item.id)));
|
|
const common = { ...defaultReview(), owner: 'Synthetic owner', comment: 'Retained decision', include: true };
|
|
const locals = Object.fromEntries(originalRecords.map(item => [item.id, common]));
|
|
expect(hydrateWorkspaceReviews(dataset, [], locals).reviews[record.id]).toEqual(common);
|
|
locals[originalRecords[1].id] = { ...common, owner: 'Different synthetic owner' };
|
|
const hydrated = hydrateWorkspaceReviews(dataset, [], locals);
|
|
expect(hydrated.reviews[record.id]).toBeUndefined();
|
|
const retained = previousPolicyReviews(record, hydrated.reviews);
|
|
expect(retained).toHaveLength(2);
|
|
const html = renderToStaticMarkup(<FindingReader record={record} title={record.title} previousPolicyReviews={retained}
|
|
linkedSheets={[]} resultReferences={[]} search="" position={0} recordCount={1} toolId="group-policy"
|
|
onPrevious={() => {}} onNext={() => {}} onReview={() => {}}/>);
|
|
expect(html).toContain('Previous baseline-specific reviews (2)');
|
|
expect(html).toContain('Different synthetic owner');
|
|
expect(html).toContain('Retained decision');
|
|
const withRisk = { [originalRecords[0].id]: { ...common, riskAcceptance: { version: 1 as const, revisions: [] } } };
|
|
expect(hydrateWorkspaceReviews(dataset, [], withRisk).reviews[record.id]).toBeUndefined();
|
|
expect(previousPolicyReviews(record, withRisk)[0].review.riskAcceptance).toEqual(withRisk[originalRecords[0].id].riskAcceptance);
|
|
});
|
|
|
|
it('keeps different projects, aggregations, scopes and technical identities separate', () => {
|
|
const { observation, evaluation } = fixture(2);
|
|
const evaluations = [evaluation, ...['project', 'aggregation', 'scope', 'identity'].map(kind => {
|
|
const copy = structuredClone(evaluation);
|
|
if (kind === 'project') copy.run.projectId = 'another-project';
|
|
if (kind === 'aggregation') copy.run.aggregationRunId = 'another-aggregation';
|
|
copy.results.forEach(item => {
|
|
item.id += `-${kind}`;
|
|
if (kind === 'scope') item.identity.scope = 'user';
|
|
if (kind === 'identity') item.identity.id = 'another-setting';
|
|
});
|
|
return copy;
|
|
})];
|
|
expect(project(observation, evaluations).sheets[0].analysis.records).toHaveLength(5);
|
|
});
|
|
});
|