## Summary - harden findings and finding-exception Filament surfaces so workflow state, governance validity, overdue urgency, and next action are operator-first - add tenant stats widgets, segmented tabs, richer governance warnings, and baseline/dashboard attention propagation for overdue and lapsed governance states - add Spec 166 artifacts plus regression coverage for findings, badges, baseline summaries, tenantless operation viewer behavior, and critical table standards ## Verification - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact` ## Filament Notes - Livewire v4.0+ compliance: yes, implementation stays on Filament v5 / Livewire v4 APIs only - Provider registration: unchanged, Laravel 12 panel/provider registration remains in `bootstrap/providers.php` - Global search: unchanged in this slice; `FindingExceptionResource` stays not globally searchable, no new globally searchable resource was introduced - Destructive actions: existing revoke/reject/approve/renew/workflow mutations remain capability-gated and confirmation-gated where already defined - Asset strategy: no new assets added; existing deploy process remains unchanged, including `php artisan filament:assets` when registered assets are used - Testing plan delivered: findings list/detail, exception register, dashboard attention, baseline summary, badge semantics, and tenantless operation viewer coverage Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #197
355 lines
16 KiB
PHP
355 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Findings;
|
|
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
final class FindingRiskGovernanceResolver
|
|
{
|
|
public function resolveWorkflowFamily(Finding $finding): string
|
|
{
|
|
return match (Finding::canonicalizeStatus((string) $finding->status)) {
|
|
Finding::STATUS_RISK_ACCEPTED => 'accepted_risk',
|
|
Finding::STATUS_RESOLVED,
|
|
Finding::STATUS_CLOSED => 'historical',
|
|
default => 'active',
|
|
};
|
|
}
|
|
|
|
public function resolveExceptionStatus(FindingException $exception, ?CarbonImmutable $now = null): string
|
|
{
|
|
$now ??= CarbonImmutable::instance(now());
|
|
|
|
$status = (string) $exception->status;
|
|
|
|
if (in_array($status, [
|
|
FindingException::STATUS_REJECTED,
|
|
FindingException::STATUS_REVOKED,
|
|
FindingException::STATUS_SUPERSEDED,
|
|
], true)) {
|
|
return $status;
|
|
}
|
|
|
|
if ($status === FindingException::STATUS_PENDING) {
|
|
return FindingException::STATUS_PENDING;
|
|
}
|
|
|
|
$expiresAt = $exception->expires_at instanceof Carbon
|
|
? CarbonImmutable::instance($exception->expires_at)
|
|
: null;
|
|
|
|
if ($expiresAt instanceof CarbonImmutable && $expiresAt->lessThanOrEqualTo($now)) {
|
|
return FindingException::STATUS_EXPIRED;
|
|
}
|
|
|
|
if ($this->isExpiring($exception, $now)) {
|
|
return FindingException::STATUS_EXPIRING;
|
|
}
|
|
|
|
return FindingException::STATUS_ACTIVE;
|
|
}
|
|
|
|
public function resolveValidityState(FindingException $exception, ?CarbonImmutable $now = null): string
|
|
{
|
|
if ($exception->isPendingRenewal()) {
|
|
return $this->resolveApprovedValidityState($exception, $now);
|
|
}
|
|
|
|
return match ($this->resolveExceptionStatus($exception, $now)) {
|
|
FindingException::STATUS_ACTIVE => FindingException::VALIDITY_VALID,
|
|
FindingException::STATUS_EXPIRING => FindingException::VALIDITY_EXPIRING,
|
|
FindingException::STATUS_EXPIRED => FindingException::VALIDITY_EXPIRED,
|
|
FindingException::STATUS_REVOKED => FindingException::VALIDITY_REVOKED,
|
|
FindingException::STATUS_REJECTED => FindingException::VALIDITY_REJECTED,
|
|
default => FindingException::VALIDITY_MISSING_SUPPORT,
|
|
};
|
|
}
|
|
|
|
public function resolveFindingState(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): string
|
|
{
|
|
$exception ??= $finding->relationLoaded('findingException')
|
|
? $finding->findingException
|
|
: $finding->findingException()->first();
|
|
|
|
$findingIsRiskAccepted = $finding->isRiskAccepted();
|
|
|
|
if (! $exception instanceof FindingException) {
|
|
return $findingIsRiskAccepted
|
|
? 'risk_accepted_without_valid_exception'
|
|
: 'ungoverned';
|
|
}
|
|
|
|
if (! $findingIsRiskAccepted) {
|
|
return $exception->isPending()
|
|
? 'pending_exception'
|
|
: 'ungoverned';
|
|
}
|
|
|
|
if ($exception->isPendingRenewal()) {
|
|
return match ($this->resolveApprovedValidityState($exception, $now)) {
|
|
FindingException::VALIDITY_VALID => 'valid_exception',
|
|
FindingException::VALIDITY_EXPIRING => 'expiring_exception',
|
|
FindingException::VALIDITY_EXPIRED => 'expired_exception',
|
|
default => 'pending_exception',
|
|
};
|
|
}
|
|
|
|
return match ($this->resolveExceptionStatus($exception, $now)) {
|
|
FindingException::STATUS_PENDING => 'pending_exception',
|
|
FindingException::STATUS_ACTIVE => 'valid_exception',
|
|
FindingException::STATUS_EXPIRING => 'expiring_exception',
|
|
FindingException::STATUS_EXPIRED => 'expired_exception',
|
|
FindingException::STATUS_REVOKED => 'revoked_exception',
|
|
FindingException::STATUS_REJECTED => 'rejected_exception',
|
|
default => $findingIsRiskAccepted
|
|
? 'risk_accepted_without_valid_exception'
|
|
: 'ungoverned',
|
|
};
|
|
}
|
|
|
|
public function isValidGovernedAcceptedRisk(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): bool
|
|
{
|
|
return in_array($this->resolveFindingState($finding, $exception, $now), [
|
|
'valid_exception',
|
|
'expiring_exception',
|
|
], true);
|
|
}
|
|
|
|
public function resolveGovernanceValidity(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): ?string
|
|
{
|
|
$exception ??= $finding->relationLoaded('findingException')
|
|
? $finding->findingException
|
|
: $finding->findingException()->first();
|
|
|
|
if ($exception instanceof FindingException) {
|
|
return $this->resolveValidityState($exception, $now);
|
|
}
|
|
|
|
return $finding->isRiskAccepted()
|
|
? FindingException::VALIDITY_MISSING_SUPPORT
|
|
: null;
|
|
}
|
|
|
|
public function resolveGovernanceAttention(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): string
|
|
{
|
|
if (! $finding->isRiskAccepted()) {
|
|
return 'not_applicable';
|
|
}
|
|
|
|
return match ($this->resolveGovernanceValidity($finding, $exception, $now)) {
|
|
FindingException::VALIDITY_VALID => 'healthy',
|
|
FindingException::VALIDITY_EXPIRING,
|
|
FindingException::VALIDITY_EXPIRED,
|
|
FindingException::VALIDITY_REVOKED,
|
|
FindingException::VALIDITY_REJECTED,
|
|
FindingException::VALIDITY_MISSING_SUPPORT => 'attention_needed',
|
|
default => 'attention_needed',
|
|
};
|
|
}
|
|
|
|
public function requiresGovernanceAttention(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): bool
|
|
{
|
|
return $this->resolveGovernanceAttention($finding, $exception, $now) === 'attention_needed';
|
|
}
|
|
|
|
public function resolveWarningMessage(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): ?string
|
|
{
|
|
$exception ??= $finding->relationLoaded('findingException')
|
|
? $finding->findingException
|
|
: $finding->findingException()->first();
|
|
|
|
if (! $exception instanceof FindingException) {
|
|
return $finding->isRiskAccepted()
|
|
? 'This finding is marked as accepted risk without a valid exception record.'
|
|
: null;
|
|
}
|
|
|
|
$exceptionStatus = $exception->isPendingRenewal()
|
|
? match ($this->resolveApprovedValidityState($exception, $now)) {
|
|
FindingException::VALIDITY_EXPIRED => FindingException::STATUS_EXPIRED,
|
|
FindingException::VALIDITY_EXPIRING => FindingException::STATUS_EXPIRING,
|
|
FindingException::VALIDITY_VALID => FindingException::STATUS_ACTIVE,
|
|
default => FindingException::STATUS_PENDING,
|
|
}
|
|
: $this->resolveExceptionStatus($exception, $now);
|
|
|
|
if ($finding->isRiskAccepted()) {
|
|
return match ($this->resolveFindingState($finding, $exception, $now)) {
|
|
'risk_accepted_without_valid_exception' => 'This finding is marked as accepted risk without a valid exception record.',
|
|
'expiring_exception' => 'The linked exception is still valid, but it is nearing expiry and needs review.',
|
|
'expired_exception' => 'The linked exception has expired and no longer governs accepted risk.',
|
|
'revoked_exception' => 'The linked exception was revoked and no longer governs accepted risk.',
|
|
'rejected_exception' => 'The linked exception was rejected and does not govern accepted risk.',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
if ($exception->requiresFreshDecisionForFinding($finding)) {
|
|
return 'This finding changed after the earlier exception decision; a fresh decision is required.';
|
|
}
|
|
|
|
return match ($exceptionStatus) {
|
|
FindingException::STATUS_EXPIRING => 'The linked exception is nearing expiry and needs review.',
|
|
FindingException::STATUS_EXPIRED => 'The linked exception has expired and no longer governs accepted risk.',
|
|
FindingException::STATUS_REVOKED => 'The linked exception was revoked and no longer governs accepted risk.',
|
|
FindingException::STATUS_REJECTED => 'The linked exception was rejected and does not govern accepted risk.',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
public function resolveHistoricalContext(Finding $finding): ?string
|
|
{
|
|
return match ((string) $finding->status) {
|
|
Finding::STATUS_RESOLVED => $this->resolvedHistoricalContext($finding),
|
|
Finding::STATUS_CLOSED => $this->closedHistoricalContext($finding),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
public function resolvePrimaryNarrative(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): string
|
|
{
|
|
return match ($this->resolveWorkflowFamily($finding)) {
|
|
'accepted_risk' => $this->resolveGovernanceAttention($finding, $exception, $now) === 'healthy'
|
|
? 'Accepted risk remains visible because current governance is still valid.'
|
|
: 'Accepted risk is still on record, but governance follow-up is needed before it can be treated as safe to ignore.',
|
|
'historical' => match ((string) $finding->status) {
|
|
Finding::STATUS_RESOLVED => 'Resolved is a historical workflow state. It does not prove the issue is permanently gone.',
|
|
Finding::STATUS_CLOSED => 'Closed is a historical workflow state. It does not prove the issue is permanently gone.',
|
|
default => 'This finding is historical workflow context.',
|
|
},
|
|
default => 'This finding is still active workflow work and should be reviewed until it is resolved, closed, or formally governed.',
|
|
};
|
|
}
|
|
|
|
public function resolvePrimaryNextAction(Finding $finding, ?FindingException $exception = null, ?CarbonImmutable $now = null): ?string
|
|
{
|
|
if ($finding->hasOpenStatus() && $finding->due_at?->isPast() === true) {
|
|
return 'Review the overdue finding and update ownership or next workflow step.';
|
|
}
|
|
|
|
if ($this->resolveWorkflowFamily($finding) === 'accepted_risk') {
|
|
return match ($this->resolveGovernanceValidity($finding, $exception, $now)) {
|
|
FindingException::VALIDITY_VALID => 'Keep the exception under review until remediation is possible.',
|
|
FindingException::VALIDITY_EXPIRING => 'Renew or review the exception before the current governance window lapses.',
|
|
FindingException::VALIDITY_EXPIRED,
|
|
FindingException::VALIDITY_REVOKED,
|
|
FindingException::VALIDITY_REJECTED,
|
|
FindingException::VALIDITY_MISSING_SUPPORT => 'Restore valid governance or move the finding back into active remediation.',
|
|
default => 'Review the current governance state before treating this accepted risk as stable.',
|
|
};
|
|
}
|
|
|
|
if ((string) $finding->status === Finding::STATUS_RESOLVED || (string) $finding->status === Finding::STATUS_CLOSED) {
|
|
return 'Review the closure context and reopen the finding if the issue has returned or governance has lapsed.';
|
|
}
|
|
|
|
if ($finding->assignee_user_id === null || $finding->owner_user_id === null) {
|
|
return 'Assign an owner and next workflow step so follow-up does not stall.';
|
|
}
|
|
|
|
return 'Review the current workflow state and decide whether to progress, resolve, close, or request governance coverage.';
|
|
}
|
|
|
|
public function syncExceptionState(FindingException $exception, ?CarbonImmutable $now = null): FindingException
|
|
{
|
|
$resolvedStatus = $this->resolveExceptionStatus($exception, $now);
|
|
$resolvedValidityState = $this->resolveValidityState($exception, $now);
|
|
|
|
if ((string) $exception->status === $resolvedStatus && (string) $exception->current_validity_state === $resolvedValidityState) {
|
|
return $exception;
|
|
}
|
|
|
|
$exception->forceFill([
|
|
'status' => $resolvedStatus,
|
|
'current_validity_state' => $resolvedValidityState,
|
|
])->save();
|
|
|
|
return $exception->refresh();
|
|
}
|
|
|
|
private function resolveApprovedValidityState(FindingException $exception, ?CarbonImmutable $now = null): string
|
|
{
|
|
$now ??= CarbonImmutable::instance(now());
|
|
|
|
$expiresAt = $this->renewalAwareDate(
|
|
$exception,
|
|
'previous_expires_at',
|
|
$exception->expires_at,
|
|
);
|
|
|
|
if ($expiresAt instanceof CarbonImmutable && $expiresAt->lessThanOrEqualTo($now)) {
|
|
return FindingException::VALIDITY_EXPIRED;
|
|
}
|
|
|
|
if ($this->isExpiring($exception, $now, renewalAware: true)) {
|
|
return FindingException::VALIDITY_EXPIRING;
|
|
}
|
|
|
|
return FindingException::VALIDITY_VALID;
|
|
}
|
|
|
|
private function isExpiring(FindingException $exception, CarbonImmutable $now, bool $renewalAware = false): bool
|
|
{
|
|
$reviewDueAt = $renewalAware
|
|
? $this->renewalAwareDate($exception, 'previous_review_due_at', $exception->review_due_at)
|
|
: ($exception->review_due_at instanceof Carbon ? CarbonImmutable::instance($exception->review_due_at) : null);
|
|
|
|
if ($reviewDueAt instanceof CarbonImmutable && $reviewDueAt->lessThanOrEqualTo($now)) {
|
|
return true;
|
|
}
|
|
|
|
$expiresAt = $renewalAware
|
|
? $this->renewalAwareDate($exception, 'previous_expires_at', $exception->expires_at)
|
|
: ($exception->expires_at instanceof Carbon ? CarbonImmutable::instance($exception->expires_at) : null);
|
|
|
|
if (! $expiresAt instanceof CarbonImmutable) {
|
|
return false;
|
|
}
|
|
|
|
return $expiresAt->lessThanOrEqualTo($now->addDays(7));
|
|
}
|
|
|
|
private function renewalAwareDate(FindingException $exception, string $metadataKey, mixed $fallback): ?CarbonImmutable
|
|
{
|
|
$currentDecision = $exception->relationLoaded('currentDecision')
|
|
? $exception->currentDecision
|
|
: $exception->currentDecision()->first();
|
|
|
|
if ($currentDecision instanceof FindingExceptionDecision && is_string($currentDecision->metadata[$metadataKey] ?? null)) {
|
|
return CarbonImmutable::parse((string) $currentDecision->metadata[$metadataKey]);
|
|
}
|
|
|
|
return $fallback instanceof Carbon
|
|
? CarbonImmutable::instance($fallback)
|
|
: null;
|
|
}
|
|
|
|
private function resolvedHistoricalContext(Finding $finding): ?string
|
|
{
|
|
$reason = (string) ($finding->resolved_reason ?? '');
|
|
|
|
return match ($reason) {
|
|
'no_longer_drifting' => 'The latest compare did not reproduce the earlier drift, but treat this as the last observed workflow outcome rather than a permanent guarantee.',
|
|
'permission_granted',
|
|
'permission_removed_from_registry',
|
|
'role_assignment_removed',
|
|
'ga_count_within_threshold' => 'The last observed workflow reason suggests the triggering condition was no longer present, but this remains historical context.',
|
|
default => 'Resolved records the workflow outcome. Review the reason and latest evidence before treating it as technical proof.',
|
|
};
|
|
}
|
|
|
|
private function closedHistoricalContext(Finding $finding): ?string
|
|
{
|
|
return match ((string) ($finding->closed_reason ?? '')) {
|
|
'accepted_risk' => 'Closed reflects workflow handling. Governance validity still determines whether accepted risk remains safe to rely on.',
|
|
default => 'Closed records the workflow outcome. Review the recorded reason before treating it as technical proof.',
|
|
};
|
|
}
|
|
}
|