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 } from './admx-policy-catalog'; import { parseGpoBackupMetadataXml } from './gpo-metadata-parser'; import { readMicrosoftBaselineZip } from './microsoft-baseline-zip-reader'; import { parseRegistryPolicyFile, reduceRegistryPolicy } from './registry-policy-parser'; import { parseSecurityTemplateInf, reduceSecurityTemplate } from './security-template-parser'; import { parseAdvancedAuditCsv, reduceAdvancedAuditPolicy } from './advanced-audit-parser'; import { resolvePolicyTarget } from './policy-target-resolver'; import { baselineTemplateContext } from '../../src/evaluation/baseline-template-context'; 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) || /(?:^|\/)(?:bkupInfo|GPReport)\.xml$/i.test(path) || /(?:^|\/)Registry\.pol$/i.test(path) || /\.(?:admx|adml)$/i.test(path) || ['security-template', 'advanced-audit-policy'].includes(classifyGpoArtifactPath(path).role); } 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) && ['registry-policy', 'security-template', 'advanced-audit-policy'].includes(classifyGpoArtifactPath(entry.relativePath).role))) .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 => ['registry-policy', 'security-template', 'advanced-audit-policy'].includes(classifyGpoArtifactPath(file.entry.relativePath).role) && 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, true); 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 }); const analysisId = stableId('microsoft-baseline-analysis', [options.baselineId, MICROSOFT_BASELINE_SETTINGS_ANALYZER.version]); // Each immutable analysis owns a catalog row, even when its ADMX contents are unchanged. admxCatalog.id = stableId('admx-catalog', [analysisId, admxCatalog.id]); admxCatalog.diagnosticCodes.forEach(code => diagnostics.add(code)); const settings: MicrosoftSecurityBaselineSetting[] = []; const parsedMetadata = new Map([...metadataByRoot.values()].flat().map(document => [document.sourcePath, document])); let partial = admxCatalog.status === 'partial'; for (const root of [...new Set(roots)].sort()) { const names = archive.files.filter(file => parentPath(file.entry.relativePath) === root && /(?:^|\/)(?:Backup|bkupInfo|GPReport)\.xml$/i.test(file.entry.relativePath)) .flatMap(file => ((parsedMetadata.get(file.entry.relativePath) ?? parseGpoBackupMetadataXml(file, options.resourcePolicy, true)).templateNames ?? []) .map(value => ({ value, sourcePath: file.entry.relativePath, sourceSha256: file.entry.sha256 }))); const template = { names, context: baselineTemplateContext(names.map(item => item.value)) }; 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 context = { backupInstanceId: backupId, sourceArtifactId: options.baselineId, artifactId: stableId('baseline-entry', [archive.containerSha256, file.entry.relativePath, file.entry.sha256]) }; const parsed = classification.role === 'security-template' ? (() => { const document = parseSecurityTemplateInf(file, classification.scope, options.resourcePolicy); return { document, observations: reduceSecurityTemplate(document, context) }; })() : classification.role === 'advanced-audit-policy' ? (() => { const document = parseAdvancedAuditCsv(file, classification.scope, options.resourcePolicy); return { document, observations: reduceAdvancedAuditPolicy(document, context) }; })() : (() => { const document = parseRegistryPolicyFile(file, classification.scope, options.resourcePolicy); return { document, observations: reduceRegistryPolicy(document, context) }; })(); const { document, observations } = parsed; document.diagnosticCodes.forEach(code => diagnostics.add(code)); if (document.status !== 'complete') partial = true; for (const observation of observations) { const identity = resolvePolicyTarget(observation, admxCatalog); if (identity.status !== 'resolved') { diagnostics.add('microsoft-baseline-policy-identity-unresolved'); partial = true; } else if (observation.deliveryMechanism === 'registry-policy' && 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.id, MICROSOFT_BASELINE_SETTINGS_ANALYZER.version ]), baselineId: options.baselineId, backupId, template, 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, sourceLines: observation.sourceLines, effectiveState: observation.effectiveState, diagnosticCodes: observation.diagnosticCodes }, identity }; settings.push(setting); } } } const analysis: MicrosoftSecurityBaselineAnalysis = { version: 1, id: analysisId, 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.filter(file => classifyGpoArtifactPath(file.entry.relativePath).role === 'registry-policy').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; }