whereKey($this->workspaceId)->first(); if (! $workspace instanceof Workspace) { return; } $operationRun = $this->resolveOperationRun($workspace, $operationRuns); if (! $operationRun instanceof OperationRun) { return; } if ($operationRun->status === OperationRunStatus::Completed->value) { return; } $operationRuns->updateRun( $operationRun, status: OperationRunStatus::Running->value, outcome: OperationRunOutcome::Pending->value, ); $windowStart = $this->resolveWindowStart($operationRun); try { $events = [ ...$this->highDriftEvents((int) $workspace->getKey(), $windowStart), ...$this->compareFailedEvents((int) $workspace->getKey(), $windowStart), ...$this->permissionMissingEvents((int) $workspace->getKey(), $windowStart), ]; $createdDeliveries = 0; foreach ($events as $event) { $createdDeliveries += $dispatchService->dispatchEvent($workspace, $event); } $operationRuns->updateRun( $operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Succeeded->value, summaryCounts: [ 'total' => count($events), 'processed' => count($events), 'created' => $createdDeliveries, ], ); } catch (Throwable $exception) { $operationRuns->updateRun( $operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, failures: [ [ 'code' => 'alerts.evaluate.failed', 'message' => $this->sanitizeErrorMessage($exception), ], ], ); throw $exception; } } private function resolveOperationRun(Workspace $workspace, OperationRunService $operationRuns): ?OperationRun { if (is_int($this->operationRunId) && $this->operationRunId > 0) { $operationRun = OperationRun::query() ->whereKey($this->operationRunId) ->where('workspace_id', (int) $workspace->getKey()) ->where('type', 'alerts.evaluate') ->first(); if ($operationRun instanceof OperationRun) { return $operationRun; } } $slotKey = CarbonImmutable::now('UTC')->format('YmdHi').'Z'; return $operationRuns->ensureWorkspaceRunWithIdentity( workspace: $workspace, type: 'alerts.evaluate', identityInputs: [ 'slot_key' => $slotKey, ], context: [ 'trigger' => 'job', 'slot_key' => $slotKey, ], initiator: null, ); } private function resolveWindowStart(OperationRun $operationRun): CarbonImmutable { $previous = OperationRun::query() ->where('workspace_id', (int) $operationRun->workspace_id) ->whereNull('tenant_id') ->where('type', 'alerts.evaluate') ->where('status', OperationRunStatus::Completed->value) ->whereNotNull('completed_at') ->where('id', '<', (int) $operationRun->getKey()) ->orderByDesc('completed_at') ->orderByDesc('id') ->first(); if ($previous instanceof OperationRun && $previous->completed_at !== null) { return CarbonImmutable::instance($previous->completed_at); } $lookbackMinutes = max(1, (int) config('tenantpilot.alerts.evaluate_initial_lookback_minutes', 15)); return CarbonImmutable::now('UTC')->subMinutes($lookbackMinutes); } /** * @return array> */ private function highDriftEvents(int $workspaceId, CarbonImmutable $windowStart): array { $findings = Finding::query() ->where('workspace_id', $workspaceId) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->whereIn('severity', [Finding::SEVERITY_HIGH, Finding::SEVERITY_CRITICAL]) ->where('status', Finding::STATUS_NEW) ->where('created_at', '>', $windowStart) ->orderBy('id') ->get(); $events = []; foreach ($findings as $finding) { $events[] = [ 'event_type' => 'high_drift', 'tenant_id' => (int) $finding->tenant_id, 'severity' => (string) $finding->severity, 'fingerprint_key' => 'finding:'.(int) $finding->getKey(), 'title' => 'High drift finding detected', 'body' => sprintf( 'Finding %d was created with severity %s.', (int) $finding->getKey(), (string) $finding->severity, ), 'metadata' => [ 'finding_id' => (int) $finding->getKey(), ], ]; } return $events; } /** * @return array> */ private function compareFailedEvents(int $workspaceId, CarbonImmutable $windowStart): array { $failedRuns = OperationRun::query() ->where('workspace_id', $workspaceId) ->whereNotNull('tenant_id') ->where('type', 'drift_generate_findings') ->where('status', OperationRunStatus::Completed->value) ->where('outcome', OperationRunOutcome::Failed->value) ->where('created_at', '>', $windowStart) ->orderBy('id') ->get(); $events = []; foreach ($failedRuns as $failedRun) { $tenantId = (int) ($failedRun->tenant_id ?? 0); if ($tenantId <= 0) { continue; } $events[] = [ 'event_type' => 'compare_failed', 'tenant_id' => $tenantId, 'severity' => 'high', 'fingerprint_key' => 'operation_run:'.(int) $failedRun->getKey(), 'title' => 'Drift compare failed', 'body' => $this->firstFailureMessage($failedRun), 'metadata' => [ 'operation_run_id' => (int) $failedRun->getKey(), ], ]; } return $events; } private function firstFailureMessage(OperationRun $run): string { $failures = is_array($run->failure_summary) ? $run->failure_summary : []; foreach ($failures as $failure) { if (! is_array($failure)) { continue; } $message = trim((string) ($failure['message'] ?? '')); if ($message !== '') { return $message; } } return 'A drift compare operation run failed.'; } private function sanitizeErrorMessage(Throwable $exception): string { $message = trim($exception->getMessage()); if ($message === '') { return 'Unexpected alert evaluation error.'; } $message = preg_replace('/https?:\/\/\S+/i', '[redacted-url]', $message) ?? $message; return mb_substr($message, 0, 500); } /** * @return array> */ private function permissionMissingEvents(int $workspaceId, CarbonImmutable $windowStart): array { $findings = Finding::query() ->where('workspace_id', $workspaceId) ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE) ->where('status', Finding::STATUS_NEW) ->where('updated_at', '>', $windowStart) ->orderBy('id') ->get(); $events = []; foreach ($findings as $finding) { $events[] = [ 'event_type' => AlertRule::EVENT_PERMISSION_MISSING, 'tenant_id' => (int) $finding->tenant_id, 'severity' => (string) $finding->severity, 'fingerprint_key' => 'finding:'.(int) $finding->getKey(), 'title' => 'Missing permission detected', 'body' => sprintf( 'Permission "%s" is missing for tenant %d (severity: %s).', (string) ($finding->evidence_jsonb['permission_key'] ?? $finding->subject_external_id ?? 'unknown'), (int) $finding->tenant_id, (string) $finding->severity, ), 'metadata' => [ 'finding_id' => (int) $finding->getKey(), 'permission_key' => (string) ($finding->evidence_jsonb['permission_key'] ?? ''), ], ]; } return $events; } }