import { createHash } from 'node:crypto'; import type { GpoLocalGroupPreferenceValue, GpoLocalUserPreferenceValue, GpoPolicyObservation, GpoPreferenceContext } from '../../src/domain/gpo-policy'; import { gpoPolicyObservationValidationErrors } from '../../src/domain/gpo-policy'; import type { GpoPolicyScope } from '../../src/domain/gpo-ingestion'; import { xmlElementStartLines } from './xml-source-lines'; export const GPP_LOCAL_USERS_GROUPS_REDUCER = { id: 'gpo-preferences-local-users-groups-reducer', version: '1.0.0' } as const; const groupsClsid = '3125e937-eb16-4b4c-9934-544fc6d24d26'; const userClsid = 'df5f1855-51e5-4d24-8b1a-d9bde98ba1d1'; const groupClsid = '6d4a79e4-529c-4481-abd0-f5bd7ea93ba7'; const guidPattern = /^\{?([a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})\}?$/i; const sidPattern = /^S-\d(?:-\d+)+$/i; type Operation = GpoPreferenceContext['operation']; type Targeting = GpoPreferenceContext['targeting']; interface CommonInstruction { version: 1; cse: 'local-users-groups'; uid: string; sequence: number; line: number; operation: Operation; disabled: boolean; removePolicy: boolean; applyOnce: boolean; targeting: Targeting; } export interface GppLocalUserInstruction extends CommonInstruction { itemType: 'user'; value: GpoLocalUserPreferenceValue; } export interface GppLocalGroupInstruction extends CommonInstruction { itemType: 'group'; value: GpoLocalGroupPreferenceValue; } export type GppLocalUsersGroupsInstruction = GppLocalUserInstruction | GppLocalGroupInstruction; export interface LocalUsersGroupsPreferenceDocument { version: 1; parser: { id: string; version: string }; sourcePath: string; sourceSha256: string; scope: GpoPolicyScope; preferenceType: 'local-users-groups'; status: 'complete' | 'partial' | 'invalid'; secretIndicatorStatus: 'not-detected' | 'present'; instructions: GppLocalUsersGroupsInstruction[]; diagnosticCodes: string[]; } export interface LocalUsersGroupsParseContext { root: Element; text: string; scope: GpoPolicyScope; parser: { id: string; version: string }; sourcePath: string; sourceSha256: string; secretIndicatorStatus: 'not-detected' | 'present'; diagnostics: Set; summarizeTargeting: (item: Element, diagnostics: Set) => Targeting | undefined; } function attribute(element: Element, name: string): string | undefined { const normalized = name.toLocaleLowerCase('en-US'); return [...element.attributes].find(candidate => candidate.localName.toLocaleLowerCase('en-US') === normalized)?.value; } function canonicalGuid(value: string | undefined): string | undefined { return value ? guidPattern.exec(value.trim())?.[1].toLocaleLowerCase('en-US') : undefined; } function booleanAttribute(element: Element, name: string, diagnostics: Set, optional = false): boolean | undefined { const value = attribute(element, name); if (value === undefined) return optional ? undefined : false; if (value === '0') return false; if (value === '1') return true; diagnostics.add('gpo-preference-boolean-invalid'); return optional ? undefined : false; } function operation(value: string | undefined): Operation | undefined { const code = (value ?? 'U').toLocaleUpperCase('en-US'); return ({ C: 'create', D: 'delete', R: 'replace', U: 'update' } as const)[code as 'C' | 'D' | 'R' | 'U']; } function itemLines(text: string): number[] { return xmlElementStartLines(text, /<(?:[A-Za-z_][\w.-]*:)?(?:User|Group)(?=[\s/>])/g); } function directChildren(element: Element, name: string): Element[] { const normalized = name.toLocaleLowerCase('en-US'); return [...element.children].filter(child => child.localName.toLocaleLowerCase('en-US') === normalized); } function textAttribute(element: Element, name: string): string | undefined { const value = attribute(element, name); return value?.trim() ? value : undefined; } function parseUserValue(properties: Element, itemOperation: Operation, diagnostics: Set): GpoLocalUserPreferenceValue | undefined { if (properties.children.length) { diagnostics.add('gpo-preference-local-user-item-unsupported'); return undefined; } const userName = textAttribute(properties, 'userName'); if (!userName) { diagnostics.add('gpo-preference-local-user-target-invalid'); return undefined; } const expires = textAttribute(properties, 'expires'); if (expires && !/^\d{4}-\d{2}-\d{2}$/.test(expires)) { diagnostics.add('gpo-preference-local-user-expiry-invalid'); return undefined; } const noChange = booleanAttribute(properties, 'noChange', diagnostics, true); return { kind: 'local-user-preference', userName, newName: itemOperation === 'update' ? textAttribute(properties, 'newName') : undefined, fullName: textAttribute(properties, 'fullName'), description: textAttribute(properties, 'description'), passwordAttributePresent: [...properties.attributes].some(candidate => candidate.localName.toLocaleLowerCase('en-US') === 'cpassword'), changePasswordAtLogon: booleanAttribute(properties, 'changeLogon', diagnostics, true), accountDisabled: booleanAttribute(properties, 'acctDisabled', diagnostics, true), passwordNeverExpires: booleanAttribute(properties, 'neverExpires', diagnostics, true), passwordChangeAllowed: noChange === undefined ? undefined : !noChange, expires }; } function parseGroupValue(properties: Element, itemOperation: Operation, diagnostics: Set): GpoLocalGroupPreferenceValue | undefined { const allowedChildren = [...properties.children].every(child => child.localName.toLocaleLowerCase('en-US') === 'members'); const membersContainers = directChildren(properties, 'members'); if (!allowedChildren || membersContainers.length > 1) { diagnostics.add('gpo-preference-local-group-item-unsupported'); return undefined; } const groupName = textAttribute(properties, 'groupName'); const groupSid = textAttribute(properties, 'groupSid'); if (!groupName || (groupSid && !sidPattern.test(groupSid))) { diagnostics.add('gpo-preference-local-group-target-invalid'); return undefined; } const currentUserCode = textAttribute(properties, 'userAction')?.toLocaleUpperCase('en-US'); if (currentUserCode && !['ADD', 'REMOVE'].includes(currentUserCode)) { diagnostics.add('gpo-preference-local-group-user-action-invalid'); return undefined; } const members: GpoLocalGroupPreferenceValue['members'] = []; for (const member of membersContainers.flatMap(container => [...container.children])) { if (member.localName.toLocaleLowerCase('en-US') !== 'member' || member.children.length) { diagnostics.add('gpo-preference-local-group-member-invalid'); return undefined; } const memberOperation = textAttribute(member, 'action')?.toLocaleUpperCase('en-US'); const sid = textAttribute(member, 'sid'); const name = textAttribute(member, 'name'); if (!['ADD', 'REMOVE'].includes(memberOperation ?? '') || (sid ? !sidPattern.test(sid) : !name)) { diagnostics.add('gpo-preference-local-group-member-invalid'); return undefined; } members.push({ sequence: members.length, operation: memberOperation === 'ADD' ? 'add' : 'remove', selector: sid ? { kind: 'sid', value: sid } : { kind: 'name', value: name! } }); } return { kind: 'local-group-preference', groupName, groupSid, newName: itemOperation === 'update' ? textAttribute(properties, 'newName') : undefined, description: textAttribute(properties, 'description'), currentUserOperation: currentUserCode ? currentUserCode === 'ADD' ? 'add' : 'remove' : undefined, suppressCurrentUserOperation: booleanAttribute(properties, 'removeAccounts', diagnostics) ?? false, deleteAllUsers: booleanAttribute(properties, 'deleteAllUsers', diagnostics) ?? false, deleteAllGroups: booleanAttribute(properties, 'deleteAllGroups', diagnostics) ?? false, members }; } export function parseLocalUsersGroupsPreference(context: LocalUsersGroupsParseContext): LocalUsersGroupsPreferenceDocument { const { root, diagnostics } = context; const rootDisabled = booleanAttribute(root, 'disabled', diagnostics) ?? false; if (canonicalGuid(attribute(root, 'clsid')) !== groupsClsid) diagnostics.add('gpo-preference-local-users-groups-root-invalid'); const items = [...root.children].filter(child => ['user', 'group'].includes(child.localName.toLocaleLowerCase('en-US'))); const unknownRootChildren = [...root.children].some(child => !['user', 'group'].includes(child.localName.toLocaleLowerCase('en-US'))); let partial = unknownRootChildren; if (unknownRootChildren) diagnostics.add('gpo-preference-local-users-groups-item-unsupported'); const lines = itemLines(context.text); if (lines.length !== items.length) diagnostics.add('gpo-preference-line-provenance-invalid'); let invalid = diagnostics.has('gpo-preference-attribute-limit') || diagnostics.has('gpo-preference-local-users-groups-root-invalid') || diagnostics.has('gpo-preference-line-provenance-invalid') || diagnostics.has('gpo-preference-boolean-invalid'); const instructions: GppLocalUsersGroupsInstruction[] = []; items.forEach((item, sequence) => { const itemType = item.localName.toLocaleLowerCase('en-US') as 'user' | 'group'; const before = diagnostics.size; const uid = canonicalGuid(attribute(item, 'uid')); const expectedClsid = itemType === 'user' ? userClsid : groupClsid; if (!uid || canonicalGuid(attribute(item, 'clsid')) !== expectedClsid) diagnostics.add('gpo-preference-local-users-groups-item-invalid'); const properties = directChildren(item, 'properties'); const allowedItemChildren = [...item.children].every(child => ['properties', 'filters'].includes(child.localName.toLocaleLowerCase('en-US'))); if (properties.length !== 1 || !allowedItemChildren) diagnostics.add('gpo-preference-local-users-groups-item-invalid'); const props = properties[0]; const itemOperation = props ? operation(attribute(props, 'action')) : undefined; if (!itemOperation) diagnostics.add('gpo-preference-action-invalid'); const removePolicy = booleanAttribute(item, 'removePolicy', diagnostics) ?? false; const applyOnce = booleanAttribute(item, 'applyOnce', diagnostics) ?? false; const itemDisabled = booleanAttribute(item, 'disabled', diagnostics) ?? false; const itemTargeting = context.summarizeTargeting(item, diagnostics); if (itemOperation === 'delete' && removePolicy) diagnostics.add('gpo-preference-remove-policy-invalid'); const unsupportedShape = props && (itemType === 'user' ? props.children.length > 0 : ![...props.children].every(child => child.localName.toLocaleLowerCase('en-US') === 'members') || directChildren(props, 'members').length > 1); const value = props && itemOperation ? itemType === 'user' ? parseUserValue(props, itemOperation, diagnostics) : parseGroupValue(props, itemOperation, diagnostics) : undefined; if (unsupportedShape) { partial = true; return; } if (diagnostics.size !== before || !uid || !itemOperation || !itemTargeting || !value) { invalid = true; return; } const common: CommonInstruction = { version: 1, cse: 'local-users-groups', uid, sequence, line: lines[sequence] ?? 1, operation: itemOperation, disabled: rootDisabled || itemDisabled, removePolicy, applyOnce, targeting: itemTargeting }; instructions.push(itemType === 'user' ? { ...common, itemType: 'user', value: value as GpoLocalUserPreferenceValue } : { ...common, itemType: 'group', value: value as GpoLocalGroupPreferenceValue }); }); if (diagnostics.has('gpo-preference-boolean-invalid')) invalid = true; return { version: 1, parser: context.parser, sourcePath: context.sourcePath, sourceSha256: context.sourceSha256, scope: context.scope, preferenceType: 'local-users-groups', status: invalid ? 'invalid' : partial ? 'partial' : 'complete', secretIndicatorStatus: context.secretIndicatorStatus, instructions: invalid ? [] : instructions, diagnosticCodes: [...diagnostics].sort() }; } export function reduceLocalUsersGroupsPreferences( document: LocalUsersGroupsPreferenceDocument, context: { backupInstanceId: string; sourceArtifactId: string; artifactId: string } ): GpoPolicyObservation[] { if (document.status === 'invalid' || !['computer', 'user'].includes(document.scope)) return []; return document.instructions.map(instruction => { const target = instruction.itemType === 'user' ? instruction.value.userName.toLocaleLowerCase('en-US') : instruction.value.groupSid?.toLocaleUpperCase('en-US') ?? instruction.value.groupName.toLocaleLowerCase('en-US'); const id = `gpo-policy-${createHash('sha256').update([ context.backupInstanceId, document.sourceSha256, instruction.uid, String(instruction.sequence), GPP_LOCAL_USERS_GROUPS_REDUCER.version ].join('\0')).digest('hex').slice(0, 32)}`; const diagnostics = new Set(); if (instruction.targeting.status === 'present') diagnostics.add('gpo-preference-targeting-unevaluated'); if (instruction.itemType === 'user' && instruction.value.passwordAttributePresent) diagnostics.add('gpo-secret-indicator-present'); 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: 'group-policy-preference', parser: document.parser, reducer: GPP_LOCAL_USERS_GROUPS_REDUCER, identity: { status: 'unresolved', namespace: 'group-policy-preference', section: instruction.itemType === 'user' ? 'local user' : 'local group', key: target }, state: 'instruction', values: [instruction.value], preference: { cse: 'local-users-groups', itemUid: instruction.uid, operation: instruction.operation, sequence: instruction.sequence, disabled: instruction.disabled, removePolicy: instruction.removePolicy, applyOnce: instruction.applyOnce, targeting: instruction.targeting }, sourceLines: [instruction.line], effectiveState: 'unknown', diagnosticCodes: [...diagnostics].sort() }; const errors = gpoPolicyObservationValidationErrors(observation); if (errors.length) throw new Error(`Intern ungültige Local-Users-and-Groups-Beobachtung: ${errors.join(' ')}`); return observation; }); }