import { describe, expect, it } from 'vitest'; import type { PolicyWorkResult, PolicyWorkStatus } from '../../src/evaluation/policy-work-results'; import { compareGpoTimeslots } from '../../src/comparison/gpo-timeslot-comparison'; // Synthetic rolled-up GPO Work results. Only the fields the comparison reads are populated. const observedA = '2026-01-01T10:00:00.000Z'; const observedB = '2026-02-01T10:00:00.000Z'; function setting(id: string, identity: string, status: PolicyWorkStatus, options: { actual?: unknown; baselineId?: string; aggregationRun?: string; scope?: string; } = {}): PolicyWorkResult { const scope = options.scope ?? 'computer'; return { id, status, title: identity, scope, identity, importedAt: observedA, sources: [], settingKey: JSON.stringify(['dummy-project', options.aggregationRun ?? 'aggregation-1', scope, identity]), comparison: { baselineId: options.baselineId ?? 'baseline-server', comparator: { actual: options.actual === undefined ? undefined : { kind: 'number', value: options.actual } } } as unknown as PolicyWorkResult['comparison'] }; } describe('GPO timeslot comparison', () => { it('pairs rolled-up settings by scope and identity and keeps a value change neutral', () => { const result = compareGpoTimeslots( { observedAt: observedA, results: [ setting('a-1', 'MinimumPasswordLength', 'Fail', { actual: 7 }), setting('a-2', 'LockoutBadCount', 'Fail', { actual: 0 }), setting('a-3', 'NoLMHash', 'Fail', { actual: 0 }), setting('a-4', 'PasswordComplexity', 'Fail', { actual: 0 }) ] }, { observedAt: observedB, results: [ setting('b-1', 'MinimumPasswordLength', 'Fail', { actual: 10 }), setting('b-2', 'LockoutBadCount', 'Fail', { actual: 0 }), setting('b-3', 'NoLMHash', 'Pass', { actual: 1 }), setting('b-4', 'PasswordComplexity', 'Not configured') ] } ); const byIdentity = Object.fromEntries(result.decisions.map(item => [item.identity, item])); expect(byIdentity.MinimumPasswordLength).toMatchObject({ state: 'confirmed-open', valueChanged: true, targetResultIds: ['b-1'] }); // Both retained values travel as Work displays them, for the Finding Reader's side-by-side reading. expect(byIdentity.MinimumPasswordLength).toMatchObject({ baselineValues: ['{"kind":"number","value":7}'], targetValues: ['{"kind":"number","value":10}'] }); expect(byIdentity.PasswordComplexity.targetValues).toEqual([]); expect(byIdentity.LockoutBadCount).toMatchObject({ state: 'confirmed-open', valueChanged: false }); expect(byIdentity.NoLMHash).toMatchObject({ state: 'verified-remediated', reasons: ['same-setting-explicit-pass'] }); // A setting missing from the newer backup set is not proof of a removed configuration. expect(byIdentity.PasswordComplexity).toMatchObject({ state: 'unverified-absent', reasons: ['target-verdict-not-decisive:Not configured'] }); }); it('rolls several baseline releases of one setting up with the Work verdict order', () => { const result = compareGpoTimeslots( { observedAt: observedA, results: [ setting('a-1', 'SeBackupPrivilege', 'Pass', { baselineId: 'baseline-dc' }), setting('a-2', 'SeBackupPrivilege', 'Fail', { baselineId: 'baseline-server' }) ] }, { observedAt: observedB, results: [ setting('b-1', 'SeBackupPrivilege', 'Pass', { baselineId: 'baseline-dc' }), setting('b-2', 'SeBackupPrivilege', 'Pass', { baselineId: 'baseline-server' }) ] } ); expect(result.decisions).toHaveLength(1); expect(result.decisions[0]).toMatchObject({ state: 'verified-remediated', baselineResultIds: ['a-1', 'a-2'] }); }); it('separates new and regressed settings and claims nothing from undecided history', () => { const result = compareGpoTimeslots( { observedAt: observedA, results: [ setting('a-1', 'SeEnableDelegationPrivilege', 'Not configured'), setting('a-2', 'SeRestorePrivilege', 'Pass'), setting('a-3', 'SeAuditPrivilege', 'Review required'), setting('a-4', 'SeLoadDriverPrivilege', 'Pass', { baselineId: 'baseline-old' }) ] }, { observedAt: observedB, results: [ setting('b-1', 'SeEnableDelegationPrivilege', 'Fail'), setting('b-2', 'SeRestorePrivilege', 'Fail'), setting('b-3', 'SeAuditPrivilege', 'Fail'), setting('b-4', 'SeLoadDriverPrivilege', 'Fail'), setting('b-5', 'DnsPolicyConfig', 'Fail') ] } ); expect(result.newDeviations.map(item => item.targetResultIds[0]).sort()).toEqual(['b-1', 'b-5']); expect(result.regressedDeviations.map(item => item.targetResultIds[0])).toEqual(['b-2']); }); it('requires a newer backup set, the same baselines and a GPO source in both timeslots', () => { const fail = { observedAt: observedA, results: [setting('a-1', 'NoLMHash', 'Fail')] }; const sameTime = compareGpoTimeslots(fail, { observedAt: observedA, results: [setting('b-1', 'NoLMHash', 'Pass')] }); expect(sameTime.decisions[0]).toMatchObject({ state: 'not-comparable', reasons: ['target-observation-not-newer'] }); expect(sameTime.sharedPassRate).toBeUndefined(); // Without stated backup times (real backups state no time zone) the timeslot order decides. const untimed = compareGpoTimeslots({ results: fail.results }, { results: [setting('b-1', 'NoLMHash', 'Pass')] }); expect(untimed.decisions[0]).toMatchObject({ state: 'verified-remediated' }); const changedBaseline = compareGpoTimeslots(fail, { observedAt: observedB, results: [setting('b-1', 'NoLMHash', 'Pass', { baselineId: 'baseline-new' })] }); expect(changedBaseline.decisions[0]).toMatchObject({ state: 'not-comparable', reasons: ['baseline-set-changed'] }); const missing = compareGpoTimeslots(fail, { observedAt: observedB, results: [] }); expect(missing).toMatchObject({ state: 'not-comparable', reasons: ['target-source-missing'] }); expect(missing.decisions[0].state).toBe('not-comparable'); }); it('marks a setting held by several aggregation runs of one timeslot as ambiguous', () => { const result = compareGpoTimeslots( { observedAt: observedA, results: [ setting('a-1', 'NoLMHash', 'Fail', { aggregationRun: 'aggregation-1' }), setting('a-2', 'NoLMHash', 'Fail', { aggregationRun: 'aggregation-2' }) ] }, { observedAt: observedB, results: [setting('b-1', 'NoLMHash', 'Pass')] } ); expect(result.decisions[0]).toMatchObject({ state: 'not-comparable', reasons: ['gpo-setting-ambiguous'] }); }); it('computes the shared pass rate only over settings decided in both timeslots', () => { const result = compareGpoTimeslots( { observedAt: observedA, results: [ setting('a-1', 'One', 'Fail'), setting('a-2', 'Two', 'Fail'), setting('a-3', 'Three', 'Pass'), setting('a-4', 'OnlyInA', 'Fail') ] }, { observedAt: observedB, results: [ setting('b-1', 'One', 'Pass'), setting('b-2', 'Two', 'Fail'), setting('b-3', 'Three', 'Pass'), setting('b-5', 'OnlyInB', 'Pass') ] } ); expect(result.sharedPassRate?.settings).toBe(3); expect(result.sharedPassRate?.baseline).toBeCloseTo(100 / 3); expect(result.sharedPassRate?.target).toBeCloseTo(200 / 3); }); });