import { createHash } from 'node:crypto'; import type { GpoBackupInstance } from '../../src/domain/gpo-ingestion'; import { gpoBackupInstanceValidationErrors } from '../../src/domain/gpo-ingestion'; import type { GpoBackupMetadataDocument } from '../../src/domain/gpo-metadata'; import { aggregateGpoBackupMetadata } from '../../src/domain/gpo-metadata'; import { isGpoSysvolPath } from '../../src/gpo/artifact-classifier'; import type { ImportResourcePolicy } from '../../src/importers/resource-policy'; import { inventoryGpoContainer, type GpoContainerInventoryResult } from './gpo-container-inventory'; import { parseAdvancedAuditCsv, reduceAdvancedAuditPolicy, type AdvancedAuditDocument } from './advanced-audit-parser'; import { parseGpoBackupMetadataXml } from './gpo-metadata-parser'; import { parseGroupPolicyPreferenceXml, reduceGroupPolicyPreferences, type GroupPolicyPreferenceDocument } from './group-policy-preference-parser'; import { parseRegistryPolicyFile, reduceRegistryPolicy, type RegistryPolicyDocument } from './registry-policy-parser'; import { parseSecurityTemplateInf, reduceSecurityTemplate, type SecurityTemplateDocument } from './security-template-parser'; import { readVerifiedZipFiles, type VerifiedZipFile } from './zip-inventory'; export interface GpoBackupIngestionOptions { projectId: string; sourceArtifactId: string; discoveredAt: string; resourcePolicy?: Partial; maximumMetadataBytes?: number; maximumSelectedArtifactBytes?: number; retainIntermediateDocuments?: boolean; } export interface GpoBackupIngestionResult { version: 1; container: GpoContainerInventoryResult; metadataDocuments: GpoBackupMetadataDocument[]; registryPolicyDocuments: RegistryPolicyDocument[]; securityTemplateDocuments: SecurityTemplateDocument[]; advancedAuditDocuments: AdvancedAuditDocument[]; preferenceDocuments: GroupPolicyPreferenceDocument[]; backupInstances: GpoBackupInstance[]; } function parentPath(path: string): string { const separator = path.lastIndexOf('/'); return separator < 0 ? '' : path.slice(0, separator); } function belongsToRoot(path: string, root: string): boolean { return !root || path === root || path.startsWith(`${root}/`); } function instanceId(sourceArtifactId: string, root: string, backupId?: string): string { return `gpo-backup-${createHash('sha256').update(`${sourceArtifactId}\0${root}\0${backupId ?? 'unresolved'}`).digest('hex').slice(0, 32)}`; } export function ingestGpoBackupContainer(bytes: Uint8Array, options: GpoBackupIngestionOptions): GpoBackupIngestionResult { if (!options.projectId.trim() || !options.sourceArtifactId.trim() || !Number.isFinite(Date.parse(options.discoveredAt))) { throw new Error('GPO-Ingestion benötigt Projekt-, Quellartefakt-ID und einen gültigen Erkennungszeitpunkt.'); } const container = inventoryGpoContainer(bytes, options.resourcePolicy); if (container.recognition.status !== 'gpo-backup-candidate') { return { version: 1, container, metadataDocuments: [], registryPolicyDocuments: [], securityTemplateDocuments: [], advancedAuditDocuments: [], preferenceDocuments: [], backupInstances: [] }; } const metadataPaths = container.inventory.entries .filter(entry => entry.role === 'backup-metadata') .map(entry => entry.relativePath) .filter(path => { const root = parentPath(path); return container.archive.entries.some(entry => entry.kind === 'file' && belongsToRoot(entry.relativePath, root) && isGpoSysvolPath(entry.relativePath)); }); const metadataRoots = metadataPaths.map(parentPath); const registryPolicyEntries = container.inventory.entries.filter(entry => entry.role === 'registry-policy' && metadataRoots.some(root => belongsToRoot(entry.relativePath, root)) ); const securityTemplateEntries = container.inventory.entries.filter(entry => entry.role === 'security-template' && metadataRoots.some(root => belongsToRoot(entry.relativePath, root)) ); const advancedAuditEntries = container.inventory.entries.filter(entry => entry.role === 'advanced-audit-policy' && metadataRoots.some(root => belongsToRoot(entry.relativePath, root)) ); const preferenceEntries = container.inventory.entries.filter(entry => entry.role === 'group-policy-preference' && metadataRoots.some(root => belongsToRoot(entry.relativePath, root)) ); const selectedPaths = [...new Set([ ...metadataPaths, ...registryPolicyEntries.map(entry => entry.relativePath), ...securityTemplateEntries.map(entry => entry.relativePath), ...advancedAuditEntries.map(entry => entry.relativePath), ...preferenceEntries.map(entry => entry.relativePath) ])]; const verifiedFiles = readVerifiedZipFiles( bytes, selectedPaths, options.resourcePolicy, options.maximumSelectedArtifactBytes ?? options.maximumMetadataBytes ?? Math.min(options.resourcePolicy?.maximumZipUncompressedBytes ?? 512 * 1024 * 1024, 128 * 1024 * 1024) ); const retainIntermediateDocuments = options.retainIntermediateDocuments ?? true; const metadataPathSet = new Set(metadataPaths); const metadataDocuments = verifiedFiles .filter(file => metadataPathSet.has(file.entry.relativePath)) .map(file => parseGpoBackupMetadataXml(file, options.resourcePolicy)); const registryEntryByPath = new Map(registryPolicyEntries.map(entry => [entry.relativePath, entry])); const securityEntryByPath = new Map(securityTemplateEntries.map(entry => [entry.relativePath, entry])); const advancedAuditEntryByPath = new Map(advancedAuditEntries.map(entry => [entry.relativePath, entry])); const preferenceEntryByPath = new Map(preferenceEntries.map(entry => [entry.relativePath, entry])); const registryPolicyDocuments: RegistryPolicyDocument[] = []; const securityTemplateDocuments: SecurityTemplateDocument[] = []; const advancedAuditDocuments: AdvancedAuditDocument[] = []; const preferenceDocuments: GroupPolicyPreferenceDocument[] = []; const documentsByRoot = new Map(); for (const document of metadataDocuments) { const root = parentPath(document.sourcePath); documentsByRoot.set(root, [...(documentsByRoot.get(root) ?? []), document]); } const rootsBySpecificity = [...documentsByRoot.keys()] .sort((left, right) => right.length - left.length || left.localeCompare(right)); const groupFilesByRoot = (paths: Set): Map => { const grouped = new Map(); for (const file of verifiedFiles) { if (!paths.has(file.entry.relativePath)) continue; const root = rootsBySpecificity.find(candidate => belongsToRoot(file.entry.relativePath, candidate)); if (root === undefined) continue; grouped.set(root, [...(grouped.get(root) ?? []), file]); } return grouped; }; const registryFilesByRoot = groupFilesByRoot(new Set(registryEntryByPath.keys())); const securityFilesByRoot = groupFilesByRoot(new Set(securityEntryByPath.keys())); const auditFilesByRoot = groupFilesByRoot(new Set(advancedAuditEntryByPath.keys())); const preferenceFilesByRoot = groupFilesByRoot(new Set(preferenceEntryByPath.keys())); const backupInstances = [...documentsByRoot] .sort(([left], [right]) => left.localeCompare(right)) .map(([root, documents]) => { const aggregate = aggregateGpoBackupMetadata(documents); const id = instanceId(options.sourceArtifactId, root, aggregate.status === 'invalid' ? undefined : aggregate.backupId); const policyObservations: GpoBackupInstance['policyObservations'] = []; const artifactDiagnostics = new Set(aggregate.diagnosticCodes); const registryByPath = new Map(); const templateByPath = new Map(); const auditByPath = new Map(); const preferenceByPath = new Map(); let blocked = aggregate.status === 'invalid'; for (const file of registryFilesByRoot.get(root) ?? []) { const artifact = registryEntryByPath.get(file.entry.relativePath)!; const document = parseRegistryPolicyFile(file, artifact.scope, options.resourcePolicy); if (retainIntermediateDocuments) registryPolicyDocuments.push(document); registryByPath.set(document.sourcePath, { status: document.status, scope: document.scope }); document.diagnosticCodes.forEach(code => artifactDiagnostics.add(code)); blocked ||= document.status === 'invalid'; policyObservations.push(...reduceRegistryPolicy(document, { backupInstanceId: id, sourceArtifactId: options.sourceArtifactId, artifactId: artifact.artifactId })); } for (const file of securityFilesByRoot.get(root) ?? []) { const artifact = securityEntryByPath.get(file.entry.relativePath)!; const document = parseSecurityTemplateInf(file, artifact.scope, options.resourcePolicy); if (retainIntermediateDocuments) securityTemplateDocuments.push(document); templateByPath.set(document.sourcePath, { status: document.status, scope: document.scope }); document.diagnosticCodes.forEach(code => artifactDiagnostics.add(code)); blocked ||= document.status === 'invalid'; policyObservations.push(...reduceSecurityTemplate(document, { backupInstanceId: id, sourceArtifactId: options.sourceArtifactId, artifactId: artifact.artifactId })); } for (const file of auditFilesByRoot.get(root) ?? []) { const artifact = advancedAuditEntryByPath.get(file.entry.relativePath)!; const document = parseAdvancedAuditCsv(file, artifact.scope, options.resourcePolicy); if (retainIntermediateDocuments) advancedAuditDocuments.push(document); auditByPath.set(document.sourcePath, { status: document.status, scope: document.scope }); document.diagnosticCodes.forEach(code => artifactDiagnostics.add(code)); blocked ||= document.status === 'invalid'; policyObservations.push(...reduceAdvancedAuditPolicy(document, { backupInstanceId: id, sourceArtifactId: options.sourceArtifactId, artifactId: artifact.artifactId })); } for (const file of preferenceFilesByRoot.get(root) ?? []) { const artifact = preferenceEntryByPath.get(file.entry.relativePath)!; const document = parseGroupPolicyPreferenceXml(file, artifact.scope, options.resourcePolicy); if (retainIntermediateDocuments) preferenceDocuments.push(document); preferenceByPath.set(document.sourcePath, { status: document.status, scope: document.scope, secretIndicatorStatus: document.secretIndicatorStatus }); document.diagnosticCodes.forEach(code => artifactDiagnostics.add(code)); blocked ||= document.status === 'invalid'; policyObservations.push(...reduceGroupPolicyPreferences(document, { backupInstanceId: id, sourceArtifactId: options.sourceArtifactId, artifactId: artifact.artifactId })); } policyObservations.forEach(observation => observation.diagnosticCodes.forEach(code => artifactDiagnostics.add(code))); const secretByPath = new Map(documents.map(document => [document.sourcePath, document.secretIndicatorStatus])); const entries = container.inventory.entries .filter(entry => belongsToRoot(entry.relativePath, root)) .map(entry => { if (entry.role === 'backup-metadata') return { ...entry, disposition: aggregate.status === 'invalid' ? 'blocked' as const : 'supported' as const, secretIndicatorStatus: secretByPath.get(entry.relativePath) ?? 'not-inspected' as const }; if (entry.role === 'registry-policy') { const document = registryByPath.get(entry.relativePath); return { ...entry, disposition: document?.status === 'invalid' ? 'blocked' as const : document?.status === 'complete' && ['computer', 'user'].includes(document.scope) ? 'supported' as const : 'inventory-only' as const, secretIndicatorStatus: 'not-inspected' as const }; } if (entry.role === 'security-template') { const document = templateByPath.get(entry.relativePath); return { ...entry, disposition: document?.status === 'invalid' ? 'blocked' as const : document?.status === 'complete' && ['computer', 'user'].includes(document.scope) ? 'supported' as const : 'inventory-only' as const, secretIndicatorStatus: 'not-inspected' as const }; } if (entry.role === 'advanced-audit-policy') { const document = auditByPath.get(entry.relativePath); return { ...entry, disposition: document?.status === 'invalid' ? 'blocked' as const : document?.status === 'complete' && document.scope === 'computer' ? 'supported' as const : 'inventory-only' as const, secretIndicatorStatus: 'not-inspected' as const }; } if (entry.role === 'group-policy-preference') { const document = preferenceByPath.get(entry.relativePath); return { ...entry, disposition: document?.status === 'invalid' ? 'blocked' as const : document?.status === 'complete' && ['computer', 'user'].includes(document.scope) ? 'supported' as const : 'inventory-only' as const, secretIndicatorStatus: document?.secretIndicatorStatus ?? 'not-inspected' as const }; } return entry; }); const instance: GpoBackupInstance = { version: 1, id, projectId: options.projectId, sourceArtifactId: options.sourceArtifactId, identity: aggregate.status !== 'invalid' && aggregate.gpoId && aggregate.backupId ? { status: 'resolved', gpoId: aggregate.gpoId, backupId: aggregate.backupId, evidence: 'backup-metadata' } : { status: 'unresolved', reasonCode: aggregate.status === 'invalid' ? 'metadata-conflict' : 'metadata-incomplete' }, domain: aggregate.status !== 'invalid' && aggregate.domainKey ? { status: 'resolved', key: aggregate.domainKey, evidence: 'backup-metadata' } : { status: 'unknown' }, collection: { status: 'observed', collectedAt: options.discoveredAt, evidence: 'backend-receipt' }, backupSet: { id: 'gpo-backup-set:' + options.sourceArtifactId, scope: 'selected-gpos', evidence: 'user-selection' }, backupCreatedAt: aggregate.status === 'invalid' ? undefined : aggregate.backupCreatedAt, ...(aggregate.links ? { links: { ...aggregate.links, evidence: 'gp-report' as const } } : {}), metadataStatus: aggregate.status === 'invalid' ? 'unreadable' : aggregate.status, sections: { computer: entries.some(entry => entry.scope === 'computer' && entry.role !== 'backup-metadata' && entry.disposition !== 'blocked') ? 'present' : entries.some(entry => entry.scope === 'computer' && entry.role !== 'backup-metadata') ? 'unreadable' : 'unknown', user: entries.some(entry => entry.scope === 'user' && entry.role !== 'backup-metadata' && entry.disposition !== 'blocked') ? 'present' : entries.some(entry => entry.scope === 'user' && entry.role !== 'backup-metadata') ? 'unreadable' : 'unknown' }, inventory: { version: 1, status: blocked ? 'blocked' : 'complete', entries, diagnosticCodes: [...new Set([...container.inventory.diagnosticCodes, ...artifactDiagnostics])].sort() }, policyObservations, discoveredAt: options.discoveredAt }; const errors = gpoBackupInstanceValidationErrors(instance); if (errors.length) throw new Error(`Intern ungültige GPO-Backupinstanz: ${errors.join(' ')}`); return instance; }); return { version: 1, container, metadataDocuments: retainIntermediateDocuments ? metadataDocuments : [], registryPolicyDocuments, securityTemplateDocuments, advancedAuditDocuments, preferenceDocuments, backupInstances }; }