669 lines
28 KiB
PHP
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);
|
|
}
|
|
}
|