import { createHash } from 'node:crypto'; import type { AssessorRepository } from '../../src/persistence/assessor-repository'; import { MICROSOFT_BASELINE_METADATA_DETECTOR_VERSION, baselineCatalogStatus, detectMicrosoftSecurityBaselineMetadata, selectMicrosoftSecurityBaseline, type MicrosoftSecurityBaselineClassification, type MicrosoftSecurityBaselineRecord, type MicrosoftSecurityBaselineSelectionContext } from '../../src/domain/microsoft-security-baseline'; import type { BackendArtifactStore } from '../artifacts/backend-artifact-store'; import { RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors'; import { MICROSOFT_BASELINE_SETTINGS_ANALYZER, type StoredMicrosoftSecurityBaselineAnalysis } from '../../src/domain/microsoft-security-baseline-analysis'; import type { MicrosoftBaselineWorkerExecutor } from './gpo-inventory-executor'; const zipSignatures = ['504b0304', '504b0506', '504b0708']; function classificationId(baselineId: string): string { const digest = createHash('sha256').update(`${baselineId}\0${MICROSOFT_BASELINE_METADATA_DETECTOR_VERSION}`).digest('hex'); return `ms-baseline-classification-${digest.slice(0, 32)}`; } function latestClassifications(classifications: readonly MicrosoftSecurityBaselineClassification[]) { const latest = new Map(); for (const classification of classifications) { const current = latest.get(classification.baselineId); if (!current || Date.parse(classification.createdAt) > Date.parse(current.createdAt) || (classification.createdAt === current.createdAt && classification.id.localeCompare(current.id) > 0)) latest.set(classification.baselineId, classification); } return latest; } function applyClassifications(records: readonly MicrosoftSecurityBaselineRecord[], classifications: readonly MicrosoftSecurityBaselineClassification[]): MicrosoftSecurityBaselineRecord[] { const latest = latestClassifications(classifications); return records.map(record => { const classification = latest.get(record.id); return classification ? { ...record, status: classification.status, metadata: classification.metadata, classificationId: classification.id } : record; }); } export class MicrosoftSecurityBaselineService { constructor( private readonly repository: AssessorRepository, private readonly artifactStore: BackendArtifactStore, private readonly maximumBytes: number, private readonly analysisExecutor?: MicrosoftBaselineWorkerExecutor ) {} async refreshDetectedMetadata(classifiedAt = new Date().toISOString()): Promise { if (!Number.isFinite(Date.parse(classifiedAt))) throw new RepositoryValidationError('The baseline classification timestamp is invalid.'); const [records, classifications] = await Promise.all([ this.repository.listMicrosoftSecurityBaselines(), this.repository.listMicrosoftSecurityBaselineClassifications() ]); const latest = latestClassifications(classifications); const currentKeys = new Set(classifications.map(item => `${item.baselineId}\0${item.detectorVersion}`)); let created = 0; for (const record of records) { const key = `${record.id}\0${MICROSOFT_BASELINE_METADATA_DETECTOR_VERSION}`; if (currentKeys.has(key)) continue; const metadata = detectMicrosoftSecurityBaselineMetadata(record.fileName); const classification: MicrosoftSecurityBaselineClassification = { version: 1, id: classificationId(record.id), baselineId: record.id, detectorVersion: MICROSOFT_BASELINE_METADATA_DETECTOR_VERSION, createdAt: classifiedAt, supersedesClassificationId: latest.get(record.id)?.id, status: baselineCatalogStatus(metadata), metadata }; await this.repository.saveMicrosoftSecurityBaselineClassification(classification); created += 1; } return created; } async list(): Promise { const [records, classifications] = await Promise.all([ this.repository.listMicrosoftSecurityBaselines(), this.repository.listMicrosoftSecurityBaselineClassifications() ]); return applyClassifications(records, classifications); } async delete(baselineId: string): Promise { const record = await this.repository.deleteMicrosoftSecurityBaseline(baselineId); if (!record) throw new RepositoryNotFoundError('The Microsoft baseline package is not present.'); await this.artifactStore.remove(record.storageKey).catch(() => undefined); return record; } async submit(request: { fileName: string; bytes: Uint8Array; receivedAt: string }): Promise { const fileName = request.fileName.trim(); if (!fileName || !/\.zip$/i.test(fileName)) throw new RepositoryValidationError('Microsoft Security Baselines must be uploaded as ZIP packages.'); if (!Number.isFinite(Date.parse(request.receivedAt))) throw new RepositoryValidationError('The baseline import timestamp is invalid.'); if (request.bytes.byteLength < 4 || request.bytes.byteLength > this.maximumBytes) throw new RepositoryValidationError('The baseline package size is invalid.'); if (!zipSignatures.includes(Buffer.from(request.bytes.subarray(0, 4)).toString('hex'))) throw new RepositoryValidationError('The uploaded file is not a valid ZIP container.'); const sha256 = createHash('sha256').update(request.bytes).digest('hex'); const existing = (await this.list()).find(item => item.sha256 === sha256); if (existing) { if (this.analysisExecutor) await this.ensureAnalysis(existing); return existing; } const receipt = await this.artifactStore.put({ bytes: request.bytes, expectedSha256: sha256, maximumBytes: this.maximumBytes }); const metadata = detectMicrosoftSecurityBaselineMetadata(fileName); const record: MicrosoftSecurityBaselineRecord = { version: 1, id: `ms-baseline-${sha256.slice(0, 32)}`, fileName, sha256, sizeBytes: receipt.sizeBytes, storageKey: receipt.storageKey, importedAt: request.receivedAt, status: baselineCatalogStatus(metadata), metadata }; try { await this.repository.saveMicrosoftSecurityBaseline(record); } catch (error) { await this.artifactStore.remove(receipt.storageKey).catch(() => undefined); throw error; } await this.refreshDetectedMetadata(request.receivedAt); const stored = (await this.list()).find(item => item.id === record.id) ?? record; if (this.analysisExecutor) await this.ensureAnalysis(stored); return stored; } async listAnalyses(baselineId?: string): Promise { return this.repository.listMicrosoftSecurityBaselineAnalyses(baselineId); } async ensureAnalysis(record: MicrosoftSecurityBaselineRecord): Promise { if (!this.analysisExecutor) throw new RepositoryValidationError('The bounded Microsoft baseline analyzer is not configured.'); const existing = (await this.repository.listMicrosoftSecurityBaselineAnalyses(record.id)) .find(item => item.analysis.analyzer.version === MICROSOFT_BASELINE_SETTINGS_ANALYZER.version); if (existing) return existing; const bytes = await this.artifactStore.read({ storageKey: record.storageKey, expectedSha256: record.sha256, maximumBytes: this.maximumBytes }); const analysis = await this.analysisExecutor.executeMicrosoftBaseline({ bytes, options: { baselineId: record.id, analyzedAt: record.importedAt, expectedPackageSha256: record.sha256 } }); const previous = (await this.repository.listMicrosoftSecurityBaselineAnalyses(record.id)) .sort((left, right) => Date.parse(right.analysis.analyzedAt) - Date.parse(left.analysis.analyzedAt) || right.analysis.id.localeCompare(left.analysis.id))[0]; const stored: StoredMicrosoftSecurityBaselineAnalysis = { version: 1, analysis, supersedesAnalysisId: previous?.analysis.id }; await this.repository.saveMicrosoftSecurityBaselineAnalysis(stored); return (await this.repository.listMicrosoftSecurityBaselineAnalyses(record.id)).find(item => item.analysis.id === analysis.id) ?? stored; } async reprocess(baselineId: string): Promise { const record = (await this.list()).find(item => item.id === baselineId); if (!record) throw new RepositoryValidationError('The Microsoft baseline package is not present.'); return this.ensureAnalysis(record); } async reprocessPending(): Promise<{ analyzed: number; failed: number }> { let analyzed = 0; let failed = 0; for (const record of await this.list()) { const current = (await this.repository.listMicrosoftSecurityBaselineAnalyses(record.id)) .some(item => item.analysis.analyzer.version === MICROSOFT_BASELINE_SETTINGS_ANALYZER.version); if (current) continue; try { await this.ensureAnalysis(record); analyzed += 1; } catch { failed += 1; } } return { analyzed, failed }; } async select(context: MicrosoftSecurityBaselineSelectionContext) { return selectMicrosoftSecurityBaseline(await this.list(), context); } }