Assessor_2.0/server/gpo/microsoft-security-baseline...

161 lines
7.9 KiB
TypeScript

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<ImportResourcePolicy>;
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<string>();
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<string, ReturnType<typeof parseGpoBackupMetadataXml>[]>();
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;
}