Assessor_2.0/tests/server/gpo-policy-snapshot-service...

266 lines
9.4 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { resolveRegistryPolicyIdentity } from '../../server/gpo/admx-policy-catalog';
import { buildPolicySnapshotAggregation } from '../../server/gpo/gpo-policy-snapshot-service';
import type { StoredGpoBackupInstance, StoredGpoPolicyAnalysis } from '../../src/domain/gpo-analysis-persistence';
import type { GpoBackupInstance } from '../../src/domain/gpo-ingestion';
import type { GpoPolicyObservation, GpoRegistryPolicyValue } from '../../src/domain/gpo-policy';
import type { EnvironmentProfile } from '../../src/domain/gpo-snapshot-aggregation';
const timestamp = '2026-08-04T12:00:00.000Z';
const projectId = 'policy-snapshot-project';
const artifactId = 'policy-snapshot-artifact';
const processingRunId = 'policy-snapshot-processing';
const analysisRunId = 'policy-snapshot-analysis';
const registryKey = 'Software\\Policies\\Synthetic';
const profile: EnvironmentProfile = {
version: 1,
id: 'environment-profile-synthetic',
projectId,
name: 'Synthetic Windows environment',
revision: 1,
contexts: [{
version: 1,
id: 'context-windows',
kind: 'operating-system',
productFamily: 'Windows Server',
productVersion: '2022',
role: 'Member Server',
scope: 'computer'
}],
createdAt: timestamp
};
function registryValue(operation: GpoRegistryPolicyValue['operation'], value?: number): GpoRegistryPolicyValue {
return {
kind: 'registry-policy',
operation,
key: registryKey,
valueName: 'Mode',
typeCode: value === undefined ? undefined : 4,
data: value === undefined ? undefined : { kind: 'uint32', value }
};
}
function observation(
backupInstanceId: string,
id: string,
sequence: number,
value: GpoRegistryPolicyValue
): GpoPolicyObservation {
return {
version: 1,
id,
backupInstanceId,
sourceArtifactId: artifactId,
artifactId: 'registry-entry-' + backupInstanceId,
sourcePath: backupInstanceId + '/DomainSysvol/GPO/Machine/Registry.pol',
sourceSha256: 'a'.repeat(64),
scope: 'computer',
deliveryMechanism: 'registry-policy',
parser: { id: 'registry-policy-preg', version: '1.0.0' },
reducer: { id: 'registry-policy-reducer', version: '1.0.0' },
identity: {
status: 'unresolved',
namespace: 'registry-policy',
section: 'HKEY_LOCAL_MACHINE',
key: registryKey + '\\Mode'
},
state: 'instruction',
values: [value],
registryPolicy: { sequence, operation: value.operation },
sourceRecords: [{ index: sequence, byteOffset: 8 + sequence * 16 }],
effectiveState: 'unknown',
diagnosticCodes: []
};
}
function backupInstance(
id: string,
gpoId: string,
observations: GpoPolicyObservation[],
expectedGpoCount: number,
completeness: 'complete' | 'selected' = 'complete'
): StoredGpoBackupInstance {
const instance: GpoBackupInstance = {
version: 1,
id,
projectId,
sourceArtifactId: artifactId,
identity: {
status: 'resolved',
gpoId,
backupId: gpoId.replace(/^(.{8})/, match => match === '11111111' ? 'aaaaaaaa' : 'bbbbbbbb'),
evidence: 'backup-metadata'
},
domain: { status: 'resolved', key: 'synthetic.example', evidence: 'backup-metadata' },
collection: { status: 'observed', collectedAt: timestamp, evidence: 'collection-manifest' },
backupSet: completeness === 'complete'
? { id: 'synthetic-set', scope: 'complete-domain-export', evidence: 'collection-manifest', expectedGpoCount }
: { id: 'synthetic-set', scope: 'selected-gpos', evidence: 'user-selection' },
backupCreatedAt: timestamp,
metadataStatus: 'complete',
sections: { computer: 'present', user: 'disabled' },
inventory: {
version: 1,
status: 'complete',
entries: [
{
version: 1,
artifactId: 'metadata-' + id,
relativePath: id + '/Backup.xml',
role: 'backup-metadata',
scope: 'shared',
disposition: 'supported',
secretIndicatorStatus: 'not-inspected'
},
{
version: 1,
artifactId: 'registry-entry-' + id,
relativePath: id + '/DomainSysvol/GPO/Machine/Registry.pol',
role: 'registry-policy',
scope: 'computer',
disposition: 'supported',
secretIndicatorStatus: 'not-detected'
}
],
diagnosticCodes: []
},
policyObservations: observations,
discoveredAt: timestamp
};
return {
version: 1,
id: 'stored-' + id,
projectId,
artifactId,
processingRunId,
instance,
createdAt: timestamp
};
}
function analysis(instances: StoredGpoBackupInstance[], unresolved = false): StoredGpoPolicyAnalysis {
const observations = instances.flatMap(instance => instance.instance.policyObservations ?? []);
const resolutions = observations.map(item => ({
version: 1 as const,
id: 'resolution-' + item.id,
projectId,
analysisRunId,
sourceProcessingRunId: processingRunId,
backupInstanceId: item.backupInstanceId,
observationId: item.id,
resolution: unresolved
? { version: 1 as const, status: 'unresolved' as const, reasonCode: 'identity-resolver-synthetic-gap' }
: resolveRegistryPolicyIdentity(item)
}));
return {
run: {
version: 1,
id: analysisRunId,
projectId,
artifactId,
sourceProcessingRunId: processingRunId,
analyzer: { id: 'gpo-policy-identity-analysis', version: '2.0.0' },
identityResolver: { id: 'gpo-technical-registry-target', version: '1.0.0' },
status: unresolved ? 'partial' : 'complete',
observationCount: resolutions.length,
resolvedCount: unresolved ? 0 : resolutions.length,
diagnosticCodes: unresolved ? ['identity-resolver-synthetic-gap'] : [],
startedAt: timestamp,
completedAt: timestamp
},
resolutions
};
}
function aggregate(instances: StoredGpoBackupInstance[], unresolved = false) {
return buildPolicySnapshotAggregation({
profile,
analyses: [analysis(instances, unresolved)],
instances,
startedAt: timestamp,
completedAt: timestamp
});
}
describe('policy snapshot aggregation', () => {
it('applies registry instruction order and preserves a final delete without claiming effectiveness', () => {
const id = 'gpo-one';
const values = [
observation(id, 'observation-set', 0, registryValue('set-value', 1)),
observation(id, 'observation-delete', 1, registryValue('delete-value'))
];
const record = aggregate([
backupInstance(id, '11111111-1111-4111-8111-111111111111', values, 1)
]);
expect(record.run).toMatchObject({
status: 'complete',
conflictCount: 0,
permitsFindingGeneration: false
});
expect(record.states).toEqual([expect.objectContaining({
status: 'deleted',
ordering: 'single-gpo',
effectiveState: 'unknown',
observationIds: ['observation-delete', 'observation-set']
})]);
});
it('keeps identical cross-GPO values and reports differing values as an explicit conflict', () => {
const first = backupInstance('gpo-one', '11111111-1111-4111-8111-111111111111', [
observation('gpo-one', 'observation-one', 0, registryValue('set-value', 1))
], 2);
const same = backupInstance('gpo-two', '22222222-2222-4222-8222-222222222222', [
observation('gpo-two', 'observation-two', 0, registryValue('set-value', 1))
], 2);
expect(aggregate([first, same]).states[0]).toMatchObject({
status: 'configured',
ordering: 'cross-gpo-identical'
});
const different = backupInstance('gpo-two', '22222222-2222-4222-8222-222222222222', [
observation('gpo-two', 'observation-two-different', 0, registryValue('set-value', 2))
], 2);
const conflict = aggregate([first, different]);
expect(conflict.run).toMatchObject({ status: 'complete', conflictCount: 1, permitsFindingGeneration: false });
expect(conflict.states[0]).toMatchObject({ status: 'conflicted', ordering: 'cross-gpo-conflict' });
expect(conflict.reviewHints).toEqual(expect.arrayContaining([
expect.objectContaining({ code: 'policy-aggregation-cross-gpo-conflict' })
]));
});
it('keeps prior-state-dependent instructions indeterminate', () => {
const record = aggregate([
backupInstance('gpo-one', '11111111-1111-4111-8111-111111111111', [
observation('gpo-one', 'observation-soft', 0, registryValue('set-value-if-absent', 1))
], 1)
]);
expect(record.run).toMatchObject({ status: 'partial', permitsFindingGeneration: false });
expect(record.states[0]).toMatchObject({
status: 'indeterminate',
completeness: 'partial',
diagnosticCodes: expect.arrayContaining(['policy-aggregation-prior-state-required'])
});
});
it('turns incomplete snapshots and unresolved identities into review hints without states or findings', () => {
const instance = backupInstance('gpo-one', '11111111-1111-4111-8111-111111111111', [
observation('gpo-one', 'observation-unresolved', 0, registryValue('set-value', 1))
], 1, 'selected');
const record = aggregate([instance], true);
expect(record.run).toMatchObject({
status: 'partial',
stateCount: 0,
unresolvedIdentityCount: 1,
permitsFindingGeneration: false
});
expect(record.reviewHints).toEqual(expect.arrayContaining([
expect.objectContaining({ code: 'policy-aggregation-snapshot-incomplete' }),
expect.objectContaining({ code: 'identity-resolver-synthetic-gap' })
]));
});
});