TenantAtlas/apps/platform/app/Filament/Resources/StoredReportResource.php
Ahmed Darrazi db83112edc
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 6m41s
WIP: commit changes for PR to platform-dev
2026-05-06 01:50:13 +02:00

669 lines
28 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Concerns\InteractsWithTenantOwnedRecords;
use App\Filament\Concerns\ResolvesPanelTenantContext;
use App\Filament\Pages\TenantDashboard;
use App\Filament\Resources\StoredReportResource\Pages;
use App\Models\StoredReport;
use App\Models\Tenant;
use App\Models\User;
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use BackedEnum;
use Carbon\CarbonInterface;
use Filament\Actions;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ViewEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Throwable;
use UnitEnum;
class StoredReportResource extends Resource
{
use InteractsWithTenantOwnedRecords;
use ResolvesPanelTenantContext;
/**
* @var array<string, string>
*/
private const REPORT_TYPE_CAPABILITIES = [
StoredReport::REPORT_TYPE_PERMISSION_POSTURE => Capabilities::PERMISSION_POSTURE_VIEW,
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => Capabilities::ENTRA_ROLES_VIEW,
];
protected static ?string $model = StoredReport::class;
protected static ?string $slug = 'stored-reports';
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
protected static bool $isGloballySearchable = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-chart-bar';
protected static string|UnitEnum|null $navigationGroup = 'Reporting';
protected static ?string $navigationLabel = 'Stored reports';
protected static ?int $navigationSort = 49;
public static function canViewAny(): bool
{
return static::visibleReportTypesForCurrentUser() !== [];
}
public static function canView(Model $record): bool
{
$tenant = static::resolveTenantContextForCurrentPanel();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return false;
}
if (! $user->canAccessTenant($tenant)) {
return false;
}
if (! $record instanceof StoredReport) {
return true;
}
if ((int) $record->tenant_id !== (int) $tenant->getKey() || (int) $record->workspace_id !== (int) $tenant->workspace_id) {
return false;
}
$capability = static::capabilityForReportType((string) $record->report_type);
return $capability !== null && $user->can($capability, $tenant);
}
public static function canCreate(): bool
{
return false;
}
public static function canEdit(Model $record): bool
{
return false;
}
public static function canDelete(Model $record): bool
{
return false;
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView, ActionSurfaceType::ReadOnlyRegistryReport)
->exempt(ActionSurfaceSlot::ListHeader, 'Stored reports are read-only in v1 and do not expose generation, rerun, export, or mutation actions.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Clickable-row inspection is the only row action for the read-only stored-report register.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Stored reports do not support bulk actions in v1.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Empty state links back to the tenant overview without implying report generation from this surface.')
->satisfy(ActionSurfaceSlot::DetailHeader, 'Historical detail exposes the single navigation action Open current report; current detail has no header action.');
}
public static function getEloquentQuery(): Builder
{
$query = static::getTenantOwnedEloquentQuery()
->with('tenant')
->whereIn('report_type', static::supportedReportTypes());
$tenant = static::resolveTenantContextForCurrentPanel();
if ($tenant instanceof Tenant) {
$query->where('workspace_id', (int) $tenant->workspace_id);
}
$visibleReportTypes = static::visibleReportTypesForCurrentUser();
if ($visibleReportTypes === []) {
return $query->whereRaw('1 = 0');
}
return $query->whereIn('report_type', $visibleReportTypes);
}
public static function resolveScopedRecordOrFail(int|string|null $record): Model
{
$query = parent::getEloquentQuery()
->with('tenant')
->whereIn('report_type', static::supportedReportTypes());
$tenant = static::resolveTenantContextForCurrentPanel();
if ($tenant instanceof Tenant) {
$query->where('workspace_id', (int) $tenant->workspace_id);
}
return static::resolveTenantOwnedRecordOrFail(
$record,
$query,
);
}
public static function form(Schema $schema): Schema
{
return $schema;
}
public static function infolist(Schema $schema): Schema
{
return $schema->schema([
Section::make('Outcome summary')
->schema([
ViewEntry::make('artifact_truth')
->hiddenLabel()
->view('filament.infolists.entries.governance-artifact-truth')
->state(fn (StoredReport $record): array => static::truthState($record))
->columnSpanFull(),
])
->columnSpanFull(),
Section::make('Stored report')
->schema([
TextEntry::make('display_reference')
->label('Artifact reference')
->state(fn (StoredReport $record): string => static::displayReference($record)),
TextEntry::make('report_type')
->label('Report family')
->formatStateUsing(fn (string $state): string => static::reportFamilyReportLabel($state)),
TextEntry::make('measured_at')
->label('Measured at')
->state(fn (StoredReport $record): ?CarbonInterface => static::measuredAt($record))
->dateTime()
->placeholder('—'),
TextEntry::make('lifecycle_state')
->label('Lifecycle')
->state(fn (StoredReport $record): string => static::lifecycleState($record))
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::GovernanceArtifactLifecycle))
->color(BadgeRenderer::color(BadgeDomain::GovernanceArtifactLifecycle))
->icon(BadgeRenderer::icon(BadgeDomain::GovernanceArtifactLifecycle))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::GovernanceArtifactLifecycle)),
TextEntry::make('retention_state')
->label('Retention')
->state(fn (): string => 'retained')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::GovernanceArtifactRetention))
->color(BadgeRenderer::color(BadgeDomain::GovernanceArtifactRetention))
->icon(BadgeRenderer::icon(BadgeDomain::GovernanceArtifactRetention))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::GovernanceArtifactRetention)),
TextEntry::make('fingerprint')
->label('Integrity anchor')
->copyable()
->fontFamily('mono')
->placeholder('—')
->columnSpanFull(),
TextEntry::make('previous_fingerprint')
->label('Previous fingerprint')
->copyable()
->fontFamily('mono')
->placeholder('—')
->columnSpanFull(),
])
->columns(2)
->columnSpanFull(),
Section::make('Permission posture summary')
->schema([
TextEntry::make('payload.posture_score')->label('Posture score')->placeholder('—'),
TextEntry::make('payload.required_count')->label('Required permissions')->placeholder('0'),
TextEntry::make('payload.granted_count')->label('Granted permissions')->placeholder('0'),
TextEntry::make('missing_count')
->label('Missing permissions')
->state(fn (StoredReport $record): int => static::permissionPostureMissingCount($record)),
TextEntry::make('at_risk_permissions')
->label('Missing or at-risk permission context')
->state(fn (StoredReport $record): array => static::permissionPostureAtRiskPermissions($record))
->bulleted()
->listWithLineBreaks()
->placeholder('No missing or at-risk permissions in the stored payload.')
->columnSpanFull(),
])
->columns(2)
->visible(fn (StoredReport $record): bool => $record->report_type === StoredReport::REPORT_TYPE_PERMISSION_POSTURE)
->columnSpanFull(),
Section::make('Entra admin roles summary')
->schema([
TextEntry::make('payload.totals.roles_total')->label('Roles total')->placeholder('0'),
TextEntry::make('payload.totals.assignments_total')->label('Assignments total')->placeholder('0'),
TextEntry::make('payload.totals.high_privilege_assignments')->label('High-privilege assignments')->placeholder('0'),
TextEntry::make('highest_risk_assignment')
->label('Highest-risk assignment')
->state(fn (StoredReport $record): ?string => static::highestRiskAssignmentLabel($record))
->placeholder('No high-privilege assignments in the stored payload.')
->columnSpanFull(),
])
->columns(2)
->visible(fn (StoredReport $record): bool => $record->report_type === StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)
->columnSpanFull(),
Section::make('Raw payload')
->schema([
ViewEntry::make('payload')
->hiddenLabel()
->view('filament.infolists.entries.snapshot-json')
->state(fn (StoredReport $record): array => is_array($record->payload) ? $record->payload : [])
->columnSpanFull(),
])
->collapsible()
->collapsed()
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->defaultSort('created_at', 'desc')
->paginated(TablePaginationProfiles::resource())
->recordUrl(fn (StoredReport $record): string => static::getUrl('view', ['record' => $record]))
->columns([
Tables\Columns\TextColumn::make('id')
->label('Reference')
->formatStateUsing(fn (int|string|null $state): string => sprintf('Stored report #%s', $state ?? '—'))
->searchable(query: fn (Builder $query, string $search): Builder => static::applyReportSearch($query, $search)),
Tables\Columns\TextColumn::make('report_type')
->label('Report family')
->formatStateUsing(fn (string $state): string => static::reportFamilyReportLabel($state))
->searchable(query: fn (Builder $query, string $search): Builder => static::applyReportSearch($query, $search)),
Tables\Columns\TextColumn::make('lifecycle_state')
->label('Lifecycle')
->getStateUsing(fn (StoredReport $record): string => static::lifecycleState($record))
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::GovernanceArtifactLifecycle))
->color(BadgeRenderer::color(BadgeDomain::GovernanceArtifactLifecycle))
->icon(BadgeRenderer::icon(BadgeDomain::GovernanceArtifactLifecycle))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::GovernanceArtifactLifecycle)),
Tables\Columns\TextColumn::make('measured_at')
->label('Measured at')
->getStateUsing(fn (StoredReport $record): ?CarbonInterface => static::measuredAt($record))
->dateTime()
->placeholder('—'),
Tables\Columns\TextColumn::make('summary')
->label('Summary')
->getStateUsing(fn (StoredReport $record): string => static::summaryText($record))
->wrap(),
Tables\Columns\TextColumn::make('fingerprint')
->label('Integrity')
->formatStateUsing(fn (?string $state): string => filled($state) ? 'Present' : '—')
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('report_type')
->label('Report family')
->options(fn (): array => static::visibleReportFamilyOptions()),
Tables\Filters\SelectFilter::make('history')
->label('Records')
->options([
'current' => 'Current records',
'all' => 'Include history',
])
->default('current')
->query(fn (Builder $query, array $data): Builder => ($data['value'] ?? 'current') === 'all'
? $query
: static::scopeCurrentRecords($query)),
])
->actions([])
->bulkActions([])
->emptyStateHeading('No stored reports yet')
->emptyStateDescription('Stored reports appear here after their origin surfaces create retained report records.')
->emptyStateIcon('heroicon-o-document-chart-bar')
->emptyStateActions([
Actions\Action::make('open_tenant_overview')
->label('Open tenant overview')
->icon('heroicon-o-home')
->url(fn (): string => static::tenantOverviewUrl()),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListStoredReports::route('/'),
'view' => Pages\ViewStoredReport::route('/{record}'),
];
}
/**
* @return array<string>
*/
public static function supportedReportTypes(): array
{
return array_keys(self::REPORT_TYPE_CAPABILITIES);
}
/**
* @return array<string, string>
*/
public static function reportFamilyOptions(): array
{
return [
StoredReport::REPORT_TYPE_PERMISSION_POSTURE => 'Permission posture',
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => 'Entra admin roles',
];
}
/**
* @return array<string, string>
*/
public static function visibleReportFamilyOptions(): array
{
return array_intersect_key(
static::reportFamilyOptions(),
array_flip(static::visibleReportTypesForCurrentUser()),
);
}
/**
* @return array<string>
*/
public static function visibleReportTypesForCurrentUser(): array
{
$tenant = static::resolveTenantContextForCurrentPanel();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User || ! $user->canAccessTenant($tenant)) {
return [];
}
return array_values(array_filter(
static::supportedReportTypes(),
static fn (string $reportType): bool => ($capability = static::capabilityForReportType($reportType)) !== null
&& $user->can($capability, $tenant),
));
}
public static function capabilityForReportType(string $reportType): ?string
{
return self::REPORT_TYPE_CAPABILITIES[$reportType] ?? null;
}
public static function reportFamilyLabel(string $reportType): string
{
return static::reportFamilyOptions()[$reportType] ?? Str::headline($reportType);
}
public static function reportFamilyReportLabel(string $reportType): string
{
return match ($reportType) {
StoredReport::REPORT_TYPE_PERMISSION_POSTURE => 'Permission posture report',
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => 'Entra admin roles report',
default => Str::headline($reportType),
};
}
public static function displayReference(StoredReport $report): string
{
$truth = app(ArtifactTruthPresenter::class)->forStoredReportFresh($report);
return $truth->displayReference ?? sprintf('Stored report #%d (%s)', (int) $report->getKey(), static::reportFamilyLabel((string) $report->report_type));
}
public static function lifecycleState(StoredReport $report): string
{
$truth = app(ArtifactTruthPresenter::class)->forStoredReportFresh($report);
return $truth->lifecycleState ?? 'current';
}
/**
* @return array<string, mixed>
*/
public static function truthState(StoredReport $report): array
{
return app(ArtifactTruthPresenter::class)->forStoredReportFresh($report)->toArray();
}
public static function measuredAt(StoredReport $report): ?CarbonInterface
{
$payload = is_array($report->payload) ? $report->payload : [];
$value = Arr::get($payload, 'measured_at') ?? Arr::get($payload, 'checked_at');
if ($value instanceof CarbonInterface) {
return $value;
}
if (is_string($value) && trim($value) !== '') {
try {
return Carbon::parse($value);
} catch (Throwable) {
// Fall back to persisted timestamps when payload timestamps are malformed.
}
}
return $report->created_at ?? $report->updated_at;
}
public static function summaryText(StoredReport $report): string
{
$highlights = static::summaryHighlights($report);
if ($highlights === []) {
return 'No bounded summary is available for this report family.';
}
return collect($highlights)
->map(static fn (array $highlight): string => sprintf('%s: %s', $highlight['label'], $highlight['value']))
->implode(' · ');
}
/**
* @return list<array{label: string, value: string}>
*/
public static function summaryHighlights(StoredReport $report): array
{
return match ((string) $report->report_type) {
StoredReport::REPORT_TYPE_PERMISSION_POSTURE => static::permissionPostureHighlights($report),
StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => static::entraAdminRolesHighlights($report),
default => [],
};
}
public static function permissionPostureMissingCount(StoredReport $report): int
{
$payload = is_array($report->payload) ? $report->payload : [];
$requiredCount = max(0, (int) ($payload['required_count'] ?? 0));
$grantedCount = max(0, (int) ($payload['granted_count'] ?? 0));
return max(0, $requiredCount - $grantedCount);
}
/**
* @return list<string>
*/
public static function permissionPostureAtRiskPermissions(StoredReport $report): array
{
$payload = is_array($report->payload) ? $report->payload : [];
$permissions = is_array($payload['permissions'] ?? null) ? $payload['permissions'] : [];
return collect($permissions)
->filter(static fn (mixed $permission): bool => is_array($permission) && ($permission['status'] ?? null) !== 'granted')
->take(5)
->map(static function (array $permission): string {
$key = trim((string) ($permission['key'] ?? 'Unknown permission'));
$status = trim((string) ($permission['status'] ?? 'unknown'));
return sprintf('%s (%s)', $key !== '' ? $key : 'Unknown permission', $status !== '' ? $status : 'unknown');
})
->values()
->all();
}
public static function highestRiskAssignmentLabel(StoredReport $report): ?string
{
$payload = is_array($report->payload) ? $report->payload : [];
$assignments = is_array($payload['high_privilege'] ?? null) ? $payload['high_privilege'] : [];
$assignment = collect($assignments)
->filter(static fn (mixed $assignment): bool => is_array($assignment))
->sortBy(static fn (array $assignment): int => match ((string) ($assignment['severity'] ?? '')) {
'critical' => 0,
'high' => 1,
'medium' => 2,
'low' => 3,
default => 4,
})
->first();
if (! is_array($assignment)) {
return null;
}
$role = trim((string) ($assignment['role_display_name'] ?? 'Unknown role'));
$principal = trim((string) ($assignment['principal_display_name'] ?? 'Unknown principal'));
$severity = trim((string) ($assignment['severity'] ?? 'unknown'));
return sprintf('%s assigned to %s (%s)', $role !== '' ? $role : 'Unknown role', $principal !== '' ? $principal : 'Unknown principal', $severity !== '' ? $severity : 'unknown');
}
public static function currentReportFor(StoredReport $report): ?StoredReport
{
$current = StoredReport::query()
->where('tenant_id', (int) $report->tenant_id)
->where('workspace_id', (int) $report->workspace_id)
->where('report_type', (string) $report->report_type)
->orderByDesc('created_at')
->orderByDesc('id')
->first();
return $current instanceof StoredReport && ! $current->is($report) ? $current : null;
}
public static function currentReportUrlFor(StoredReport $report): ?string
{
$current = static::currentReportFor($report);
if (! $current instanceof StoredReport) {
return null;
}
$tenant = $current->tenant ?? static::resolveTenantContextForCurrentPanel();
if (! $tenant instanceof Tenant) {
return null;
}
return static::getUrl('view', ['record' => $current], panel: 'tenant', tenant: $tenant);
}
public static function scopeCurrentRecords(Builder $query): Builder
{
return $query->whereNotExists(function ($subQuery): void {
$subQuery
->selectRaw('1')
->from('stored_reports as newer_stored_reports')
->whereColumn('newer_stored_reports.tenant_id', 'stored_reports.tenant_id')
->whereColumn('newer_stored_reports.workspace_id', 'stored_reports.workspace_id')
->whereColumn('newer_stored_reports.report_type', 'stored_reports.report_type')
->where(function ($newerQuery): void {
$newerQuery
->whereColumn('newer_stored_reports.created_at', '>', 'stored_reports.created_at')
->orWhere(function ($tieQuery): void {
$tieQuery
->whereColumn('newer_stored_reports.created_at', 'stored_reports.created_at')
->whereColumn('newer_stored_reports.id', '>', 'stored_reports.id');
});
});
});
}
public static function applyReportSearch(Builder $query, string $search): Builder
{
$search = trim($search);
if ($search === '') {
return $query;
}
$normalizedSearch = Str::lower($search);
$matchingReportTypes = collect(static::reportFamilyOptions())
->filter(static fn (string $label, string $reportType): bool => str_contains(Str::lower($label), $normalizedSearch)
|| str_contains(Str::lower(static::reportFamilyReportLabel($reportType)), $normalizedSearch)
|| str_contains(Str::lower($reportType), $normalizedSearch))
->keys()
->all();
return $query->where(function (Builder $searchQuery) use ($search, $matchingReportTypes): void {
$searchQuery->where('report_type', 'like', '%'.$search.'%');
if (is_numeric($search)) {
$searchQuery->orWhereKey((int) $search);
}
if ($matchingReportTypes !== []) {
$searchQuery->orWhereIn('report_type', $matchingReportTypes);
}
});
}
/**
* @return list<array{label: string, value: string}>
*/
private static function permissionPostureHighlights(StoredReport $report): array
{
$payload = is_array($report->payload) ? $report->payload : [];
$postureScore = $payload['posture_score'] ?? null;
$requiredCount = (int) ($payload['required_count'] ?? 0);
$grantedCount = (int) ($payload['granted_count'] ?? 0);
return [
['label' => 'Posture score', 'value' => is_numeric($postureScore) ? (string) ((int) $postureScore) : '—'],
['label' => 'Required', 'value' => (string) $requiredCount],
['label' => 'Granted', 'value' => (string) $grantedCount],
['label' => 'Missing', 'value' => (string) static::permissionPostureMissingCount($report)],
];
}
/**
* @return list<array{label: string, value: string}>
*/
private static function entraAdminRolesHighlights(StoredReport $report): array
{
$payload = is_array($report->payload) ? $report->payload : [];
$totals = is_array($payload['totals'] ?? null) ? $payload['totals'] : [];
return [
['label' => 'Roles', 'value' => (string) ((int) ($totals['roles_total'] ?? 0))],
['label' => 'Assignments', 'value' => (string) ((int) ($totals['assignments_total'] ?? 0))],
['label' => 'High privilege', 'value' => (string) ((int) ($totals['high_privilege_assignments'] ?? 0))],
['label' => 'Highest risk', 'value' => static::highestRiskAssignmentLabel($report) ?? '—'],
];
}
private static function tenantOverviewUrl(): string
{
$tenant = static::resolveTenantContextForCurrentPanel();
if (! $tenant instanceof Tenant) {
return '#';
}
return TenantDashboard::getUrl(panel: 'tenant', tenant: $tenant);
}
}