import { createHash } from 'node:crypto'; import type { ImportResourcePolicy } from '../../src/importers/resource-policy'; import { MICROSOFT_BASELINE_SETTINGS_ANALYZER, microsoftSecurityBaselineAnalysisValidationErrors, type MicrosoftSecurityBaselineAnalysis, type MicrosoftSecurityBaselineSetting } from '../../src/domain/microsoft-security-baseline-analysis'; import { aggregateGpoBackupMetadata } from '../../src/domain/gpo-metadata'; import { classifyGpoArtifactPath } from '../../src/gpo/artifact-classifier'; import { buildAdmxPolicyCatalog, resolveRegistryPolicyIdentity } from './admx-policy-catalog'; import { parseGpoBackupMetadataXml } from './gpo-metadata-parser'; import { readMicrosoftBaselineZip } from './microsoft-baseline-zip-reader'; import { parseRegistryPolicyFile, reduceRegistryPolicy } from './registry-policy-parser'; export interface MicrosoftSecurityBaselineAnalysisOptions { baselineId: string; analyzedAt: string; expectedPackageSha256?: string; resourcePolicy?: Partial; maximumSelectedBytes?: number; } 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 stableId(prefix: string, parts: string[]): string { return `${prefix}-${createHash('sha256').update(parts.join('\0')).digest('hex').slice(0, 32)}`; } function selectedBaselineEntry(path: string): boolean { return /(?:^|\/)Backup\.xml$/i.test(path) || /(?:^|\/)Registry\.pol$/i.test(path) || /\.(?:admx|adml)$/i.test(path); } export function analyzeMicrosoftSecurityBaselinePackage( bytes: Uint8Array, options: MicrosoftSecurityBaselineAnalysisOptions ): MicrosoftSecurityBaselineAnalysis { if (!options.baselineId.trim() || !Number.isFinite(Date.parse(options.analyzedAt)) || (options.expectedPackageSha256 !== undefined && !/^[a-f0-9]{64}$/.test(options.expectedPackageSha256))) { throw new Error('Die Microsoft-Baselineanalyse benötigt eine gültige Baseline-ID, Zeit und optionale Paketprüfsumme.'); } const archive = readMicrosoftBaselineZip( bytes, entry => selectedBaselineEntry(entry.relativePath), options.resourcePolicy, options.maximumSelectedBytes ?? 32 * 1024 * 1024 ); if (options.expectedPackageSha256 && archive.containerSha256 !== options.expectedPackageSha256) { throw new Error('Die Microsoft-Baselineprüfsumme stimmt nicht mit dem unveränderten Original überein.'); } const diagnostics = new Set(); const metadataFiles = archive.files.filter(file => /(?:^|\/)Backup\.xml$/i.test(file.entry.relativePath)); const roots = metadataFiles.map(file => parentPath(file.entry.relativePath)) .filter(root => archive.entries.some(entry => entry.kind === 'file' && belongsToRoot(entry.relativePath, root) && classifyGpoArtifactPath(entry.relativePath).role === 'registry-policy')) .sort((left, right) => right.length - left.length || left.localeCompare(right)); if (!roots.length) diagnostics.add('microsoft-baseline-registry-policy-missing'); const selectedRegistryFiles = archive.files.filter(file => classifyGpoArtifactPath(file.entry.relativePath).role === 'registry-policy' && roots.some(root => belongsToRoot(file.entry.relativePath, root))); const metadataByRoot = new Map[]>(); for (const file of metadataFiles) { const root = parentPath(file.entry.relativePath); if (!roots.includes(root)) continue; const document = parseGpoBackupMetadataXml(file, options.resourcePolicy); metadataByRoot.set(root, [...(metadataByRoot.get(root) ?? []), document]); document.diagnosticCodes.forEach(code => diagnostics.add(code)); } const catalogFiles = archive.files.filter(file => /\.(?:admx|adml)$/i.test(file.entry.relativePath)); const admxCatalog = buildAdmxPolicyCatalog(catalogFiles, { kind: 'microsoft-security-baseline', baselineId: options.baselineId, packageSha256: archive.containerSha256 }); admxCatalog.diagnosticCodes.forEach(code => diagnostics.add(code)); const settings: MicrosoftSecurityBaselineSetting[] = []; let partial = admxCatalog.status === 'partial'; for (const root of [...new Set(roots)].sort()) { const aggregate = aggregateGpoBackupMetadata(metadataByRoot.get(root) ?? []); aggregate.diagnosticCodes.forEach(code => diagnostics.add(code)); if (aggregate.status !== 'complete' || !aggregate.backupId) partial = true; const backupId = aggregate.status !== 'invalid' && aggregate.backupId ? aggregate.backupId : stableId('baseline-backup-unresolved', [options.baselineId, root]); const registryFiles = selectedRegistryFiles.filter(file => belongsToRoot(file.entry.relativePath, root)); for (const file of registryFiles) { const classification = classifyGpoArtifactPath(file.entry.relativePath); const document = parseRegistryPolicyFile(file, classification.scope, options.resourcePolicy); document.diagnosticCodes.forEach(code => diagnostics.add(code)); if (document.status !== 'complete') partial = true; const observations = reduceRegistryPolicy(document, { backupInstanceId: backupId, sourceArtifactId: options.baselineId, artifactId: stableId('baseline-entry', [archive.containerSha256, file.entry.relativePath, file.entry.sha256]) }); for (const observation of observations) { const identity = resolveRegistryPolicyIdentity(observation, admxCatalog); if (identity.status !== 'resolved') { diagnostics.add('microsoft-baseline-policy-identity-unresolved'); partial = true; } else if (identity.identity.admx.status !== 'matched') { diagnostics.add(identity.identity.admx.status === 'ambiguous' ? 'microsoft-baseline-admx-identity-ambiguous' : 'microsoft-baseline-admx-metadata-not-found'); partial = true; } const setting: MicrosoftSecurityBaselineSetting = { version: 1, id: stableId('microsoft-baseline-setting', [ options.baselineId, backupId, observation.sourceSha256, observation.registryPolicy?.sequence.toString() ?? '', MICROSOFT_BASELINE_SETTINGS_ANALYZER.version ]), baselineId: options.baselineId, backupId, observation: { sourcePath: observation.sourcePath, sourceSha256: observation.sourceSha256, scope: observation.scope, deliveryMechanism: observation.deliveryMechanism, parser: observation.parser, reducer: observation.reducer, state: observation.state, values: observation.values, registryPolicy: observation.registryPolicy, sourceRecords: observation.sourceRecords, effectiveState: observation.effectiveState, diagnosticCodes: observation.diagnosticCodes }, identity }; settings.push(setting); } } } const analysis: MicrosoftSecurityBaselineAnalysis = { version: 1, id: stableId('microsoft-baseline-analysis', [options.baselineId, MICROSOFT_BASELINE_SETTINGS_ANALYZER.version]), baselineId: options.baselineId, packageSha256: archive.containerSha256, analyzer: MICROSOFT_BASELINE_SETTINGS_ANALYZER, analyzedAt: options.analyzedAt, status: partial ? 'partial' : 'complete', admxCatalog, backupCount: new Set(roots).size, registryPolicyFileCount: selectedRegistryFiles.length, settings: settings.sort((left, right) => left.id.localeCompare(right.id)), diagnosticCodes: [...diagnostics].sort() }; const errors = microsoftSecurityBaselineAnalysisValidationErrors(analysis); if (errors.length) throw new Error('Intern ungültige Microsoft-Baselineanalyse: ' + errors.join(' ')); return analysis; }