import { createHash } from 'node:crypto'; import type { GpoPolicyObservation, GpoRegistryPolicyData, GpoRegistryPolicyValue } from '../../src/domain/gpo-policy'; import { gpoPolicyObservationValidationErrors } from '../../src/domain/gpo-policy'; import type { GpoPolicyScope } from '../../src/domain/gpo-ingestion'; import type { ImportResourcePolicy } from '../../src/importers/resource-policy'; import { ImportError } from '../../src/shared/errors'; import type { VerifiedZipFile } from './zip-inventory'; export const REGISTRY_POLICY_PARSER = { id: 'gpo-registry-policy-binary', version: '1.0.0' } as const; export const REGISTRY_POLICY_REDUCER = { id: 'gpo-registry-policy-instruction-reducer', version: '1.0.0' } as const; const maximumFileBytes = 16 * 1024 * 1024; const maximumRecords = 50_000; const maximumDataBytes = 65_535; const supportedTypes = new Set([0, 1, 2, 3, 4, 5, 7, 11]); type RegistryTypeCode = 0 | 1 | 2 | 3 | 4 | 5 | 7 | 11; export interface RegistryPolicyInstruction { version: 1; sequence: number; byteOffset: number; key: string; valueName: string; typeCode: RegistryTypeCode; data: GpoRegistryPolicyData; } export interface RegistryPolicyDocument { version: 1; parser: typeof REGISTRY_POLICY_PARSER; sourcePath: string; sourceSha256: string; scope: GpoPolicyScope; status: 'complete' | 'partial' | 'invalid'; instructions: RegistryPolicyInstruction[]; diagnosticCodes: string[]; } class RegistryPolicyParseFailure extends Error { constructor(readonly code: string, message: string) { super(message); } } function parserError(code: string, message: string, details: Record = {}): never { throw new ImportError(message, [{ severity: 'error', code, message, stage: 'format-decoding', details }]); } function fail(code: string, message: string): never { throw new RegistryPolicyParseFailure(code, message); } function uint32(bytes: Uint8Array, offset: number, bigEndian = false): number { if (offset < 0 || offset + 4 > bytes.byteLength) fail('gpo-registry-policy-truncated', 'Die Registry-Policy-Datei ist abgeschnitten.'); const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 4); return view.getUint32(0, !bigEndian); } function utf16Character(bytes: Uint8Array, offset: number, expected: number, diagnosticCode: string): number { if (offset + 2 > bytes.byteLength || bytes[offset] !== expected || bytes[offset + 1] !== 0) { fail(diagnosticCode, 'Die Registry-Policy-Datensatzstruktur ist ungültig.'); } return offset + 2; } function decodeUtf16(bytes: Uint8Array, code: string): string { if (bytes.byteLength % 2 !== 0) fail(code, 'Ein UTF-16LE-Feld besitzt eine ungerade Bytezahl.'); try { return new TextDecoder('utf-16le', { fatal: true }).decode(bytes); } catch { fail(code, 'Ein Registry-Policy-Feld ist nicht als UTF-16LE dekodierbar.'); } } function readIdentifier(bytes: Uint8Array, offset: number, code: string): { value: string; nextOffset: number } { for (let cursor = offset; cursor + 3 < bytes.byteLength; cursor += 2) { if (bytes[cursor] === 0 && bytes[cursor + 1] === 0 && bytes[cursor + 2] === 0x3b && bytes[cursor + 3] === 0) { return { value: decodeUtf16(bytes.subarray(offset, cursor), code), nextOffset: cursor + 4 }; } } fail(code, 'Ein Registry-Policy-Feld besitzt keinen gültigen Abschluss.'); } function validKey(key: string): boolean { if (!key || key.length > 32_767 || key.startsWith('\\') || key.endsWith('\\') || /^(?:HKEY_|HKLM\\|HKCU\\)/i.test(key)) return false; return key.split('\\').every(segment => segment.length > 0 && [...segment].every(character => { const code = character.charCodeAt(0); return (code >= 0x20 && code <= 0x5b) || (code >= 0x5d && code <= 0x7e); })); } function validValueName(valueName: string): boolean { return valueName.length <= 259 && [...valueName].every(character => { const code = character.charCodeAt(0); return code >= 0x20 && code <= 0x7e; }); } function decodeString(bytes: Uint8Array, diagnosticCode: string): string { if (bytes.byteLength < 2 || bytes.byteLength % 2 !== 0 || bytes[bytes.byteLength - 2] !== 0 || bytes[bytes.byteLength - 1] !== 0) { fail(diagnosticCode, 'Ein Registry-Policy-String ist nicht nullterminiert.'); } const value = decodeUtf16(bytes.subarray(0, bytes.byteLength - 2), diagnosticCode); if (value.includes('\u0000')) fail(diagnosticCode, 'Ein Registry-Policy-String enthält ein unerwartetes Nullzeichen.'); return value; } function decodeData(typeCode: RegistryTypeCode, bytes: Uint8Array): GpoRegistryPolicyData { if (typeCode === 0) { if (bytes.byteLength !== 0) fail('gpo-registry-policy-none-size-invalid', 'Eine reine Registry-Schlüsselanlage darf keine Daten enthalten.'); return { kind: 'none' }; } if (typeCode === 1 || typeCode === 2) { return { kind: 'string', value: decodeString(bytes, 'gpo-registry-policy-string-invalid') }; } if (typeCode === 3) return { kind: 'binary', hexadecimal: Buffer.from(bytes).toString('hex') }; if (typeCode === 4 || typeCode === 5) { if (bytes.byteLength !== 4) fail('gpo-registry-policy-integer-size-invalid', 'Ein Registry-Policy-DWORD benötigt exakt vier Bytes.'); return { kind: 'uint32', value: uint32(bytes, 0, typeCode === 5) }; } if (typeCode === 7) { if (bytes.byteLength < 4 || bytes.byteLength % 2 !== 0 || bytes.slice(-4).some(byte => byte !== 0)) { fail('gpo-registry-policy-multi-string-invalid', 'Ein Registry-Policy-MULTI_SZ ist nicht doppelt nullterminiert.'); } const decoded = decodeUtf16(bytes, 'gpo-registry-policy-multi-string-invalid'); const values = decoded.slice(0, -2).split('\u0000'); if (values.length === 1 && values[0] === '') return { kind: 'multi-string', values: [] }; if (values.some(value => !value || value.includes('\u0000'))) { fail('gpo-registry-policy-multi-string-invalid', 'Ein Registry-Policy-MULTI_SZ enthält einen leeren Zwischeneintrag.'); } return { kind: 'multi-string', values }; } if (bytes.byteLength !== 8) fail('gpo-registry-policy-integer-size-invalid', 'Ein Registry-Policy-QWORD benötigt exakt acht Bytes.'); const view = new DataView(bytes.buffer, bytes.byteOffset, 8); return { kind: 'uint64', value: view.getBigUint64(0, true).toString() }; } function targetList(data: GpoRegistryPolicyData, allowSubkeys: boolean): string[] | undefined { if (data.kind !== 'string') return undefined; const targets = data.value.split(';'); if (!targets.length || targets.some(target => !target || !validValueName(target) || (allowSubkeys ? target.includes('\\') : false))) return undefined; return targets; } function instructionValue(instruction: RegistryPolicyInstruction): { value: GpoRegistryPolicyValue; diagnostics: string[] } { const lowerName = instruction.valueName.toLocaleLowerCase('en-US'); const common = { kind: 'registry-policy' as const, key: instruction.key }; if (instruction.typeCode === 0) { if (instruction.valueName !== '' || instruction.data.kind !== 'none') { fail('gpo-registry-policy-none-invalid', 'REG_NONE ist nur als leere Schlüsselanlage zulässig.'); } return { value: { ...common, operation: 'create-key', typeCode: 0, data: instruction.data }, diagnostics: [] }; } if (lowerName === '**deletevalues') { const targets = instruction.typeCode === 1 ? targetList(instruction.data, false) : undefined; if (!targets) fail('gpo-registry-policy-special-value-invalid', '**DeleteValues besitzt ungültige Typ- oder Zieldaten.'); return { value: { ...common, operation: 'delete-values', targets }, diagnostics: [] }; } if (lowerName.startsWith('**del.') && instruction.valueName.length > 6) { if (instruction.typeCode !== 1 || instruction.data.kind !== 'string' || instruction.data.value !== ' ') { fail('gpo-registry-policy-special-value-invalid', '**Del. besitzt ungültige Typ- oder Platzhalterdaten.'); } return { value: { ...common, operation: 'delete-value', valueName: instruction.valueName.slice(6) }, diagnostics: [] }; } if (lowerName === '**delvals.') { if (instruction.typeCode !== 1 || instruction.data.kind !== 'string' || instruction.data.value !== ' ') { fail('gpo-registry-policy-special-value-invalid', '**DelVals. besitzt ungültige Typ- oder Platzhalterdaten.'); } return { value: { ...common, operation: 'delete-all-values' }, diagnostics: [] }; } if (lowerName === '**deletekeys') { const targets = instruction.typeCode === 1 ? targetList(instruction.data, true) : undefined; if (!targets) fail('gpo-registry-policy-special-value-invalid', '**DeleteKeys besitzt ungültige Typ- oder Zieldaten.'); return { value: { ...common, operation: 'delete-keys', targets }, diagnostics: [] }; } if (lowerName === '**securekey') { if (instruction.typeCode !== 4 || instruction.data.kind !== 'uint32') { fail('gpo-registry-policy-special-value-invalid', '**SecureKey besitzt ungültige Typdaten.'); } return { value: { ...common, operation: 'secure-key', secure: instruction.data.value === 1 }, diagnostics: [] }; } if (lowerName.startsWith('**soft.') && instruction.valueName.length > 7) { return { value: { ...common, operation: 'set-value-if-absent', valueName: instruction.valueName.slice(7), typeCode: instruction.typeCode, data: instruction.data }, diagnostics: [] }; } if (lowerName.startsWith('**')) { return { value: { ...common, operation: 'unsupported-special', valueName: instruction.valueName, typeCode: instruction.typeCode, data: instruction.data }, diagnostics: ['gpo-registry-policy-special-value-unsupported'] }; } return { value: { ...common, operation: 'set-value', valueName: instruction.valueName, typeCode: instruction.typeCode, data: instruction.data }, diagnostics: [] }; } export function parseRegistryPolicyFile( file: VerifiedZipFile, scope: GpoPolicyScope, _resourceOverrides?: Partial ): RegistryPolicyDocument { if (file.bytes.byteLength > maximumFileBytes) { parserError('gpo-registry-policy-size-limit', 'Eine Registry-Policy-Datei überschreitet das Größenlimit.', { sizeBytes: file.bytes.byteLength }); } const diagnostics = new Set(); const instructions: RegistryPolicyInstruction[] = []; let partial = !['computer', 'user'].includes(scope); if (partial) diagnostics.add('gpo-registry-policy-scope-unsupported'); try { const bytes = file.bytes; if (bytes.byteLength < 8 || bytes[0] !== 0x50 || bytes[1] !== 0x52 || bytes[2] !== 0x65 || bytes[3] !== 0x67) { fail('gpo-registry-policy-signature-invalid', 'Eine Registry-Policy-Datei besitzt keine gültige PReg-Signatur.'); } if (uint32(bytes, 4) !== 1) fail('gpo-registry-policy-version-unsupported', 'Die Registry-Policy-Version wird nicht unterstützt.'); let offset = 8; while (offset < bytes.byteLength) { if (instructions.length >= maximumRecords) { parserError('gpo-registry-policy-record-limit', 'Eine Registry-Policy-Datei überschreitet das Datensatzlimit.', { records: instructions.length }); } const byteOffset = offset; offset = utf16Character(bytes, offset, 0x5b, 'gpo-registry-policy-record-open-invalid'); const keyField = readIdentifier(bytes, offset, 'gpo-registry-policy-key-invalid'); offset = keyField.nextOffset; const valueField = readIdentifier(bytes, offset, 'gpo-registry-policy-value-name-invalid'); offset = valueField.nextOffset; if (!validKey(keyField.value)) fail('gpo-registry-policy-key-invalid', 'Ein Registry-Policy-Schlüssel ist ungültig.'); if (!validValueName(valueField.value)) fail('gpo-registry-policy-value-name-invalid', 'Ein Registry-Policy-Wertname ist ungültig.'); const typeCode = uint32(bytes, offset); if (!supportedTypes.has(typeCode)) fail('gpo-registry-policy-type-unsupported', 'Ein Registry-Policy-Datentyp wird nicht unterstützt.'); offset += 4; offset = utf16Character(bytes, offset, 0x3b, 'gpo-registry-policy-type-delimiter-invalid'); const size = uint32(bytes, offset); if (size > maximumDataBytes) fail('gpo-registry-policy-data-size-invalid', 'Ein Registry-Policy-Datensatz überschreitet das Datenlimit.'); offset += 4; offset = utf16Character(bytes, offset, 0x3b, 'gpo-registry-policy-size-delimiter-invalid'); if (offset + size > bytes.byteLength) fail('gpo-registry-policy-truncated', 'Die Registry-Policy-Daten sind abgeschnitten.'); const data = decodeData(typeCode as RegistryTypeCode, bytes.subarray(offset, offset + size)); offset += size; offset = utf16Character(bytes, offset, 0x5d, 'gpo-registry-policy-record-close-invalid'); instructions.push({ version: 1, sequence: instructions.length, byteOffset, key: keyField.value, valueName: valueField.value, typeCode: typeCode as RegistryTypeCode, data }); } for (const instruction of instructions) { const interpreted = instructionValue(instruction); interpreted.diagnostics.forEach(code => { diagnostics.add(code); partial = true; }); } } catch (error) { if (error instanceof ImportError) throw error; if (!(error instanceof RegistryPolicyParseFailure)) throw error; diagnostics.add(error.code); return { version: 1, parser: REGISTRY_POLICY_PARSER, sourcePath: file.entry.relativePath, sourceSha256: file.entry.sha256, scope, status: 'invalid', instructions: [], diagnosticCodes: [...diagnostics] }; } return { version: 1, parser: REGISTRY_POLICY_PARSER, sourcePath: file.entry.relativePath, sourceSha256: file.entry.sha256, scope, status: partial ? 'partial' : 'complete', instructions, diagnosticCodes: [...diagnostics].sort() }; } export function reduceRegistryPolicy( document: RegistryPolicyDocument, context: { backupInstanceId: string; sourceArtifactId: string; artifactId: string } ): GpoPolicyObservation[] { if (document.status === 'invalid' || !['computer', 'user'].includes(document.scope)) return []; return document.instructions.map(instruction => { const interpreted = instructionValue(instruction); const target = interpreted.value.valueName ?? interpreted.value.targets?.join(';') ?? interpreted.value.operation; const id = 'gpo-policy-' + createHash('sha256').update([ context.backupInstanceId, context.artifactId, document.sourceSha256, instruction.sequence.toString(), REGISTRY_POLICY_REDUCER.version ].join('\0')).digest('hex').slice(0, 32); const observation: GpoPolicyObservation = { version: 1, id, backupInstanceId: context.backupInstanceId, sourceArtifactId: context.sourceArtifactId, artifactId: context.artifactId, sourcePath: document.sourcePath, sourceSha256: document.sourceSha256, scope: document.scope as 'computer' | 'user', deliveryMechanism: 'registry-policy', parser: document.parser, reducer: REGISTRY_POLICY_REDUCER, identity: { status: 'unresolved', namespace: 'registry-policy', section: document.scope === 'computer' ? 'HKEY_LOCAL_MACHINE' : 'HKEY_CURRENT_USER', key: instruction.key + '\\' + target }, state: 'instruction', values: [interpreted.value], registryPolicy: { sequence: instruction.sequence, operation: interpreted.value.operation }, sourceRecords: [{ index: instruction.sequence, byteOffset: instruction.byteOffset }], effectiveState: 'unknown', diagnosticCodes: interpreted.diagnostics }; const errors = gpoPolicyObservationValidationErrors(observation); if (errors.length) throw new Error('Intern ungültige Registry-Policy-Beobachtung: ' + errors.join(' ')); return observation; }); }