schema([ Section::make('Finding') ->schema([ TextEntry::make('finding_type')->badge()->label('Type'), TextEntry::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingStatus)) ->color(BadgeRenderer::color(BadgeDomain::FindingStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::FindingStatus)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingStatus)), TextEntry::make('severity') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingSeverity)) ->color(BadgeRenderer::color(BadgeDomain::FindingSeverity)) ->icon(BadgeRenderer::icon(BadgeDomain::FindingSeverity)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)), TextEntry::make('fingerprint')->label('Fingerprint')->copyable(), TextEntry::make('scope_key')->label('Scope')->copyable(), TextEntry::make('subject_display_name')->label('Subject')->placeholder('—'), TextEntry::make('subject_type')->label('Subject type'), TextEntry::make('subject_external_id')->label('External ID')->copyable(), TextEntry::make('baseline_run_id') ->label('Baseline run') ->url(fn (Finding $record): ?string => $record->baseline_run_id ? InventorySyncRunResource::getUrl('view', ['record' => $record->baseline_run_id], tenant: Tenant::current()) : null) ->openUrlInNewTab(), TextEntry::make('current_run_id') ->label('Current run') ->url(fn (Finding $record): ?string => $record->current_run_id ? InventorySyncRunResource::getUrl('view', ['record' => $record->current_run_id], tenant: Tenant::current()) : null) ->openUrlInNewTab(), TextEntry::make('acknowledged_at')->dateTime()->placeholder('—'), TextEntry::make('acknowledged_by_user_id')->label('Acknowledged by')->placeholder('—'), TextEntry::make('created_at')->label('Created')->dateTime(), ]) ->columns(2) ->columnSpanFull(), Section::make('Diff') ->schema([ ViewEntry::make('settings_diff') ->label('') ->view('filament.infolists.entries.normalized-diff') ->state(function (Finding $record): array { $tenant = Tenant::current(); if (! $tenant) { return ['summary' => ['message' => 'No tenant context'], 'added' => [], 'removed' => [], 'changed' => []]; } $baselineId = Arr::get($record->evidence_jsonb ?? [], 'baseline.policy_version_id'); $currentId = Arr::get($record->evidence_jsonb ?? [], 'current.policy_version_id'); $baselineVersion = is_numeric($baselineId) ? PolicyVersion::query()->where('tenant_id', $tenant->getKey())->find((int) $baselineId) : null; $currentVersion = is_numeric($currentId) ? PolicyVersion::query()->where('tenant_id', $tenant->getKey())->find((int) $currentId) : null; $diff = app(DriftFindingDiffBuilder::class)->buildSettingsDiff($baselineVersion, $currentVersion); $addedCount = (int) Arr::get($diff, 'summary.added', 0); $removedCount = (int) Arr::get($diff, 'summary.removed', 0); $changedCount = (int) Arr::get($diff, 'summary.changed', 0); if (($addedCount + $removedCount + $changedCount) === 0) { Arr::set( $diff, 'summary.message', 'No normalized changes were found. This drift finding may be based on fields that are intentionally excluded from normalization.' ); } return $diff; }) ->visible(fn (Finding $record): bool => Arr::get($record->evidence_jsonb ?? [], 'summary.kind') === 'policy_snapshot') ->columnSpanFull(), ViewEntry::make('scope_tags_diff') ->label('') ->view('filament.infolists.entries.scope-tags-diff') ->state(function (Finding $record): array { $tenant = Tenant::current(); if (! $tenant) { return ['summary' => ['message' => 'No tenant context'], 'added' => [], 'removed' => [], 'changed' => []]; } $baselineId = Arr::get($record->evidence_jsonb ?? [], 'baseline.policy_version_id'); $currentId = Arr::get($record->evidence_jsonb ?? [], 'current.policy_version_id'); $baselineVersion = is_numeric($baselineId) ? PolicyVersion::query()->where('tenant_id', $tenant->getKey())->find((int) $baselineId) : null; $currentVersion = is_numeric($currentId) ? PolicyVersion::query()->where('tenant_id', $tenant->getKey())->find((int) $currentId) : null; return app(DriftFindingDiffBuilder::class)->buildScopeTagsDiff($baselineVersion, $currentVersion); }) ->visible(fn (Finding $record): bool => Arr::get($record->evidence_jsonb ?? [], 'summary.kind') === 'policy_scope_tags') ->columnSpanFull(), ViewEntry::make('assignments_diff') ->label('') ->view('filament.infolists.entries.assignments-diff') ->state(function (Finding $record): array { $tenant = Tenant::current(); if (! $tenant) { return ['summary' => ['message' => 'No tenant context'], 'added' => [], 'removed' => [], 'changed' => []]; } $baselineId = Arr::get($record->evidence_jsonb ?? [], 'baseline.policy_version_id'); $currentId = Arr::get($record->evidence_jsonb ?? [], 'current.policy_version_id'); $baselineVersion = is_numeric($baselineId) ? PolicyVersion::query()->where('tenant_id', $tenant->getKey())->find((int) $baselineId) : null; $currentVersion = is_numeric($currentId) ? PolicyVersion::query()->where('tenant_id', $tenant->getKey())->find((int) $currentId) : null; return app(DriftFindingDiffBuilder::class)->buildAssignmentsDiff($tenant, $baselineVersion, $currentVersion); }) ->visible(fn (Finding $record): bool => Arr::get($record->evidence_jsonb ?? [], 'summary.kind') === 'policy_assignments') ->columnSpanFull(), ]) ->collapsed() ->columnSpanFull(), Section::make('Evidence (Sanitized)') ->schema([ ViewEntry::make('evidence_jsonb') ->label('') ->view('filament.infolists.entries.snapshot-json') ->state(fn (Finding $record) => $record->evidence_jsonb ?? []) ->columnSpanFull(), ]) ->columnSpanFull(), ]); } public static function table(Table $table): Table { return $table ->defaultSort('created_at', 'desc') ->columns([ Tables\Columns\TextColumn::make('finding_type')->badge()->label('Type'), Tables\Columns\TextColumn::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingStatus)) ->color(BadgeRenderer::color(BadgeDomain::FindingStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::FindingStatus)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingStatus)), Tables\Columns\TextColumn::make('severity') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingSeverity)) ->color(BadgeRenderer::color(BadgeDomain::FindingSeverity)) ->icon(BadgeRenderer::icon(BadgeDomain::FindingSeverity)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)), Tables\Columns\TextColumn::make('subject_display_name')->label('Subject')->placeholder('—'), Tables\Columns\TextColumn::make('subject_type')->label('Subject type')->searchable(), Tables\Columns\TextColumn::make('subject_external_id')->label('External ID')->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('scope_key')->label('Scope')->toggleable(isToggledHiddenByDefault: true), Tables\Columns\TextColumn::make('created_at')->since()->label('Created'), ]) ->filters([ Tables\Filters\SelectFilter::make('status') ->options([ Finding::STATUS_NEW => 'New', Finding::STATUS_ACKNOWLEDGED => 'Acknowledged', ]) ->default(Finding::STATUS_NEW), Tables\Filters\SelectFilter::make('finding_type') ->options([ Finding::FINDING_TYPE_DRIFT => 'Drift', ]) ->default(Finding::FINDING_TYPE_DRIFT), Tables\Filters\Filter::make('scope_key') ->form([ TextInput::make('scope_key') ->label('Scope key') ->placeholder('Inventory selection hash') ->maxLength(255), ]) ->query(function (Builder $query, array $data): Builder { $scopeKey = $data['scope_key'] ?? null; if (! is_string($scopeKey) || $scopeKey === '') { return $query; } return $query->where('scope_key', $scopeKey); }), Tables\Filters\Filter::make('run_ids') ->label('Run IDs') ->form([ TextInput::make('baseline_run_id') ->label('Baseline run id') ->numeric(), TextInput::make('current_run_id') ->label('Current run id') ->numeric(), ]) ->query(function (Builder $query, array $data): Builder { $baselineRunId = $data['baseline_run_id'] ?? null; if (is_numeric($baselineRunId)) { $query->where('baseline_run_id', (int) $baselineRunId); } $currentRunId = $data['current_run_id'] ?? null; if (is_numeric($currentRunId)) { $query->where('current_run_id', (int) $currentRunId); } return $query; }), ]) ->actions([ Actions\Action::make('acknowledge') ->label('Acknowledge') ->icon('heroicon-o-check') ->color('gray') ->requiresConfirmation() ->visible(fn (Finding $record): bool => $record->status === Finding::STATUS_NEW) ->authorize(function (Finding $record): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } return $user->can('update', $record); }) ->action(function (Finding $record): void { $tenant = Tenant::current(); $user = auth()->user(); if (! $tenant || ! $user instanceof User) { return; } if ((int) $record->tenant_id !== (int) $tenant->getKey()) { Notification::make() ->title('Finding belongs to a different tenant') ->danger() ->send(); return; } $record->acknowledge($user); Notification::make() ->title('Finding acknowledged') ->success() ->send(); }), Actions\ViewAction::make(), ]) ->bulkActions([ BulkActionGroup::make([ BulkAction::make('acknowledge_selected') ->label('Acknowledge selected') ->icon('heroicon-o-check') ->color('gray') ->authorize(function (): bool { $tenant = Tenant::current(); $user = auth()->user(); if (! $tenant || ! $user instanceof User) { return false; } $probe = new Finding(['tenant_id' => $tenant->getKey()]); return $user->can('update', $probe); }) ->authorizeIndividualRecords('update') ->requiresConfirmation() ->action(function (Collection $records): void { $tenant = Tenant::current(); $user = auth()->user(); if (! $tenant || ! $user instanceof User) { return; } $firstRecord = $records->first(); if ($firstRecord instanceof Finding) { Gate::authorize('update', $firstRecord); } $acknowledgedCount = 0; $skippedCount = 0; foreach ($records as $record) { if (! $record instanceof Finding) { $skippedCount++; continue; } if ((int) $record->tenant_id !== (int) $tenant->getKey()) { $skippedCount++; continue; } if ($record->status !== Finding::STATUS_NEW) { $skippedCount++; continue; } $record->acknowledge($user); $acknowledgedCount++; } $body = "Acknowledged {$acknowledgedCount} finding".($acknowledgedCount === 1 ? '' : 's').'.'; if ($skippedCount > 0) { $body .= " Skipped {$skippedCount}."; } Notification::make() ->title('Bulk acknowledge completed') ->body($body) ->success() ->send(); }) ->deselectRecordsAfterCompletion(), ]), ]); } public static function getEloquentQuery(): Builder { $tenantId = Tenant::current()->getKey(); return parent::getEloquentQuery() ->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), ]) ->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId)); } public static function getPages(): array { return [ 'index' => Pages\ListFindings::route('/'), 'view' => Pages\ViewFinding::route('/{record}'), ]; } }