161 lines
7.9 KiB
TypeScript
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;
|
|
}
|