import { createHash } from 'node:crypto'; import type { FindingDefinition } from '../../src/domain/canonical-model'; import { POLICY_FINDING_FACTORY, policyFindingCandidateInvariantCodes, storedPolicyFindingRunValidationErrors, type CreatePolicyFindingRunRequest, type PolicyFindingOccurrence, type PolicyFindingRejection, type StoredPolicyFindingRun } from '../../src/domain/policy-finding'; import type { EnvironmentProfile, StoredPolicySnapshotAggregation } from '../../src/domain/gpo-snapshot-aggregation'; import type { StoredPolicyEvaluation } from '../../src/domain/policy-evaluation'; import type { AssessorRepository } from '../../src/persistence/assessor-repository'; import { RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors'; 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 PolicyFindingBuildInput { evaluation: StoredPolicyEvaluation; aggregation: StoredPolicySnapshotAggregation; profile: EnvironmentProfile; startedAt: string; completedAt: string; } function definitionFor( result: StoredPolicyEvaluation['results'][number], riskRuleId: string, createdAt: string ): FindingDefinition { const canonicalKey = `policy-risk:${riskRuleId}:${result.identity.id}`; return { id: stableId('finding-definition', canonicalKey), canonicalKey, identity: { version: 1, strategy: 'tool-rule-id', durable: true, evidence: [riskRuleId, result.identity.id] }, title: `Konfigurierte Policy-Abweichung (${riskRuleId})`, description: 'Die eindeutig konfigurierte Policy-Einstellung weicht von der ausdruecklichen Baselineempfehlung ab. Scope unbekannt; Wirksamkeit nicht verifiziert.', category: 'group-policy-baseline', frameworkReferences: [], createdAt, updatedAt: createdAt }; } export function buildPolicyFindingRun(input: PolicyFindingBuildInput): StoredPolicyFindingRun { if (input.evaluation.run.projectId !== input.aggregation.run.projectId || input.evaluation.run.projectId !== input.profile.projectId || input.evaluation.run.aggregationRunId !== input.aggregation.run.id || input.evaluation.run.environmentProfileId !== input.profile.id) { throw new RepositoryValidationError('Policy-Evaluation, Snapshot und Environment Profile gehoeren nicht zusammen.'); } const runId = stableId('policy-finding-run', input.evaluation.run.id + '\0' + POLICY_FINDING_FACTORY.version); const eligible = input.evaluation.results .filter(result => result.findingEligibility === 'eligible') .sort((left, right) => left.id.localeCompare(right.id)); const definitions = new Map(); const occurrences: PolicyFindingOccurrence[] = []; const rejections: PolicyFindingRejection[] = []; const states = new Map(input.aggregation.states.map(state => [state.id, state])); for (const result of eligible) { const codes = new Set(policyFindingCandidateInvariantCodes(input.evaluation, result)); const state = result.aggregationStateId ? states.get(result.aggregationStateId) : undefined; if (!state || state.identity.id !== result.identity.id) codes.add('policy-finding-aggregation-state-missing'); else { if (state.status !== 'configured') codes.add('policy-finding-customer-state-not-configured'); if (state.completeness !== 'complete') codes.add('policy-finding-customer-state-incomplete'); if (state.targeting !== 'none') codes.add('policy-finding-targeting-unresolved'); if (state.effectiveState !== 'unknown') codes.add('policy-finding-effective-state-invalid'); } if (input.aggregation.snapshot.completeness !== 'complete') codes.add('policy-finding-snapshot-incomplete'); const riskRuleId = result.riskRuleIds[0]; const riskRules = input.evaluation.run.ruleCatalog.rules .filter(rule => rule.riskRule?.id === riskRuleId && result.ruleIds.includes(rule.id)) .map(rule => rule.riskRule!); const severities = new Set(riskRules.map(rule => rule.severity)); if (!riskRuleId || riskRules.length < 1 || severities.size !== 1) codes.add('policy-finding-risk-rule-unresolved'); if (codes.size) { rejections.push({ version: 1, evaluationResultId: result.id, diagnosticCodes: [...codes].sort() }); continue; } const definition = definitionFor(result, riskRuleId, input.evaluation.run.completedAt); definitions.set(definition.id, definition); occurrences.push({ version: 1, id: stableId('policy-finding-occurrence', result.id + '\0' + POLICY_FINDING_FACTORY.version), factoryRunId: runId, projectId: input.evaluation.run.projectId, evaluationRunId: input.evaluation.run.id, evaluationResultId: result.id, aggregationRunId: input.aggregation.run.id, aggregationStateId: result.aggregationStateId!, snapshotId: input.aggregation.run.snapshotId, environmentProfileId: input.profile.id, environmentProfileRevision: input.profile.revision, definitionId: definition.id, identityId: result.identity.id, riskRuleId, severity: [...severities][0], classification: 'confirmed_configured_deviation', outcome: 'fail', scopeState: 'unknown', effectiveState: 'not-verified', evidenceIds: sortedUnique(input.evaluation.evidence .filter(item => item.evaluationResultId === result.id).map(item => item.id)), createdAt: input.completedAt }); } const record: StoredPolicyFindingRun = { version: 1, run: { version: 1, id: runId, projectId: input.evaluation.run.projectId, evaluationRunId: input.evaluation.run.id, factory: POLICY_FINDING_FACTORY, status: rejections.length ? 'partial' : 'complete', eligibleResultCount: eligible.length, findingCount: occurrences.length, rejectedResultCount: rejections.length, startedAt: input.startedAt, completedAt: input.completedAt }, definitions: [...definitions.values()].sort((left, right) => left.id.localeCompare(right.id)), occurrences: occurrences.sort((left, right) => left.id.localeCompare(right.id)), rejections: rejections.sort((left, right) => left.evaluationResultId.localeCompare(right.evaluationResultId)) }; const errors = storedPolicyFindingRunValidationErrors(record); if (errors.length) throw new RepositoryValidationError(errors.join(' ')); return record; } export class GpoPolicyFindingService { private readonly now: () => Date; constructor(private readonly repository: AssessorRepository, options: { now?: () => Date } = {}) { this.now = options.now ?? (() => new Date()); } async create(projectId: string, request: CreatePolicyFindingRunRequest): Promise { if (!request || typeof request.evaluationRunId !== 'string' || !request.evaluationRunId.trim()) { throw new RepositoryValidationError('Die Policy-Finding-Anfrage ist ungueltig.'); } const evaluation = (await this.repository.listPolicyEvaluations(projectId)) .find(record => record.run.id === request.evaluationRunId); if (!evaluation) throw new RepositoryNotFoundError('Die Policy-Evaluation ist im Projekt nicht vorhanden.'); const expectedId = stableId('policy-finding-run', evaluation.run.id + '\0' + POLICY_FINDING_FACTORY.version); const existing = (await this.repository.listPolicyFindingRuns(projectId)) .find(record => record.run.id === expectedId); if (existing) return existing; const aggregation = (await this.repository.listPolicySnapshotAggregations(projectId)) .find(record => record.run.id === evaluation.run.aggregationRunId); if (!aggregation) throw new RepositoryNotFoundError('Der Policy-Snapshot der Evaluation ist nicht vorhanden.'); const profile = (await this.repository.listEnvironmentProfiles(projectId)) .find(record => record.id === evaluation.run.environmentProfileId); if (!profile) throw new RepositoryNotFoundError('Das Environment Profile der Evaluation ist nicht vorhanden.'); const startedAt = this.now().toISOString(); const record = buildPolicyFindingRun({ evaluation, aggregation, profile, startedAt, completedAt: this.now().toISOString() }); await this.repository.savePolicyFindingRun(record); return record; } list(projectId: string): Promise { return this.repository.listPolicyFindingRuns(projectId); } }