Assessor_2.0/tests/server/registry-policy-parser.test.ts

207 lines
8.9 KiB
TypeScript

import { createHash } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import {
parseRegistryPolicyFile,
reduceRegistryPolicy
} from '../../server/gpo/registry-policy-parser';
import type { VerifiedZipFile } from '../../server/gpo/zip-inventory';
type RecordInput = { key: string; valueName: string; typeCode: number; data: Uint8Array };
function utf16(value: string): Buffer {
return Buffer.from(value, 'utf16le');
}
function stringData(value: string): Buffer {
return Buffer.concat([utf16(value), Buffer.alloc(2)]);
}
function multiStringData(values: string[]): Buffer {
return Buffer.concat([utf16(values.join('\0')), Buffer.alloc(4)]);
}
function dword(value: number, bigEndian = false): Buffer {
const bytes = Buffer.alloc(4);
if (bigEndian) bytes.writeUInt32BE(value);
else bytes.writeUInt32LE(value);
return bytes;
}
function qword(value: bigint): Buffer {
const bytes = Buffer.alloc(8);
bytes.writeBigUInt64LE(value);
return bytes;
}
function policyFile(records: RecordInput[], version = 1): Uint8Array {
const header = Buffer.alloc(8);
header.write('PReg', 0, 'ascii');
header.writeUInt32LE(version, 4);
const encoded = records.map(record => {
const type = Buffer.alloc(4);
type.writeUInt32LE(record.typeCode);
const size = Buffer.alloc(4);
size.writeUInt32LE(record.data.byteLength);
return Buffer.concat([
utf16('['),
utf16(record.key), Buffer.alloc(2), utf16(';'),
utf16(record.valueName), Buffer.alloc(2), utf16(';'),
type, utf16(';'),
size, utf16(';'),
record.data,
utf16(']')
]);
});
return Buffer.concat([header, ...encoded]);
}
function verifiedFile(bytes: Uint8Array): VerifiedZipFile {
return {
entry: {
index: 1,
relativePath: 'synthetic/DomainSysvol/GPO/Machine/Registry.pol',
kind: 'file',
compressionMethod: 8,
compressedBytes: bytes.byteLength,
uncompressedBytes: bytes.byteLength,
crc32: '00000000',
sha256: createHash('sha256').update(bytes).digest('hex')
},
bytes
};
}
function context() {
return { backupInstanceId: 'backup-synthetic', sourceArtifactId: 'source-synthetic', artifactId: 'entry-synthetic' };
}
describe('Registry.pol parser and instruction reducer', () => {
it('accepts REG_NONE only as an empty key-creation instruction', () => {
const document = parseRegistryPolicyFile(verifiedFile(policyFile([{
key: 'Software\\Policies\\Synthetic\\Child',
valueName: '',
typeCode: 0,
data: Buffer.alloc(0)
}])), 'computer');
expect(document).toMatchObject({ status: 'complete' });
expect(reduceRegistryPolicy(document, context())).toMatchObject([{
registryPolicy: { sequence: 0, operation: 'create-key' },
values: [{
kind: 'registry-policy',
operation: 'create-key',
key: 'Software\\Policies\\Synthetic\\Child',
typeCode: 0,
data: { kind: 'none' }
}]
}]);
expect(parseRegistryPolicyFile(verifiedFile(policyFile([{
key: 'Software\\Policies\\Synthetic\\Child',
valueName: 'unexpected',
typeCode: 0,
data: Buffer.alloc(0)
}])), 'computer')).toMatchObject({
status: 'invalid',
instructions: [],
diagnosticCodes: ['gpo-registry-policy-none-invalid']
});
});
it('decodes the documented Registry.pol types without losing record order', () => {
const document = parseRegistryPolicyFile(verifiedFile(policyFile([
{ key: 'Software\\Policies\\Synthetic', valueName: 'Text', typeCode: 1, data: stringData('strict') },
{ key: 'Software\\Policies\\Synthetic', valueName: 'Expanded', typeCode: 2, data: stringData('%SYSTEMROOT%') },
{ key: 'Software\\Policies\\Synthetic', valueName: 'Bytes', typeCode: 3, data: Buffer.from([0, 127, 255]) },
{ key: 'Software\\Policies\\Synthetic', valueName: 'Little', typeCode: 4, data: dword(42) },
{ key: 'Software\\Policies\\Synthetic', valueName: 'Big', typeCode: 5, data: dword(43, true) },
{ key: 'Software\\Policies\\Synthetic', valueName: 'List', typeCode: 7, data: multiStringData(['one', 'two']) },
{ key: 'Software\\Policies\\Synthetic', valueName: 'Wide', typeCode: 11, data: qword(9_007_199_254_740_993n) }
])), 'computer');
expect(document).toMatchObject({
status: 'complete',
instructions: [
{ sequence: 0, data: { kind: 'string', value: 'strict' } },
{ sequence: 1, data: { kind: 'string', value: '%SYSTEMROOT%' } },
{ sequence: 2, data: { kind: 'binary', hexadecimal: '007fff' } },
{ sequence: 3, data: { kind: 'uint32', value: 42 } },
{ sequence: 4, data: { kind: 'uint32', value: 43 } },
{ sequence: 5, data: { kind: 'multi-string', values: ['one', 'two'] } },
{ sequence: 6, data: { kind: 'uint64', value: '9007199254740993' } }
]
});
});
it('preserves documented delete, secure and soft operations as ordered instructions', () => {
const document = parseRegistryPolicyFile(verifiedFile(policyFile([
{ key: 'Software\\Policies\\Synthetic', valueName: '**DeleteValues', typeCode: 1, data: stringData('One;Two') },
{ key: 'Software\\Policies\\Synthetic', valueName: '**Del.Three', typeCode: 1, data: stringData(' ') },
{ key: 'Software\\Policies\\Synthetic', valueName: '**DelVals.', typeCode: 1, data: stringData(' ') },
{ key: 'Software\\Policies\\Synthetic', valueName: '**DeleteKeys', typeCode: 1, data: stringData('ChildA;ChildB') },
{ key: 'Software\\Policies\\Synthetic', valueName: '**SecureKey', typeCode: 4, data: dword(1) },
{ key: 'Software\\Policies\\Synthetic', valueName: '**soft.Mode', typeCode: 4, data: dword(1) }
])), 'computer');
const observations = reduceRegistryPolicy(document, context());
expect(observations.map(observation => observation.registryPolicy)).toEqual([
{ sequence: 0, operation: 'delete-values' },
{ sequence: 1, operation: 'delete-value' },
{ sequence: 2, operation: 'delete-all-values' },
{ sequence: 3, operation: 'delete-keys' },
{ sequence: 4, operation: 'secure-key' },
{ sequence: 5, operation: 'set-value-if-absent' }
]);
expect(observations[0]).toMatchObject({
state: 'instruction',
values: [{ kind: 'registry-policy', operation: 'delete-values', targets: ['One', 'Two'] }],
sourceRecords: [{ index: 0, byteOffset: 8 }],
effectiveState: 'unknown'
});
expect(observations[4]).toMatchObject({ values: [{ operation: 'secure-key', secure: true }] });
});
it('keeps unknown reserved operations partial and does not silently reinterpret them', () => {
const document = parseRegistryPolicyFile(verifiedFile(policyFile([
{ key: 'Software\\Policies\\Synthetic', valueName: '**FutureOperation', typeCode: 1, data: stringData('opaque') }
])), 'computer');
expect(document).toMatchObject({
status: 'partial',
diagnosticCodes: ['gpo-registry-policy-special-value-unsupported']
});
expect(reduceRegistryPolicy(document, context())).toMatchObject([{
values: [{ operation: 'unsupported-special' }],
diagnosticCodes: ['gpo-registry-policy-special-value-unsupported']
}]);
});
it('fails closed on an invalid header, version, truncation or special-operation shape', () => {
const validRecord = { key: 'Software\\Policies\\Synthetic', valueName: 'Mode', typeCode: 4, data: dword(1) };
const invalidSignature = policyFile([validRecord]);
invalidSignature[0] = 0;
expect(parseRegistryPolicyFile(verifiedFile(invalidSignature), 'computer')).toMatchObject({
status: 'invalid', instructions: [], diagnosticCodes: ['gpo-registry-policy-signature-invalid']
});
expect(parseRegistryPolicyFile(verifiedFile(policyFile([validRecord], 2)), 'computer')).toMatchObject({
status: 'invalid', instructions: [], diagnosticCodes: ['gpo-registry-policy-version-unsupported']
});
const truncated = policyFile([validRecord]).slice(0, -1);
expect(parseRegistryPolicyFile(verifiedFile(truncated), 'computer')).toMatchObject({
status: 'invalid', instructions: [], diagnosticCodes: ['gpo-registry-policy-record-close-invalid']
});
const invalidSpecial = parseRegistryPolicyFile(verifiedFile(policyFile([
{ key: 'Software\\Policies\\Synthetic', valueName: '**SecureKey', typeCode: 1, data: stringData('1') }
])), 'computer');
expect(invalidSpecial).toMatchObject({
status: 'invalid', instructions: [], diagnosticCodes: ['gpo-registry-policy-special-value-invalid']
});
});
it('does not reduce a Registry.pol file whose scope is not technically known', () => {
const document = parseRegistryPolicyFile(verifiedFile(policyFile([
{ key: 'Software\\Policies\\Synthetic', valueName: 'Mode', typeCode: 4, data: dword(1) }
])), 'unknown');
expect(document).toMatchObject({ status: 'partial', diagnosticCodes: ['gpo-registry-policy-scope-unsupported'] });
expect(reduceRegistryPolicy(document, context())).toEqual([]);
});
});