186 lines
10 KiB
TypeScript
186 lines
10 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 } 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<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)
|
|
|| /(?:^|\/)(?: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<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)
|
|
&& ['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<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, 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;
|
|
}
|