*/ private const array TERMINAL_STATUSES = [ FindingException::STATUS_REJECTED, FindingException::STATUS_REVOKED, FindingException::STATUS_SUPERSEDED, ]; /** * @param array $visibleTenants * @return array{ * rows: list>, * counts: array{open: int, recently_closed: int}, * } */ public function build(Workspace $workspace, array $visibleTenants, string $registerState = 'open'): array { $visibleTenantIds = array_values(array_map( static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(), $visibleTenants, )); if ($visibleTenantIds === []) { return [ 'rows' => [], 'counts' => [ 'open' => 0, 'recently_closed' => 0, ], ]; } $rows = FindingException::query() ->where('workspace_id', (int) $workspace->getKey()) ->whereIn('managed_environment_id', $visibleTenantIds) ->with(['tenant', 'owner:id,name', 'currentDecision', 'evidenceReferences', 'finding.currentRun', 'finding.baselineRun']) ->get() ->map(fn (FindingException $exception): ?array => $this->buildRow($exception)) ->filter() ->values(); /** @var Collection> $openRows */ $openRows = $rows ->where('register_state', 'open') ->sortBy([ ['due_at', 'asc'], ['exception_id', 'asc'], ]) ->values(); /** @var Collection> $recentlyClosedRows */ $recentlyClosedRows = $rows ->where('register_state', 'recently_closed') ->sortByDesc('decision_at') ->values(); return [ 'rows' => match ($registerState) { 'recently_closed' => $recentlyClosedRows->all(), default => $openRows->all(), }, 'counts' => [ 'open' => $openRows->count(), 'recently_closed' => $recentlyClosedRows->count(), ], ]; } /** * @return array|null */ private function buildRow(FindingException $exception): ?array { $currentDecision = $exception->currentDecision; if (! $currentDecision instanceof FindingExceptionDecision) { return null; } $registerState = $this->resolveRegisterState($exception, $currentDecision); if ($registerState === null) { return null; } $proofMetadata = $this->buildProofMetadata($exception); $operationRunMetadata = $this->buildOperationRunMetadata($exception); return [ 'exception_id' => (int) $exception->getKey(), 'register_state' => $registerState, 'tenant_name' => $exception->tenant?->name, 'owner_name' => $exception->owner?->name, 'status' => (string) $exception->status, 'current_validity_state' => (string) $exception->current_validity_state, 'next_action_label' => $registerState === 'open' ? $this->resolveNextActionLabel($exception, $currentDecision) : 'Decision closed', 'closure_reason' => $registerState === 'recently_closed' ? (string) $currentDecision->reason : null, 'due_at' => $exception->review_due_at ?? $exception->expires_at, 'decision_at' => $currentDecision->decided_at, ...$proofMetadata, ...$operationRunMetadata, ]; } /** * @return array{ * proof_count: int, * proof_state: string, * proof_label: string, * proof_url: string|null, * proof_url_label: string|null, * proof_unavailable_reason: string|null, * } */ private function buildProofMetadata(FindingException $exception): array { $references = $this->evidenceReferences($exception); $proofCount = $references->count(); if ($proofCount === 0) { return [ 'proof_count' => 0, 'proof_state' => 'not_linked', 'proof_label' => 'No linked proof', 'proof_url' => null, 'proof_url_label' => null, 'proof_unavailable_reason' => null, ]; } if ($proofCount === 1) { $directLink = $this->resolveDirectProofLink($exception, $references->first()); if (is_array($directLink)) { return [ 'proof_count' => $proofCount, 'proof_state' => $directLink['state'], 'proof_label' => $this->proofLabel($proofCount), 'proof_url' => $directLink['url'], 'proof_url_label' => $directLink['label'], 'proof_unavailable_reason' => null, ]; } } return [ 'proof_count' => $proofCount, 'proof_state' => 'linked_detail_section', 'proof_label' => $this->proofLabel($proofCount), 'proof_url' => $this->findingExceptionDetailUrl($exception), 'proof_url_label' => 'View proof', 'proof_unavailable_reason' => null, ]; } /** * @return array{ * operation_run_state: string, * operation_run_url: string|null, * operation_run_label: string, * } */ private function buildOperationRunMetadata(FindingException $exception): array { $run = $this->sourceFindingOperationRun($exception); if (! $run instanceof OperationRun) { $runs = $this->evidenceOperationRunsFor($exception) ->unique(fn (OperationRun $run): int => (int) $run->getKey()) ->values(); if ($runs->count() !== 1) { return [ 'operation_run_state' => $runs->isEmpty() ? 'not_linked' : 'run_not_available', 'operation_run_url' => null, 'operation_run_label' => 'No operation linked', ]; } /** @var OperationRun $run */ $run = $runs->first(); } if (! $this->canViewOperationRun($run, $exception)) { return [ 'operation_run_state' => 'run_not_available', 'operation_run_url' => null, 'operation_run_label' => 'No operation linked', ]; } return [ 'operation_run_state' => 'linked_run', 'operation_run_url' => OperationRunLinks::tenantlessView($run), 'operation_run_label' => 'View operation', ]; } private function sourceFindingOperationRun(FindingException $exception): ?OperationRun { $finding = $exception->relationLoaded('finding') ? $exception->finding : $exception->finding()->with(['currentRun', 'baselineRun'])->first(); if (! $finding instanceof Finding) { return null; } if ($finding->currentRun instanceof OperationRun) { return $finding->currentRun; } return $finding->baselineRun instanceof OperationRun ? $finding->baselineRun : null; } /** * @return Collection */ private function evidenceReferences(FindingException $exception): Collection { if ($exception->relationLoaded('evidenceReferences')) { return $exception->evidenceReferences ->filter(fn (mixed $reference): bool => $reference instanceof FindingExceptionEvidenceReference) ->values(); } return $exception->evidenceReferences()->get(); } /** * @return array{state: string, url: string, label: string}|null */ private function resolveDirectProofLink(FindingException $exception, ?FindingExceptionEvidenceReference $reference): ?array { if (! $reference instanceof FindingExceptionEvidenceReference) { return null; } if ($this->isEvidenceSnapshotReference($reference)) { $snapshot = $this->resolveEvidenceSnapshot($exception, $reference); if ($snapshot instanceof EvidenceSnapshot && $this->canViewEvidenceSnapshot($snapshot)) { return [ 'state' => 'linked_evidence', 'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin', tenant: $snapshot->tenant), 'label' => 'View evidence', ]; } } if ($this->isStoredReportReference($reference)) { $report = $this->resolveStoredReport($exception, $reference); if ($report instanceof StoredReport && $this->canViewStoredReport($report)) { return [ 'state' => 'linked_report', 'url' => StoredReportResource::getUrl('view', ['record' => $report], panel: 'admin', tenant: $report->tenant), 'label' => 'View report', ]; } } return null; } /** * @return Collection */ private function evidenceOperationRunsFor(FindingException $exception): Collection { return $this->evidenceReferences($exception) ->filter(fn (FindingExceptionEvidenceReference $reference): bool => $this->isEvidenceSnapshotReference($reference)) ->map(fn (FindingExceptionEvidenceReference $reference): ?EvidenceSnapshot => $this->resolveEvidenceSnapshot($exception, $reference)) ->filter(fn (?EvidenceSnapshot $snapshot): bool => $snapshot instanceof EvidenceSnapshot) ->map(fn (EvidenceSnapshot $snapshot): ?OperationRun => $snapshot->operationRun) ->filter(fn (?OperationRun $run): bool => $run instanceof OperationRun) ->values(); } private function resolveEvidenceSnapshot(FindingException $exception, FindingExceptionEvidenceReference $reference): ?EvidenceSnapshot { $sourceId = $this->numericSourceId($reference); if ($sourceId === null) { return null; } return EvidenceSnapshot::query() ->with(['tenant', 'operationRun']) ->whereKey($sourceId) ->where('workspace_id', (int) $exception->workspace_id) ->where('managed_environment_id', (int) $exception->managed_environment_id) ->first(); } private function resolveStoredReport(FindingException $exception, FindingExceptionEvidenceReference $reference): ?StoredReport { $sourceId = $this->numericSourceId($reference); if ($sourceId === null) { return null; } return StoredReport::query() ->with('tenant') ->whereKey($sourceId) ->where('workspace_id', (int) $exception->workspace_id) ->where('managed_environment_id', (int) $exception->managed_environment_id) ->whereIn('report_type', StoredReportResource::supportedReportTypes()) ->first(); } private function canViewEvidenceSnapshot(EvidenceSnapshot $snapshot): bool { $user = auth()->user(); $tenant = $snapshot->tenant; return $user instanceof User && $tenant instanceof ManagedEnvironment && $user->canAccessTenant($tenant) && $user->can(Capabilities::EVIDENCE_VIEW, $tenant); } private function canViewStoredReport(StoredReport $report): bool { $user = auth()->user(); $tenant = $report->tenant; $capability = StoredReportResource::capabilityForReportType((string) $report->report_type); return $user instanceof User && $tenant instanceof ManagedEnvironment && is_string($capability) && $user->canAccessTenant($tenant) && $user->can($capability, $tenant); } private function canViewOperationRun(OperationRun $run, FindingException $exception): bool { $user = auth()->user(); return $user instanceof User && $this->runMatchesExceptionScope($run, $exception) && Gate::forUser($user)->allows('view', $run); } private function runMatchesExceptionScope(OperationRun $run, FindingException $exception): bool { return (int) $run->workspace_id === (int) $exception->workspace_id && (int) $run->managed_environment_id === (int) $exception->managed_environment_id; } private function findingExceptionDetailUrl(FindingException $exception): ?string { $tenant = $exception->tenant; if (! $tenant instanceof ManagedEnvironment) { return null; } return FindingExceptionResource::getUrl('view', ['record' => $exception], panel: 'admin', tenant: $tenant); } private function proofLabel(int $proofCount): string { return $proofCount === 1 ? '1 proof item' : $proofCount.' proof items'; } private function numericSourceId(FindingExceptionEvidenceReference $reference): ?int { if (! is_string($reference->source_id) && ! is_numeric($reference->source_id)) { return null; } $sourceId = trim((string) $reference->source_id); if ($sourceId === '' || ! ctype_digit($sourceId)) { return null; } $sourceId = (int) $sourceId; return $sourceId > 0 ? $sourceId : null; } private function isEvidenceSnapshotReference(FindingExceptionEvidenceReference $reference): bool { return in_array((string) $reference->source_type, ['evidence_snapshot', EvidenceSnapshot::class], true); } private function isStoredReportReference(FindingExceptionEvidenceReference $reference): bool { return in_array((string) $reference->source_type, ['stored_report', StoredReport::class], true); } private function resolveRegisterState(FindingException $exception, FindingExceptionDecision $currentDecision): ?string { $status = (string) $exception->status; if (in_array($status, self::TERMINAL_STATUSES, true)) { return $this->isRecentlyClosed($currentDecision->decided_at) ? 'recently_closed' : null; } return 'open'; } private function resolveNextActionLabel(FindingException $exception, FindingExceptionDecision $currentDecision): string { if ($exception->isPendingRenewal() || $currentDecision->decision_type === FindingExceptionDecision::TYPE_RENEWAL_REQUESTED) { return 'Review renewal'; } if ($exception->isPending()) { return 'Review approval'; } return 'Review follow-up'; } private function isRecentlyClosed(?CarbonInterface $decidedAt): bool { if (! $decidedAt instanceof CarbonInterface) { return false; } return $decidedAt->greaterThanOrEqualTo(now()->startOfDay()->subDays(self::RECENTLY_CLOSED_DAYS)); } }