189 lines
8.5 KiB
TypeScript
189 lines
8.5 KiB
TypeScript
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<string, FindingDefinition>();
|
|
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 = states.get(result.aggregationStateId);
|
|
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<StoredPolicyFindingRun> {
|
|
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<StoredPolicyFindingRun[]> {
|
|
return this.repository.listPolicyFindingRuns(projectId);
|
|
}
|
|
}
|