TenantAtlas/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php
Ahmed Darrazi be780a8b48
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m36s
chore(decision-register): polish evidence operation run link and tests
2026-05-15 13:41:43 +02:00

468 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\GovernanceDecisions;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\StoredReportResource;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\FindingExceptionEvidenceReference;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\StoredReport;
use App\Models\User;
use App\Models\Workspace;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunLinks;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
final readonly class GovernanceDecisionRegisterBuilder
{
private const int RECENTLY_CLOSED_DAYS = 30;
/**
* @var list<string>
*/
private const array TERMINAL_STATUSES = [
FindingException::STATUS_REJECTED,
FindingException::STATUS_REVOKED,
FindingException::STATUS_SUPERSEDED,
];
/**
* @param array<int, ManagedEnvironment> $visibleTenants
* @return array{
* rows: list<array<string, mixed>>,
* 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<int, array<string, mixed>> $openRows */
$openRows = $rows
->where('register_state', 'open')
->sortBy([
['due_at', 'asc'],
['exception_id', 'asc'],
])
->values();
/** @var Collection<int, array<string, mixed>> $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<string, mixed>|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<int, FindingExceptionEvidenceReference>
*/
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<int, OperationRun>
*/
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));
}
}