import { createHash } from 'node:crypto'; import type { GpoPolicyObservation, GpoPreferenceContext, GpoServicePreferenceValue } 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_SERVICES_REDUCER = { id: 'gpo-preferences-services-reducer', version: '1.0.0' } as const; const servicesClsid = '2cfb484a-4e96-4b5d-a0b6-093d2f91e6ae'; const serviceClsid = 'ab6f0b67-341f-4e51-92f9-005fbfba1a43'; 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; type Targeting = GpoPreferenceContext['targeting']; export interface GppServiceInstruction { version: 1; cse: 'services'; uid: string; sequence: number; line: number; operation: 'configure'; disabled: boolean; removePolicy: boolean; applyOnce: boolean; targeting: Targeting; value: GpoServicePreferenceValue; } export interface ServicesPreferenceDocument { version: 1; parser: { id: string; version: string }; sourcePath: string; sourceSha256: string; scope: GpoPolicyScope; preferenceType: 'services'; status: 'complete' | 'partial' | 'invalid'; secretIndicatorStatus: 'not-detected' | 'present'; instructions: GppServiceInstruction[]; diagnosticCodes: string[]; } export interface ServicesParseContext { 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; } const serviceActions = { NOCHANGE: 'no-change', START: 'start', STOP: 'stop', RESTART: 'restart', RESTART_IF_REQUIRED: 'restart-if-required' } as const; const startupTypes = { NOCHANGE: 'no-change', AUTOMATIC: 'automatic', BOOT: 'boot', DISABLED: 'disabled', MANUAL: 'manual', SYSTEM: 'system' } as const; const failureActions = { START: 'start', STOP: 'stop', RESTART: 'restart', NOACTION: 'no-action', RESTART_IF_REQUIRED: 'restart-if-required' } as const; const allowedPropertyAttributes = new Set([ 'serviceaction', 'startuptype', 'servicename', 'timeout', 'accountname', 'cpassword', 'interact', 'firstfailure', 'secondfailure', 'thirdfailure', 'resetfailcountdelay', 'restartservicedelay', 'restartcomputerdelay', 'restartmessage', 'program', 'args', 'append' ]); 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 undefined; } function integerAttribute(element: Element, name: string, diagnostics: Set): number | undefined { const value = attribute(element, name); if (value === undefined) return undefined; if (!/^\d+$/.test(value)) { diagnostics.add('gpo-preference-service-number-invalid'); return undefined; } const parsed = Number(value); if (!Number.isSafeInteger(parsed)) { diagnostics.add('gpo-preference-service-number-invalid'); return undefined; } return parsed; } function enumAttribute>( element: Element, name: string, values: T, diagnostics: Set, defaultKey?: keyof T ): T[keyof T] | undefined { const raw = attribute(element, name); const key = (raw === undefined && defaultKey ? String(defaultKey) : raw?.toLocaleUpperCase('en-US')) as keyof T | undefined; if (key !== undefined && Object.hasOwn(values, key)) return values[key]; diagnostics.add('gpo-preference-service-enum-invalid'); return undefined; } 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 itemLines(text: string): number[] { return xmlElementStartLines(text, /<(?:[A-Za-z_][\w.-]*:)?NTService(?=[\s/>])/g); } function parseServiceValue(properties: Element, diagnostics: Set): GpoServicePreferenceValue | undefined { const serviceName = attribute(properties, 'serviceName'); if (!serviceName?.trim()) { diagnostics.add('gpo-preference-service-target-invalid'); return undefined; } const serviceAction = enumAttribute(properties, 'serviceAction', serviceActions, diagnostics, 'NOCHANGE'); const startupType = enumAttribute(properties, 'startupType', startupTypes, diagnostics); const firstFailure = attribute(properties, 'firstFailure') === undefined ? undefined : enumAttribute(properties, 'firstFailure', failureActions, diagnostics); const secondFailure = attribute(properties, 'secondFailure') === undefined ? undefined : enumAttribute(properties, 'secondFailure', failureActions, diagnostics); const thirdFailure = attribute(properties, 'thirdFailure') === undefined ? undefined : enumAttribute(properties, 'thirdFailure', failureActions, diagnostics); const timeout = integerAttribute(properties, 'timeout', diagnostics); const interact = booleanAttribute(properties, 'interact', diagnostics, true); const resetFailCountDelay = integerAttribute(properties, 'resetFailCountDelay', diagnostics); const restartServiceDelay = integerAttribute(properties, 'restartServiceDelay', diagnostics); const restartComputerDelay = integerAttribute(properties, 'restartComputerDelay', diagnostics); if (!serviceAction || !startupType) return undefined; return { kind: 'service-preference', serviceName, serviceAction, startupType, timeout, accountName: attribute(properties, 'accountName'), passwordAttributePresent: [...properties.attributes] .some(candidate => candidate.localName.toLocaleLowerCase('en-US') === 'cpassword'), interact, firstFailure, secondFailure, thirdFailure, resetFailCountDelay, restartServiceDelay, restartComputerDelay, restartMessage: attribute(properties, 'restartMessage'), program: attribute(properties, 'program'), args: attribute(properties, 'args'), append: attribute(properties, 'append') }; } export function parseServicesPreference(context: ServicesParseContext): ServicesPreferenceDocument { const { root, diagnostics } = context; const rootDisabled = booleanAttribute(root, 'disabled', diagnostics) ?? false; if (canonicalGuid(attribute(root, 'clsid')) !== servicesClsid) diagnostics.add('gpo-preference-services-root-invalid'); const items = directChildren(root, 'NTService'); const unsupportedScope = context.scope !== 'computer'; const unsupportedRootChild = [...root.children].some(child => child.localName.toLocaleLowerCase('en-US') !== 'ntservice'); let partial = unsupportedScope || unsupportedRootChild; if (unsupportedScope) diagnostics.add('gpo-preference-services-scope-unsupported'); if (unsupportedRootChild) diagnostics.add('gpo-preference-services-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-services-root-invalid') || diagnostics.has('gpo-preference-line-provenance-invalid') || diagnostics.has('gpo-preference-boolean-invalid'); const instructions: GppServiceInstruction[] = []; items.forEach((item, sequence) => { const before = diagnostics.size; const uid = canonicalGuid(attribute(item, 'uid')); if (!uid || canonicalGuid(attribute(item, 'clsid')) !== serviceClsid) diagnostics.add('gpo-preference-service-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-service-item-invalid'); const props = properties[0]; const unsupportedShape = props && (props.children.length > 0 || [...props.attributes] .some(candidate => !allowedPropertyAttributes.has(candidate.localName.toLocaleLowerCase('en-US')))); if (unsupportedShape) { diagnostics.add('gpo-preference-service-item-unsupported'); partial = true; return; } 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); const value = props ? parseServiceValue(props, diagnostics) : undefined; if (diagnostics.size !== before || !uid || !itemTargeting || !value) { invalid = true; return; } instructions.push({ version: 1, cse: 'services', uid, sequence, line: lines[sequence] ?? 1, operation: 'configure', disabled: rootDisabled || itemDisabled, removePolicy, applyOnce, targeting: itemTargeting, value }); }); if (diagnostics.has('gpo-preference-boolean-invalid') || diagnostics.has('gpo-preference-service-number-invalid') || diagnostics.has('gpo-preference-service-enum-invalid')) invalid = true; return { version: 1, parser: context.parser, sourcePath: context.sourcePath, sourceSha256: context.sourceSha256, scope: context.scope, preferenceType: 'services', status: invalid ? 'invalid' : partial ? 'partial' : 'complete', secretIndicatorStatus: context.secretIndicatorStatus, instructions: invalid || unsupportedScope ? [] : instructions, diagnosticCodes: [...diagnostics].sort() }; } export function reduceServicesPreferences( document: ServicesPreferenceDocument, context: { backupInstanceId: string; sourceArtifactId: string; artifactId: string } ): GpoPolicyObservation[] { if (document.status === 'invalid' || document.scope !== 'computer') return []; return document.instructions.map(instruction => { const id = `gpo-policy-${createHash('sha256').update([ context.backupInstanceId, context.artifactId, document.sourceSha256, instruction.uid, String(instruction.sequence), GPP_SERVICES_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.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: 'computer', deliveryMechanism: 'group-policy-preference', parser: document.parser, reducer: GPP_SERVICES_REDUCER, identity: { status: 'unresolved', namespace: 'group-policy-preference', section: 'service', key: instruction.value.serviceName.toLocaleLowerCase('en-US') }, state: 'instruction', values: [instruction.value], preference: { cse: 'services', 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 Services-Beobachtung: ${errors.join(' ')}`); return observation; }); }