import { createHash } from 'node:crypto'; import type { StoredGpoBackupInstance, StoredGpoPolicyAnalysis } from '../../src/domain/gpo-analysis-persistence'; import type { GpoPolicyObservation, GpoPolicyValue, GpoRegistryPolicyValue } from '../../src/domain/gpo-policy'; import { GPO_POLICY_SNAPSHOT_AGGREGATOR, environmentProfileValidationErrors, storedPolicySnapshotAggregationValidationErrors, type AggregatedPolicyState, type CreateEnvironmentProfileRequest, type CreatePolicySnapshotAggregationRequest, type EnvironmentProfile, type EnvironmentProfileContext, type PolicyAggregationReviewHint, type StoredPolicySnapshotAggregation } from '../../src/domain/gpo-snapshot-aggregation'; import type { GpoTechnicalPolicyIdentity } from '../../src/domain/gpo-policy-identity'; import type { AssessorRepository } from '../../src/persistence/assessor-repository'; import { RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors'; import { canonicalJson } from '../../src/shared/canonical-json'; import { buildGpoAnalysisSnapshots } from '../../src/gpo/snapshot-builder'; 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)); } function applicableContexts(profile: EnvironmentProfile, observation: GpoPolicyObservation): EnvironmentProfileContext[] { return profile.contexts.filter(context => context.scope === 'mixed' || context.scope === observation.scope); } function valuesUnique(values: GpoPolicyValue[]): GpoPolicyValue[] { return [...new Map(values.map(value => [canonicalJson(value), value])).entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([, value]) => value); } function observationSequence(observation: GpoPolicyObservation): number { return observation.registryPolicy?.sequence ?? observation.preference?.sequence ?? 0; } interface ResolvedObservation { observation: GpoPolicyObservation; identity: GpoTechnicalPolicyIdentity; } interface BackupContribution { status: AggregatedPolicyState['status']; values: GpoPolicyValue[]; observationIds: string[]; targeting: AggregatedPolicyState['targeting']; diagnosticCodes: string[]; } const deleteOperations = new Set([ 'delete-value', 'delete-values', 'delete-all-values', 'delete-keys' ]); function reduceBackupContribution(entries: ResolvedObservation[]): BackupContribution { const observations = [...entries].sort((left, right) => observationSequence(left.observation) - observationSequence(right.observation) || left.observation.id.localeCompare(right.observation.id)); const observationIds = sortedUnique(observations.map(entry => entry.observation.id)); const diagnostics = new Set(observations.flatMap(entry => entry.observation.diagnosticCodes)); const targetingUnknown = observations.some(entry => entry.observation.preference?.targeting.status === 'present'); if (targetingUnknown) diagnostics.add('policy-aggregation-targeting-unknown'); if (observations.some(entry => entry.observation.state === 'conflicted')) { diagnostics.add('policy-aggregation-source-conflict'); return { status: 'conflicted', values: valuesUnique(observations.flatMap(entry => entry.observation.values)), observationIds, targeting: targetingUnknown ? 'unknown' : 'none', diagnosticCodes: [...diagnostics].sort() }; } const mechanism = observations[0].observation.deliveryMechanism; if (mechanism !== 'registry-policy') { diagnostics.add('policy-aggregation-delivery-semantics-unsupported'); return { status: 'indeterminate', values: valuesUnique(observations.flatMap(entry => entry.observation.values)), observationIds, targeting: targetingUnknown ? 'unknown' : 'none', diagnosticCodes: [...diagnostics].sort() }; } const last = observations[observations.length - 1].observation; const value = last.values[0] as GpoRegistryPolicyValue; if (targetingUnknown || !value || value.kind !== 'registry-policy' || value.operation === 'unsupported-special' || value.operation === 'set-value-if-absent') { diagnostics.add(value?.operation === 'set-value-if-absent' ? 'policy-aggregation-prior-state-required' : 'policy-aggregation-instruction-indeterminate'); return { status: 'indeterminate', values: valuesUnique(observations.flatMap(entry => entry.observation.values)), observationIds, targeting: targetingUnknown ? 'unknown' : 'none', diagnosticCodes: [...diagnostics].sort() }; } return { status: deleteOperations.has(value.operation) ? 'deleted' : 'configured', values: [value], observationIds, targeting: 'none', diagnosticCodes: [...diagnostics].sort() }; } function makeHint( runId: string, code: string, observationIds: string[], backupInstanceIds: string[], identityId?: string ): PolicyAggregationReviewHint { const sortedObservations = sortedUnique(observationIds); const sortedBackups = sortedUnique(backupInstanceIds); return { version: 1, id: stableId('policy-aggregation-hint', [ runId, code, identityId ?? '', ...sortedObservations, ...sortedBackups ].join('\0')), code, identityId, observationIds: sortedObservations, backupInstanceIds: sortedBackups }; } function aggregateIdentityGroup( runId: string, profile: EnvironmentProfile, entries: ResolvedObservation[] ): { state: AggregatedPolicyState; hints: PolicyAggregationReviewHint[] } { const identity = entries[0].identity; const mechanism = entries[0].observation.deliveryMechanism; const byBackup = new Map(); for (const entry of entries) { byBackup.set(entry.observation.backupInstanceId, [...(byBackup.get(entry.observation.backupInstanceId) ?? []), entry]); } const contributions = [...byBackup.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([backupInstanceId, values]) => ({ backupInstanceId, contribution: reduceBackupContribution(values) })); const diagnostics = new Set(contributions.flatMap(item => item.contribution.diagnosticCodes)); const signatures = new Set(contributions.map(item => canonicalJson({ status: item.contribution.status, values: item.contribution.values }))); const hasIndeterminate = contributions.some(item => item.contribution.status === 'indeterminate'); const hasConflict = contributions.some(item => item.contribution.status === 'conflicted') || signatures.size > 1; const status: AggregatedPolicyState['status'] = hasIndeterminate ? 'indeterminate' : hasConflict ? 'conflicted' : contributions[0].contribution.status; if (hasConflict) diagnostics.add('policy-aggregation-cross-gpo-conflict'); if (hasIndeterminate) diagnostics.add('policy-aggregation-indeterminate'); const observationIds = sortedUnique(contributions.flatMap(item => item.contribution.observationIds)); const backupInstanceIds = sortedUnique(contributions.map(item => item.backupInstanceId)); const contextIds = sortedUnique(entries.flatMap(entry => applicableContexts(profile, entry.observation).map(context => context.id))); const values = valuesUnique(contributions.flatMap(item => item.contribution.values)); const targeting = contributions.some(item => item.contribution.targeting === 'unknown') ? 'unknown' : 'none'; const ordering: AggregatedPolicyState['ordering'] = hasIndeterminate ? 'indeterminate' : contributions.length === 1 ? 'single-gpo' : hasConflict ? 'cross-gpo-conflict' : 'cross-gpo-identical'; const hints: PolicyAggregationReviewHint[] = []; if (status === 'conflicted') { hints.push(makeHint(runId, 'policy-aggregation-cross-gpo-conflict', observationIds, backupInstanceIds, identity.id)); } if (status === 'indeterminate') { hints.push(makeHint(runId, targeting === 'unknown' ? 'policy-aggregation-targeting-unknown' : 'policy-aggregation-indeterminate', observationIds, backupInstanceIds, identity.id)); } return { state: { version: 1, id: stableId('aggregated-policy-state', runId + '\0' + identity.id + '\0' + mechanism), aggregationRunId: runId, identity, deliveryMechanism: mechanism, status, values, observationIds, backupInstanceIds, applicableContextIds: contextIds, ordering, targeting, completeness: status === 'indeterminate' ? 'partial' : 'complete', effectiveState: 'unknown', diagnosticCodes: [...diagnostics].sort() }, hints }; } export interface PolicySnapshotAggregationBuildInput { profile: EnvironmentProfile; analyses: StoredGpoPolicyAnalysis[]; instances: StoredGpoBackupInstance[]; startedAt: string; completedAt: string; } export function buildPolicySnapshotAggregation(input: PolicySnapshotAggregationBuildInput): StoredPolicySnapshotAggregation { const sourceAnalysisRunIds = sortedUnique(input.analyses.map(analysis => analysis.run.id)); const runId = stableId('policy-snapshot-aggregation', [ input.profile.projectId, input.profile.id, GPO_POLICY_SNAPSHOT_AGGREGATOR.version, ...sourceAnalysisRunIds ].join('\0')); const snapshots = buildGpoAnalysisSnapshots(input.instances.map(record => record.instance), { createdAt: input.startedAt }); if (snapshots.length !== 1) { throw new RepositoryValidationError('Die ausgewaehlten Analyselaeufe muessen genau einen konsistent abgrenzbaren Backup-Snapshot bilden.'); } const snapshot = snapshots[0]; const hints: PolicyAggregationReviewHint[] = []; const diagnostics = new Set(snapshot.diagnosticCodes); const domainMismatch = Boolean(input.profile.domainKey) && (snapshot.domain.status !== 'resolved' || snapshot.domain.key !== input.profile.domainKey); const blocked = snapshot.status === 'inconsistent' || domainMismatch; if (domainMismatch) diagnostics.add('policy-aggregation-environment-domain-mismatch'); if (snapshot.status === 'inconsistent') diagnostics.add('policy-aggregation-snapshot-inconsistent'); if (blocked) { hints.push(makeHint(runId, domainMismatch ? 'policy-aggregation-environment-domain-mismatch' : 'policy-aggregation-snapshot-inconsistent', [], snapshot.instanceIds)); } if (snapshot.completeness !== 'complete') { diagnostics.add('policy-aggregation-snapshot-incomplete'); hints.push(makeHint(runId, 'policy-aggregation-snapshot-incomplete', [], snapshot.instanceIds)); } const observations = new Map(); for (const record of input.instances) { for (const observation of record.instance.policyObservations ?? []) observations.set(observation.id, observation); } const groups = new Map(); let unresolvedIdentityCount = 0; if (!blocked) { for (const analysis of input.analyses) { for (const resolution of analysis.resolutions) { const observation = observations.get(resolution.observationId); if (!observation) throw new RepositoryValidationError('Eine Identity-Aufloesung verweist nicht auf die ausgewaehlte Snapshotbeobachtung.'); if (resolution.resolution.status === 'unresolved') { unresolvedIdentityCount += 1; diagnostics.add(resolution.resolution.reasonCode); hints.push(makeHint(runId, resolution.resolution.reasonCode, [observation.id], [observation.backupInstanceId])); continue; } const contexts = applicableContexts(input.profile, observation); if (!contexts.length) { hints.push(makeHint(runId, 'policy-scope-outside-environment-profile', [observation.id], [observation.backupInstanceId], resolution.resolution.identity.id)); continue; } const key = resolution.resolution.identity.id + '\0' + observation.deliveryMechanism; groups.set(key, [...(groups.get(key) ?? []), { observation, identity: resolution.resolution.identity }]); } } } const states: AggregatedPolicyState[] = []; for (const [, entries] of [...groups].sort(([left], [right]) => left.localeCompare(right))) { const aggregated = aggregateIdentityGroup(runId, input.profile, entries); states.push(aggregated.state); hints.push(...aggregated.hints); aggregated.state.diagnosticCodes.forEach(code => diagnostics.add(code)); } states.sort((left, right) => left.id.localeCompare(right.id)); const uniqueHints = [...new Map(hints.map(hint => [hint.id, hint])).values()] .sort((left, right) => left.id.localeCompare(right.id)); const partial = snapshot.status !== 'consistent' || snapshot.completeness !== 'complete' || unresolvedIdentityCount > 0 || states.some(state => state.status === 'indeterminate'); const record: StoredPolicySnapshotAggregation = { version: 1, run: { version: 1, id: runId, projectId: input.profile.projectId, environmentProfileId: input.profile.id, sourceAnalysisRunIds, snapshotId: snapshot.id, aggregator: GPO_POLICY_SNAPSHOT_AGGREGATOR, status: blocked ? 'blocked' : partial ? 'partial' : 'complete', stateCount: states.length, conflictCount: states.filter(state => state.status === 'conflicted').length, unresolvedIdentityCount, reviewHintCount: uniqueHints.length, permitsFindingGeneration: false, diagnosticCodes: [...diagnostics].sort(), startedAt: input.startedAt, completedAt: input.completedAt }, snapshot, states, reviewHints: uniqueHints }; const errors = storedPolicySnapshotAggregationValidationErrors(record); if (errors.length) throw new RepositoryValidationError(errors.join(' ')); return record; } export class GpoPolicySnapshotService { private readonly now: () => Date; constructor(private readonly repository: AssessorRepository, options: { now?: () => Date } = {}) { this.now = options.now ?? (() => new Date()); } async createEnvironmentProfile(projectId: string, request: CreateEnvironmentProfileRequest): Promise { if (!request || typeof request.name !== 'string' || !request.name.trim() || !Array.isArray(request.contexts) || request.contexts.some(context => !context || typeof context.id !== 'string' || !['operating-system', 'product'].includes(context.kind) || typeof context.productFamily !== 'string' || typeof context.productVersion !== 'string' || typeof context.role !== 'string' || !['computer', 'user', 'mixed'].includes(context.scope)) || (request.supersedesProfileId !== undefined && typeof request.supersedesProfileId !== 'string') || (request.domainKey !== undefined && typeof request.domainKey !== 'string')) { throw new RepositoryValidationError('Die Environment-Profile-Anfrage ist ungueltig.'); } if (!await this.repository.getProject(projectId)) throw new RepositoryNotFoundError('Das Projekt des Environment Profiles ist nicht vorhanden.'); const profiles = await this.repository.listEnvironmentProfiles(projectId); const predecessor = request.supersedesProfileId ? profiles.find(profile => profile.id === request.supersedesProfileId) : undefined; if (request.supersedesProfileId && !predecessor) { throw new RepositoryNotFoundError('Das zu ersetzende Environment Profile ist nicht vorhanden.'); } const contexts = request.contexts.map(context => ({ ...context, version: 1 as const })) .sort((left, right) => left.id.localeCompare(right.id)); const semantic = canonicalJson({ projectId, name: request.name.trim(), supersedesProfileId: request.supersedesProfileId, domainKey: request.domainKey?.trim(), contexts }); const id = stableId('environment-profile', semantic); const existing = profiles.find(profile => profile.id === id); if (existing) return existing; const profile: EnvironmentProfile = { version: 1, id, projectId, name: request.name.trim(), revision: (predecessor?.revision ?? 0) + 1, supersedesProfileId: predecessor?.id, domainKey: request.domainKey?.trim() || undefined, contexts, createdAt: this.now().toISOString() }; const errors = environmentProfileValidationErrors(profile); if (errors.length) throw new RepositoryValidationError(errors.join(' ')); await this.repository.saveEnvironmentProfile(profile); return profile; } listEnvironmentProfiles(projectId: string): Promise { return this.repository.listEnvironmentProfiles(projectId); } async aggregate(projectId: string, request: CreatePolicySnapshotAggregationRequest): Promise { if (!request || typeof request.environmentProfileId !== 'string' || !request.environmentProfileId.trim() || !Array.isArray(request.sourceAnalysisRunIds) || request.sourceAnalysisRunIds.some(id => typeof id !== 'string' || !id.trim())) { throw new RepositoryValidationError('Die Policy-Snapshot-Aggregationsanfrage ist ungueltig.'); } const profile = (await this.repository.listEnvironmentProfiles(projectId)) .find(candidate => candidate.id === request.environmentProfileId); if (!profile) throw new RepositoryNotFoundError('Das Environment Profile ist im Projekt nicht vorhanden.'); const requestedIds = sortedUnique(request.sourceAnalysisRunIds); if (!requestedIds.length || requestedIds.length !== request.sourceAnalysisRunIds.length) { throw new RepositoryValidationError('Mindestens ein eindeutiger Identity-Analysis-Run ist erforderlich.'); } const allAnalyses = await this.repository.listGpoPolicyAnalyses(projectId); const analyses = requestedIds.map(id => allAnalyses.find(candidate => candidate.run.id === id)); if (analyses.some(analysis => !analysis)) { throw new RepositoryNotFoundError('Mindestens ein Identity-Analysis-Run ist im Projekt nicht vorhanden.'); } const selected = analyses as StoredGpoPolicyAnalysis[]; const expectedRunId = stableId('policy-snapshot-aggregation', [ projectId, profile.id, GPO_POLICY_SNAPSHOT_AGGREGATOR.version, ...requestedIds ].join('\0')); const existing = (await this.repository.listPolicySnapshotAggregations(projectId)) .find(record => record.run.id === expectedRunId); if (existing) return existing; const processingRunIds = new Set(selected.map(analysis => analysis.run.sourceProcessingRunId)); const instances = (await this.repository.listGpoBackupInstances(projectId)) .filter(record => processingRunIds.has(record.processingRunId)); if (!instances.length) throw new RepositoryNotFoundError('Die ausgewaehlten Analyselaeufe besitzen keine Backupinstanzen.'); const startedAt = this.now().toISOString(); const record = buildPolicySnapshotAggregation({ profile, analyses: selected, instances, startedAt, completedAt: this.now().toISOString() }); await this.repository.savePolicySnapshotAggregation(record); return record; } listAggregations(projectId: string): Promise { return this.repository.listPolicySnapshotAggregations(projectId); } }