119 lines
6.5 KiB
TypeScript
119 lines
6.5 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
parseGroupPolicyPreferenceXml,
|
|
reduceGroupPolicyPreferences
|
|
} from '../../server/gpo/group-policy-preference-parser';
|
|
import type { VerifiedZipFile } from '../../server/gpo/zip-inventory';
|
|
import { ImportError } from '../../src/shared/errors';
|
|
|
|
function verifiedFile(content: string | Uint8Array, name = 'Registry.xml'): VerifiedZipFile {
|
|
const bytes = typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
return {
|
|
entry: {
|
|
index: 1,
|
|
relativePath: `synthetic/DomainSysvol/GPO/Machine/Preferences/Registry/${name}`,
|
|
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' };
|
|
}
|
|
|
|
const rootClsid = '{A3CCFC41-DFDB-43a5-8D26-0FE8B954DA51}';
|
|
const itemClsid = '{9CD4B2F4-923D-47f5-A062-E897DD1DAD50}';
|
|
|
|
describe('Group Policy Preferences parser and Registry reducer', () => {
|
|
it('preserves Registry action, order, common options and opaque item-level targeting', () => {
|
|
const document = parseGroupPolicyPreferenceXml(verifiedFile([
|
|
`<?xml version="1.0"?><RegistrySettings clsid="${rootClsid}" disabled="0">`,
|
|
` <Registry clsid="${itemClsid}" uid="{11111111-1111-4111-8111-111111111111}" removePolicy="1" applyOnce="0">`,
|
|
' <Properties action="R" default="0" hive="HKEY_LOCAL_MACHINE" key="Software\\Synthetic" name="Mode" type="REG_SZ" value="Strict"/>',
|
|
' <Filters><FilterGroup bool="AND" not="0" name="Synthetic\\Operators" sid="S-1-5-21-1-2-3-1001"/></Filters>',
|
|
' </Registry>',
|
|
` <Registry clsid="${itemClsid}" uid="{22222222-2222-4222-8222-222222222222}">`,
|
|
' <Properties action="D" default="0" hive="HKEY_LOCAL_MACHINE" key="Software\\Synthetic" name="Legacy" type="REG_DWORD"/>',
|
|
' </Registry>',
|
|
'</RegistrySettings>'
|
|
].join('\r\n')), 'computer');
|
|
|
|
expect(document).toMatchObject({ status: 'complete', preferenceType: 'registry', secretIndicatorStatus: 'not-detected' });
|
|
const observations = reduceGroupPolicyPreferences(document, context());
|
|
expect(observations).toHaveLength(2);
|
|
expect(observations[0]).toMatchObject({
|
|
deliveryMechanism: 'group-policy-preference',
|
|
state: 'instruction',
|
|
identity: { namespace: 'group-policy-preference', section: 'registry' },
|
|
preference: {
|
|
cse: 'registry', operation: 'replace', sequence: 0, disabled: false, removePolicy: true, applyOnce: false,
|
|
targeting: { status: 'present', filterTypes: ['filtergroup'], evaluability: 'unknown' }
|
|
},
|
|
values: [{
|
|
kind: 'registry-preference', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\\Synthetic', target: 'value',
|
|
valueName: 'Mode', valueType: 'REG_SZ', rawData: 'Strict'
|
|
}],
|
|
sourceLines: [2],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: ['gpo-preference-targeting-unevaluated']
|
|
});
|
|
expect(observations[1]).toMatchObject({ preference: { operation: 'delete', sequence: 1 }, sourceLines: [6] });
|
|
});
|
|
|
|
it('recognizes but never returns cpassword values', () => {
|
|
const secret = 'synthetic-secret-must-not-leave-parser';
|
|
const document = parseGroupPolicyPreferenceXml(verifiedFile(
|
|
`<RegistrySettings clsid="${rootClsid}"><Registry clsid="${itemClsid}" uid="{33333333-3333-4333-8333-333333333333}" cpassword="${secret}"><Properties hive="HKEY_CURRENT_USER" key="Software\\Synthetic" name="Mode" type="REG_SZ" value="Safe"/></Registry></RegistrySettings>`
|
|
), 'user');
|
|
expect(document).toMatchObject({ status: 'complete', secretIndicatorStatus: 'present' });
|
|
expect(document.diagnosticCodes).toContain('gpo-secret-indicator-present');
|
|
expect(JSON.stringify(document)).not.toContain(secret);
|
|
expect(JSON.stringify(reduceGroupPolicyPreferences(document, context()))).not.toContain(secret);
|
|
});
|
|
|
|
it('keeps unsupported Preference CSEs partial without producing policy observations', () => {
|
|
const document = parseGroupPolicyPreferenceXml(verifiedFile(
|
|
'<Printers clsid="{1F577D12-3D1B-471e-A1B7-060317597B9C}"><SharedPrinter cpassword="masked"/></Printers>',
|
|
'Groups.xml'
|
|
), 'computer');
|
|
expect(document).toMatchObject({
|
|
preferenceType: 'unsupported', status: 'partial', secretIndicatorStatus: 'present', instructions: [],
|
|
diagnosticCodes: ['gpo-preference-type-unsupported', 'gpo-secret-indicator-present']
|
|
});
|
|
expect(reduceGroupPolicyPreferences(document, context())).toEqual([]);
|
|
});
|
|
|
|
it('fails closed for malformed supported actions, targets, common flags and targeting', () => {
|
|
const document = parseGroupPolicyPreferenceXml(verifiedFile(
|
|
`<RegistrySettings clsid="${rootClsid}"><Registry clsid="${itemClsid}" uid="{44444444-4444-4444-8444-444444444444}" removePolicy="1"><Properties action="D" hive="HKEY_LOCAL_MACHINE" key="Software\\Synthetic"/><Filters><FilterGroup bool="XOR" not="0"/></Filters></Registry></RegistrySettings>`
|
|
), 'computer');
|
|
expect(document.status).toBe('invalid');
|
|
expect(document.instructions).toEqual([]);
|
|
expect(document.diagnosticCodes).toEqual(expect.arrayContaining([
|
|
'gpo-preference-remove-policy-invalid', 'gpo-preference-targeting-invalid'
|
|
]));
|
|
});
|
|
|
|
it('keeps unsupported Registry subproperties partial instead of inventing bitfield semantics', () => {
|
|
const document = parseGroupPolicyPreferenceXml(verifiedFile(
|
|
'<RegistrySettings clsid="' + rootClsid + '"><Registry clsid="' + itemClsid + '" uid="{55555555-5555-4555-8555-555555555555}"><Properties action="U" hive="HKEY_LOCAL_MACHINE" key="Software\Synthetic" name="Flags" type="REG_DWORD" value="1"><SubProp id="synthetic" value="1" mask="1"/></Properties></Registry></RegistrySettings>'
|
|
), 'computer');
|
|
expect(document).toMatchObject({
|
|
status: 'partial', instructions: [], diagnosticCodes: ['gpo-preference-registry-item-unsupported']
|
|
});
|
|
expect(reduceGroupPolicyPreferences(document, context())).toEqual([]);
|
|
});
|
|
it('rejects active XML constructs before parsing', () => {
|
|
expect(() => parseGroupPolicyPreferenceXml(verifiedFile(
|
|
`<!DOCTYPE RegistrySettings [<!ENTITY x SYSTEM "file:///synthetic">]><RegistrySettings clsid="${rootClsid}">&x;</RegistrySettings>`
|
|
), 'computer')).toThrow(ImportError);
|
|
});
|
|
});
|