completed_at || ! $current->completed_at) { throw new RuntimeException('Baseline/current run must be finished.'); } /** @var array $selection */ $selection = is_array($current->context) ? $current->context : []; $policyTypes = Arr::get($selection, 'policy_types'); if (! is_array($policyTypes)) { $policyTypes = []; } $policyTypes = array_values(array_filter(array_map('strval', $policyTypes))); $created = 0; $resolvedSeverity = $this->resolveSeverityForFindingType($tenant, Finding::FINDING_TYPE_DRIFT); Policy::query() ->where('tenant_id', $tenant->getKey()) ->whereIn('policy_type', $policyTypes) ->orderBy('id') ->chunk(200, function ($policies) use ($tenant, $baseline, $current, $scopeKey, $resolvedSeverity, &$created): void { foreach ($policies as $policy) { if (! $policy instanceof Policy) { continue; } $baselineVersion = $this->versionForRun($policy, $baseline); $currentVersion = $this->versionForRun($policy, $current); if ($baselineVersion instanceof PolicyVersion || $currentVersion instanceof PolicyVersion) { $policyType = (string) ($policy->policy_type ?? ''); $platform = is_string($policy->platform ?? null) ? $policy->platform : null; $baselineSnapshot = $baselineVersion instanceof PolicyVersion && is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : []; $currentSnapshot = $currentVersion instanceof PolicyVersion && is_array($currentVersion->snapshot) ? $currentVersion->snapshot : []; $baselineNormalized = $this->settingsNormalizer->normalizeForDiff($baselineSnapshot, $policyType, $platform); $currentNormalized = $this->settingsNormalizer->normalizeForDiff($currentSnapshot, $policyType, $platform); $baselineSnapshotHash = $this->hasher->hashNormalized($baselineNormalized); $currentSnapshotHash = $this->hasher->hashNormalized($currentNormalized); if ($baselineSnapshotHash !== $currentSnapshotHash) { $changeType = match (true) { $baselineVersion instanceof PolicyVersion && ! $currentVersion instanceof PolicyVersion => 'removed', ! $baselineVersion instanceof PolicyVersion && $currentVersion instanceof PolicyVersion => 'added', default => 'modified', }; $fingerprint = $this->hasher->fingerprint( tenantId: (int) $tenant->getKey(), scopeKey: $scopeKey, subjectType: 'policy', subjectExternalId: (string) $policy->external_id, changeType: $changeType, baselineHash: $baselineSnapshotHash, currentHash: $currentSnapshotHash, ); $rawEvidence = [ 'change_type' => $changeType, 'summary' => [ 'kind' => 'policy_snapshot', 'changed_fields' => ['snapshot_hash'], ], 'baseline' => [ 'policy_id' => $policy->external_id, 'policy_version_id' => $baselineVersion?->getKey(), 'snapshot_hash' => $baselineSnapshotHash, ], 'current' => [ 'policy_id' => $policy->external_id, 'policy_version_id' => $currentVersion?->getKey(), 'snapshot_hash' => $currentSnapshotHash, ], ]; $finding = Finding::query()->firstOrNew([ 'tenant_id' => $tenant->getKey(), 'fingerprint' => $fingerprint, ]); $wasNew = ! $finding->exists; $finding->forceFill([ 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'scope_key' => $scopeKey, 'baseline_operation_run_id' => $baseline->getKey(), 'current_operation_run_id' => $current->getKey(), 'subject_type' => 'policy', 'subject_external_id' => (string) $policy->external_id, 'severity' => $resolvedSeverity, 'evidence_jsonb' => $this->evidence->sanitize($rawEvidence), ]); if ($wasNew) { $finding->forceFill([ 'status' => Finding::STATUS_NEW, 'acknowledged_at' => null, 'acknowledged_by_user_id' => null, ]); } $finding->save(); if ($wasNew) { $created++; } } } if (! $baselineVersion instanceof PolicyVersion || ! $currentVersion instanceof PolicyVersion) { continue; } $baselineAssignments = is_array($baselineVersion->assignments) ? $baselineVersion->assignments : []; $currentAssignments = is_array($currentVersion->assignments) ? $currentVersion->assignments : []; $baselineAssignmentsHash = $this->hasher->hashNormalized($baselineAssignments); $currentAssignmentsHash = $this->hasher->hashNormalized($currentAssignments); if ($baselineAssignmentsHash !== $currentAssignmentsHash) { $fingerprint = $this->hasher->fingerprint( tenantId: (int) $tenant->getKey(), scopeKey: $scopeKey, subjectType: 'assignment', subjectExternalId: (string) $policy->external_id, changeType: 'modified', baselineHash: (string) ($baselineAssignmentsHash ?? ''), currentHash: (string) ($currentAssignmentsHash ?? ''), ); $rawEvidence = [ 'change_type' => 'modified', 'summary' => [ 'kind' => 'policy_assignments', 'changed_fields' => ['assignments_hash'], ], 'baseline' => [ 'policy_id' => $policy->external_id, 'policy_version_id' => $baselineVersion->getKey(), 'assignments_hash' => $baselineAssignmentsHash, ], 'current' => [ 'policy_id' => $policy->external_id, 'policy_version_id' => $currentVersion->getKey(), 'assignments_hash' => $currentAssignmentsHash, ], ]; $finding = Finding::query()->firstOrNew([ 'tenant_id' => $tenant->getKey(), 'fingerprint' => $fingerprint, ]); $wasNew = ! $finding->exists; $finding->forceFill([ 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'scope_key' => $scopeKey, 'baseline_operation_run_id' => $baseline->getKey(), 'current_operation_run_id' => $current->getKey(), 'subject_type' => 'assignment', 'subject_external_id' => (string) $policy->external_id, 'severity' => $resolvedSeverity, 'evidence_jsonb' => $this->evidence->sanitize($rawEvidence), ]); if ($wasNew) { $finding->forceFill([ 'status' => Finding::STATUS_NEW, 'acknowledged_at' => null, 'acknowledged_by_user_id' => null, ]); } $finding->save(); if ($wasNew) { $created++; } } $baselineScopeTagIds = $this->scopeTagsNormalizer->normalizeIdsForHash($baselineVersion->scope_tags); $currentScopeTagIds = $this->scopeTagsNormalizer->normalizeIdsForHash($currentVersion->scope_tags); if ($baselineScopeTagIds === null || $currentScopeTagIds === null) { continue; } $baselineScopeTagsHash = $this->hasher->hashNormalized($baselineScopeTagIds); $currentScopeTagsHash = $this->hasher->hashNormalized($currentScopeTagIds); if ($baselineScopeTagsHash === $currentScopeTagsHash) { continue; } $fingerprint = $this->hasher->fingerprint( tenantId: (int) $tenant->getKey(), scopeKey: $scopeKey, subjectType: 'scope_tag', subjectExternalId: (string) $policy->external_id, changeType: 'modified', baselineHash: $baselineScopeTagsHash, currentHash: $currentScopeTagsHash, ); $rawEvidence = [ 'change_type' => 'modified', 'summary' => [ 'kind' => 'policy_scope_tags', 'changed_fields' => ['scope_tags_hash'], ], 'baseline' => [ 'policy_id' => $policy->external_id, 'policy_version_id' => $baselineVersion->getKey(), 'scope_tags_hash' => $baselineScopeTagsHash, ], 'current' => [ 'policy_id' => $policy->external_id, 'policy_version_id' => $currentVersion->getKey(), 'scope_tags_hash' => $currentScopeTagsHash, ], ]; $finding = Finding::query()->firstOrNew([ 'tenant_id' => $tenant->getKey(), 'fingerprint' => $fingerprint, ]); $wasNew = ! $finding->exists; $finding->forceFill([ 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'scope_key' => $scopeKey, 'baseline_operation_run_id' => $baseline->getKey(), 'current_operation_run_id' => $current->getKey(), 'subject_type' => 'scope_tag', 'subject_external_id' => (string) $policy->external_id, 'severity' => $resolvedSeverity, 'evidence_jsonb' => $this->evidence->sanitize($rawEvidence), ]); if ($wasNew) { $finding->forceFill([ 'status' => Finding::STATUS_NEW, 'acknowledged_at' => null, 'acknowledged_by_user_id' => null, ]); } $finding->save(); if ($wasNew) { $created++; } } }); return $created; } private function versionForRun(Policy $policy, OperationRun $run): ?PolicyVersion { if (! $run->completed_at) { return null; } return PolicyVersion::query() ->where('tenant_id', $policy->tenant_id) ->where('policy_id', $policy->getKey()) ->where('captured_at', '<=', $run->completed_at) ->latest('captured_at') ->first(); } private function resolveSeverityForFindingType(Tenant $tenant, string $findingType): string { $workspace = $tenant->workspace; if (! $workspace instanceof Workspace && is_numeric($tenant->workspace_id)) { $workspace = Workspace::query()->whereKey((int) $tenant->workspace_id)->first(); } if (! $workspace instanceof Workspace) { return Finding::SEVERITY_MEDIUM; } $resolved = $this->settingsResolver->resolveValue( workspace: $workspace, domain: 'drift', key: 'severity_mapping', tenant: $tenant, ); if (! is_array($resolved)) { return Finding::SEVERITY_MEDIUM; } foreach ($resolved as $mappedFindingType => $mappedSeverity) { if (! is_string($mappedFindingType) || ! is_string($mappedSeverity)) { continue; } if ($mappedFindingType !== $findingType) { continue; } $normalizedSeverity = strtolower($mappedSeverity); if (in_array($normalizedSeverity, $this->supportedSeverities(), true)) { return $normalizedSeverity; } break; } return Finding::SEVERITY_MEDIUM; } /** * @return array */ private function supportedSeverities(): array { return [ Finding::SEVERITY_LOW, Finding::SEVERITY_MEDIUM, Finding::SEVERITY_HIGH, Finding::SEVERITY_CRITICAL, ]; } }