import { createHash } from 'node:crypto'; import type { GpoPolicyObservation, GpoPolicyValue } 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 { resolveImportResourcePolicy, validateDecodedTextResources } from '../../src/importers/resource-policy'; import { ImportError } from '../../src/shared/errors'; import type { VerifiedZipFile } from './zip-inventory'; export const SECURITY_TEMPLATE_PARSER = { id: 'gpo-security-template-inf', version: '1.1.0' } as const; export const SECURITY_TEMPLATE_REDUCER = { id: 'gpo-security-template-reducer', version: '1.0.0' } as const; const supportedSections = new Set(['system access', 'kerberos policy', 'privilege rights', 'registry values']); const metadataSections = new Set(['unicode', 'version']); const maximumFileBytes = 4 * 1024 * 1024; const maximumLines = 50_000; const maximumFieldCharacters = 4_095; export interface SecurityTemplateInstruction { version: 1; section: string; key: string; rawValue: string; line: number; } export interface SecurityTemplateDocument { version: 1; parser: typeof SECURITY_TEMPLATE_PARSER; sourcePath: string; sourceSha256: string; scope: GpoPolicyScope; status: 'complete' | 'partial' | 'invalid'; instructions: SecurityTemplateInstruction[]; diagnosticCodes: string[]; } function parserError(code: string, message: string, details: Record = {}): never { throw new ImportError(message, [{ severity: 'error', code, message, stage: 'format-decoding', details }]); } function decodeSecurityTemplate(bytes: Uint8Array): string { if (bytes.byteLength > maximumFileBytes) { parserError('gpo-security-template-size-limit', 'Ein Security Template überschreitet das Größenlimit.', { sizeBytes: bytes.byteLength }); } let encoding = 'utf-8'; let content = bytes; if (bytes[0] === 0xff && bytes[1] === 0xfe) { encoding = 'utf-16le'; content = bytes.subarray(2); } else if (bytes[0] === 0xfe && bytes[1] === 0xff) { encoding = 'utf-16be'; content = bytes.subarray(2); } else if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) content = bytes.subarray(3); try { return new TextDecoder(encoding, { fatal: true }).decode(content); } catch { parserError('gpo-security-template-encoding-rejected', 'Das Security Template ist nicht eindeutig als UTF-8 oder BOM-markiertes UTF-16 dekodierbar.'); } } export function parseSecurityTemplateInf( file: VerifiedZipFile, scope: GpoPolicyScope, resourceOverrides?: Partial ): SecurityTemplateDocument { const text = decodeSecurityTemplate(file.bytes); const policy = resolveImportResourcePolicy({ ...resourceOverrides, maximumDecodedCharacters: Math.min(resourceOverrides?.maximumDecodedCharacters ?? maximumFileBytes, maximumFileBytes) }); validateDecodedTextResources(text, 'inf', policy); const lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n'); if (lines.length > maximumLines) parserError('gpo-security-template-line-limit', 'Das Security Template überschreitet das Zeilenlimit.', { lines: lines.length }); const diagnostics = new Set(); const instructions: SecurityTemplateInstruction[] = []; let section: string | undefined; let invalid = false; for (let index = 0; index < lines.length; index += 1) { const rawLine = lines[index]; const line = index + 1; if (rawLine.length > maximumFieldCharacters) { diagnostics.add('gpo-security-template-field-limit'); invalid = true; continue; } const trimmed = rawLine.trim(); if (!trimmed || trimmed.startsWith(';')) continue; const sectionMatch = /^\[([^\[\]]+)\]$/.exec(trimmed); if (sectionMatch) { section = sectionMatch[1].trim().toLocaleLowerCase('en-US'); if (!section || section.length > 256) { diagnostics.add('gpo-security-template-section-invalid'); invalid = true; } else if (!supportedSections.has(section) && !metadataSections.has(section)) diagnostics.add('gpo-security-template-section-unsupported'); continue; } // A damaged section boundary makes the attribution of following entries unsafe. if (trimmed.startsWith('[')) { diagnostics.add('gpo-security-template-section-invalid'); invalid = true; section = undefined; continue; } if (!section) { diagnostics.add('gpo-security-template-entry-outside-section'); invalid = true; continue; } // Other INF sections have their own grammars, e.g. comma-separated service entries. if (metadataSections.has(section) || !supportedSections.has(section)) continue; const separator = rawLine.indexOf('='); if (separator < 1) { diagnostics.add('gpo-security-template-entry-invalid'); continue; } const key = rawLine.slice(0, separator).trim(); const rawValue = rawLine.slice(separator + 1).trim(); if (!key || key.length > 1_024 || rawValue.length > maximumFieldCharacters) { diagnostics.add('gpo-security-template-entry-invalid'); continue; } if (section === 'registry values' && !/^\s*\d+\s*,/.test(rawValue)) { diagnostics.add('gpo-security-template-registry-value-invalid'); continue; } instructions.push({ version: 1, section, key, rawValue, line }); } return { version: 1, parser: SECURITY_TEMPLATE_PARSER, sourcePath: file.entry.relativePath, sourceSha256: file.entry.sha256, scope, status: invalid ? 'invalid' : diagnostics.size ? 'partial' : 'complete', instructions: invalid ? [] : instructions, diagnosticCodes: [...diagnostics].sort() }; } function policyValue(instruction: SecurityTemplateInstruction): GpoPolicyValue | undefined { if (instruction.section === 'privilege rights') { return { kind: 'principal-set', principals: instruction.rawValue.split(',').map(value => value.trim()).filter(Boolean) }; } if (instruction.section === 'registry values') { const separator = instruction.rawValue.indexOf(','); if (separator < 1) return undefined; const typeCode = Number(instruction.rawValue.slice(0, separator).trim()); if (!Number.isSafeInteger(typeCode) || typeCode < 0) return undefined; return { kind: 'registry', typeCode, rawData: instruction.rawValue.slice(separator + 1).trim() }; } return { kind: 'scalar', raw: instruction.rawValue }; } function canonicalValue(value: GpoPolicyValue): string { if (value.kind === 'principal-set') return JSON.stringify({ ...value, principals: [...value.principals].sort() }); return JSON.stringify(value); } export function reduceSecurityTemplate( document: SecurityTemplateDocument, context: { backupInstanceId: string; sourceArtifactId: string; artifactId: string } ): GpoPolicyObservation[] { if (document.status === 'invalid' || !['computer', 'user'].includes(document.scope)) return []; const groups = new Map(); for (const instruction of document.instructions) { const key = `${instruction.section}\0${instruction.key.toLocaleLowerCase('en-US')}`; groups.set(key, [...(groups.get(key) ?? []), instruction]); } return [...groups.values()].map(instructions => { const values = instructions.map(policyValue); if (values.some(value => value === undefined)) return undefined; const typedValues = values as GpoPolicyValue[]; const distinctValues = [...new Map(typedValues.map(value => [canonicalValue(value), value])).values()]; const first = instructions[0]; const conflicted = distinctValues.length > 1; const id = `gpo-policy-${createHash('sha256').update([ context.backupInstanceId, context.artifactId, document.sourceSha256, first.section, first.key.toLocaleLowerCase('en-US'), SECURITY_TEMPLATE_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: 'security-template', parser: document.parser, reducer: SECURITY_TEMPLATE_REDUCER, identity: { status: 'unresolved', namespace: 'security-template', section: first.section, key: first.key }, state: conflicted ? 'conflicted' : 'configured', values: distinctValues, sourceLines: instructions.map(instruction => instruction.line), effectiveState: 'unknown', diagnosticCodes: conflicted ? ['gpo-security-template-value-conflict'] : [] }; const errors = gpoPolicyObservationValidationErrors(observation); if (errors.length) throw new Error(`Intern ungültige Security-Template-Beobachtung: ${errors.join(' ')}`); return observation; }).filter((observation): observation is GpoPolicyObservation => Boolean(observation)); }