operationRun = $run; } /** * @return array */ public function middleware(): array { return [new TrackOperationRun]; } public function handle( BaselineSnapshotIdentity $identity, AuditLogger $auditLogger, OperationRunService $operationRunService, ): void { if (! $this->operationRun instanceof OperationRun) { $this->fail(new RuntimeException('OperationRun context is required for CaptureBaselineSnapshotJob.')); return; } $context = is_array($this->operationRun->context) ? $this->operationRun->context : []; $profileId = (int) ($context['baseline_profile_id'] ?? 0); $sourceTenantId = (int) ($context['source_tenant_id'] ?? 0); $profile = BaselineProfile::query()->find($profileId); if (! $profile instanceof BaselineProfile) { throw new RuntimeException("BaselineProfile #{$profileId} not found."); } $sourceTenant = Tenant::query()->find($sourceTenantId); if (! $sourceTenant instanceof Tenant) { throw new RuntimeException("Source Tenant #{$sourceTenantId} not found."); } $initiator = $this->operationRun->user_id ? User::query()->find($this->operationRun->user_id) : null; $effectiveScope = BaselineScope::fromJsonb($context['effective_scope'] ?? null); $this->auditStarted($auditLogger, $sourceTenant, $profile, $initiator); $snapshotItems = $this->collectSnapshotItems($sourceTenant, $effectiveScope, $identity); $identityHash = $identity->computeIdentity($snapshotItems); $snapshot = $this->findOrCreateSnapshot( $profile, $identityHash, $snapshotItems, ); $wasNewSnapshot = $snapshot->wasRecentlyCreated; if ($profile->status === BaselineProfile::STATUS_ACTIVE) { $profile->update(['active_snapshot_id' => $snapshot->getKey()]); } $summaryCounts = [ 'total' => count($snapshotItems), 'processed' => count($snapshotItems), 'succeeded' => count($snapshotItems), 'failed' => 0, ]; $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Succeeded->value, summaryCounts: $summaryCounts, ); $updatedContext = is_array($this->operationRun->context) ? $this->operationRun->context : []; $updatedContext['result'] = [ 'snapshot_id' => (int) $snapshot->getKey(), 'snapshot_identity_hash' => $identityHash, 'was_new_snapshot' => $wasNewSnapshot, 'items_captured' => count($snapshotItems), ]; $this->operationRun->update(['context' => $updatedContext]); $this->auditCompleted($auditLogger, $sourceTenant, $profile, $snapshot, $initiator, $snapshotItems); } /** * @return array}> */ private function collectSnapshotItems( Tenant $sourceTenant, BaselineScope $scope, BaselineSnapshotIdentity $identity, ): array { $query = InventoryItem::query() ->where('tenant_id', $sourceTenant->getKey()); if (! $scope->isEmpty()) { $query->whereIn('policy_type', $scope->policyTypes); } $items = []; $query->orderBy('policy_type') ->orderBy('external_id') ->chunk(500, function ($inventoryItems) use (&$items, $identity): void { foreach ($inventoryItems as $inventoryItem) { $metaJsonb = is_array($inventoryItem->meta_jsonb) ? $inventoryItem->meta_jsonb : []; $baselineHash = $identity->hashItemContent($metaJsonb); $items[] = [ 'subject_type' => 'policy', 'subject_external_id' => (string) $inventoryItem->external_id, 'policy_type' => (string) $inventoryItem->policy_type, 'baseline_hash' => $baselineHash, 'meta_jsonb' => [ 'display_name' => $inventoryItem->display_name, 'category' => $inventoryItem->category, 'platform' => $inventoryItem->platform, ], ]; } }); return $items; } /** * @param array}> $snapshotItems */ private function findOrCreateSnapshot( BaselineProfile $profile, string $identityHash, array $snapshotItems, ): BaselineSnapshot { $existing = BaselineSnapshot::query() ->where('workspace_id', $profile->workspace_id) ->where('baseline_profile_id', $profile->getKey()) ->where('snapshot_identity_hash', $identityHash) ->first(); if ($existing instanceof BaselineSnapshot) { return $existing; } $snapshot = BaselineSnapshot::create([ 'workspace_id' => (int) $profile->workspace_id, 'baseline_profile_id' => (int) $profile->getKey(), 'snapshot_identity_hash' => $identityHash, 'captured_at' => now(), 'summary_jsonb' => [ 'total_items' => count($snapshotItems), 'policy_type_counts' => $this->countByPolicyType($snapshotItems), ], ]); foreach (array_chunk($snapshotItems, 100) as $chunk) { $rows = array_map( fn (array $item): array => [ 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'subject_type' => $item['subject_type'], 'subject_external_id' => $item['subject_external_id'], 'policy_type' => $item['policy_type'], 'baseline_hash' => $item['baseline_hash'], 'meta_jsonb' => json_encode($item['meta_jsonb']), 'created_at' => now(), 'updated_at' => now(), ], $chunk, ); BaselineSnapshotItem::insert($rows); } return $snapshot; } /** * @param array $items * @return array */ private function countByPolicyType(array $items): array { $counts = []; foreach ($items as $item) { $type = (string) $item['policy_type']; $counts[$type] = ($counts[$type] ?? 0) + 1; } ksort($counts); return $counts; } private function auditStarted( AuditLogger $auditLogger, Tenant $tenant, BaselineProfile $profile, ?User $initiator, ): void { $auditLogger->log( tenant: $tenant, action: 'baseline.capture.started', context: [ 'metadata' => [ 'operation_run_id' => (int) $this->operationRun->getKey(), 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_profile_name' => (string) $profile->name, ], ], actorId: $initiator?->id, actorEmail: $initiator?->email, actorName: $initiator?->name, resourceType: 'baseline_profile', resourceId: (string) $profile->getKey(), ); } private function auditCompleted( AuditLogger $auditLogger, Tenant $tenant, BaselineProfile $profile, BaselineSnapshot $snapshot, ?User $initiator, array $snapshotItems, ): void { $auditLogger->log( tenant: $tenant, action: 'baseline.capture.completed', context: [ 'metadata' => [ 'operation_run_id' => (int) $this->operationRun->getKey(), 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_profile_name' => (string) $profile->name, 'snapshot_id' => (int) $snapshot->getKey(), 'snapshot_identity_hash' => (string) $snapshot->snapshot_identity_hash, 'items_captured' => count($snapshotItems), 'was_new_snapshot' => $snapshot->wasRecentlyCreated, ], ], actorId: $initiator?->id, actorEmail: $initiator?->email, actorName: $initiator?->name, resourceType: 'operation_run', resourceId: (string) $this->operationRun->getKey(), ); } }