TenantAtlas/app/Models/Finding.php
2026-03-18 13:56:09 +01:00

188 lines
5.0 KiB
PHP

<?php
namespace App\Models;
use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;
class Finding extends Model
{
/** @use HasFactory<\Database\Factories\FindingFactory> */
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<int, string>
*/
public static function openStatuses(): array
{
return [
self::STATUS_NEW,
self::STATUS_TRIAGED,
self::STATUS_IN_PROGRESS,
self::STATUS_REOPENED,
];
}
/**
* @return array<int, string>
*/
public static function terminalStatuses(): array
{
return [
self::STATUS_RESOLVED,
self::STATUS_CLOSED,
self::STATUS_RISK_ACCEPTED,
];
}
/**
* @return array<int, string>
*/
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 resolvedSubjectDisplayName(): ?string
{
$displayName = $this->getAttribute('subject_display_name');
if (is_string($displayName) && trim($displayName) !== '') {
return trim($displayName);
}
$fallback = Arr::get($this->evidence_jsonb ?? [], 'display_name');
$fallback = is_string($fallback) ? trim($fallback) : null;
return $fallback !== '' ? $fallback : null;
}
public function scopeWithSubjectDisplayName(Builder $query): Builder
{
return $query->addSelect([
'subject_display_name' => InventoryItem::query()
->select('display_name')
->whereColumn('inventory_items.tenant_id', 'findings.tenant_id')
->whereColumn('inventory_items.external_id', 'findings.subject_external_id')
->limit(1),
]);
}
}