import { createHash } from 'node:crypto'; import type { AssessorRepository } from '../../src/persistence/assessor-repository'; import { RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors'; import { canonicalJson } from '../../src/shared/canonical-json'; import type { MicrosoftSecurityBaselineRecord } from '../../src/domain/microsoft-security-baseline'; import type { StoredMicrosoftSecurityBaselineAnalysis, MicrosoftSecurityBaselineSetting } from '../../src/domain/microsoft-security-baseline-analysis'; import type { AggregatedPolicyState, EnvironmentProfile, StoredPolicySnapshotAggregation } from '../../src/domain/gpo-snapshot-aggregation'; import { POLICY_COMPARATOR_CATALOG, POLICY_DEPENDENCY_RULESET, POLICY_EVALUATION_ENGINE, POLICY_RISK_RULESET, storedPolicyEvaluationValidationErrors, type CreatePolicyEvaluationRequest, type PolicyComparatorOutcome, type PolicyEvaluationEvidence, type PolicyEvaluationResult, type StoredPolicyEvaluation } from '../../src/domain/policy-evaluation'; import { comparePolicyValues } from '../../src/evaluation/policy-comparator'; import { selectBaselineTemplates } from '../../src/evaluation/baseline-template-context'; import { RELEASED_POLICY_EVALUATION_RULE_CATALOG, policyEvaluationRuleCatalogValidationErrors, type PolicyEvaluationRuleCatalog } from '../../src/domain/policy-evaluation-rules'; function stableId(prefix: string, value: string): string { return prefix + '-' + createHash('sha256').update(value).digest('hex').slice(0, 32); } function sortedUnique(values: string[]): string[] { return [...new Set(values)].sort((left, right) => left.localeCompare(right)); } interface EvaluationBuildInput { profile: EnvironmentProfile; aggregation: StoredPolicySnapshotAggregation; baselineRecords: MicrosoftSecurityBaselineRecord[]; baselineAnalyses: StoredMicrosoftSecurityBaselineAnalysis[]; startedAt: string; completedAt: string; ruleCatalog: PolicyEvaluationRuleCatalog; } interface EvaluationEvidenceInput { kind: PolicyEvaluationEvidence['kind']; sourceIds: string[]; } function evidenceFor(resultId: string, inputs: EvaluationEvidenceInput[]): PolicyEvaluationEvidence[] { return inputs.flatMap(input => sortedUnique(input.sourceIds).map(sourceId => ({ version: 1 as const, id: stableId('policy-evaluation-evidence', resultId + '\0' + input.kind + '\0' + sourceId), evaluationResultId: resultId, kind: input.kind, sourceId }))); } function evaluateBaselineIdentity( runId: string, analysis: StoredMicrosoftSecurityBaselineAnalysis, baseline: MicrosoftSecurityBaselineRecord, candidates: MicrosoftSecurityBaselineSetting[], state: AggregatedPolicyState | undefined, profile: EnvironmentProfile ): { result: PolicyEvaluationResult; evidence: PolicyEvaluationEvidence[] } { const diagnostics = new Set(); const selection = selectBaselineTemplates(candidates, profile); const settings = selection.settings; if (selection.status === 'selected') diagnostics.add('policy-evaluation-template-context-selected'); if (selection.status === 'no-match') diagnostics.add('policy-evaluation-template-context-no-match'); const identity = settings[0].identity.status === 'resolved' ? settings[0].identity.identity : undefined; if (!identity) throw new RepositoryValidationError('Eine Baselinegruppe besitzt keine aufgeloeste technische Identitaet.'); if (analysis.analysis.status !== 'complete' || analysis.analysis.admxCatalog.status !== 'complete') { diagnostics.add('policy-evaluation-baseline-incomplete'); } const recommendationSignatures = new Set(settings.map(setting => canonicalJson(setting.observation.values[0]))); if (recommendationSignatures.size > 1) diagnostics.add('policy-evaluation-baseline-consensus-conflict'); if (recommendationSignatures.size > 1 && selection.status === 'unresolved') { diagnostics.add('policy-evaluation-template-context-required'); } const customerSignatures = new Set(state?.values.map(value => canonicalJson(value)) ?? []); if (state && customerSignatures.size !== 1) diagnostics.add('policy-evaluation-customer-value-ambiguous'); const customerState: PolicyEvaluationResult['customerState'] = !state || state.status === 'deleted' ? 'missing' : state.status === 'conflicted' ? 'conflicted' : state.status === 'indeterminate' ? 'indeterminate' : 'configured'; if (customerState === 'conflicted') diagnostics.add('policy-evaluation-customer-conflict'); if (customerState === 'indeterminate') diagnostics.add('policy-evaluation-customer-state-incomplete'); let compared: PolicyComparatorOutcome = { version: 1, comparator: { kind: 'equality' }, relation: customerState === 'missing' ? 'different' : 'incomparable', baseline: undefined, diagnosticCodes: [] }; if (selection.status !== 'no-match' && recommendationSignatures.size === 1 && customerState === 'missing') { compared = comparePolicyValues(undefined, settings[0].observation.values[0], { kind: 'equality' }); if (compared.baseline !== undefined) { compared = { ...compared, relation: 'different', diagnosticCodes: [] }; } else { compared.diagnosticCodes.forEach(code => diagnostics.add(code)); } } else if (selection.status !== 'no-match' && recommendationSignatures.size === 1 && customerState === 'configured' && customerSignatures.size === 1 && state) { compared = comparePolicyValues(state.values[0], settings[0].observation.values[0], { kind: 'equality' }); compared.diagnosticCodes.forEach(code => diagnostics.add(code)); } const review = selection.status === 'no-match' || recommendationSignatures.size !== 1 || ['conflicted', 'indeterminate'].includes(customerState) || compared.relation === 'incomparable'; const classification: PolicyEvaluationResult['classification'] = review ? 'review_required' : compared.relation === 'match' ? 'configured_match' : 'confirmed_configured_deviation'; const resultId = stableId('policy-evaluation-result', runId + '\0' + analysis.analysis.id + '\0' + identity.id); const baselineAnalysisIds = [analysis.analysis.id]; const baselineSettingIds = sortedUnique(settings.map(setting => setting.id)); const result: PolicyEvaluationResult = { version: 1, id: resultId, evaluationRunId: runId, aggregationStateId: customerState === 'missing' ? undefined : state?.id, baselineAnalysisId: analysis.analysis.id, baselineId: baseline.id, baselineLabel: baseline.fileName.replace(/\.zip$/i, ''), customerState, identity, classification, baselineState: compared.diagnosticCodes.length ? 'unsupported-setting' : 'explicit-recommendation', comparator: compared, ruleIds: [], dependencyRuleIds: [], riskRuleIds: [], contextIds: sortedUnique([...selection.contextIds, ...(state?.applicableContextIds ?? [])]), observationIds: customerState === 'missing' ? [] : sortedUnique(state?.observationIds ?? []), backupInstanceIds: customerState === 'missing' ? [] : sortedUnique(state?.backupInstanceIds ?? []), baselineAnalysisIds, baselineSettingIds, baselineTemplateSelection: { status: selection.status, contextIds: selection.contextIds, candidateSettingIds: sortedUnique(candidates.map(setting => setting.id)) }, findingEligibility: 'ineligible', effectiveState: 'unknown', diagnosticCodes: [...diagnostics].sort() }; return { result, evidence: evidenceFor(resultId, [ { kind: 'customer-observation', sourceIds: result.observationIds }, { kind: 'gpo-backup', sourceIds: result.backupInstanceIds }, { kind: 'baseline-setting', sourceIds: result.baselineTemplateSelection!.candidateSettingIds }, { kind: 'baseline-analysis', sourceIds: baselineAnalysisIds }, { kind: 'environment-context', sourceIds: result.contextIds } ]) }; } export function buildPolicyEvaluation(input: EvaluationBuildInput): StoredPolicyEvaluation { const ruleCatalogErrors = policyEvaluationRuleCatalogValidationErrors(input.ruleCatalog); if (ruleCatalogErrors.length) throw new RepositoryValidationError(ruleCatalogErrors.join(' ')); const baselineAnalysisIds = sortedUnique(input.baselineAnalyses.map(record => record.analysis.id)); const runId = stableId('policy-evaluation-run', [ input.profile.projectId, input.aggregation.run.id, POLICY_EVALUATION_ENGINE.version, POLICY_COMPARATOR_CATALOG.version, POLICY_DEPENDENCY_RULESET.version, POLICY_RISK_RULESET.version, canonicalJson(input.ruleCatalog), ...baselineAnalysisIds ].join('\0')); const statesByIdentity = new Map(input.aggregation.states.map(state => [state.identity.id, state])); const baselinesById = new Map(input.baselineRecords.map(record => [record.id, record])); const built = input.baselineAnalyses.flatMap(analysis => { const baseline = baselinesById.get(analysis.analysis.baselineId); if (!baseline) throw new RepositoryValidationError('Eine Baselineanalyse verweist auf keinen Baselinekatalogeintrag.'); const grouped = new Map(); for (const setting of analysis.analysis.settings) { if (setting.identity.status !== 'resolved') continue; // A structurally parsed baseline row without an actual Microsoft // recommendation is not a benchmark and must not produce PASS/FAIL. if (setting.observation.values[0] === undefined) continue; const id = setting.identity.identity.id; grouped.set(id, [...(grouped.get(id) ?? []), setting]); } return [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right)) .map(([identityId, settings]) => evaluateBaselineIdentity(runId, analysis, baseline, settings, statesByIdentity.get(identityId), input.profile)); }); const results = built.map(item => item.result).sort((left, right) => left.id.localeCompare(right.id)); const evidence = built.flatMap(item => item.evidence).sort((left, right) => left.id.localeCompare(right.id)); const diagnostics = sortedUnique([ ...input.aggregation.run.diagnosticCodes, ...results.flatMap(result => result.diagnosticCodes) ]); const blocked = input.aggregation.run.status === 'blocked'; const partial = blocked || results.some(result => result.classification === 'review_required'); const record: StoredPolicyEvaluation = { version: 1, run: { version: 1, id: runId, projectId: input.profile.projectId, aggregationRunId: input.aggregation.run.id, environmentProfileId: input.profile.id, baselineAnalysisIds, evaluator: POLICY_EVALUATION_ENGINE, comparatorCatalog: POLICY_COMPARATOR_CATALOG, ruleCatalog: input.ruleCatalog, dependencyRules: POLICY_DEPENDENCY_RULESET, riskRules: POLICY_RISK_RULESET, status: blocked ? 'blocked' : partial ? 'partial' : 'complete', resultCount: results.length, notApplicableCount: results.filter(result => result.classification === 'not_applicable').length, matchCount: results.filter(result => result.classification === 'configured_match').length, deviationCount: results.filter(result => result.classification === 'confirmed_configured_deviation').length, reviewCount: results.filter(result => result.classification === 'review_required').length, projectedFindingCount: 0, diagnosticCodes: diagnostics, startedAt: input.startedAt, completedAt: input.completedAt }, results, evidence }; const errors = storedPolicyEvaluationValidationErrors(record); if (errors.length) throw new RepositoryValidationError(errors.join(' ')); return record; } export class GpoPolicyEvaluationService { private readonly now: () => Date; private readonly ruleCatalog: PolicyEvaluationRuleCatalog; constructor(private readonly repository: AssessorRepository, options: { now?: () => Date; ruleCatalog?: PolicyEvaluationRuleCatalog; } = {}) { this.now = options.now ?? (() => new Date()); this.ruleCatalog = structuredClone(options.ruleCatalog ?? RELEASED_POLICY_EVALUATION_RULE_CATALOG); const errors = policyEvaluationRuleCatalogValidationErrors(this.ruleCatalog); if (errors.length) throw new RepositoryValidationError(errors.join(' ')); } async evaluate(projectId: string, request: CreatePolicyEvaluationRequest): Promise { if (!request || typeof request.aggregationRunId !== 'string' || !request.aggregationRunId.trim() || !Array.isArray(request.baselineAnalysisIds) || request.baselineAnalysisIds.some(id => typeof id !== 'string' || !id.trim())) { throw new RepositoryValidationError('Die Policy-Evaluation-Anfrage ist ungueltig.'); } const baselineAnalysisIds = sortedUnique(request.baselineAnalysisIds); if (!baselineAnalysisIds.length || baselineAnalysisIds.length !== request.baselineAnalysisIds.length) { throw new RepositoryValidationError('Mindestens ein eindeutiger Baseline-Analysis-Run ist erforderlich.'); } const aggregation = (await this.repository.listPolicySnapshotAggregations(projectId)) .find(record => record.run.id === request.aggregationRunId); if (!aggregation) throw new RepositoryNotFoundError('Der Policy-Snapshot-Aggregationslauf ist im Projekt nicht vorhanden.'); const profile = (await this.repository.listEnvironmentProfiles(projectId)) .find(record => record.id === aggregation.run.environmentProfileId); if (!profile) throw new RepositoryNotFoundError('Das Environment Profile des Aggregationslaufs ist nicht vorhanden.'); const allAnalyses = await this.repository.listMicrosoftSecurityBaselineAnalyses(); const analyses = baselineAnalysisIds.map(id => allAnalyses.find(record => record.analysis.id === id)); if (analyses.some(record => !record)) throw new RepositoryNotFoundError('Mindestens eine Baselineanalyse ist nicht vorhanden.'); const selected = analyses as StoredMicrosoftSecurityBaselineAnalysis[]; const expectedId = stableId('policy-evaluation-run', [ projectId, aggregation.run.id, POLICY_EVALUATION_ENGINE.version, POLICY_COMPARATOR_CATALOG.version, POLICY_DEPENDENCY_RULESET.version, POLICY_RISK_RULESET.version, canonicalJson(this.ruleCatalog), ...baselineAnalysisIds ].join('\0')); const existing = (await this.repository.listPolicyEvaluations(projectId)).find(record => record.run.id === expectedId); if (existing) return existing; const baselineRecords = await this.repository.listMicrosoftSecurityBaselines(); const startedAt = this.now().toISOString(); const record = buildPolicyEvaluation({ profile, aggregation, baselineRecords, baselineAnalyses: selected, ruleCatalog: this.ruleCatalog, startedAt, completedAt: this.now().toISOString() }); await this.repository.savePolicyEvaluation(record); return record; } list(projectId: string): Promise { return this.repository.listPolicyEvaluations(projectId); } catalog(): PolicyEvaluationRuleCatalog { return structuredClone(this.ruleCatalog); } }