*/ use DerivesWorkspaceIdFromTenant; use HasFactory; public const string FINDING_TYPE_DRIFT = 'drift'; public const string FINDING_TYPE_PERMISSION_POSTURE = 'permission_posture'; public const string FINDING_TYPE_ENTRA_ADMIN_ROLES = 'entra_admin_roles'; public const string SEVERITY_LOW = 'low'; public const string SEVERITY_MEDIUM = 'medium'; public const string SEVERITY_HIGH = 'high'; public const string SEVERITY_CRITICAL = 'critical'; public const string STATUS_NEW = 'new'; public const string STATUS_ACKNOWLEDGED = 'acknowledged'; public const string STATUS_TRIAGED = 'triaged'; public const string STATUS_IN_PROGRESS = 'in_progress'; public const string STATUS_REOPENED = 'reopened'; public const string STATUS_RESOLVED = 'resolved'; public const string STATUS_CLOSED = 'closed'; public const string STATUS_RISK_ACCEPTED = 'risk_accepted'; protected $guarded = []; protected $casts = [ 'acknowledged_at' => 'datetime', 'closed_at' => 'datetime', 'due_at' => 'datetime', 'evidence_jsonb' => 'array', 'first_seen_at' => 'datetime', 'in_progress_at' => 'datetime', 'last_seen_at' => 'datetime', 'reopened_at' => 'datetime', 'resolved_at' => 'datetime', 'sla_days' => 'integer', 'times_seen' => 'integer', 'triaged_at' => 'datetime', ]; public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } public function baselineRun(): BelongsTo { return $this->belongsTo(OperationRun::class, 'baseline_operation_run_id'); } public function currentRun(): BelongsTo { return $this->belongsTo(OperationRun::class, 'current_operation_run_id'); } public function acknowledgedByUser(): BelongsTo { return $this->belongsTo(User::class, 'acknowledged_by_user_id'); } public function ownerUser(): BelongsTo { return $this->belongsTo(User::class, 'owner_user_id'); } public function assigneeUser(): BelongsTo { return $this->belongsTo(User::class, 'assignee_user_id'); } public function closedByUser(): BelongsTo { return $this->belongsTo(User::class, 'closed_by_user_id'); } /** * @return array */ public static function openStatuses(): array { return [ self::STATUS_NEW, self::STATUS_TRIAGED, self::STATUS_IN_PROGRESS, self::STATUS_REOPENED, ]; } /** * @return array */ public static function terminalStatuses(): array { return [ self::STATUS_RESOLVED, self::STATUS_CLOSED, self::STATUS_RISK_ACCEPTED, ]; } /** * @return array */ public static function openStatusesForQuery(): array { return [ ...self::openStatuses(), self::STATUS_ACKNOWLEDGED, ]; } public static function canonicalizeStatus(?string $status): ?string { if ($status === self::STATUS_ACKNOWLEDGED) { return self::STATUS_TRIAGED; } return $status; } public static function isOpenStatus(?string $status): bool { return is_string($status) && in_array($status, self::openStatusesForQuery(), true); } public static function isTerminalStatus(?string $status): bool { $canonical = self::canonicalizeStatus($status); return is_string($canonical) && in_array($canonical, self::terminalStatuses(), true); } public function hasOpenStatus(): bool { return self::isOpenStatus($this->status); } public function acknowledge(User $user): void { if ($this->status === self::STATUS_ACKNOWLEDGED) { return; } $this->forceFill([ 'status' => self::STATUS_ACKNOWLEDGED, 'acknowledged_at' => now(), 'acknowledged_by_user_id' => $user->getKey(), ]); $this->save(); } /** * Auto-resolve the finding. */ public function resolve(string $reason): void { $this->status = self::STATUS_RESOLVED; $this->resolved_at = now(); $this->resolved_reason = $reason; $this->save(); } /** * Re-open a resolved finding. */ public function reopen(array $evidence): void { $this->status = self::STATUS_NEW; $this->resolved_at = null; $this->resolved_reason = null; $this->evidence_jsonb = $evidence; $this->save(); } }