finished_at || ! $current->finished_at) { throw new RuntimeException('Baseline/current run must be finished.'); } /** @var array $selection */ $selection = is_array($current->selection_payload) ? $current->selection_payload : []; $policyTypes = Arr::get($selection, 'policy_types'); if (! is_array($policyTypes)) { $policyTypes = []; } $policyTypes = array_values(array_filter(array_map('strval', $policyTypes))); $created = 0; Policy::query() ->where('tenant_id', $tenant->getKey()) ->whereIn('policy_type', $policyTypes) ->orderBy('id') ->chunk(200, function ($policies) use ($tenant, $baseline, $current, $scopeKey, &$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_run_id' => $baseline->getKey(), 'current_run_id' => $current->getKey(), 'subject_type' => 'policy', 'subject_external_id' => (string) $policy->external_id, 'severity' => Finding::SEVERITY_MEDIUM, '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_run_id' => $baseline->getKey(), 'current_run_id' => $current->getKey(), 'subject_type' => 'assignment', 'subject_external_id' => (string) $policy->external_id, 'severity' => Finding::SEVERITY_MEDIUM, '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_run_id' => $baseline->getKey(), 'current_run_id' => $current->getKey(), 'subject_type' => 'scope_tag', 'subject_external_id' => (string) $policy->external_id, 'severity' => Finding::SEVERITY_MEDIUM, '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, InventorySyncRun $run): ?PolicyVersion { if (! $run->finished_at) { return null; } return PolicyVersion::query() ->where('tenant_id', $policy->tenant_id) ->where('policy_id', $policy->getKey()) ->where('captured_at', '<=', $run->finished_at) ->latest('captured_at') ->first(); } }