266 lines
9.4 KiB
TypeScript
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' })
|
|
]));
|
|
});
|
|
});
|
|
|