diff --git a/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php b/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php index be39c87e..3db0f99b 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php +++ b/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php @@ -7,6 +7,7 @@ use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; use App\Models\AuditLog as AuditLogModel; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\SupportAccessGrant; use App\Models\User; use App\Models\Workspace; @@ -22,6 +23,7 @@ use App\Support\Navigation\CanonicalNavigationContext; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\Navigation\WorkspaceHubEnvironmentFilter; +use App\Support\OperationRunLinks; use App\Support\OperateHub\OperateHubShell; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\ActionSurfaceDefaults; @@ -40,6 +42,7 @@ use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -388,6 +391,174 @@ public function environmentFilterChip(): ?array ]; } + /** + * @return array + */ + public function auditDisclosurePayload(): array + { + $selectedAudit = $this->selectedAuditRecord(); + $focusAudit = $selectedAudit instanceof AuditLogModel + ? $selectedAudit + : $this->auditBaseQuery()->first(); + + return [ + 'scope_label' => $this->auditScopeLabel(), + 'scope_description' => $this->auditScopeDescription(), + 'event_count' => (clone $this->auditBaseQuery())->reorder()->count(), + 'focus' => $focusAudit instanceof AuditLogModel ? $this->auditProofPayload($focusAudit) : null, + 'has_selected_event' => $selectedAudit instanceof AuditLogModel, + ]; + } + + /** + * @return array|null + */ + public function selectedAuditProofPayload(): ?array + { + $record = $this->selectedAuditRecord(); + + return $record instanceof AuditLogModel + ? $this->auditProofPayload($record) + : null; + } + + private function auditScopeLabel(): string + { + return $this->filteredTenant() instanceof ManagedEnvironment + ? 'Environment audit scope' + : 'Workspace audit scope'; + } + + private function auditScopeDescription(): string + { + $tenant = $this->filteredTenant(); + + if ($tenant instanceof ManagedEnvironment) { + return 'Filtered by direct environment attribution for '.$tenant->name.'. Selected event detail remains inside this filter.'; + } + + return 'Workspace-wide audit disclosure across authorized environment and workspace events. Clean entry ignores remembered environment context.'; + } + + /** + * @return array + */ + private function auditProofPayload(AuditLogModel $record): array + { + $outcome = $record->normalizedOutcome()->value; + + return [ + 'id' => (int) $record->getKey(), + 'summary' => $record->summaryText(), + 'actor' => [ + 'label' => $record->actorDisplayLabel(), + 'type' => $record->actorSnapshot()->type->label(), + 'email' => $record->actorSnapshot()->email, + ], + 'action' => AuditActionId::labelFor((string) $record->action), + 'target' => [ + 'label' => $record->targetDisplayLabel() ?? 'No target snapshot', + 'type' => $record->resource_type ? Str::headline((string) $record->resource_type) : 'Workspace event', + ], + 'outcome' => [ + 'label' => BadgeRenderer::label(BadgeDomain::AuditOutcome)($outcome), + 'color' => BadgeRenderer::color(BadgeDomain::AuditOutcome)($outcome), + ], + 'time' => $record->recorded_at?->toDayDateTimeString() ?? 'Time unavailable', + 'scope' => [ + 'label' => $record->tenant?->name ?? 'Workspace-wide event', + 'detail' => is_numeric($record->workspace_id) + ? 'Workspace #'.(int) $record->workspace_id + : 'Workspace unavailable', + ], + 'inspect_url' => $this->auditLogUrl(['event' => (int) $record->getKey()]), + 'related_links' => $this->auditProofLinks($record), + 'context_items' => $this->auditReadableContextItems($record), + 'technical_metadata' => $this->auditTechnicalMetadata($record), + 'diagnostics_available' => $this->canViewAuditDiagnostics($record), + ]; + } + + /** + * @return list + */ + private function auditProofLinks(AuditLogModel $record): array + { + $links = []; + $operationRun = $record->operationRun; + + if ($operationRun instanceof OperationRun && $this->canViewOperationRun($operationRun)) { + $links[] = [ + 'label' => 'Operation proof', + 'url' => OperationRunLinks::tenantlessView($operationRun), + ]; + } + + $targetLink = $this->auditTargetLink($record); + + if (is_array($targetLink)) { + $links[] = [ + 'label' => $targetLink['label'], + 'url' => $targetLink['url'], + ]; + } + + return array_values(array_unique($links, SORT_REGULAR)); + } + + private function canViewOperationRun(OperationRun $run): bool + { + $user = auth()->user(); + + return $user instanceof User && Gate::forUser($user)->allows('view', $run); + } + + /** + * @return list + */ + private function auditReadableContextItems(AuditLogModel $record): array + { + return collect($record->contextItems()) + ->reject(fn (array $item): bool => $this->looksLikeRawDiagnosticValue($item['label'].' '.$item['value'])) + ->values() + ->all(); + } + + /** + * @return array + */ + private function auditTechnicalMetadata(AuditLogModel $record): array + { + if (! $this->canViewAuditDiagnostics($record)) { + return []; + } + + return $record->technicalMetadata(); + } + + private function canViewAuditDiagnostics(AuditLogModel $record): bool + { + $tenant = $record->tenant; + $user = auth()->user(); + + return $tenant instanceof ManagedEnvironment + && $user instanceof User + && $user->can(Capabilities::SUPPORT_DIAGNOSTICS_VIEW, $tenant); + } + + private function looksLikeRawDiagnosticValue(string $text): bool + { + $normalized = Str::lower($text); + + foreach (['raw', 'payload', 'provider secret', 'provider response', 'stack trace', 'debug', 'internal exception', 'token', 'credential'] as $needle) { + if (str_contains($normalized, $needle)) { + return true; + } + } + + return false; + } + private function authorizePageAccess(): void { $user = auth()->user(); diff --git a/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php b/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php index bff1a46d..95c5dcf8 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php +++ b/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php @@ -6,14 +6,22 @@ use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; use App\Filament\Resources\EvidenceSnapshotResource; +use App\Filament\Resources\ReviewPackResource; +use App\Filament\Resources\StoredReportResource; use App\Models\EnvironmentReview; use App\Models\EvidenceSnapshot; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; +use App\Models\ReviewPack; +use App\Models\StoredReport; use App\Models\User; use App\Models\Workspace; +use App\Support\Badges\BadgeDomain; +use App\Support\Badges\BadgeRenderer; use App\Support\EnvironmentReviewStatus; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\WorkspaceHubEnvironmentFilter; +use App\Support\OperationRunLinks; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; @@ -35,6 +43,7 @@ use Illuminate\Auth\AuthenticationException; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use UnitEnum; @@ -171,7 +180,7 @@ public function table(Table $table): Table ->persistSearchInSession() ->persistSortInSession() ->searchable() - ->searchPlaceholder('Search tenant or next step') + ->searchPlaceholder('Search evidence or next step') ->records(function ( ?string $sortColumn, ?string $sortDirection, @@ -275,6 +284,420 @@ public function environmentFilterChip(): ?array ]; } + /** + * @return array + */ + public function evidenceDisclosurePayload(): array + { + $snapshots = $this->scopedSnapshots(); + $primarySnapshot = $snapshots->first(); + $primaryTenant = $primarySnapshot?->tenant; + $primaryReviewPack = $primarySnapshot instanceof EvidenceSnapshot + ? $this->latestReviewPackForSnapshot($primarySnapshot) + : null; + $primaryStoredReport = $primaryTenant instanceof ManagedEnvironment + ? $this->latestStoredReportForTenant($primaryTenant) + : null; + $primaryOperationRun = $this->primaryOperationRun($primarySnapshot, $primaryReviewPack); + $primaryAction = $this->primaryEvidenceAction($primarySnapshot, $primaryReviewPack, $primaryStoredReport, $primaryOperationRun); + $primaryState = $this->primaryProofState($primarySnapshot, $primaryReviewPack, $primaryStoredReport, $primaryOperationRun); + + return [ + 'scope_label' => $this->evidenceScopeLabel(), + 'scope_description' => $this->evidenceScopeDescription($snapshots), + 'snapshot_count' => $snapshots->count(), + 'primary_title' => $primaryTenant instanceof ManagedEnvironment + ? $primaryTenant->name + : 'No evidence for this scope', + 'primary_summary' => $primarySnapshot instanceof EvidenceSnapshot + ? $this->productSafeEvidenceReason($this->snapshotOutcome($primarySnapshot)->primaryReason) + : 'No evidence snapshot has been generated for the active workspace scope.', + 'primary_state' => $primaryState, + 'primary_action' => $primaryAction, + 'cards' => [ + $this->snapshotProofCard($primarySnapshot), + $this->reviewPackProofCard($primaryReviewPack, $primarySnapshot), + $this->storedReportProofCard($primaryStoredReport, $primaryTenant), + $this->operationProofCard($primaryOperationRun), + ], + 'path_items' => [ + $this->snapshotPathItem($primarySnapshot), + $this->reviewPackPathItem($primaryReviewPack, $primarySnapshot), + $this->storedReportPathItem($primaryStoredReport, $primaryTenant), + $this->operationPathItem($primaryOperationRun), + ], + ]; + } + + /** + * @return Collection + */ + private function scopedSnapshots(): Collection + { + $snapshotIds = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch) + ->pluck('snapshot_id') + ->map(static fn (mixed $snapshotId): int => (int) $snapshotId) + ->all(); + + if ($snapshotIds === []) { + return collect(); + } + + return $this->latestAccessibleSnapshots() + ->filter(static fn (EvidenceSnapshot $snapshot): bool => in_array((int) $snapshot->getKey(), $snapshotIds, true)) + ->values(); + } + + /** + * @param Collection $snapshots + */ + private function evidenceScopeDescription(Collection $snapshots): string + { + $tenant = $this->filteredTenant(); + + if ($tenant instanceof ManagedEnvironment) { + return 'Filtered to '.$tenant->name.'. Proof states below are derived from records directly attributed to this environment.'; + } + + if ($snapshots->isEmpty()) { + return 'Workspace-wide proof view. No accessible evidence snapshot currently matches the active search or filters.'; + } + + return sprintf( + 'Workspace-wide proof view across %d accessible environment%s.', + $snapshots->count(), + $snapshots->count() === 1 ? '' : 's', + ); + } + + private function evidenceScopeLabel(): string + { + $tenant = $this->filteredTenant(); + + return $tenant instanceof ManagedEnvironment + ? 'Environment proof scope' + : 'Workspace proof scope'; + } + + private function latestReviewPackForSnapshot(EvidenceSnapshot $snapshot): ?ReviewPack + { + $reviewPack = $snapshot->reviewPacks + ->sortByDesc(fn (ReviewPack $pack): int => $pack->generated_at?->getTimestamp() ?? 0) + ->first(); + + return $reviewPack instanceof ReviewPack ? $reviewPack : null; + } + + private function latestStoredReportForTenant(ManagedEnvironment $tenant): ?StoredReport + { + $user = auth()->user(); + + if (! $user instanceof User) { + return null; + } + + $visibleReportTypes = collect(StoredReportResource::supportedReportTypes()) + ->filter(function (string $reportType) use ($tenant, $user): bool { + $capability = StoredReportResource::capabilityForReportType($reportType); + + return is_string($capability) && $user->can($capability, $tenant); + }) + ->values() + ->all(); + + if ($visibleReportTypes === []) { + return null; + } + + return StoredReport::query() + ->where('workspace_id', (int) $tenant->workspace_id) + ->where('managed_environment_id', (int) $tenant->getKey()) + ->whereIn('report_type', $visibleReportTypes) + ->latest('created_at') + ->first(); + } + + private function primaryOperationRun(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack): ?OperationRun + { + $run = $snapshot?->operationRun; + + if ($run instanceof OperationRun && $this->canViewOperationRun($run)) { + return $run; + } + + $run = $reviewPack?->operationRun; + + return $run instanceof OperationRun && $this->canViewOperationRun($run) + ? $run + : null; + } + + private function canViewOperationRun(OperationRun $run): bool + { + $user = auth()->user(); + + return $user instanceof User && Gate::forUser($user)->allows('view', $run); + } + + /** + * @return array{label:string,url:string}|null + */ + private function primaryEvidenceAction(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?StoredReport $storedReport, ?OperationRun $operationRun): ?array + { + if ($snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open evidence snapshot', + 'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'), + ]; + } + + if ($reviewPack instanceof ReviewPack && $reviewPack->tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open review pack', + 'url' => ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $reviewPack->tenant, panel: 'admin'), + ]; + } + + if ($storedReport instanceof StoredReport && $storedReport->tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open stored report', + 'url' => StoredReportResource::getUrl('view', ['record' => $storedReport], tenant: $storedReport->tenant, panel: 'admin'), + ]; + } + + if ($operationRun instanceof OperationRun) { + return [ + 'label' => OperationRunLinks::openLabel(), + 'url' => OperationRunLinks::tenantlessView($operationRun), + ]; + } + + return null; + } + + /** + * @return array + */ + private function snapshotProofCard(?EvidenceSnapshot $snapshot): array + { + if (! $snapshot instanceof EvidenceSnapshot) { + return $this->unavailableProofCard( + 'Evidence snapshot', + 'Not generated', + 'No active evidence snapshot is available in this scope.', + 'gray', + ); + } + + $outcome = $this->snapshotOutcome($snapshot); + $isEmptySnapshot = $this->isEmptyEvidenceSnapshot($snapshot); + + return [ + 'label' => 'Evidence snapshot', + 'value' => $isEmptySnapshot ? 'Proof incomplete' : $outcome->primaryLabel, + 'path_state' => $isEmptySnapshot ? 'Empty' : $outcome->primaryLabel, + 'description' => $isEmptySnapshot + ? 'A proof record exists, but no usable captured evidence is available yet.' + : $this->productSafeEvidenceReason($outcome->primaryReason), + 'color' => $outcome->primaryBadge->color, + 'url' => $snapshot->tenant instanceof ManagedEnvironment + ? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin') + : null, + 'meta' => $snapshot->generated_at?->diffForHumans() ?? 'Freshness unavailable', + ]; + } + + /** + * @return array + */ + private function reviewPackProofCard(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): array + { + if (! $reviewPack instanceof ReviewPack) { + return $this->unavailableProofCard( + 'Review pack', + $snapshot instanceof EvidenceSnapshot ? 'Not generated' : 'Not applicable', + $snapshot instanceof EvidenceSnapshot + ? 'No review pack has been generated from the current evidence snapshot.' + : 'A review pack requires an evidence snapshot first.', + 'gray', + ); + } + + return [ + 'label' => 'Review pack', + 'value' => BadgeRenderer::label(BadgeDomain::ReviewPackStatus)((string) $reviewPack->status), + 'description' => $reviewPack->isReady() + ? 'Customer-review artifact exists for this evidence path.' + : 'Review pack exists but is not ready for sharing.', + 'color' => BadgeRenderer::color(BadgeDomain::ReviewPackStatus)((string) $reviewPack->status), + 'url' => $reviewPack->tenant instanceof ManagedEnvironment + ? ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $reviewPack->tenant, panel: 'admin') + : null, + 'meta' => $reviewPack->generated_at?->diffForHumans() ?? 'Generated time unavailable', + ]; + } + + /** + * @return array + */ + private function storedReportProofCard(?StoredReport $storedReport, ?ManagedEnvironment $tenant): array + { + if (! $tenant instanceof ManagedEnvironment) { + return $this->unavailableProofCard( + 'Stored report / export', + 'Not applicable', + 'Stored report availability is evaluated after an evidence scope exists.', + 'gray', + ); + } + + if (! $storedReport instanceof StoredReport) { + return $this->unavailableProofCard( + 'Stored report / export', + 'Unavailable', + 'No repo-supported stored report is available for this environment scope.', + 'gray', + ); + } + + return [ + 'label' => 'Stored report / export', + 'value' => 'Available', + 'description' => StoredReportResource::reportFamilyReportLabel((string) $storedReport->report_type), + 'color' => 'success', + 'url' => StoredReportResource::getUrl('view', ['record' => $storedReport], tenant: $tenant, panel: 'admin'), + 'meta' => $storedReport->created_at?->diffForHumans() ?? 'Report time unavailable', + ]; + } + + /** + * @return array + */ + private function operationProofCard(?OperationRun $operationRun): array + { + if (! $operationRun instanceof OperationRun) { + return $this->unavailableProofCard( + 'Operation proof', + 'Unavailable', + 'No authorized operation run is linked to the current proof path.', + 'gray', + ); + } + + return [ + 'label' => 'Operation proof', + 'value' => 'Available', + 'description' => OperationRunLinks::identifier($operationRun), + 'color' => 'info', + 'url' => OperationRunLinks::tenantlessView($operationRun), + 'meta' => $operationRun->completed_at?->diffForHumans() ?? $operationRun->created_at?->diffForHumans() ?? 'Run time unavailable', + ]; + } + + /** + * @return array{label:string,value:string,description:string,color:string,url:null,meta:string} + */ + private function unavailableProofCard(string $label, string $value, string $description, string $color): array + { + return [ + 'label' => $label, + 'value' => $value, + 'description' => $description, + 'color' => $color, + 'url' => null, + 'meta' => 'Derived from current repo truth', + ]; + } + + /** + * @return array + */ + private function snapshotPathItem(?EvidenceSnapshot $snapshot): array + { + return $this->pathItemFromCard($this->snapshotProofCard($snapshot)); + } + + /** + * @return array + */ + private function reviewPackPathItem(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): array + { + return $this->pathItemFromCard($this->reviewPackProofCard($reviewPack, $snapshot)); + } + + /** + * @return array + */ + private function storedReportPathItem(?StoredReport $storedReport, ?ManagedEnvironment $tenant): array + { + return $this->pathItemFromCard($this->storedReportProofCard($storedReport, $tenant)); + } + + /** + * @return array + */ + private function operationPathItem(?OperationRun $operationRun): array + { + return $this->pathItemFromCard($this->operationProofCard($operationRun)); + } + + /** + * @param array $card + * @return array + */ + private function pathItemFromCard(array $card): array + { + return [ + 'label' => (string) $card['label'], + 'state' => (string) ($card['path_state'] ?? $card['value']), + 'description' => (string) $card['description'], + 'color' => (string) $card['color'], + 'url' => is_string($card['url'] ?? null) ? $card['url'] : null, + ]; + } + + /** + * @return array{label:string,color:string,reason:string,impact:string}|null + */ + private function primaryProofState( + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?OperationRun $operationRun, + ): ?array { + if (! $snapshot instanceof EvidenceSnapshot || ! $this->isEmptyEvidenceSnapshot($snapshot)) { + return null; + } + + return [ + 'label' => 'Proof incomplete', + 'color' => 'warning', + 'reason' => 'Primary evidence snapshot is empty.', + 'impact' => $this->emptySnapshotImpact($reviewPack, $storedReport, $operationRun), + ]; + } + + private function emptySnapshotImpact(?ReviewPack $reviewPack, ?StoredReport $storedReport, ?OperationRun $operationRun): string + { + if ($reviewPack instanceof ReviewPack && $storedReport instanceof StoredReport && $operationRun instanceof OperationRun) { + return 'Supporting proof exists through the review pack, stored report, and operation record.'; + } + + return 'Supporting proof is limited; use the available evidence path items before relying on this snapshot.'; + } + + private function isEmptyEvidenceSnapshot(EvidenceSnapshot $snapshot): bool + { + return $this->snapshotTruth($snapshot)->contentState === 'empty'; + } + + private function productSafeEvidenceReason(string $reason): string + { + return $reason === 'The artifact row exists, but it does not contain usable captured content.' + ? 'A proof record exists, but no usable captured evidence is available yet.' + : $reason; + } + private function snapshotTruth(EvidenceSnapshot $snapshot, bool $fresh = false): ArtifactTruthEnvelope { $presenter = app(ArtifactTruthPresenter::class); @@ -401,7 +824,12 @@ private function latestAccessibleSnapshots(): Collection ->all(); $query = EvidenceSnapshot::query() - ->with('tenant') + ->with([ + 'tenant', + 'operationRun', + 'reviewPacks.operationRun', + 'items', + ]) ->where('workspace_id', $this->workspaceId()) ->where('status', 'active') ->latest('generated_at'); @@ -453,15 +881,15 @@ private function rowForSnapshot(EvidenceSnapshot $snapshot, array $currentReview 'managed_environment_id' => $tenantId, 'snapshot_id' => (int) $snapshot->getKey(), 'generated_at' => $snapshot->generated_at?->toDateTimeString(), - 'artifact_truth_label' => $outcome->primaryLabel, + 'artifact_truth_label' => $truth->contentState === 'empty' ? 'Proof incomplete' : $outcome->primaryLabel, 'artifact_truth_color' => $outcome->primaryBadge->color, 'artifact_truth_icon' => $outcome->primaryBadge->icon, - 'artifact_truth_explanation' => $outcome->primaryReason, + 'artifact_truth_explanation' => $this->productSafeEvidenceReason($outcome->primaryReason), 'artifact_truth' => [ - 'label' => $outcome->primaryLabel, + 'label' => $truth->contentState === 'empty' ? 'Proof incomplete' : $outcome->primaryLabel, 'color' => $outcome->primaryBadge->color, 'icon' => $outcome->primaryBadge->icon, - 'explanation' => $outcome->primaryReason, + 'explanation' => $this->productSafeEvidenceReason($outcome->primaryReason), ], 'next_step' => $nextStep, 'view_url' => $snapshot->tenant diff --git a/apps/platform/app/Providers/Filament/AdminPanelProvider.php b/apps/platform/app/Providers/Filament/AdminPanelProvider.php index 7b8b4fcd..8d684542 100644 --- a/apps/platform/app/Providers/Filament/AdminPanelProvider.php +++ b/apps/platform/app/Providers/Filament/AdminPanelProvider.php @@ -173,6 +173,11 @@ public function panel(Panel $panel): Panel ->icon('heroicon-o-bell-alert') ->group(fn (): string => __('localization.navigation.monitoring')) ->sort(23), + NavigationItem::make(fn (): string => __('localization.navigation.evidence_overview')) + ->url(fn (): string => WorkspaceHubRegistry::cleanUrl(route('admin.evidence.overview'))) + ->icon('heroicon-o-shield-check') + ->group(fn (): string => __('localization.navigation.monitoring')) + ->sort(27), NavigationItem::make(fn (): string => __('localization.navigation.audit_log')) ->url(fn (): string => WorkspaceHubRegistry::cleanUrl(route('admin.monitoring.audit-log'))) ->icon('heroicon-o-clipboard-document-list') diff --git a/apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php b/apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php index 5170cf31..316295d2 100644 --- a/apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php +++ b/apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php @@ -64,6 +64,9 @@ public function build(): NavigationBuilder ->icon(AlertDeliveryResource::getNavigationIcon()) ->visible(fn (): bool => AlertDeliveryResource::canViewAny()), ])), + NavigationItem::make(__('localization.navigation.evidence_overview')) + ->url(fn (): string => $this->workspaceHubUrl(route('admin.evidence.overview'))) + ->icon('heroicon-o-shield-check'), NavigationItem::make(__('localization.navigation.audit_log')) ->url(fn (): string => $this->workspaceHubUrl(route('admin.monitoring.audit-log'))) ->icon('heroicon-o-clipboard-document-list'), diff --git a/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php b/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php index 4e222687..cb51b9ae 100644 --- a/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php +++ b/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php @@ -1171,7 +1171,7 @@ private function contentExplanation(string $contentState): string 'missing_input' => 'The artifact is blocked by missing upstream inputs or failed capture prerequisites.', 'metadata_only' => 'Only metadata was captured for this artifact. Use diagnostics for context, not as the primary truth signal.', 'reference_only' => 'Only reference-level placeholders were captured for this artifact.', - 'empty' => 'The artifact row exists, but it does not contain usable captured content.', + 'empty' => 'A proof record exists, but no usable captured evidence is available yet.', 'unsupported' => 'Structured support is limited for this artifact family, so the current rendering should be treated as diagnostic only.', default => 'The artifact content is available for the intended workflow.', }; diff --git a/apps/platform/lang/de/localization.php b/apps/platform/lang/de/localization.php index dd883a14..7c3f455b 100644 --- a/apps/platform/lang/de/localization.php +++ b/apps/platform/lang/de/localization.php @@ -89,6 +89,7 @@ 'integrations' => 'Integrationen', 'manage_workspaces' => 'Workspaces verwalten', 'operations' => 'Operationen', + 'evidence_overview' => 'Nachweise', 'audit_log' => 'Audit-Log', 'alerts' => 'Alerts', 'governance' => 'Governance', diff --git a/apps/platform/lang/en/localization.php b/apps/platform/lang/en/localization.php index 2a3d8859..135838c9 100644 --- a/apps/platform/lang/en/localization.php +++ b/apps/platform/lang/en/localization.php @@ -89,6 +89,7 @@ 'integrations' => 'Integrations', 'manage_workspaces' => 'Manage workspaces', 'operations' => 'Operations', + 'evidence_overview' => 'Evidence', 'audit_log' => 'Audit Log', 'alerts' => 'Alerts', 'governance' => 'Governance', diff --git a/apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php b/apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php index d91d145c..395ca41a 100644 --- a/apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php +++ b/apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php @@ -1,44 +1,205 @@ - @php($selectedAudit = $this->selectedAuditRecord()) - @php($selectedAuditLink = $this->selectedAuditTargetLink()) - @php($environmentFilterChip = $this->environmentFilterChip()) + @php + $selectedAudit = $this->selectedAuditRecord(); + $selectedAuditLink = $this->selectedAuditTargetLink(); + $selectedAuditProof = $this->selectedAuditProofPayload(); + $environmentFilterChip = $this->environmentFilterChip(); + $payload = $this->auditDisclosurePayload(); + $focus = $payload['focus']; + @endphp - -
-
- Summary-first audit history +
+
+
+
+
+ + {{ $payload['scope_label'] }} + + + Audit proof workbench + +
+ +

+ Which event proves what happened? +

+ +

+ {{ $payload['scope_description'] }} +

+
+ + @if ($environmentFilterChip !== null) +
+ @include('filament.partials.workspace-hub-environment-filter-chip', [ + 'label' => $environmentFilterChip['label'], + 'clearUrl' => $environmentFilterChip['clear_url'], + 'description' => $environmentFilterChip['description'], + ]) +
+ @endif
- -
- Review governance, operational, and workspace-admin events in reverse chronological order without leaving the canonical Monitoring route. -
- -
- Actor, outcome, target, and readable context stay visible even when the original record changes or disappears later. -
- -
- The selected event is URL-addressable through the event query parameter. If the event is no longer visible in the current history view, the page quietly falls back to the unselected log. -
- - @if ($environmentFilterChip !== null) - @include('filament.partials.workspace-hub-environment-filter-chip', [ - 'label' => $environmentFilterChip['label'], - 'clearUrl' => $environmentFilterChip['clear_url'], - 'description' => $environmentFilterChip['description'], - ]) - @endif
- - @if ($selectedAudit) - - @include('filament.pages.monitoring.partials.audit-log-inspect-event', [ - 'selectedAudit' => $selectedAudit, - 'selectedAuditLink' => $selectedAuditLink, - ]) - - @endif +
+
+
+ @if ($focus) +
+
+ + {{ $focus['outcome']['label'] }} + + + {{ $payload['has_selected_event'] ? 'Selected event proof' : 'Latest event proof' }} + +
- {{ $this->table }} +
+
+ Event proof +
+ +

+ {{ $focus['summary'] }} +

+
+ +
+
+
Actor
+
{{ $focus['actor']['label'] }}
+
{{ $focus['actor']['type'] }}
+
+ +
+
Action
+
{{ $focus['action'] }}
+
+ +
+
Target
+
{{ $focus['target']['label'] }}
+
{{ $focus['target']['type'] }}
+
+ +
+
Outcome
+
+ + {{ $focus['outcome']['label'] }} + +
+
+ +
+
Time
+
{{ $focus['time'] }}
+
+
+ +
+ + Inspect event proof + + + @foreach ($focus['related_links'] as $link) + + {{ $link['label'] }} + + @endforeach +
+
+ @else +
+

+ No audit events in scope +

+

+ No event currently proves actor, action, target, outcome, and time for the active view. +

+
+ @endif +
+ + @if ($selectedAudit) +
+ @include('filament.pages.monitoring.partials.audit-log-inspect-event', [ + 'selectedAudit' => $selectedAudit, + 'selectedAuditLink' => $selectedAuditLink, + 'selectedAuditProof' => $selectedAuditProof, + ]) +
+ @endif +
+ + +
+ +
+
+

+ Audit event history +

+

+ Secondary event inventory for filtering, search, and historical investigation after the proof summary is clear. +

+
+ + {{ $this->table }} +
+
diff --git a/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php b/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php index 90585fa0..60781ee9 100644 --- a/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php +++ b/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php @@ -1,20 +1,208 @@ - @php($environmentFilterChip = $this->environmentFilterChip()) + @php + $environmentFilterChip = $this->environmentFilterChip(); + $payload = $this->evidenceDisclosurePayload(); + @endphp -
- -
-

This workspace evidence overview stays workspace-scoped; environment-owned entries appear as an explicit page filter.

+
+
+
+
+
+ + {{ $payload['scope_label'] }} + + + Evidence proof workbench + +
+ +

+ What proof is available for this scope? +

+ +

+ {{ $payload['scope_description'] }} +

+
@if ($environmentFilterChip !== null) - @include('filament.partials.workspace-hub-environment-filter-chip', [ - 'label' => $environmentFilterChip['label'], - 'clearUrl' => $environmentFilterChip['clear_url'], - ]) +
+ @include('filament.partials.workspace-hub-environment-filter-chip', [ + 'label' => $environmentFilterChip['label'], + 'clearUrl' => $environmentFilterChip['clear_url'], + ]) +
@endif
- +
- {{ $this->table }} +
+
+
+
+
+
+ Primary proof path +
+ +

+ {{ $payload['primary_title'] }} +

+ +

+ {{ $payload['primary_summary'] }} +

+ + @if ($payload['primary_state'] !== null) +
+ + {{ $payload['primary_state']['label'] }} + + +
+
+
+ Reason +
+

+ {{ $payload['primary_state']['reason'] }} +

+
+ +
+
+ Impact +
+

+ {{ $payload['primary_state']['impact'] }} +

+
+
+
+ @endif +
+ + @if ($payload['primary_action']) + + {{ $payload['primary_action']['label'] }} + + @else + + No open proof action + + @endif +
+
+ +
+ @foreach ($payload['cards'] as $card) +
+
+
+ {{ $card['label'] }} +
+ +
+ + {{ $card['value'] }} + +
+ +

+ {{ $card['description'] }} +

+ +
+ {{ $card['meta'] }} + + @if ($card['url']) + + Open + + @endif +
+
+
+ @endforeach +
+
+ + +
+ +
+
+

+ Evidence inventory +

+

+ Secondary context for scanning historical proof records after the current path is clear. +

+
+ + {{ $this->table }} +
diff --git a/apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php b/apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php index 47df85cc..e299f604 100644 --- a/apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php +++ b/apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php @@ -1,17 +1,21 @@ @php $selectedAudit = $selectedAudit ?? null; $selectedAuditLink = $selectedAuditLink ?? null; + $selectedAuditProof = $selectedAuditProof ?? null; @endphp @if ($selectedAudit)
- - {{ \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }} - - + + {{ $selectedAuditProof['outcome']['label'] ?? \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }} + + + Event proof + + {{ \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditActorType)($selectedAudit->actorSnapshot()->type->value) }} - + @if (is_array($selectedAuditLink)) +
Actor @@ -41,6 +45,15 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text @endif
+
+
+ Action +
+
+ {{ $selectedAuditProof['action'] ?? \App\Support\Audit\AuditActionId::labelFor((string) $selectedAudit->action) }} +
+
+
Target @@ -53,6 +66,26 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text
+
+
+ Outcome +
+
+ + {{ $selectedAuditProof['outcome']['label'] ?? \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }} + +
+
+ +
+
+ Time +
+
+ {{ $selectedAuditProof['time'] ?? $selectedAudit->recorded_at?->toDayDateTimeString() ?? 'Time unavailable' }} +
+
+
Scope @@ -72,13 +105,15 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text Readable context
- @if ($selectedAudit->contextItems() === []) + @php($contextItems = $selectedAuditProof['context_items'] ?? []) + + @if ($contextItems === [])
No additional context was recorded for this event.
@else
- @foreach ($selectedAudit->contextItems() as $item) + @foreach ($contextItems as $item)
{{ $item['label'] }} @@ -92,24 +127,39 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text @endif
-
-
- Technical metadata -
+
+ + Diagnostics - Collapsed + -
- @foreach ($selectedAudit->technicalMetadata() as $label => $value) -
-
- {{ $label }} -
-
- {{ $value }} -
-
- @endforeach -
-
+ @php($technicalMetadata = $selectedAuditProof['technical_metadata'] ?? []) + + @if ($technicalMetadata === []) +

+ Technical metadata is unavailable for this event in the current capability context. +

+ @else +
+ Technical metadata +
+ +
+ @foreach ($technicalMetadata as $label => $value) +
+
+ {{ $label }} +
+
+ {{ $value }} +
+
+ @endforeach +
+ @endif +
-@endif \ No newline at end of file +@endif diff --git a/apps/platform/routes/web.php b/apps/platform/routes/web.php index 63fd17be..daecbb93 100644 --- a/apps/platform/routes/web.php +++ b/apps/platform/routes/web.php @@ -449,18 +449,6 @@ ->get('/admin/evidence/overview', \App\Filament\Pages\Monitoring\EvidenceOverview::class) ->name('admin.evidence.overview'); -Route::middleware([ - 'web', - 'panel:admin', - 'ensure-correct-guard:web', - DisableBladeIconComponents::class, - DispatchServingFilamentEvent::class, - FilamentAuthenticate::class, - 'ensure-workspace-selected', -]) - ->get('/admin/evidence/overview', \App\Filament\Pages\Monitoring\EvidenceOverview::class) - ->name('admin.evidence.overview'); - Route::middleware([ 'web', 'panel:admin', diff --git a/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php b/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php index 20a97cd8..b6c08bde 100644 --- a/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php +++ b/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php @@ -106,7 +106,7 @@ ->assertNoConsoleLogs(); visit(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()])) - ->waitForText('Summary-first audit history') + ->waitForText('Which event proves what happened?') ->assertSee('Close details') ->assertSee('Readable context') ->assertNoJavaScriptErrors() @@ -123,7 +123,7 @@ 'environment_id' => (int) $tenant->getKey(), 'search' => $tenant->name, ])) - ->waitForText('This workspace evidence overview stays workspace-scoped; environment-owned entries appear as an explicit page filter.') + ->waitForText('What proof is available for this scope?') ->assertSee($tenant->name) ->assertSee('Clear filters') ->assertNoJavaScriptErrors() diff --git a/apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php b/apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php new file mode 100644 index 00000000..4f8db3ba --- /dev/null +++ b/apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php @@ -0,0 +1,485 @@ +browser()->timeout(60_000); + +it('Spec329 smokes Evidence Overview proof-first disclosure and filter clearing', function (): void { + $fixture = spec329BrowserDisclosureFixture(); + spec329AuthenticateDisclosureBrowser($this, $fixture['user'], $fixture['environmentA']); + + $cleanPath = json_encode((string) parse_url(route('admin.evidence.overview'), PHP_URL_PATH), JSON_THROW_ON_ERROR); + + $page = visit(route('admin.evidence.overview')) + ->resize(1440, 1100) + ->waitForText('What proof is available for this scope?') + ->assertSee(__('localization.shell.no_environment_selected')) + ->assertDontSee('Environment filter:') + ->assertSee('Evidence proof workbench') + ->assertSee('Primary proof path') + ->assertSee('Evidence path') + ->assertSee('Evidence snapshot') + ->assertSee('Review pack') + ->assertSee('Stored report / export') + ->assertSee('Operation proof') + ->assertSee('Proof incomplete') + ->assertSee('A proof record exists, but no usable captured evidence is available yet.') + ->assertSee('Primary evidence snapshot is empty.') + ->assertSee('Supporting proof exists through the review pack, stored report, and operation record.') + ->assertSee('Empty') + ->assertSee('Ready') + ->assertSee('Available') + ->assertSourceHas('Search evidence or next step') + ->assertSee('Evidence inventory') + ->assertSee($fixture['environmentA']->name) + ->assertSee($fixture['environmentB']->name) + ->assertDontSee('The artifact row exists, but it does not contain usable captured content.') + ->assertDontSee('artifact row exists') + ->assertSourceMissing('Search tenant or next') + ->assertDontSee('Empty...') + ->assertDontSee('Re...') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider secret should stay hidden') + ->assertDontSee('stack trace should stay hidden') + ->assertDontSee('debug metadata should stay hidden') + ->assertDontSee('internal exception should stay hidden') + ->assertDontSee('current tenant') + ->assertDontSee('tenant filter') + ->assertDontSee('entitled tenant') + ->assertDontSee('all tenants') + ->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true) + ->assertScript('(() => { + const action = document.querySelector("[data-testid=\"evidence-primary-proof-action\"]"); + + if (! action) { + return false; + } + + const box = action.getBoundingClientRect(); + + return action.textContent.trim() === "Open evidence snapshot" + && box.height > 0 + && box.height <= 44 + && action.scrollWidth <= Math.ceil(action.clientWidth) + 1 + && getComputedStyle(action).whiteSpace === "nowrap"; + })()', true) + ->assertScript('(() => { + const badges = Array.from(document.querySelectorAll("[data-testid=\"evidence-path-state-badge\"]")); + + return badges.length >= 4 + && badges.every((badge) => { + const label = badge.textContent.trim(); + const box = badge.getBoundingClientRect(); + + return ! ["Empty...", "Re..."].includes(label) + && box.width > 0 + && badge.scrollWidth <= Math.ceil(badge.clientWidth) + 1; + }); + })()', true) + ->assertScript('(() => { + const grid = document.querySelector("[data-testid=\"evidence-disclosure-workbench\"]"); + const main = document.querySelector("[data-testid=\"evidence-proof-primary\"]"); + const aside = document.querySelector("[data-testid=\"evidence-proof-aside\"]"); + + if (! grid || ! main || ! aside) { + return false; + } + + const children = Array.from(grid.children); + const mainBox = main.getBoundingClientRect(); + const asideBox = aside.getBoundingClientRect(); + + return window.innerWidth >= 1024 + && grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]") + && aside.tagName === "ASIDE" + && children.indexOf(main) !== -1 + && children.indexOf(aside) > children.indexOf(main) + && asideBox.left > mainBox.right + && Math.abs(asideBox.top - mainBox.top) <= 8; + })()', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('evidence-overview--clean')); + + spec329CopyDisclosureScreenshot('evidence-overview--clean'); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $fixture['environmentA']->getKey(), + ])) + ->waitForText('Environment filter:') + ->assertSee('Environment filter: '.$fixture['environmentA']->name) + ->assertSee('What proof is available for this scope?') + ->assertSee($fixture['environmentA']->name) + ->assertSee('Proof incomplete') + ->assertSee('Primary evidence snapshot is empty.') + ->assertDontSee('The artifact row exists, but it does not contain usable captured content.') + ->assertDontSee('Empty...') + ->assertDontSee('Re...') + ->assertDontSee($fixture['environmentB']->name) + ->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('evidence-overview--filtered')); + + spec329CopyDisclosureScreenshot('evidence-overview--filtered'); + + spec329ClearDisclosureEnvironmentFilter($page) + ->waitForText(__('localization.shell.no_environment_selected')) + ->assertDontSee('Environment filter:') + ->waitForText($fixture['environmentB']->name) + ->assertScript("window.location.pathname === {$cleanPath}", true) + ->assertScript('! window.location.search.includes("environment_id=")', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('evidence-overview--after-clear')); + + spec329CopyDisclosureScreenshot('evidence-overview--after-clear'); + + $page->script('window.location.reload();'); + + $page + ->waitForText(__('localization.shell.no_environment_selected')) + ->assertDontSee('Environment filter:') + ->assertSee($fixture['environmentB']->name) + ->assertScript("window.location.pathname === {$cleanPath}", true) + ->assertScript('! window.location.search.includes("environment_id=")', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('evidence-overview--after-reload')); + + spec329CopyDisclosureScreenshot('evidence-overview--after-reload'); +}); + +it('Spec329 smokes Audit Log event-proof disclosure and filter clearing', function (): void { + $fixture = spec329BrowserDisclosureFixture(); + spec329AuthenticateDisclosureBrowser($this, $fixture['user'], $fixture['environmentA']); + + $cleanPath = json_encode((string) parse_url(route('admin.monitoring.audit-log'), PHP_URL_PATH), JSON_THROW_ON_ERROR); + + $page = visit(route('admin.monitoring.audit-log', [ + 'event' => (int) $fixture['auditA']->getKey(), + ])) + ->resize(1440, 1100) + ->waitForText('Which event proves what happened?') + ->assertSee(__('localization.shell.no_environment_selected')) + ->assertDontSee('Environment filter:') + ->assertSee('Audit proof workbench') + ->assertSee('Selected event proof') + ->assertSee('Event proof') + ->assertSee('Actor') + ->assertSee('Action') + ->assertSee('Target') + ->assertSee('Outcome') + ->assertSee('Time') + ->assertSee('Related proof') + ->assertSee('Operation proof') + ->assertSee('Readable context') + ->assertSee('Audit event history') + ->assertSee('Spec329 Browser Operator A') + ->assertSee('Permission posture checked') + ->assertSee('Permission posture report') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider secret should stay hidden') + ->assertDontSee('stack trace should stay hidden') + ->assertDontSee('debug metadata should stay hidden') + ->assertDontSee('internal exception should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertDontSee('current tenant') + ->assertDontSee('tenant filter') + ->assertDontSee('entitled tenant') + ->assertDontSee('all tenants') + ->assertScript('document.querySelector("[data-testid=\"audit-disclosure-diagnostics\"]")?.open === false', true) + ->assertScript('document.querySelector("[data-testid=\"audit-event-diagnostics\"]")?.open === false', true) + ->assertScript('(() => { + const grid = document.querySelector("[data-testid=\"audit-disclosure-workbench\"]"); + const main = document.querySelector("[data-testid=\"audit-proof-primary\"]"); + const aside = document.querySelector("[data-testid=\"audit-proof-aside\"]"); + + if (! grid || ! main || ! aside) { + return false; + } + + const children = Array.from(grid.children); + const mainBox = main.getBoundingClientRect(); + const asideBox = aside.getBoundingClientRect(); + + return window.innerWidth >= 1024 + && grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]") + && aside.tagName === "ASIDE" + && children.indexOf(main) !== -1 + && children.indexOf(aside) > children.indexOf(main) + && asideBox.left > mainBox.right + && Math.abs(asideBox.top - mainBox.top) <= 8; + })()', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('audit-log--clean')); + + spec329CopyDisclosureScreenshot('audit-log--clean'); + + $page = visit(route('admin.monitoring.audit-log', [ + 'environment_id' => (int) $fixture['environmentA']->getKey(), + 'event' => (int) $fixture['auditA']->getKey(), + ])) + ->waitForText('Environment filter:') + ->assertSee('Environment filter: '.$fixture['environmentA']->name) + ->assertSee('Which event proves what happened?') + ->assertSee('Permission posture checked') + ->assertDontSee('Workspace selected by browser proof B') + ->assertScript('document.querySelector("[data-testid=\"audit-disclosure-diagnostics\"]")?.open === false', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('audit-log--filtered')); + + spec329CopyDisclosureScreenshot('audit-log--filtered'); + + spec329ClearDisclosureEnvironmentFilter($page) + ->waitForText(__('localization.shell.no_environment_selected')) + ->assertDontSee('Environment filter:') + ->waitForText('Workspace selected by browser proof B') + ->assertScript("window.location.pathname === {$cleanPath}", true) + ->assertScript('! window.location.search.includes("environment_id=")', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('audit-log--after-clear')); + + spec329CopyDisclosureScreenshot('audit-log--after-clear'); + + $page->script('window.location.reload();'); + + $page + ->waitForText(__('localization.shell.no_environment_selected')) + ->assertDontSee('Environment filter:') + ->assertSee('Workspace selected by browser proof B') + ->assertScript("window.location.pathname === {$cleanPath}", true) + ->assertScript('! window.location.search.includes("environment_id=")', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec329DisclosureScreenshot('audit-log--after-reload')); + + spec329CopyDisclosureScreenshot('audit-log--after-reload'); +}); + +/** + * @return array{ + * user: User, + * environmentA: ManagedEnvironment, + * environmentB: ManagedEnvironment, + * auditA: AuditLog, + * auditB: AuditLog + * } + */ +function spec329BrowserDisclosureFixture(): array +{ + $environmentA = ManagedEnvironment::factory()->active()->create([ + 'name' => 'Spec329 Browser Tenant Environment A', + 'external_id' => 'spec329-browser-environment-a', + ]); + + [$user, $environmentA] = createUserWithTenant( + tenant: $environmentA, + role: 'owner', + workspaceRole: 'owner', + ); + + $environmentB = ManagedEnvironment::factory()->active()->create([ + 'workspace_id' => (int) $environmentA->workspace_id, + 'name' => 'Spec329 Browser Environment B', + 'external_id' => 'spec329-browser-environment-b', + ]); + + createUserWithTenant( + tenant: $environmentB, + user: $user, + role: 'owner', + workspaceRole: 'owner', + ); + + $runA = OperationRun::factory()->forTenant($environmentA)->create([ + 'type' => 'tenant.evidence.snapshot.generate', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + 'context' => [ + 'raw_payload' => 'raw payload should stay hidden', + 'provider_secret' => 'provider secret should stay hidden', + 'stack_trace' => 'stack trace should stay hidden', + 'debug_metadata' => 'debug metadata should stay hidden', + 'internal_exception' => 'internal exception should stay hidden', + ], + 'completed_at' => now()->subMinutes(10), + ]); + + $runB = OperationRun::factory()->forTenant($environmentB)->create([ + 'type' => 'policy.sync', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + 'completed_at' => now()->subMinutes(5), + ]); + + $snapshotA = EvidenceSnapshot::query()->create([ + 'managed_environment_id' => (int) $environmentA->getKey(), + 'workspace_id' => (int) $environmentA->workspace_id, + 'operation_run_id' => (int) $runA->getKey(), + 'status' => EvidenceSnapshotStatus::Active->value, + 'completeness_state' => EvidenceCompletenessState::Complete->value, + 'summary' => [ + 'missing_dimensions' => 0, + 'stale_dimensions' => 0, + 'raw_payload' => 'raw payload should stay hidden', + ], + 'generated_at' => now()->subMinutes(4), + 'expires_at' => now()->addDays(30), + ]); + + EvidenceSnapshot::query()->create([ + 'managed_environment_id' => (int) $environmentB->getKey(), + 'workspace_id' => (int) $environmentB->workspace_id, + 'operation_run_id' => (int) $runB->getKey(), + 'status' => EvidenceSnapshotStatus::Active->value, + 'completeness_state' => EvidenceCompletenessState::Partial->value, + 'summary' => [ + 'missing_dimensions' => 1, + 'stale_dimensions' => 0, + ], + 'generated_at' => now()->subMinutes(9), + 'expires_at' => now()->addDays(30), + ]); + + ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environmentA->getKey(), + 'workspace_id' => (int) $environmentA->workspace_id, + 'evidence_snapshot_id' => (int) $snapshotA->getKey(), + 'operation_run_id' => (int) $runA->getKey(), + 'status' => ReviewPackStatus::Ready->value, + ]); + + $storedReport = StoredReport::factory()->permissionPosture([ + 'raw_payload' => 'raw payload should stay hidden', + ])->create([ + 'managed_environment_id' => (int) $environmentA->getKey(), + 'workspace_id' => (int) $environmentA->workspace_id, + 'fingerprint' => 'spec329-browser-report', + ]); + + $auditA = AuditLog::query()->create([ + 'workspace_id' => (int) $environmentA->workspace_id, + 'managed_environment_id' => (int) $environmentA->getKey(), + 'operation_run_id' => (int) $runA->getKey(), + 'actor_email' => 'spec329-a@example.test', + 'actor_name' => 'Spec329 Browser Operator A', + 'actor_type' => 'human', + 'action' => 'permission_posture.checked', + 'status' => 'success', + 'resource_type' => 'stored_report', + 'resource_id' => (string) $storedReport->getKey(), + 'target_label' => 'Permission posture report', + 'summary' => 'Permission posture checked', + 'metadata' => [ + 'reason' => 'Evidence review', + 'raw_payload' => 'raw payload should stay hidden', + 'provider_secret' => 'provider secret should stay hidden', + 'stack_trace' => 'stack trace should stay hidden', + 'debug_metadata' => 'debug metadata should stay hidden', + 'internal_exception' => 'internal exception should stay hidden', + 'provider_response' => 'provider response should stay hidden', + ], + 'recorded_at' => now()->subMinutes(2), + ]); + + $auditB = AuditLog::query()->create([ + 'workspace_id' => (int) $environmentB->workspace_id, + 'managed_environment_id' => (int) $environmentB->getKey(), + 'actor_email' => 'spec329-b@example.test', + 'actor_name' => 'Spec329 Browser Operator B', + 'actor_type' => 'human', + 'action' => 'workspace.selected', + 'status' => 'success', + 'resource_type' => 'workspace', + 'resource_id' => (string) $environmentB->workspace_id, + 'target_label' => 'Workspace '.$environmentB->workspace_id, + 'summary' => 'Workspace selected by browser proof B', + 'recorded_at' => now()->subMinute(), + ]); + + return [ + 'user' => $user, + 'environmentA' => $environmentA, + 'environmentB' => $environmentB, + 'auditA' => $auditA, + 'auditB' => $auditB, + ]; +} + +function spec329AuthenticateDisclosureBrowser( + mixed $test, + User $user, + ManagedEnvironment $rememberedEnvironment, +): void { + $workspaceId = (int) $rememberedEnvironment->workspace_id; + + $session = [ + WorkspaceContext::SESSION_KEY => $workspaceId, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), + ], + ]; + + $test->actingAs($user)->withSession($session); + + foreach ($session as $key => $value) { + session()->put($key, $value); + } + + setAdminPanelContext($rememberedEnvironment); +} + +function spec329ClearDisclosureEnvironmentFilter(mixed $page): mixed +{ + $page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true); + $page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);'); + + return $page; +} + +function spec329DisclosureScreenshot(string $name): string +{ + return 'spec329-'.$name; +} + +function spec329CopyDisclosureScreenshot(string $name, ?string $targetFilename = null): void +{ + $filename = spec329DisclosureScreenshot($name).'.png'; + $source = base_path('tests/Browser/Screenshots/'.$filename); + $targetDirectory = repo_path('specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots'); + $targetFilename ??= $filename; + + if (! is_dir($targetDirectory)) { + @mkdir($targetDirectory, 0755, true); + } + + if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) { + return; + } + + if (! is_file($source)) { + $source = \Pest\Browser\Support\Screenshot::path($filename); + } + + if (is_file($source)) { + @copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename); + } +} diff --git a/apps/platform/tests/Feature/Filament/AuditLogDetailInspectionTest.php b/apps/platform/tests/Feature/Filament/AuditLogDetailInspectionTest.php index 1641f19d..83611e93 100644 --- a/apps/platform/tests/Feature/Filament/AuditLogDetailInspectionTest.php +++ b/apps/platform/tests/Feature/Filament/AuditLogDetailInspectionTest.php @@ -62,6 +62,7 @@ function auditLogDetailTestRecord(ManagedEnvironment $tenant, array $attributes ->assertCanSeeTableRecords([$audit]) ->assertSet('selectedAuditLogId', (int) $audit->getKey()) ->assertSee('Readable context') + ->assertSee('Diagnostics - Collapsed') ->assertSee('Technical metadata') ->assertSee('Nightly iOS backup') ->assertSee('Open backup set'); @@ -88,6 +89,7 @@ function auditLogDetailTestRecord(ManagedEnvironment $tenant, array $attributes ->assertCanSeeTableRecords([$audit]) ->assertSet('selectedAuditLogId', (int) $audit->getKey()) ->assertSee('Archived backup') + ->assertSee('Diagnostics - Collapsed') ->assertSee('Technical metadata') ->assertDontSee('Open backup set'); }); diff --git a/apps/platform/tests/Feature/Filament/AuditLogPageTest.php b/apps/platform/tests/Feature/Filament/AuditLogPageTest.php index 35803cc8..f80c8c00 100644 --- a/apps/platform/tests/Feature/Filament/AuditLogPageTest.php +++ b/apps/platform/tests/Feature/Filament/AuditLogPageTest.php @@ -44,18 +44,19 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes = ], $attributes)); } -it('renders the canonical audit route as a summary-first monitoring surface', function (): void { +it('renders the canonical audit route as an event-proof-first monitoring surface', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(route('admin.monitoring.audit-log')) ->assertOk() - ->assertSee('Summary-first audit history') - ->assertSee('Review governance, operational, and workspace-admin events in reverse chronological order'); + ->assertSee('Which event proves what happened?') + ->assertSee('Audit proof workbench') + ->assertSee('Workspace-wide audit disclosure across authorized environment and workspace events.'); }); -it('keeps preselected audit detail subordinate to the summary-first route', function (): void { +it('keeps preselected audit detail inside the event-proof-first route', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $audit = auditLogPageTestRecord($tenant, [ @@ -66,7 +67,8 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes = ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()])) ->assertOk() - ->assertSee('Summary-first audit history') + ->assertSee('Which event proves what happened?') + ->assertSee('Selected event proof') ->assertSee('Preselected audit detail') ->assertDontSee('Focused review lane'); }); diff --git a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php index fcf74070..14897ee4 100644 --- a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php +++ b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php @@ -156,7 +156,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a expect(workspaceSidebarLabelsByGroup())->toBe([ '' => ['Overview'], - 'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Audit Log'], + 'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Evidence', 'Audit Log'], 'Reporting' => ['Reviews', 'Customer reviews'], 'Settings' => ['Manage workspaces', 'Integrations', 'Settings'], 'Governance' => ['Governance inbox', 'Decision register'], @@ -170,6 +170,10 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a 'operations', fn ($workspace): string => route('admin.operations.index', ['workspace' => $workspace]), ], + 'evidence overview' => [ + 'evidence overview', + fn (): string => route('admin.evidence.overview'), + ], 'customer reviews' => [ 'customer reviews', fn (): string => CustomerReviewWorkspace::getUrl(panel: 'admin'), @@ -205,7 +209,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a expect(workspaceSidebarLabelsByGroup())->toBe([ '' => ['Overview'], - 'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Audit Log'], + 'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Evidence', 'Audit Log'], 'Reporting' => ['Reviews', 'Customer reviews'], 'Settings' => ['Manage workspaces', 'Integrations', 'Settings'], 'Governance' => ['Governance inbox', 'Decision register'], diff --git a/apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php b/apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php index 3831edb1..fec0c175 100644 --- a/apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php +++ b/apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php @@ -45,6 +45,7 @@ ->assertSet('selectedAuditLogId', (int) $audit->getKey()) ->assertSee('Workspace selected for Workspace 1') ->assertSee('Readable context') + ->assertSee('Diagnostics - Collapsed') ->assertSee('Technical metadata') ->assertActionVisible('close_selected_audit_event'); }); @@ -187,19 +188,21 @@ 'recorded_at' => now(), ]); - $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get(route('admin.monitoring.audit-log')) - ->assertOk() - ->assertDontSee('Close details') - ->assertDontSee('Open operation'); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); + Filament::setTenant(null, true); - $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()])) - ->assertOk() - ->assertSee('Close details') - ->assertSee('Open operation'); + Livewire::actingAs($user) + ->test(AuditLogPage::class) + ->assertActionDoesNotExist('close_selected_audit_event') + ->assertActionDoesNotExist('open_selected_audit_target'); + + Livewire::withQueryParams([ + 'event' => (int) $audit->getKey(), + ]) + ->actingAs($user) + ->test(AuditLogPage::class) + ->assertActionVisible('close_selected_audit_event') + ->assertActionVisible('open_selected_audit_target'); }); it('surfaces origin context quietly when deep-linked to a selected audit event', function (): void { diff --git a/apps/platform/tests/Feature/Monitoring/Spec329EvidenceAuditDisclosureProductizationTest.php b/apps/platform/tests/Feature/Monitoring/Spec329EvidenceAuditDisclosureProductizationTest.php new file mode 100644 index 00000000..a1370929 --- /dev/null +++ b/apps/platform/tests/Feature/Monitoring/Spec329EvidenceAuditDisclosureProductizationTest.php @@ -0,0 +1,317 @@ +toBeString() + ->and($map)->toContain('Evidence Overview route') + ->and($map)->toContain('Audit Log route') + ->and($map)->toContain('Evidence Snapshots') + ->and($map)->toContain('Review Packs') + ->and($map)->toContain('Stored Reports / export artifacts') + ->and($map)->toContain('Actor/action/target/outcome/time') + ->and($map)->toContain('Diagnostics/raw metadata availability'); +}); + +it('Spec329 renders Evidence Overview as a proof-first disclosure surface', function (): void { + [$user, $environment, $snapshot, $reviewPack, $storedReport, $run] = spec329EvidenceFixture(); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(route('admin.evidence.overview')) + ->assertOk() + ->assertSee('What proof is available for this scope?') + ->assertSee('Evidence proof workbench') + ->assertSee('Evidence path') + ->assertSee('Evidence snapshot') + ->assertSee('Review pack') + ->assertSee('Operation proof') + ->assertSee('Stored report / export') + ->assertSee('Spec 329 Demo Tenant - Produktion') + ->assertSee('Proof incomplete') + ->assertSee('A proof record exists, but no usable captured evidence is available yet.') + ->assertSee('Primary evidence snapshot is empty.') + ->assertSee('Supporting proof exists through the review pack, stored report, and operation record.') + ->assertSee('Empty') + ->assertSee('Ready') + ->assertSee('Available') + ->assertSee('Search evidence or next step') + ->assertSee('Diagnostics - Collapsed') + ->assertSee('Evidence inventory') + ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $environment, panel: 'admin'), false) + ->assertSee(ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $environment, panel: 'admin'), false) + ->assertSee(StoredReportResource::getUrl('view', ['record' => $storedReport], tenant: $environment, panel: 'admin'), false) + ->assertSee(\App\Support\OperationRunLinks::tenantlessView($run), false) + ->assertDontSee('The artifact row exists, but it does not contain usable captured content.') + ->assertDontSee('artifact row exists') + ->assertDontSee('Search tenant or next') + ->assertDontSee('Empty...') + ->assertDontSee('Re...') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider secret should stay hidden') + ->assertDontSee('stack trace should stay hidden') + ->assertDontSee('debug metadata should stay hidden') + ->assertDontSee('internal exception should stay hidden') + ->assertDontSee('current tenant') + ->assertDontSee('tenant filter') + ->assertDontSee('entitled tenant') + ->assertDontSee('all tenants') + ->assertDontSee('production tenant'); + + session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id); + + $component = Livewire::actingAs($user)->test(EvidenceOverview::class); + + expect($component->instance()->getTable()->getSearchPlaceholder()) + ->toBe('Search evidence or next step') + ->and((string) file_get_contents(app_path('Filament/Pages/Monitoring/EvidenceOverview.php'))) + ->not->toContain('Search tenant or next'); +}); + +it('Spec329 renders Audit Log as an event-proof-first disclosure surface', function (): void { + [$user, $environment, $audit, $run] = spec329AuditFixture(); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()])) + ->assertOk() + ->assertSee('Which event proves what happened?') + ->assertSee('Audit proof workbench') + ->assertSee('Event proof') + ->assertSee('Actor') + ->assertSee('Action') + ->assertSee('Target') + ->assertSee('Outcome') + ->assertSee('Time') + ->assertSee('Related proof') + ->assertSee('Diagnostics - Collapsed') + ->assertSee('Audit event history') + ->assertSee('Spec329 Operator') + ->assertSee('Permission posture checked') + ->assertSee('Permission posture report') + ->assertSee(\App\Support\OperationRunLinks::tenantlessView($run), false) + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider secret should stay hidden') + ->assertDontSee('stack trace should stay hidden') + ->assertDontSee('debug metadata should stay hidden') + ->assertDontSee('internal exception should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertDontSee('current tenant') + ->assertDontSee('tenant filter') + ->assertDontSee('entitled tenant') + ->assertDontSee('all tenants') + ->assertDontSee('production tenant'); +}); + +it('Spec329 keeps audit selected event state inside the active environment filter', function (): void { + [$user, $environmentA, $environmentB, $auditA, $auditB] = spec329FilteredAuditFixture(); + + Livewire::withQueryParams([ + 'environment_id' => (int) $environmentA->getKey(), + 'event' => (int) $auditB->getKey(), + ]) + ->actingAs($user) + ->test(AuditLogPage::class) + ->assertSee('Environment filter:') + ->assertSee('Which event proves what happened?') + ->assertSet('tableFilters.managed_environment_id.value', (string) $environmentA->getKey()) + ->assertSet('selectedAuditLogId', null) + ->assertCanSeeTableRecords([$auditA]) + ->assertCanNotSeeTableRecords([$auditB]); + + Livewire::withQueryParams([ + 'environment_id' => (int) $environmentA->getKey(), + 'event' => (int) $auditA->getKey(), + ]) + ->actingAs($user) + ->test(AuditLogPage::class) + ->assertSet('selectedAuditLogId', (int) $auditA->getKey()) + ->assertSee('Event proof') + ->assertSee('Audit A proof'); +}); + +it('Spec329 rejects cross-workspace environment filters for both disclosure pages', function (): void { + $environment = ManagedEnvironment::factory()->active()->create(); + [$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner'); + $foreignEnvironment = ManagedEnvironment::factory()->active()->create(); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(route('admin.evidence.overview', ['environment_id' => (int) $foreignEnvironment->getKey()])) + ->assertNotFound(); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(route('admin.monitoring.audit-log', ['environment_id' => (int) $foreignEnvironment->getKey()])) + ->assertNotFound(); +}); + +/** + * @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: EvidenceSnapshot, 3: ReviewPack, 4: StoredReport, 5: OperationRun} + */ +function spec329EvidenceFixture(): array +{ + $environment = ManagedEnvironment::factory()->active()->create([ + 'name' => 'Spec 329 Demo Tenant - Produktion', + ]); + [$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner'); + + $run = OperationRun::factory()->forTenant($environment)->create([ + 'type' => 'tenant.evidence.snapshot.generate', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + 'context' => [ + 'raw_payload' => 'raw payload should stay hidden', + 'provider_secret' => 'provider secret should stay hidden', + 'stack_trace' => 'stack trace should stay hidden', + 'debug_metadata' => 'debug metadata should stay hidden', + 'internal_exception' => 'internal exception should stay hidden', + ], + ]); + + $snapshot = EvidenceSnapshot::query()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'operation_run_id' => (int) $run->getKey(), + 'status' => EvidenceSnapshotStatus::Active->value, + 'completeness_state' => EvidenceCompletenessState::Complete->value, + 'summary' => [ + 'missing_dimensions' => 0, + 'stale_dimensions' => 0, + 'raw_payload' => 'raw payload should stay hidden', + ], + 'generated_at' => now(), + 'expires_at' => now()->addDays(30), + ]); + + $reviewPack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'operation_run_id' => (int) $run->getKey(), + 'status' => ReviewPackStatus::Ready->value, + ]); + + $storedReport = StoredReport::factory()->permissionPosture()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'fingerprint' => 'spec329-evidence-report', + ]); + + return [$user, $environment, $snapshot, $reviewPack, $storedReport, $run]; +} + +/** + * @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: AuditLog, 3: OperationRun} + */ +function spec329AuditFixture(): array +{ + $environment = ManagedEnvironment::factory()->active()->create([ + 'name' => 'Spec329 Audit Environment', + ]); + [$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner'); + + $run = OperationRun::factory()->forTenant($environment)->create([ + 'type' => 'permission_posture.check', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + ]); + + $audit = AuditLog::query()->create([ + 'workspace_id' => (int) $environment->workspace_id, + 'managed_environment_id' => (int) $environment->getKey(), + 'operation_run_id' => (int) $run->getKey(), + 'actor_email' => 'spec329@example.test', + 'actor_name' => 'Spec329 Operator', + 'actor_type' => 'human', + 'action' => 'permission_posture.checked', + 'status' => 'success', + 'resource_type' => 'stored_report', + 'resource_id' => '329', + 'target_label' => 'Permission posture report', + 'summary' => 'Permission posture checked', + 'metadata' => [ + 'reason' => 'Evidence review', + 'raw_payload' => 'raw payload should stay hidden', + 'provider_secret' => 'provider secret should stay hidden', + 'stack_trace' => 'stack trace should stay hidden', + 'debug_metadata' => 'debug metadata should stay hidden', + 'internal_exception' => 'internal exception should stay hidden', + 'provider_response' => 'provider response should stay hidden', + ], + 'recorded_at' => now(), + ]); + + return [$user, $environment, $audit, $run]; +} + +/** + * @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: ManagedEnvironment, 3: AuditLog, 4: AuditLog} + */ +function spec329FilteredAuditFixture(): array +{ + $environmentA = ManagedEnvironment::factory()->active()->create(['name' => 'Spec329 Audit A']); + [$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner'); + + $environmentB = ManagedEnvironment::factory()->active()->create([ + 'workspace_id' => (int) $environmentA->workspace_id, + 'name' => 'Spec329 Audit B', + ]); + createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner'); + + $auditA = AuditLog::query()->create([ + 'workspace_id' => (int) $environmentA->workspace_id, + 'managed_environment_id' => (int) $environmentA->getKey(), + 'actor_email' => 'spec329-a@example.test', + 'actor_name' => 'Spec329 A', + 'actor_type' => 'human', + 'action' => 'workspace.selected', + 'status' => 'success', + 'resource_type' => 'workspace', + 'resource_id' => (string) $environmentA->workspace_id, + 'target_label' => 'Workspace A', + 'summary' => 'Audit A proof', + 'recorded_at' => now(), + ]); + + $auditB = AuditLog::query()->create([ + 'workspace_id' => (int) $environmentB->workspace_id, + 'managed_environment_id' => (int) $environmentB->getKey(), + 'actor_email' => 'spec329-b@example.test', + 'actor_name' => 'Spec329 B', + 'actor_type' => 'human', + 'action' => 'workspace.selected', + 'status' => 'success', + 'resource_type' => 'workspace', + 'resource_id' => (string) $environmentB->workspace_id, + 'target_label' => 'Workspace B', + 'summary' => 'Audit B proof', + 'recorded_at' => now()->addMinute(), + ]); + + return [$user, $environmentA, $environmentB, $auditA, $auditB]; +} diff --git a/apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php b/apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php index 7617743b..37fc0a3b 100644 --- a/apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php +++ b/apps/platform/tests/Feature/Navigation/Spec321AlertsAuditEnvironmentFilterContractTest.php @@ -302,16 +302,20 @@ function spec321QueryKeys(string $url): array ->assertNotFound(); }); -it('alerts_and_audit_log_sidebar_entry_is_workspace_wide', function (): void { +it('alerts_evidence_overview_and_audit_log_sidebar_entries_are_workspace_wide', function (): void { [, $environmentA] = spec321WorkspaceFixture(); $alertUrl = route('filament.admin.alerts'); + $evidenceUrl = route('admin.evidence.overview'); $auditUrl = route('admin.monitoring.audit-log'); expect(WorkspaceHubRegistry::cleanUrl($alertUrl))->toBe($alertUrl) + ->and(WorkspaceHubRegistry::cleanUrl($evidenceUrl))->toBe($evidenceUrl) ->and(WorkspaceHubRegistry::cleanUrl($auditUrl))->toBe($auditUrl) ->and(WorkspaceHubRegistry::cleanUrl(route('filament.admin.alerts', ['environment_id' => (int) $environmentA->getKey()]))) ->toBe($alertUrl) + ->and(WorkspaceHubRegistry::cleanUrl(route('admin.evidence.overview', ['environment_id' => (int) $environmentA->getKey()]))) + ->toBe($evidenceUrl) ->and(WorkspaceHubRegistry::cleanUrl(route('admin.monitoring.audit-log', ['environment_id' => (int) $environmentA->getKey()]))) ->toBe($auditUrl); }); diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--after-clear.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--after-clear.png new file mode 100644 index 00000000..36492378 Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--after-clear.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--after-reload.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--after-reload.png new file mode 100644 index 00000000..36492378 Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--after-reload.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--clean.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--clean.png new file mode 100644 index 00000000..c9eb3d5b Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--clean.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--filtered.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--filtered.png new file mode 100644 index 00000000..af99444c Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-audit-log--filtered.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--after-clear.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--after-clear.png new file mode 100644 index 00000000..ff5e830a Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--after-clear.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--after-reload.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--after-reload.png new file mode 100644 index 00000000..ff5e830a Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--after-reload.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--clean.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--clean.png new file mode 100644 index 00000000..a57e8af0 Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--clean.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--filtered.png b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--filtered.png new file mode 100644 index 00000000..e583b7e8 Binary files /dev/null and b/specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/spec329-evidence-overview--filtered.png differ diff --git a/specs/329-evidence-audit-log-disclosure-productization/checklists/requirements.md b/specs/329-evidence-audit-log-disclosure-productization/checklists/requirements.md new file mode 100644 index 00000000..62330177 --- /dev/null +++ b/specs/329-evidence-audit-log-disclosure-productization/checklists/requirements.md @@ -0,0 +1,65 @@ +# Requirements Checklist: Spec 329 - Evidence / Audit Log Disclosure Productization + +**Purpose**: Validate preparation artifact quality before implementation. +**Created**: 2026-05-19 +**Feature**: `specs/329-evidence-audit-log-disclosure-productization/spec.md` + +## Content Quality + +- [x] No implementation details leak into product requirements beyond required repo constraints. +- [x] User value and operator/auditor workflow are clear. +- [x] Scope is bounded to two existing runtime surfaces. +- [x] Non-goals explicitly prevent backend/workflow overbuild. +- [x] Dependencies and historical specs are listed. + +## Repo Truth And Safety + +- [x] Existing route/class/view/partial paths are named. +- [x] Repo truth map exists and uses required classifications. +- [x] No new persisted truth is proposed. +- [x] No migrations/packages/env/queues/scheduler/storage changes are expected. +- [x] No legacy tenant query alias support is allowed. +- [x] No false immutability/certification/compliance/health claims are allowed. + +## Workspace / Environment Contract + +- [x] Clean workspace-wide entry is specified. +- [x] Canonical `environment_id` filter is specified. +- [x] Visible chip and clear filter are specified. +- [x] Legacy aliases are rejected. +- [x] Cross-workspace environment guard is specified. +- [x] Audit route shell/middleware drift is called out for implementation verification. + +## RBAC / Audit / Diagnostics + +- [x] Existing capabilities and policies remain authoritative. +- [x] Unauthorized action behavior is specified. +- [x] Diagnostics are collapsed/hidden by default. +- [x] Dangerous actions are out of scope unless spec/plan are updated. +- [x] No raw payloads/provider secrets/debug traces are default-visible. +- [x] Audit event first-read fields are specified. +- [x] Evidence path first-read fields are specified. + +## Testability + +- [x] Feature tests are listed. +- [x] Browser smoke flows are listed. +- [x] Navigation/scope guard tests are listed. +- [x] `pint --dirty` and `git diff --check` are listed. +- [x] Full-suite status must be reported honestly. + +## Surface Guardrail Review + +- [x] UI Surface Impact is completed and not contradicted by no-impact wording. +- [x] Decision-first role is classified for both pages. +- [x] Audience-aware disclosure hierarchy is explicit. +- [x] OperationRun link-only impact is explicit. +- [x] Provider boundary posture is explicit. +- [x] Test lane and browser family are explicit. + +## Readiness Decision + +- [x] Spec is ready for implementation planning. +- [x] No open question blocks a bounded implementation loop. +- [x] Review outcome class: acceptable-special-case. +- [x] Workflow outcome: keep. diff --git a/specs/329-evidence-audit-log-disclosure-productization/plan.md b/specs/329-evidence-audit-log-disclosure-productization/plan.md new file mode 100644 index 00000000..531689ab --- /dev/null +++ b/specs/329-evidence-audit-log-disclosure-productization/plan.md @@ -0,0 +1,379 @@ +# Implementation Plan: Spec 329 - Evidence / Audit Log Disclosure Productization + +**Branch**: `329-evidence-audit-log-disclosure-productization` | **Date**: 2026-05-19 | **Spec**: `specs/329-evidence-audit-log-disclosure-productization/spec.md` +**Input**: User-provided Spec 329 and repo inspection. + +## Summary + +Productize the existing Evidence Overview and Audit Log into proof-first and event-proof-first disclosure surfaces. The implementation must keep current routes, source truth, RBAC, and workspace/environment contracts, introduce no backend foundation, and make the first viewport answer: + +```text +What proof is available for this scope? +Which event proves what happened? +``` + +Evidence Overview will elevate proof availability, freshness, evidence path, review/export/report state, and operation proof before its inventory table. Audit Log will elevate actor/action/target/outcome/time, selected/latest event proof, and related proof before raw metadata and the event table. Diagnostics and raw metadata stay collapsed and capability-aware. + +## Implementation Close-Out + +Implemented on 2026-05-19. The runtime change stayed inside the existing Evidence Overview and Audit Log routes/pages, added the existing Evidence Overview route to the Workspace Monitoring sidebar with the concise `Evidence` / `Nachweise` navigation label, removed the duplicated Evidence Overview route registration, kept the existing tables as secondary context, and added targeted Feature plus Pest Browser coverage. No route/archetype/coverage classification changed, so UI registry documents were not updated; the active spec package carries close-out proof through `repo-truth-map.md`, tasks, tests, and screenshots. + +Post-review UI corrections on 2026-05-19 keep dynamic Environment display names unchanged even when they contain `Tenant`, replace implementation-heavy empty-snapshot copy with product-safe proof language, add an explicit `Proof incomplete` hierarchy for empty primary snapshots, keep right-panel Evidence Path badge labels short and unclipped (`Empty`, `Ready`, `Available`), and replace the static table search placeholder with `Search evidence or next step`. + +## Technical Context + +**Language/Version**: PHP 8.4.15, Laravel 12.52.0. +**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Tailwind CSS 4.2.2. +**Storage**: PostgreSQL; no schema change expected. +**Testing**: Pest 4 Feature/Livewire/Browser tests. +**Validation Lanes**: confidence and browser; targeted navigation guard tests. +**Target Platform**: Laravel Sail locally; Dokploy/container deployment posture unchanged. +**Project Type**: Laravel monolith under `apps/platform`. +**Performance Goals**: DB-only page render; no Graph/provider API calls during render; no broad new query family beyond existing source queries unless bounded/eager-loaded. +**Constraints**: No new persisted truth, migration, package, queue, scheduler, storage, env var, deployment asset, compatibility route, or legacy alias support. +**Scale/Scope**: Two existing Filament pages, their views/partial, feature-local payload helpers if needed, focused tests, and browser smoke. + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: changed existing operator-facing strategic surfaces. +- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: + - `/admin/evidence/overview` + - `/admin/audit-log` + - `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` + - `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php` + - `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php` + - `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php` + - `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php` +- **No-impact class, if applicable**: N/A. +- **Native vs custom classification summary**: Native Filament pages/tables plus existing Blade composition; no new UI framework. +- **Shared-family relevance**: evidence/report viewers, audit event detail, status messaging, proof links, OperationRun links, workspace/environment filter chip, diagnostics disclosure. +- **State layers in scope**: page payload, URL query (`environment_id`, `event`, `supportAccess` where existing), table state, selected audit event state, diagnostics disclosure. +- **Audience modes in scope**: auditor, customer-adjacent reviewer, operator-MSP, manager, support reviewer where authorized. +- **Decision/diagnostic/raw hierarchy plan**: proof/event first, evidence/context second, diagnostics collapsed third, raw/support hidden. +- **Raw/support gating plan**: collapsed by default and capability-gated through existing support diagnostics capability where any raw metadata is exposed. +- **One-primary-action / duplicate-truth control**: each workbench owns one proof/open next action; table and raw/detail helpers remain secondary. +- **Handling modes by drift class or surface**: review-mandatory for UI-025 and UI-044 strategic surfaces; document-in-feature for any UI coverage registry no-change decision. +- **Repository-signal treatment**: Spec 325 target images are visual direction only; runtime claims must be repo-verified or unavailable. +- **Special surface test profiles**: `global-context-shell`, `monitoring-state-page`, `shared-detail-family`. +- **Required tests or manual smoke**: Feature/Livewire tests for layout/RBAC/scope/disclosure plus Pest Browser smoke for clean/filtered/clear/reload/non-empty/empty/diagnostics/table-secondary behavior. +- **Exception path and spread control**: none expected. Any new dangerous action, export engine, schema, capability, or raw-disclosure mechanism requires spec/plan update first. +- **Active feature PR close-out entry**: Smoke Coverage. +- **UI/Productization coverage decision**: active spec package carries productization proof. Update UI coverage registry only if route/archetype/coverage classification changes; otherwise document why UI-025/UI-044 plus Spec 329 artifacts are sufficient. +- **Coverage artifacts to update**: none expected unless implementation changes route/archetype state. +- **Navigation / Filament provider-panel handling**: no panel provider registration changes expected. Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`. +- **Navigation update**: add the existing Evidence Overview route to the Workspace Monitoring sidebar through the manual `WorkspaceSidebarNavigation` path and the admin panel's default workspace navigation items using a concise area label; no panel provider registration change. +- **Screenshot or page-report need**: screenshots required; full page report optional unless implementation materially changes coverage classification. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes. +- **Systems touched**: Evidence/Audit pages, EvidenceSnapshot/ReviewPack/StoredReport/OperationRun/AuditLog models, resource policies, `OperationRunLinks`, `RelatedNavigationResolver`, `BadgeRenderer`, `ArtifactTruthPresenter`, workspace hub filter/reset helpers. +- **Shared abstractions reused**: existing policies/capabilities, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, `OperationRunLinks`, `RelatedNavigationResolver`, `BadgeRenderer`, `ArtifactTruthPresenter`. +- **New abstraction introduced? why?**: none. Page-local private helpers only if needed to keep pages/views reviewable. +- **Why the existing abstraction was sufficient or insufficient**: existing paths already provide truth, authorization, related links, and filters. They do not currently impose the proof-first/event-proof-first hierarchy. +- **Bounded deviation / spread control**: no public reusable disclosure system; keep presentation local to these two surfaces. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: link/proof presentation only. +- **Central contract reused**: `OperationRunLinks`, `OperationRunUrl`, existing OperationRun policies and detail routes. +- **Delegated UX behaviors**: open operation/proof links only where existing link helpers and authorization allow. +- **Surface-owned behavior kept local**: proof availability labels and unavailable states. +- **Queued DB-notification policy**: unchanged / N/A. +- **Terminal notification path**: unchanged. +- **Exception path**: none. + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: no new provider seam. +- **Provider-owned seams**: existing Microsoft/Entra/Intune terms only where existing source records use them. +- **Platform-core seams**: workspace, environment, evidence, audit, proof, operation, report, disclosure. +- **Neutral platform terms / contracts preserved**: workspace, environment, actor, action, target, outcome, time, proof, diagnostics, raw metadata. +- **Retained provider-specific semantics and why**: provider-specific report or audit target copy may remain where source data is explicitly provider-bound. +- **Bounded extraction or follow-up path**: none for Spec 329. + +## Constitution Check + +- **Inventory-first, snapshots-second**: Evidence snapshots remain explicit artifact truth. No new snapshot or inventory persistence is introduced. +- **Read/write separation by default**: Pages remain read-first. Any unexpected mutation or destructive action requires spec/plan update, confirmation, authorization, audit, notification, and tests. +- **Single Contract Path to Graph**: No Graph/provider API calls may be added to page render. +- **Deterministic Capabilities**: Reuse existing `Capabilities`, `CapabilityResolver`, `WorkspaceCapabilityResolver`, resource policies, and report-type capability mapping. +- **Proportionality / anti-bloat**: No new source of truth, persisted entity, enum/status family, public abstraction, proof engine, or cross-domain UI framework. +- **Workspace isolation**: Clean URLs stay workspace-wide. `environment_id` resolves through current workspace and actor entitlement. +- **Tenant/environment language**: Runtime copy must avoid tenant as platform context. Provider-specific tenant wording is allowed only where explicitly external/provider-bound. +- **OperationRun UX**: Deep links only through existing OperationRun link helpers; no operation start or lifecycle changes. +- **UI-COV-001**: Existing strategic surfaces UI-025 and UI-044 change. Active spec package must carry repo-truth map, tests, and browser screenshots; implementation close-out must decide whether route inventory/coverage matrix updates are needed. +- **TEST-GOV-001**: Targeted Feature and Browser tests are explicit; no broad heavy-governance lane unless implementation reveals structural risk. +- **Filament-native UI**: Use native Filament components and shared primitives first; custom Blade must preserve Filament visual language, accessibility, and disclosure hierarchy. +- **Filament v5 / Livewire v4**: Livewire v4.0+ compliance required. No Livewire v3 or Filament v3/v4 APIs. + +## Current Repo Truth Summary + +Existing verified surfaces: + +- `EvidenceOverview` is a Filament `Page` at `/admin/evidence/overview`, with an existing table over latest active accessible `EvidenceSnapshot` records. +- Evidence page currently uses `EvidenceSnapshot`, `EnvironmentReview`, `ArtifactTruthPresenter`, `EvidenceSnapshotResource` links, `WorkspaceHubEnvironmentFilter`, and clear/reset helpers. +- `AuditLog` is a Filament `Page` at `/admin/audit-log`, with an existing table over scoped `AuditLog` records, event selection through `event`, support-access filter, related navigation links, and environment filter chip. +- `AuditLog` model derives actor snapshots, target snapshots, outcome labels, readable context items, and technical metadata. +- `AuditLog` selected-event partial currently renders `Technical metadata` directly when an event is selected; Spec 329 must move that behind collapsed/capability-aware disclosure. +- `EvidenceSnapshot`, `ReviewPack`, and `AuditLog` have `operationRun()` relations. `OperationRunLinks::related()` already maps evidence snapshot and review pack generation runs to artifact links. +- `StoredReportResource` supports permission posture and Entra admin role report types with capability checks and disabled global search. +- `WorkspaceHubEnvironmentFilter::fromRequest()` accepts canonical `environment_id`, scopes to current workspace, checks actor access, and rejects inaccessible/cross-workspace IDs. +- Navigation tests already cover canonical environment filter, clear filter, legacy alias rejection, and workspace hub no-drift behavior for several related surfaces. + +Known productization gaps: + +- Evidence Overview is table-first and does not yet show a proof readiness workbench, evidence path panel, export/report availability panel, or collapsed diagnostics affordance. +- Audit Log is summary-first but not yet event-proof-first; actor/action/target/outcome/time should dominate the first-read, and raw technical metadata must not be default-visible. +- Current Audit Log route middleware includes `ensure-environment-context-selected`; implementation must verify this does not force Environment shell ownership or remembered Environment fallback. +- `routes/web.php` contains a duplicated `/admin/evidence/overview` route registration; implementation may document or clean this only if safe and in scope. + +## Existing Repository Surfaces Likely Affected + +Runtime files, only during later implementation: + +- `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` +- `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php` +- `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php` +- `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php` +- `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php` +- `apps/platform/resources/lang/en/*` and `apps/platform/resources/lang/de/*` only if surrounding page-copy conventions require localized strings. + +Tests, only during later implementation: + +- `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php` +- `apps/platform/tests/Feature/Monitoring/EvidenceOverviewWorkspaceHubContractTest.php` +- `apps/platform/tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php` +- `apps/platform/tests/Feature/Filament/AuditLogPageTest.php` +- `apps/platform/tests/Feature/Filament/AuditLogDetailInspectionTest.php` +- `apps/platform/tests/Feature/Filament/AuditLogAuthorizationTest.php` +- `apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php` +- `apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php` +- `apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php` +- `apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php` + +Spec/UI artifacts: + +- `specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md` +- screenshot artifacts under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/` +- optional UI coverage registry updates only if implementation materially changes route/archetype/coverage state. + +## Domain / Model Implications + +- No new model, table, migration, enum, status family, source of truth, or persisted display state. +- Evidence proof state must derive from: + - `EvidenceSnapshot.status`, `completeness_state`, `summary`, `generated_at`, `expires_at`. + - `EvidenceSnapshot::items()`, `reviewPacks()`, `environmentReviews()`, and `operationRun()`. + - `ReviewPack.status`, `generated_at`, `expires_at`, `file_size`, and related review/snapshot/run. + - `StoredReport.report_type`, `payload`, `fingerprint`, report-type capability, and environment/workspace scope. + - Existing finding exception evidence references where linked and authorized. +- Audit proof state must derive from: + - `AuditLog` actor snapshot, action, target snapshot, normalized outcome, recorded time, managed environment/workspace scope, operation run relation, readable context, and related navigation resolver. +- If exact evidence, report, export, operation, risk/decision, or proof link is missing, render explicit unavailable/missing/not generated/not applicable state. + +## UI / Filament Implications + +- Filament v5 and Livewire v4.0+ compliance must be preserved. +- Panel providers remain registered in `apps/platform/bootstrap/providers.php`; no panel provider changes expected. +- No globally searchable resource is added or changed. Related resources must remain disabled for global search or backed by safe View/Edit pages. +- Use Filament sections/tables/actions and shared badge/filter primitives where suitable. +- Avoid fake charts, fake compliance readiness, fake immutable/certified badges, and generic KPI dashboards. +- Main Evidence structure: + - header/scope + - proof readiness workbench + - evidence path panel + - export/report availability panel + - evidence inventory/table as secondary context + - collapsed diagnostics disclosure +- Main Audit structure: + - header/scope + - audit proof workbench + - selected/latest event proof panel + - actor/action/target/outcome/time first-read + - audit event table as secondary context + - collapsed raw metadata/diagnostics disclosure +- Right-side proof/disclosure panel should be desktop aside and mobile stack where practical. + +## Livewire / Page State Implications + +- Evidence clean entry must clear remembered/stale Environment-like table filters and session state. +- Audit clean entry must clear remembered/stale Environment-like table filters and session state. +- `environment_id` query state remains the only shareable environment filter key. +- Audit `event` query remains selected-event state and must be normalized against current query/table filters and authorization. +- `supportAccess` may remain existing Audit Log state if it does not conflict with disclosure hierarchy. +- Clear filter must remove `environment_id` and environment-like table/session state through existing helpers. + +## RBAC / Policy Implications + +Reuse existing authorization: + +- Workspace page access through `WorkspaceContext` / `WorkspaceCapabilityResolver`. +- Environment access through current accessible environment queries and `User::canAccessTenant()`. +- Evidence visibility through `Capabilities::EVIDENCE_VIEW` and `EvidenceSnapshotPolicy`. +- Review pack visibility/download through `Capabilities::REVIEW_PACK_VIEW`, `ReviewPackPolicy`, and existing download route authorization. +- Stored report visibility through report-type capabilities in `StoredReportResource`. +- Audit page access through `Capabilities::AUDIT_VIEW`. +- Operation proof visibility through existing `OperationRunPolicy`, link helpers, and related resource policies. +- Diagnostics/raw metadata through `Capabilities::SUPPORT_DIAGNOSTICS_VIEW` or stricter existing capability. + +No new permission semantics should be added unless implementation proves existing capabilities cannot express the action and spec/plan/tasks are updated first. + +## Audit / Evidence / Disclosure Implications + +- No new audit event is required for read-only page rendering unless current page-open audit conventions are extended repo-wide. +- Evidence should appear as proof path/state: + - available + - incomplete + - stale + - unavailable + - not generated + - not applicable +- Audit should appear as event proof: + - actor + - action + - target + - outcome + - time + - scope + - related proof +- Do not show raw provider payloads, debug metadata, internal exception traces, provider secrets, raw OperationRun payloads, raw audit metadata blobs, or stack traces by default. +- If diagnostics disclosure is present, it must be collapsed and capability-aware. + +## Data / Migration Implications + +Expected outcome: + +- No migrations. +- No seeders. +- No data backfills. +- No packages. +- No env vars. +- No queues/scheduler/storage changes. +- No deployment asset changes. +- No backwards compatibility layer. +- No legacy tenant query alias support. + +If implementation discovers an actual schema need, stop and update spec/plan/tasks/repo-truth-map first. Default decision remains no migration. + +## Localization / Copy Implications + +- Runtime copy must be concise, customer/auditor-safe, and operator-readable. +- Stable visible strings should be EN/DE localized if current project pattern routes page copy through language files. +- Avoid platform-context `tenant` wording. Use `Workspace` and `Environment` for shell/filter/product context. +- Provider-bound tenant wording may remain only when describing an external Microsoft/Entra tenant identifier or provider payload outside the default decision view. + +## Implementation Phases + +### Phase 1 - Repo Truth And Current UI Audit + +- Re-read spec, plan, tasks, and `repo-truth-map.md`. +- Inspect current Evidence Overview, Audit Log, selected-event partial, models, policies, related links, and tests. +- Update `repo-truth-map.md` before runtime changes if implementation discovers new source truth or gaps. +- Confirm no migration/package/env/queue/storage need. + +### Phase 2 - Tests First + +- Add tests for repo truth map existence. +- Add Feature/Livewire tests for evidence proof-first layout, audit event-proof-first layout, evidence path, raw metadata hidden, export/report availability, RBAC, canonical environment filter, legacy aliases, cross-workspace guard, and tenant-copy guard. + +### Phase 3 - Evidence Overview Productization + +- Refactor the existing page into proof-first layout. +- Bind to existing evidence snapshot, review pack, stored report, operation proof, review/decision/risk sources where repo-supported. +- Keep table available as secondary context. +- Keep diagnostics collapsed and raw metadata hidden. + +### Phase 4 - Audit Log Productization + +- Refactor the existing page into event-proof-first layout. +- Ensure actor/action/target/outcome/time/scope are first-read. +- Move selected-event technical metadata behind collapsed/capability-aware disclosure. +- Keep audit table available as secondary context. + +### Phase 5 - Shared Disclosure UX + +- Add consistent disclosure rule panel/affordance across both pages: + - decision/proof visible + - evidence/event visible + - diagnostics collapsed + - raw/support hidden +- Show unavailable/deferred states honestly. + +### Phase 6 - Scope / Filter Integration + +- Preserve clean workspace-wide entry. +- Preserve `?environment_id=` filter, visible chip, clear filter, reload/back/forward behavior. +- Preserve legacy alias rejection and cross-workspace guard. +- Verify Audit Log route middleware does not force Environment shell ownership. + +### Phase 7 - Browser Smoke And Screenshots + +- Add targeted Browser smoke for evidence clean/filtered/clear/reload/non-empty/empty, audit clean/filtered/clear/reload/non-empty/empty, diagnostics hidden, table secondary, and no platform-context tenant wording. +- Save screenshots under the spec artifacts path when generated. + +### Phase 8 - Validation And Close-Out + +- Run targeted Feature/navigation tests, Browser smoke, filtered guard tests, `pint --dirty`, and `git diff --check`. +- Report full suite status honestly if not run. +- Record no migrations/seeders/packages/env/queues/scheduler/storage/deployment asset/backcompat/legacy alias support. + +## Testing Strategy + +Required tests: + +- `it('documents_evidence_audit_log_repo_truth_map')` +- `it('renders_evidence_overview_proof_first_layout')` +- `it('renders_audit_log_event_proof_first_layout')` +- `it('shows_evidence_path_without_raw_metadata_by_default')` +- `it('shows_audit_actor_action_target_outcome_time_before_raw_metadata')` +- `it('shows_export_or_report_availability_only_when_repo_supported')` +- `it('hides_evidence_and_audit_raw_diagnostics_by_default')` +- `it('respects_evidence_audit_and_diagnostics_capabilities')` +- `it('evidence_overview_supports_canonical_environment_filter')` +- `it('audit_log_supports_canonical_environment_filter')` +- `it('evidence_and_audit_reject_legacy_environment_aliases')` +- `it('evidence_and_audit_reject_cross_workspace_environment_filter')` +- `it('evidence_and_audit_do_not_use_tenant_as_platform_context_copy')` +- `tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php` + +Required Browser smoke: + +- Evidence Overview clean workspace. +- Evidence Overview environment-filtered. +- Evidence clear filter and reload. +- Audit Log clean workspace. +- Audit Log environment-filtered. +- Audit clear filter and reload. +- Evidence non-empty proof state. +- Audit non-empty event state. +- Evidence empty state. +- Audit empty state. +- Diagnostics hidden by default. +- Tables remain secondary. +- No platform-context tenant wording. + +## Rollout / Deployment Considerations + +- No env vars expected. +- No migrations expected. +- No queue/scheduler changes expected. +- No storage/volume changes expected. +- No deployment asset changes expected unless implementation registers new Filament assets, which is not expected. If assets are registered, deployment must include `cd apps/platform && php artisan filament:assets`. +- Staging validation should include targeted Browser smoke for light mode, workspace/environment filter behavior, and disclosure hierarchy before production promotion. + +## Risk Controls + +- Do not implement before `repo-truth-map.md` exists. +- Do not show any metric, proof state, export state, operation proof, review/risk link, or diagnostic affordance unless mapped to repo truth. +- If a planned UI element has no safe source or authorization path, render unavailable/not generated/not applicable or omit it. +- Do not introduce backend foundation to make a UI card true. +- Do not support legacy query aliases. +- Do not rewrite completed Specs 314-328. + +## Candidate Selection Gate + +Passed. The candidate was directly user-provided as Spec 329, explicitly deferred by Specs 326-328, not already present as an active/completed package, aligned with UI-025/UI-044 strategic surface coverage, and scoped to two existing proof/disclosure pages. + +## Spec Readiness Gate + +Expected pass after `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and `checklists/requirements.md` are created and preparation analysis has no blocking findings. diff --git a/specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md b/specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md new file mode 100644 index 00000000..cf6df347 --- /dev/null +++ b/specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md @@ -0,0 +1,120 @@ +# Spec 329 Repo Truth Map + +Status: implemented +Created: 2026-05-19 +Implemented: 2026-05-19 +Purpose: classify each Evidence Overview and Audit Log disclosure element before and after runtime implementation. This map is based on repository inspection and the Spec 329 implementation diff. + +## Classification Legend + +- `repo-verified`: exact runtime source exists and was inspected. +- `foundation-real`: backend model/service/policy exists, but exact page binding still needs implementation verification. +- `derived from existing model`: display value can be derived from existing persisted/domain truth. +- `empty/unavailable state`: no safe source/action exists for v1; show explicit unavailable state or omit. +- `deferred future capability`: outside Spec 329 and must not be shown as live runtime truth. + +## Required Data Areas + +| Data area | Repo source | Preparation finding | +|---|---|---| +| Evidence Overview route | `apps/platform/routes/web.php`, route `admin.evidence.overview` | repo-real path is `/admin/evidence/overview`; route appears duplicated and should be verified during implementation | +| Workspace sidebar Evidence entry | `WorkspaceSidebarNavigation`, `AdminPanelProvider`, route `admin.evidence.overview` | repo-real sidebar entry under Monitoring links to the existing workspace-owned route | +| Evidence Overview page | `EvidenceOverview` and `evidence-overview.blade.php` | repo-real current layout is scope text plus table | +| Evidence Snapshots | `EvidenceSnapshot`, `EvidenceSnapshotResource`, `EvidenceSnapshotPolicy` | repo-real snapshot status, completeness, summary, generated/expiry timestamps, tenant/workspace scope, operation run relation, detail route | +| Evidence Snapshot Items | `EvidenceSnapshotItem` relation | foundation-real item inventory for deeper proof path; raw item/payload detail must not be default-visible | +| Review Packs | `ReviewPack`, `ReviewPackResource`, `ReviewPackPolicy`, `ReviewPackDownloadController` | repo-real statuses and detail/download surfaces; Evidence Overview currently does not expose pack availability | +| Stored Reports / export artifacts | `StoredReport`, `StoredReportResource` | repo-real report types and capability-bound detail resources; no generic export engine is implied | +| OperationRuns | `OperationRun`, `OperationRunLinks` | repo-real operation proof links for evidence snapshot/review pack generation where linked to run | +| Audit Log route | `apps/platform/routes/web.php`, route `admin.monitoring.audit-log` | repo-real path is `/admin/audit-log`; middleware includes `ensure-environment-context-selected` and must be verified for workspace-hub shell safety | +| Audit Log page | `AuditLog` and `audit-log.blade.php` | repo-real current page is summary-first history with selected-event detail | +| Audit Log events | `AuditLog` model | repo-real actor/action/target/outcome/time/scope fields and derived snapshots | +| Actor/action/target/outcome/time | `AuditLog::actorSnapshot()`, `targetSnapshot()`, `normalizedOutcome()`, `recorded_at`, `action` | repo-verified fields; action label via `AuditActionId::labelFor()` | +| Risk/Decision links if present | `FindingException`, `FindingExceptionEvidenceReference`, `RelatedNavigationResolver` | foundation-real; only show where related route and authorization exist | +| Customer Review Workspace evidence links | `CustomerReviewWorkspace`, `EvidenceSnapshotAuditLogTest`, review/evidence source query params | foundation-real context for evidence proof links; no redesign in Spec 329 | +| Governance Inbox evidence links | `GovernanceInbox`, Spec 327 repo truth | foundation-real context only; no redesign in Spec 329 | +| Operations proof links | `OperationRunLinks::tenantlessView()`, `OperationRunLinks::related()` | repo-real for operation proof/details and linked evidence/review pack artifacts | +| Environment filter state | `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `ClearsWorkspaceHubEnvironmentFilterState`, `CanonicalAdminEnvironmentFilterState`, filter chip partial | repo-real canonical `environment_id`, clear filter, alias rejection, cross-workspace guard | +| Diagnostics/raw metadata availability | `AuditLog::technicalMetadata()`, `AuditLog::metadata`, `OperationRun.context`, snapshot/report payload fields | repo-real raw/support sources exist but must stay collapsed/hidden and capability-aware | + +## UI Element Map + +| UI element | Surface | Source model/service/page | Status source | Authorization/capability | Workspace/Environment scope | OperationRun/evidence/audit/export link | Fallback/empty state | Classification | +|---|---|---|---|---|---|---|---|---| +| Evidence Overview route | Evidence Overview | `admin.evidence.overview` | route | workspace middleware + page access | current workspace | none | 404/workspace chooser per middleware | repo-verified | +| Workspace sidebar Evidence entry | Workspace sidebar | `WorkspaceSidebarNavigation`, `AdminPanelProvider`, route `admin.evidence.overview` | static navigation item | workspace sidebar visibility | current workspace | `/admin/evidence/overview` | item absent only if sidebar group is unavailable | repo-verified | +| Evidence Overview title/question | Evidence Overview | page/view stable copy | static copy | page access | workspace/filter | none | static title | repo-verified | +| Workspace scope label | Evidence Overview | `WorkspaceContext` and shell | current workspace/session | workspace membership | workspace shell | none | 404 if unavailable | repo-verified | +| Environment filter chip | Evidence Overview | `environmentFilterChip()`, shared chip partial | `WorkspaceHubEnvironmentFilter` + table state | actor must access environment | `?environment_id={id}` only | none | no chip on clean URL | repo-verified | +| Clear filter action | Evidence Overview | `clearOverviewFilters()`, resetter | generated clean route | page access | removes canonical/table/session state | none | hidden when unfiltered | repo-verified | +| Legacy alias rejection | Evidence Overview | `WorkspaceHubFilterStateResetter` + navigation tests | forbidden query/session keys | page access | aliases do not set filter | none | workspace-wide view or safe 404 | repo-verified | +| Cross-workspace environment guard | Evidence Overview | `WorkspaceHubEnvironmentFilter::fromRequest()` and `normalizeTenantFilter()` | environment scoped by workspace/access | workspace and environment entitlement | current workspace only | none | 404 / safe no-access | repo-verified | +| Proof readiness workbench | Evidence Overview | new page-local payload over existing rows | derived from latest accessible snapshots and related artifacts | evidence/report/review/run capabilities | current workspace/filter | evidence/review/report/operation links where authorized | `No evidence for this scope` | derived from existing model | +| Evidence snapshot state | Evidence Overview | `EvidenceSnapshot.status`, `completeness_state`, `ArtifactTruthPresenter` | persisted fields + derived presenter | `evidence.view` and `EvidenceSnapshotPolicy` for links | current workspace/filter | `EvidenceSnapshotResource::getUrl('view')` | `Evidence snapshot unavailable` | repo-verified | +| Evidence freshness | Evidence Overview | `generated_at`, `expires_at`, `ArtifactTruthPresenter` | timestamps and derived freshness | evidence visibility | current workspace/filter | evidence snapshot detail | `Freshness unavailable` | derived from existing model | +| Evidence path: snapshot | Evidence Overview | `EvidenceSnapshot` | active/current snapshot | evidence visibility | current workspace/filter | evidence snapshot detail | unavailable/not generated | repo-verified | +| Evidence path: review pack | Evidence Overview | `ReviewPack`, `EvidenceSnapshot::reviewPacks()` | status/generated/expired fields | `review_pack.view`, `ReviewPackPolicy` | current workspace/filter | review pack detail/download if authorized | `Review pack unavailable` / `Not generated` | foundation-real | +| Evidence path: operation proof | Evidence Overview | `EvidenceSnapshot::operationRun()`, `ReviewPack::operationRun()`, `OperationRunLinks` | relation/run id | operation visibility | current workspace/filter | operation detail | `Operation proof unavailable` | foundation-real | +| Evidence path: stored report/export | Evidence Overview | `StoredReport`, `StoredReportResource` | report type/fingerprint/payload | report-type capability | current workspace/filter | stored report detail | `Stored report unavailable` | foundation-real | +| Evidence path: decision/risk record | Evidence Overview | `FindingExceptionEvidenceReference`, related resources | evidence reference relation | finding exception/evidence capabilities | current workspace/filter | finding/exception/evidence route if authorized | `Decision proof unavailable` | foundation-real | +| Evidence path: audit trail | Evidence Overview | `AuditLog` events for evidence actions | action/resource metadata | `audit.view` | current workspace/filter | audit log filtered/selected link if implemented | `Audit event unavailable` | foundation-real | +| Export/report availability panel | Evidence Overview | `ReviewPack`, `StoredReport` | existing statuses and report types | review/report capabilities | current workspace/filter | review pack download/detail, stored report detail | `Unavailable` / `Not generated` | foundation-real | +| Evidence inventory table | Evidence Overview | existing Filament table | latest accessible snapshots | evidence visibility | current workspace/filter | row URL to evidence snapshot | existing empty state | repo-verified | +| Evidence diagnostics disclosure | Evidence Overview | raw snapshot/report/run payloads | raw fields exist | `support_diagnostics.view` or stricter | current scope | existing detail/support surfaces only | collapsed/hidden | foundation-real | +| Raw provider payloads | Evidence Overview | raw Graph/provider payloads | not safe default | support-only future | N/A | N/A | never default-visible | deferred future capability | +| Audit Log route | Audit Log | `admin.monitoring.audit-log` | route | workspace middleware + `audit.view` | current workspace | none | 404/403 per existing resolver | repo-verified | +| Audit Log title/question | Audit Log | page/view stable copy | static copy | audit page access | workspace/filter | none | static title | repo-verified | +| Workspace scope label | Audit Log | `WorkspaceContext` and shell | current workspace/session | workspace membership | workspace shell | none | 404 if unavailable | repo-verified | +| Environment filter chip | Audit Log | `environmentFilterChip()`, shared chip partial | `WorkspaceHubEnvironmentFilter` + table state | actor must access environment | `?environment_id={id}` only | none | no chip on clean URL | repo-verified | +| Clear filter action | Audit Log | empty state/header clear flow + resetter | generated clean route | audit page access | removes canonical/table/session state | none | hidden/unavailable when unfiltered | repo-verified | +| Legacy alias rejection | Audit Log | resetter and navigation tests | forbidden query/session keys | audit page access | aliases do not set filter | none | workspace-wide view or safe 404; explicit Spec 329 coverage required | foundation-real | +| Cross-workspace environment guard | Audit Log | `WorkspaceHubEnvironmentFilter::fromRequest()`, `authorizedTenants()` | environment scoped by workspace/access | workspace and environment entitlement | current workspace only | none | 404 / safe no-access | repo-verified | +| Audit proof workbench | Audit Log | new page-local payload over `AuditLog` | latest/selected visible event | `audit.view` | current workspace/filter | selected event, related record, operation link | `No audit events in scope` | derived from existing model | +| Selected event proof panel | Audit Log | `selectedAuditRecord()`, selected-event partial | `event` query + normalized table/filter visibility | `audit.view` and row scope | current workspace/filter | related record/proof via resolver | no selected event panel | repo-verified | +| Actor | Audit Log | `AuditLog::actorSnapshot()`, `actorDisplayLabel()` | actor fields/metadata | `audit.view` | current workspace/filter | selected event proof | `Actor unavailable` | repo-verified | +| Action | Audit Log | `action`, `AuditActionId::labelFor()` | action id | `audit.view` | current workspace/filter | selected event proof | `Action unavailable` | repo-verified | +| Target | Audit Log | `targetSnapshot()`, `targetDisplayLabel()` | target fields | `audit.view` | current workspace/filter | related target link if authorized | `No target snapshot` | repo-verified | +| Outcome | Audit Log | `normalizedOutcome()`, `BadgeRenderer` | outcome/status | `audit.view` | current workspace/filter | selected event proof | `Outcome unavailable` | repo-verified | +| Time | Audit Log | `recorded_at` | timestamp | `audit.view` | current workspace/filter | selected event proof | `Time unavailable` | repo-verified | +| Scope | Audit Log | `workspace`, `tenant`, `workspace_id`, `managed_environment_id` | relationship/ids | `audit.view`, environment entitlement | workspace/filter | selected event proof | workspace-wide event | repo-verified | +| Related operation proof | Audit Log | `AuditLog::operationRun()`, `RelatedNavigationResolver`, `OperationRunLinks` | operation relation/resource target | operation/source authorization | current workspace/filter | operation detail/source record | `Operation proof unavailable` | foundation-real | +| Related evidence/export proof | Audit Log | resource type/id + resolver | target relation where supported | source authorization | current workspace/filter | source detail route | `Related proof unavailable` | foundation-real | +| Readable context | Audit Log | `AuditLog::contextItems()` | safe scalar metadata subset | `audit.view` | current workspace/filter | selected event proof | no additional context | repo-verified | +| Technical metadata | Audit Log | `AuditLog::technicalMetadata()` | technical fields | raw/diagnostics capability | current scope | collapsed diagnostics only | hidden by default; current default exposure must change | repo-verified | +| Raw audit metadata blob | Audit Log | `AuditLog.metadata` | raw JSON/array | support/raw capability only | current scope | collapsed diagnostics only if ever exposed | hidden by default | foundation-real | +| Support access history filter/export | Audit Log | existing header actions | supportAccess query/export action | current page access; export needs review | workspace/filter | CSV stream for support actions only | existing action hidden/available per current page; not a generic audit export claim | repo-verified | +| Audit table/history | Audit Log | existing Filament table | scoped query, filters, columns | `audit.view` + environment entitlement | workspace/filter | inspect action with event query | existing empty state | repo-verified | +| Disclosure rule panel | Both | page-local copy/state | static hierarchy + capabilities | page access | current scope | links only when authorized | compact panel | derived from existing model | +| Tenant platform copy guard | Both | runtime copy/tests | string assertions | N/A | page copy | N/A | use Workspace/Environment; implementation test required | repo-verified | + +## Required Runtime Element Decisions + +| Element | v1 decision | +|---|---| +| New evidence backend | deferred future capability; do not build | +| New audit ingestion engine | deferred future capability; do not build | +| New immutable/certification/integrity claim | deferred future capability; do not claim | +| Generic compliance readiness badge | deferred future capability; do not show | +| Generic export engine | deferred future capability; use only existing ReviewPack/StoredReport/download truth | +| Evidence freshness | derive from existing generated/expires/artifact truth only | +| Review pack state | derive from existing `ReviewPack.status` and timestamps only | +| Stored report availability | derive from existing `StoredReport` records and report-type capabilities only | +| Operation proof | link only through existing run relations/helpers and authorization | +| Audit event selected panel | actor/action/target/outcome/time first; raw metadata collapsed | +| Diagnostics | collapsed/hidden by default and capability-aware if exposed | +| Raw provider payloads | never default-visible | +| Dangerous/mutating actions | do not add unless spec/plan updated first | +| Legacy query aliases | rejected/neutralized; do not support | + +## Implementation Update Rule + +If implementation discovers that a planned UI element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty/unavailable state` or `deferred future capability`. Do not create backend foundation inside Spec 329 without updating `spec.md`, `plan.md`, `tasks.md`, and this map first. + +## Implementation Close-Out + +- Evidence Overview now renders a proof-first workbench from existing `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, artifact-truth, policy, and workspace-hub filter sources. The existing inventory table remains secondary context, and the existing route is reachable from the Workspace Monitoring sidebar. +- Audit Log now renders an event-proof-first workbench from existing `AuditLog` actor/action/target/outcome/time/scope fields, related navigation, and operation proof links. The existing event history table and selected-event inspect flow remain available. +- Diagnostics/raw metadata are not default-visible. Evidence diagnostics are collapsed with guidance to use authorized detail surfaces; audit technical metadata is behind collapsed, capability-aware disclosure. +- The duplicated `/admin/evidence/overview` route registration was removed; the canonical route name and path remain unchanged. +- UI coverage registry files were not changed because route names, paths, archetypes, and strategic surface classifications remain the existing UI-025 and UI-044 entries. Spec 329 carries the implementation proof through this repo truth map, targeted tests, and browser screenshots. +- Browser screenshots are stored in `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`. +- No migrations, seeders, packages, environment variables, queues, scheduler changes, storage changes, deployment assets, backwards compatibility layer, or legacy tenant alias support were added. diff --git a/specs/329-evidence-audit-log-disclosure-productization/spec.md b/specs/329-evidence-audit-log-disclosure-productization/spec.md new file mode 100644 index 00000000..0a55e89f --- /dev/null +++ b/specs/329-evidence-audit-log-disclosure-productization/spec.md @@ -0,0 +1,607 @@ +# Feature Specification: Spec 329 - Evidence / Audit Log Disclosure Productization + +**Feature Branch**: `329-evidence-audit-log-disclosure-productization` +**Created**: 2026-05-19 +**Status**: Implemented +**Type**: Runtime UI productization / evidence-proof surface / audit disclosure UX +**Runtime posture**: Narrow runtime UI implementation. Repo-based. No invented backend foundation. +**Input**: User-provided full Spec 329 draft. + +## Dependencies And Historical Context + +Depends on: + +- Spec 314 - Workspace Hub Navigation Context Contract. +- Spec 315 - Environment CTA Explicit Filter Contract. +- Spec 316 - Workspace Hub Clear Filter Contract. +- Spec 317 - Legacy Tenant / Environment Context Cleanup. +- Spec 318 - Admin Surface Scope & Shell Context Audit. +- Spec 319 - Environment-Owned Surface Routing & Shell Context Contract. +- Spec 320 - Workspace-Owned Analysis Surface Registration & Shell Cutover. +- Spec 321 - Alerts / Audit Log Environment Filter Contract Decision. +- Spec 322 - Browser No-Drift Regression Guard. +- Spec 325 - Screenshot-Anchored Strategic Target Images. +- Spec 326 - Customer Review Workspace v1 Productization. +- Spec 327 - Governance Inbox Decision-First Workbench Productization. +- Spec 328 - Operations Hub Decision-First Workbench Productization. + +Repo truth adjustment: the user draft allowed `/admin/evidence` or an existing canonical route. Current repository truth is `admin.evidence.overview` at `/admin/evidence/overview` and `admin.monitoring.audit-log` at `/admin/audit-log`. Spec 329 productizes those existing routes and must not create replacement routes, new evidence/audit engines, new export pipelines, new persistence, or new compliance certification semantics. + +Spec 325 target images are visual calibration only. They are not runtime truth for proof availability, export readiness, immutability, certification, RBAC, disclosure levels, evidence freshness, or audit event verification. + +## Spec Candidate Check + +- **Problem**: Evidence Overview and Audit Log are repo-real, but they still risk reading as technical metadata and event tables instead of proof/disclosure surfaces that answer what proof exists and which event proves what happened. +- **Today's failure**: Evidence snapshots, review packs, operation proof, stored reports, actor/action/target/outcome/time, and raw metadata are not consistently ordered by decision value. Audit selected-event detail currently exposes technical metadata in the default selected-event view, and Evidence Overview is still table-first. +- **User-visible improvement**: Auditors, security reviewers, MSP operators, and service delivery teams can see scope, proof availability, evidence path, actor/action/target/outcome/time, related proof, disclosure posture, and diagnostics status before any raw metadata. +- **Smallest enterprise-capable version**: Productize only the existing Evidence Overview and Audit Log pages using existing `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, `AuditLog`, policies/capabilities, related links, and workspace hub filter helpers. Tables remain secondary context. +- **Explicit non-goals**: No new audit engine, evidence store, immutable storage, legal attestation, compliance framework mapping, external auditor portal, export engine, report generation engine, retention/hold system, AI summarization, package, queue, scheduler, storage, env var, migration, seed, compatibility route, or legacy query alias. +- **Permanent complexity imported**: Feature-local page payloads, targeted Feature/Livewire tests, one Browser smoke, screenshots, and `repo-truth-map.md`. No new persisted truth, public abstraction, enum/status family, status taxonomy, or cross-domain UI framework. +- **Why now**: Specs 314-322 stabilized workspace/environment context. Specs 326-328 established the strategic productization pattern. Spec 328 explicitly deferred Evidence / Audit Log Disclosure Productization as the next proof/disclosure lane. +- **Why not local**: A column rename or small copy tweak would not change the first-read hierarchy. A new evidence/audit backend would overbuild. The narrow correct slice is a repo-truth-bounded productization pass on two existing pages. +- **Approval class**: Core Enterprise. +- **Red flags triggered**: Strategic UI productization and evidence/audit disclosure semantics. Defense: scope is limited to existing pages and existing truth sources, forbids new backend/state frameworks, and prevents false proof/certification claims. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12** +- **Decision**: approve. + +## Candidate Source And Completed-Spec Guardrail + +- **Candidate source**: Direct user-provided manual promotion for Spec 329, aligned with the follow-up list in Specs 326, 327, and 328 and the Audit Log / Evidence Overview strategic rows in `docs/ui-ux-enterprise-audit/`. +- **Current package check**: No `specs/329-*` package, branch, or completed package existed before this preparation run. +- **Related completed-spec check**: Specs 314-328 include historical/completed foundation and productization signals. They are dependency context only and must not be rewritten by Spec 329. +- **Close alternatives deferred**: Environment Dashboard / Baseline Compare Productization, Restore Safety Workflow Productization, and Provider Readiness Productization remain follow-up candidates 330-332. +- **Smallest viable implementation slice**: Existing Evidence Overview and Audit Log only: header/scope, proof/event workbench, evidence path/event proof panel, export/report availability, table as secondary context, collapsed diagnostics, RBAC-aware links/actions, canonical `environment_id` filter behavior, empty states, and targeted tests/browser smoke. + +## Spec Scope Fields + +- **Scope**: workspace canonical-view proof/disclosure surfaces, optionally filtered by canonical `environment_id`. +- **Primary Routes**: + - Existing Evidence Overview route: `/admin/evidence/overview`. + - Existing Evidence route name: `admin.evidence.overview`. + - Existing Evidence page class: `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`. + - Existing Evidence view: `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`. + - Existing Audit Log route: `/admin/audit-log`. + - Existing Audit route name: `admin.monitoring.audit-log`. + - Existing Audit page class: `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`. + - Existing Audit view: `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php`. + - Existing Audit selected-event partial: `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php`. +- **Data Ownership**: + - Evidence truth: `EvidenceSnapshot.status`, `completeness_state`, `summary`, `generated_at`, `expires_at`, `operation_run_id`, `workspace_id`, and `managed_environment_id`. + - Evidence path truth: `EvidenceSnapshotItem`, `ReviewPack`, `StoredReport`, `EnvironmentReview`, `FindingExceptionEvidenceReference`, and `OperationRun` links where existing relations/queries prove availability. + - Audit truth: `AuditLog.actor_*`, `actor_type`, `actor_label`, `action`, `resource_type`, `resource_id`, `target_label`, `status`, `outcome`, `summary`, `metadata`, `operation_run_id`, `recorded_at`, `workspace_id`, and `managed_environment_id`. + - Operation proof truth: `OperationRunLinks::tenantlessView()`, `OperationRunLinks::related()`, `AuditLog::operationRun()`, `EvidenceSnapshot::operationRun()`, and `ReviewPack::operationRun()`. + - Workspace/environment scope: `WorkspaceContext`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, and the shared environment filter chip partial. +- **RBAC**: + - Workspace membership required. + - Evidence page data uses existing environment entitlement and `evidence.view`. + - Evidence snapshot details use `EvidenceSnapshotPolicy`. + - Review pack links/download/open states use `review_pack.view` / `ReviewPackPolicy` and existing download authorization. + - Stored report links use report-type capabilities such as `permission_posture.view` and `entra_roles.view`. + - Audit Log page access uses workspace membership and `audit.view`. + - Operation proof links use existing operation visibility and related-route authorization. + - Diagnostics/raw metadata visibility uses `support_diagnostics.view` or a stricter existing capability if implementation finds one. + - Non-member or cross-workspace environment access remains deny-as-not-found. + - Member with missing capability must not see protected records, raw metadata, or unauthorized actions. + +For canonical-view specs: + +- **Default filter behavior when environment context is active**: clean `/admin/evidence/overview` and `/admin/audit-log` remain workspace-wide and must not inherit remembered Environment context, Filament tenant context, session table filters, or legacy query aliases. +- **Explicit entitlement checks preventing cross-environment leakage**: `?environment_id=` must resolve through the current workspace and actor entitlement. Cross-workspace or inaccessible IDs return safe no-access / 404. + +## UI Surface Impact + +Does this spec add, remove, rename, or materially change any reachable UI surface? + +- [ ] No UI surface impact +- [x] Existing page changed +- [ ] New page/route added +- [x] Navigation changed +- [ ] Filament panel/provider surface changed +- [ ] New modal/drawer/wizard/action added +- [x] New table/form/state added +- [ ] Customer-facing surface changed +- [ ] Dangerous action changed +- [x] Status/evidence/review presentation changed +- [x] Workspace/environment context presentation changed + +## UI/Productization Coverage + +- **Route/page/surface**: `/admin/evidence/overview`, `/admin/audit-log`, `EvidenceOverview`, `AuditLog`, their Blade views, and Audit selected-event partial. +- **Current or new page archetype**: Evidence / Audit strategic surfaces, matching `docs/ui-ux-enterprise-audit/route-inventory.md` rows UI-025 and UI-044. +- **Design depth**: Strategic Surface. +- **Repo-truth level**: repo-verified route/page/model foundations; individual runtime elements are classified in `repo-truth-map.md`. +- **Existing pattern reused**: Filament Page, Filament table, Filament Sections where suitable, badges, shared environment filter chip, `BadgeRenderer`, `ArtifactTruthPresenter`, `OperationRunLinks`, resource policies, and current workspace hub resetter/filter helpers. +- **New pattern required**: no new runtime framework; page-local workbench composition only. +- **Screenshot required**: yes, Browser smoke screenshots under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`. +- **Page audit required**: no full new audit unless implementation materially changes route inventory or archetype. Evidence Overview is already UI-044 but lacks a page report; implementation may document no registry update if Spec 329 carries the page productization proof, or update UI coverage artifacts if the route/archetype state changes. +- **Customer-safe review required**: yes for default copy because evidence/audit surfaces are auditor-adjacent. Default views must avoid raw JSON, debug vocabulary, false certification, and unsupported verification claims. +- **Dangerous-action review required**: no dangerous actions expected. If implementation unexpectedly adds download/export/open support actions, they must remain navigation/download actions with existing authorization. Any destructive/high-impact action requires spec/plan update first and must use `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests. +- **Coverage files updated or explicitly not needed**: + - [ ] `docs/ui-ux-enterprise-audit/route-inventory.md` + - [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` + - [ ] `docs/ui-ux-enterprise-audit/page-reports/...` + - [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md` + - [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md` + - [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md` + - [ ] `N/A - no reachable UI surface impact` + - [x] Active spec package must carry repo-truth map, tests, browser screenshots, and close-out coverage decision. Registry updates are required only if runtime changes alter route/archetype/coverage classification. + +## Cross-Cutting / Shared Pattern Reuse + +- **Cross-cutting feature?**: yes. +- **Interaction class(es)**: evidence/report viewers, audit event detail, status messaging, proof links, OperationRun links, environment filter chip, diagnostics disclosure, table empty states, export/download/open actions. +- **Systems touched**: `EvidenceOverview`, `AuditLog`, audit event partial, `EvidenceSnapshotResource`, `ReviewPackResource`, `StoredReportResource`, `OperationRunLinks`, `RelatedNavigationResolver`, `BadgeRenderer`, `ArtifactTruthPresenter`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, resource policies and capabilities. +- **Existing pattern(s) to extend**: existing evidence table, audit table, audit selected-event detail, environment chip, related navigation resolver, artifact truth presentation, OperationRun links, resource policies. +- **Shared contract / presenter / builder / renderer to reuse**: `BadgeRenderer`, `ArtifactTruthPresenter`, `OperationRunLinks`, `RelatedNavigationResolver`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, existing policy/capability resolvers. +- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for evidence snapshots, audit events, related operation links, badges, authorization, and filter/reset behavior. They are insufficient only in first-read hierarchy and default disclosure ordering on these pages. +- **Allowed deviation and why**: bounded page-local payload/view helpers are allowed if needed to reduce Blade complexity. New public evidence/audit disclosure frameworks, status taxonomies, presenter layers, or proof engines are not allowed. +- **Consistency impact**: Evidence, review pack, stored report, operation, audit, scope, diagnostic, export, and action labels must stay aligned across source resources and related links. +- **Review focus**: Verify no fake proof, no false green state, no raw diagnostics by default, no unauthorized links/actions, no shell-scope regression, no tenant platform copy, and no duplicate local truth layer. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: link and proof availability semantics only. No new OperationRun creation, queueing, dedupe, lifecycle transition, summary-count writer, or notification behavior. +- **Shared OperationRun UX contract/layer reused**: `OperationRunLinks`, `OperationRunUrl`, related resource links, existing operation visibility, and existing operation detail pages. +- **Delegated start/completion UX behaviors**: N/A - no operation start. +- **Local surface-owned behavior that remains**: show `Operation proof available`, `Operation proof unavailable`, or authorized open operation link based on existing relations/links. +- **Queued DB-notification policy**: unchanged / N/A. +- **Terminal notification path**: unchanged. +- **Exception required?**: none. + +## Provider Boundary / Platform Core Check + +- **Shared provider/platform boundary touched?**: no new provider seam. +- **Boundary classification**: platform-core proof/disclosure views over existing provider-backed evidence and audit records. +- **Seams affected**: display/routing over evidence snapshots, review packs, stored reports, OperationRuns, audit events, environment filters, and diagnostics disclosure. +- **Neutral platform terms preserved or introduced**: workspace, environment, evidence, proof, audit event, actor, action, target, outcome, time, export artifact, diagnostics, raw metadata. +- **Provider-specific semantics retained and why**: Microsoft/Entra/Intune terms may appear only where the underlying provider record or report already uses them. Do not surface raw provider IDs, Graph payloads, provider responses, or provider diagnostics by default. +- **Why this does not deepen provider coupling accidentally**: no Graph calls, provider contracts, provider connection changes, provider-shaped persistence, or provider taxonomy changes. +- **Follow-up path**: Environment Dashboard / Baseline Compare, Restore Safety Workflow, and Provider Readiness remain separate specs. + +## UI / Surface Guardrail Impact + +| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note | +|---|---:|---|---|---|---:|---| +| Evidence Overview page | yes | Native Filament page plus existing Blade composition | evidence/report viewer, proof path, filter chip | page, URL query, table state, derived payload | no | Existing route only | +| Workspace sidebar Evidence entry | yes | Native Filament navigation item | workspace hub navigation | route/link state only | no | Existing route only | +| Audit Log page | yes | Native Filament page plus existing Blade composition | audit event proof, selected detail, filter chip | page, URL query, selected event, table state | no | Existing route only | +| Evidence proof workbench | yes | Filament sections / page-local Blade | proof status and artifact links | page payload | no | Derived from repo truth | +| Audit proof workbench | yes | Filament sections / page-local Blade | actor/action/target/outcome/time | page payload and selected event | no | Derived from repo truth | +| Evidence/Audit tables | yes | existing Filament tables | secondary evidence/event inventory | table state | no | Tables remain available | +| Diagnostics disclosure | yes | collapsed/progressive disclosure only | support/raw detail | detail links/action visibility | no | Authorized and collapsed by default | + +## Decision-First Surface Role + +| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction | +|---|---|---|---|---|---|---|---| +| Evidence Overview | Primary proof availability surface | Reviewer decides whether this workspace/environment scope contains usable proof | scope, evidence availability, freshness, evidence path, review pack/export/report availability, operation proof state | evidence inventory table, snapshot detail, review pack detail/download, stored report detail, operation detail, diagnostics | Primary because it answers proof readiness before artifact inspection | Follows evidence/proof path, not storage object browsing | Prevents scanning raw snapshots first | +| Audit Log | Primary audit event proof surface | Reviewer decides which event proves what happened | scope, actor, action, target, outcome, time, related record/proof, disclosure status | selected event context, related record, operation detail, raw metadata diagnostics | Primary because it proves actor/action/target/outcome/time | Follows disclosure, not raw event history | Prevents raw metadata from becoming first-read | +| Existing tables | Secondary Context | Operator scans inventory/history after proof summary is clear | concise rows, filters, sort, inspect/open affordance | row detail/source route | Secondary because tables support investigation/history | Keeps existing monitoring power | Reduces table-first dominance | +| Diagnostics disclosure | Tertiary Evidence / Diagnostics | Support/operator inspects technical data after proof path | collapsed availability only | raw metadata, technical IDs, support diagnostics where authorized | Not primary; diagnostics support proof | Preserves debug depth | Prevents default raw-console experience | + +## Audience-Aware Disclosure + +| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention | +|---|---|---|---|---|---|---|---| +| Evidence Overview | auditor, security reviewer, operator-MSP, manager, support reviewer | proof availability, evidence freshness, evidence snapshot, review pack/export/report state, operation proof state, scope | secondary inventory table and artifact detail links | raw snapshot item payloads, raw Graph/provider data, stack traces, debug metadata | open evidence snapshot or review/export proof where authorized | raw metadata, provider payloads, unsupported verification claims, unauthorized links | top workbench states proof state once; table adds inventory context | +| Audit Log | auditor, security reviewer, operator-MSP, support reviewer | actor, action, target, outcome, time, scope, related proof, disclosure status | selected-event readable context and related links | raw metadata, technical IDs, internal exception/debug data, provider payloads | inspect/open selected event or related proof where authorized | raw metadata, diagnostics, provider payloads, secrets | workbench states event proof once; selected detail adds proof/context | + +## UI/UX Surface Classification + +| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Evidence Overview | Workbench / Evidence | Proof-first evidence workspace | Open evidence proof or review/export artifact | explicit primary proof link plus existing table row detail | existing table row URL remains | proof panel/table links | none introduced | `/admin/evidence/overview` | existing evidence/review/report/operation detail routes | active workspace, optional `environment_id` chip | Evidence / Proof | scope, snapshot state, freshness, review/export/report/operation proof state | none | +| Audit Log | Workbench / Audit | Event-proof audit history | Inspect event or open related proof | selected event panel and inspect action | existing inspect action | proof panel/table actions | none introduced | `/admin/audit-log` | same route with `event` query and related routes | active workspace, optional `environment_id` chip | Audit Event | actor, action, target, outcome, time, scope | none | +| Diagnostics disclosure | Diagnostics / Support Raw | Collapsed diagnostic context | Expand or open diagnostics if authorized | disclosure/detail action | N/A | below/inside proof panel | none | same pages | existing authorized detail surfaces | authorized-only label | Diagnostics | collapsed status only | none | + +## Operator Surface Contract + +| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions | +|---|---|---|---|---|---|---|---|---|---|---| +| Evidence Overview | Auditor / MSP operator / security reviewer | Decide whether current scope has proof and where to open it | Workspace evidence proof workbench | What proof is available for this scope? | scope, evidence snapshot, freshness, review pack/export, stored report, operation proof, evidence path, unavailable states | raw snapshot payloads, provider responses, debug metadata, raw OperationRun context | proof availability, freshness, artifact availability, disclosure state | none on page by default | open evidence snapshot, open review pack/report/operation proof where authorized | none introduced | +| Audit Log | Auditor / governance admin / support reviewer | Decide which event proves what happened | Workspace audit event proof workbench | Which event proves what happened? | actor, action, target, outcome, time, scope, related proof, disclosure level | raw metadata, technical IDs, provider payloads, stack traces, debug metadata | event outcome, actor type, target type, scope, proof availability, disclosure state | none on page by default | inspect event, open related proof/record where authorized | none introduced | + +## Proportionality Review + +- **New source of truth?**: no. +- **New persisted entity/table/artifact?**: no. `repo-truth-map.md` is a Spec Kit preparation artifact, not runtime truth. +- **New abstraction?**: no public abstraction. Page-local private helpers are allowed only when they reduce Blade complexity and stay feature-local. +- **New enum/state/reason family?**: no domain state. Display states must derive from existing snapshot, review pack, stored report, operation, audit, policy, and capability truth. +- **New cross-domain UI framework/taxonomy?**: no. +- **Current operator problem**: Existing Evidence and Audit pages must answer proof/disclosure questions without forcing raw table/metadata inspection first. +- **Existing structure is insufficient because**: Current pages expose tables and selected-event detail but do not consistently prioritize proof path, actor/action/target/outcome/time, availability, freshness, and disclosure hierarchy before raw metadata. +- **Narrowest correct implementation**: Refactor existing page layout and derived payloads, bind to existing sources, keep diagnostics collapsed, and add targeted tests/browser smoke. +- **Ownership cost**: Feature-local layout/payload tests, one Browser smoke, screenshots, and spec truth map. No durable backend model or new framework cost. +- **Alternative intentionally rejected**: new evidence engine, new audit ingestion, new compliance/certification layer, new export engine, raw log viewer, AI summary, broad design system work, or route replacement. +- **Release truth**: current-release runtime UI productization over existing evidence/audit foundations. + +### Compatibility posture + +This feature assumes pre-production runtime posture. Backward compatibility, historical aliases, migration shims, dual-write logic, legacy route redirects, and legacy query aliases are out of scope. Existing legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) must not be supported for Evidence Overview or Audit Log filtering. + +## Testing / Lane / Runtime Impact + +- **Test purpose / classification**: Feature, Filament/Livewire/HTTP, Browser. +- **Validation lane(s)**: confidence plus browser for critical workspace/environment UI/scope smoke. +- **Why this classification and these lanes are sufficient**: The change is user-facing Filament page productization with RBAC, evidence truth, audit event truth, scope, and disclosure behavior. Feature tests prove data/scope/action rules; Browser smoke proves rendered shell/filter/reload/disclosure/table hierarchy behavior. +- **New or expanded test families**: additions under `tests/Feature/Monitoring`, `tests/Feature/Evidence`, `tests/Feature/Audit`, `tests/Feature/Navigation`, and `tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php`. +- **Fixture / helper cost impact**: reuse existing factories/helpers for `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, `AuditLog`, workspace/environment session context, and navigation filter tests. Do not widen expensive defaults. +- **Heavy-family visibility / justification**: browser addition is explicit and named for Spec 329. +- **Special surface test profile**: `global-context-shell`, `monitoring-state-page`, and `shared-detail-family`. +- **Standard-native relief or required special coverage**: special coverage required for canonical filter, clear/reload, evidence path, event proof first-read, diagnostics hidden, RBAC action visibility, empty/non-empty states, and no platform-context tenant copy. +- **Reviewer handoff**: confirm diagnostics are collapsed, raw metadata hidden, RBAC actions hidden/disabled correctly, no false proof/certification claims, clean workspace entry, canonical filter, clear filter, cross-workspace guard, and table/history remain secondary context. +- **Budget / baseline / trend impact**: no expected material lane-cost shift beyond one targeted browser smoke. +- **Escalation needed**: document-in-feature if browser coverage becomes too expensive or requires fixture broadening. +- **Active feature PR close-out entry**: Smoke Coverage. +- **Planned validation commands**: + - `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Evidence tests/Feature/Audit tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact` + - `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php --compact` + - `cd apps/platform && ./vendor/bin/sail artisan test --filter='Evidence|AuditLog|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact` + - `cd apps/platform && ./vendor/bin/sail pint --dirty` + - `git diff --check` + +## Summary + +Productize TenantPilot's existing Evidence Overview and Audit Log into proof-first and disclosure-aware workspaces. + +The pages must answer: + +```text +What proof is available for this scope? +``` + +and: + +```text +Which event proves what happened? +``` + +The implementation must lead with scope, proof availability, actor/action/target/outcome/time, evidence path, export/report availability where repo-supported, disclosure posture, and collapsed diagnostics. Existing tables remain available as secondary context. + +## Product Context + +TenantPilot is a governance-of-record platform. Evidence snapshots, review packs, stored reports, OperationRuns, audit logs, and accepted-risk/review records turn technical state and operations into proof consumable by MSP operators, security reviewers, customer stakeholders, auditors, service delivery managers, and support. + +Evidence-first does not mean raw evidence first. It means proof path, provenance, scope, freshness, and disclosure level appear before raw technical details. + +## Problem Statement + +Evidence and Audit foundations exist, but the current UI can still drift into admin-tool patterns: + +- Evidence Overview is still primarily a snapshot table. +- Audit event inspection can show technical metadata by default. +- Scope, freshness, review/export availability, OperationRun proof, and raw disclosure hierarchy are not first-read. +- Customer/auditor-safe proof language and operator-only diagnostics can be mixed too early. +- Unsupported immutability, verification, health, or certification claims must be prevented. + +## Product Decision + +Evidence Overview and Audit Log are workspace-owned proof/disclosure surfaces. + +They may optionally be filtered by Environment using: + +```text +?environment_id={id} +``` + +When filtered: + +- Shell remains Workspace-only. +- Visible Environment filter chip appears. +- Clear filter returns to clean workspace-wide surface. +- Reload/back/forward remain aligned. + +They are not Environment-owned pages, raw log viewers, compliance suites, export engines, or backend evidence/audit engines. + +## User Scenarios & Testing + +### User Story 1 - Evidence proof availability first-read (Priority: P1) + +As an auditor or MSP operator, I can open Evidence Overview and immediately understand whether the current workspace or filtered environment has proof available, stale, incomplete, unavailable, or not generated. + +**Why this priority**: Evidence Overview is the product proof surface. If it remains table-first, reviewers must reconstruct proof readiness from artifacts. + +**Independent Test**: Render Evidence Overview with complete, stale, missing, and empty evidence fixtures. Assert the main proof question, evidence path, freshness/availability states, review/export/report/operation proof states, and secondary table are visible while raw metadata is absent by default. + +**Acceptance Scenarios**: + +1. Given a workspace has evidence snapshots, when the page loads cleanly, then the proof workbench shows proof availability and scope before the table. +2. Given a filtered environment has no evidence, when the page loads with `environment_id`, then the page shows a visible chip and honest no-evidence state without implying proof exists. +3. Given raw snapshot/provider/debug payloads exist in underlying records, when the page first renders, then those values are not visible by default. + +### User Story 2 - Audit event proof first-read (Priority: P1) + +As a governance admin or security reviewer, I can open Audit Log and immediately see actor, action, target, outcome, time, scope, and related proof before raw metadata. + +**Why this priority**: Audit Log is a core proof surface. Raw event history must not overpower who did what, when, against what target, and with what outcome. + +**Independent Test**: Render Audit Log with selected and unselected event fixtures. Assert actor/action/target/outcome/time fields appear in the workbench/selected-event panel and raw metadata is collapsed/hidden by default. + +**Acceptance Scenarios**: + +1. Given audit events exist, when the page loads, then it answers which event proves what happened. +2. Given an event is selected through `event`, when it is visible in scope, then the selected panel shows actor, action, target, outcome, time, scope, and related proof before diagnostics. +3. Given a selected event is outside the active environment filter or workspace entitlement, then it is not displayed as selected proof. + +### User Story 3 - Scope and filter contract remains stable (Priority: P1) + +As an operator, I can open clean and environment-filtered Evidence/Audit URLs without shell drift, remembered Environment fallback, legacy alias support, or cross-workspace leakage. + +**Why this priority**: Specs 314-322 are prerequisite contracts. Proof and audit disclosure is unsafe if scope is ambiguous. + +**Independent Test**: Open clean and `?environment_id=` URLs for Evidence Overview and Audit Log, clear the filter, reload, use legacy aliases, and attempt cross-workspace environment IDs. + +**Acceptance Scenarios**: + +1. Given a clean URL, when Evidence or Audit loads, then the data is workspace-wide and no Environment chip appears. +2. Given a valid `environment_id` in the current workspace, when the page loads, then the chip appears and data is filtered where supported. +3. Given legacy aliases or table filters in the URL, when the page loads, then they do not create Environment filter state. +4. Given a cross-workspace Environment ID, when the page loads, then safe no-access / 404 is returned. + +### User Story 4 - RBAC-safe disclosure and proof links (Priority: P2) + +As a least-privilege user, I only see evidence/audit/export/operation/diagnostic actions that I am allowed to access, and unavailable states do not leak sensitive raw data. + +**Why this priority**: Evidence and audit content can contain sensitive operational proof, provider context, or support-only metadata. + +**Independent Test**: Render the pages as users with and without evidence, audit, review pack, stored report, operation, and diagnostics capabilities. Assert protected actions are hidden/disabled and raw diagnostics remain hidden without `support_diagnostics.view`. + +**Acceptance Scenarios**: + +1. Given a user lacks `audit.view`, when they request Audit Log, then access is forbidden or denied according to existing workspace capability semantics. +2. Given a user lacks evidence/report/review/operation capability, when proof exists, then the proof state is unavailable or linkless without leaking records. +3. Given a user lacks diagnostics capability, when raw metadata exists, then raw/support disclosure is hidden by default and cannot be opened from the page. + +## Edge Cases + +- No evidence snapshots exist in workspace. +- Evidence exists only in another workspace. +- Evidence snapshot exists but review pack does not. +- Review pack exists but is queued, generating, failed, expired, or unavailable. +- Stored reports exist only for report types the user cannot view. +- OperationRun proof relation is missing, unauthorized, or has no safe route. +- Audit events exist with null `managed_environment_id` and should appear only in workspace-wide Audit Log. +- Audit events have missing evolved actor/target/outcome fields but legacy metadata can derive readable labels. +- Selected audit `event` is invalid, unauthorized, outside filter, or cross-workspace. +- Raw metadata includes internal keys, provider payloads, stack trace-like text, or debug metadata. +- `environment_id` is malformed, array-valued, cross-workspace, or inaccessible. +- Legacy aliases appear with or without canonical `environment_id`. +- Existing route middleware or shell helpers must not force active Environment shell ownership on workspace hub pages. + +## Functional Requirements + +- **FR-001**: Evidence Overview MUST have a proof-first layout before the evidence inventory/table. +- **FR-002**: Evidence Overview MUST show the stable question `What proof is available for this scope?`. +- **FR-003**: Evidence Overview MUST show scope, evidence snapshot state, freshness/availability, review pack/export availability, stored report/export availability where repo-supported, OperationRun proof availability, and evidence path. +- **FR-004**: Evidence Overview MUST show honest states only: evidence available, evidence incomplete, evidence unavailable, evidence stale, review pack unavailable, export available, export unavailable, not generated, not applicable, or unavailable. +- **FR-005**: Evidence Overview MUST keep the existing evidence inventory/table available as secondary context. +- **FR-006**: Audit Log MUST have an event-proof-first layout before the audit event table. +- **FR-007**: Audit Log MUST show the stable question `Which event proves what happened?`. +- **FR-008**: Audit Log default first-read MUST emphasize actor, action, target, outcome, time, and scope. +- **FR-009**: Audit Log selected/latest event proof panel MUST show related record/proof where repo-supported and authorized. +- **FR-010**: Audit Log MUST keep the existing audit event table available as secondary context. +- **FR-011**: Raw metadata, raw payloads, provider responses, stack traces, provider secrets, internal exception traces, debug metadata, raw OperationRun payloads, and raw audit metadata blobs MUST NOT be visible by default. +- **FR-012**: Diagnostics disclosure MUST be collapsed and capability-aware wherever exposed. +- **FR-013**: Evidence and Audit pages MUST show the shared disclosure hierarchy: decision/proof visible, evidence/event visible, diagnostics collapsed, raw/support hidden. +- **FR-014**: Visible runtime elements MUST be backed by `repo-verified`, `foundation-real`, `derived from existing model`, `empty/unavailable state`, or `deferred future capability` classification in `repo-truth-map.md`. +- **FR-015**: No visible UI copy may claim immutable, certified, legally attested, tamper-proof, auditor-approved, compliance-ready, fully verified, 100 percent verified, or health-complete states unless repo truth explicitly proves them. +- **FR-016**: Clean Evidence and Audit URLs MUST be workspace-wide, with Workspace shell only and no Environment chip. +- **FR-017**: Filtered Evidence and Audit URLs MUST use only `environment_id`, show the visible Environment chip, filter data where supported, and keep Workspace shell ownership. +- **FR-018**: Clear filter MUST return to a clean workspace URL and clear URL, Livewire, Filament table, deferred table, and persisted session Environment-like state. +- **FR-019**: Legacy aliases `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters` as URL source MUST NOT create Environment filter state. +- **FR-020**: Cross-workspace or unauthorized `environment_id` MUST return safe no-access / 404 and MUST NOT switch Workspace. +- **FR-021**: Evidence link/actions MUST respect `evidence.view`, `EvidenceSnapshotPolicy`, environment entitlement, and workspace membership. +- **FR-022**: Review pack/open/download link/actions MUST respect `review_pack.view`, `ReviewPackPolicy`, and existing download authorization. +- **FR-023**: Stored report link/actions MUST respect existing report-type capabilities and `StoredReportResource` visibility. +- **FR-024**: Audit access MUST respect workspace membership and `audit.view`. +- **FR-025**: Operation proof links MUST route through existing operation link helpers and authorization. +- **FR-026**: Diagnostics/raw metadata access MUST require `support_diagnostics.view` or stricter existing support/raw capability. +- **FR-027**: Unauthorized actions MUST be hidden, disabled with existing convention, or replaced with safe unavailable state. +- **FR-028**: No migration, seeder, package, env var, queue, scheduler, storage, deployment asset, compatibility route, or legacy alias support may be introduced unless spec/plan/tasks are updated before implementation. +- **FR-029**: Filament v5 and Livewire v4.0+ patterns MUST be preserved. No Filament v3/v4 APIs or Livewire v3 references are allowed. +- **FR-030**: No Graph calls or provider API calls may occur during page render. + +## Non-Functional Requirements + +- **NFR-001**: Workspace and Environment isolation MUST remain enforceable in queries and authorization. +- **NFR-002**: Evidence/audit pages MUST remain DB-only render paths over existing persisted records. +- **NFR-003**: Page copy MUST use customer/auditor-safe disclosure language and avoid platform-context `tenant` wording. +- **NFR-003a**: Dynamic Environment display names are data and MAY contain `Tenant`; static platform-context copy MUST avoid retired tenant-first wording. +- **NFR-003b**: Empty primary evidence snapshots MUST use product-safe proof language, show `Proof incomplete`, explain that the primary evidence snapshot is empty, and make supporting proof impact explicit without exposing implementation-heavy artifact-row language. +- **NFR-004**: Page layouts MUST prefer native Filament components and shared primitives before custom Blade/Tailwind. +- **NFR-005**: The change MUST not create a new cross-surface disclosure framework, proof state engine, or status taxonomy. +- **NFR-006**: Browser verification MUST cover clean, filtered, clear, reload, non-empty, empty, diagnostics collapsed, table secondary, and tenant-copy guard states. + +## Out Of Scope + +- New evidence backend. +- New audit event ingestion. +- New immutable storage implementation. +- New legal attestation/certification engine. +- New compliance framework mapping. +- New external auditor portal. +- New export pipeline. +- New report generation engine. +- New retention/hold system. +- AI summarization. +- Customer Review Workspace redesign. +- Operations Hub redesign. +- Governance Inbox redesign. +- New migrations by default. +- New packages, env vars, queues, scheduler, storage, deployment assets, or external services. + +## Required Repo Truth Map + +Before runtime changes, `repo-truth-map.md` MUST exist under this spec directory and map each UI element to: + +- UI element. +- Surface. +- Source model/service/page. +- Status source. +- Authorization/capability. +- Workspace/Environment scope. +- OperationRun/evidence/audit/export link. +- Fallback/empty state. +- Classification. + +Required data areas: + +- Evidence Snapshots. +- Review Packs. +- Stored Reports / export artifacts. +- OperationRuns. +- Audit Log events. +- Actor/action/target/outcome/time fields. +- Risk/Decision links if present. +- Customer Review Workspace evidence links. +- Governance Inbox evidence links. +- Operations proof links. +- Environment filter state. +- Diagnostics/raw metadata availability. + +## Acceptance Criteria + +### Evidence Overview + +- [ ] Evidence Overview has proof-first layout. +- [ ] Main proof question is visible. +- [ ] Evidence path is visible. +- [ ] Evidence snapshot state is visible. +- [ ] Review pack/export state is visible where repo-supported. +- [ ] Stored report/export state is visible where repo-supported. +- [ ] OperationRun proof state is visible where repo-supported. +- [ ] Evidence inventory/table remains available as secondary context. +- [ ] Raw metadata is hidden by default. + +### Audit Log + +- [ ] Audit Log has event-proof-first layout. +- [ ] Main audit proof question is visible. +- [ ] Actor/action/target/outcome/time are first-read. +- [ ] Selected/latest event proof panel exists. +- [ ] Audit event table remains available as secondary context. +- [ ] Raw metadata is hidden by default. + +### Disclosure Safety + +- [ ] Diagnostics are collapsed by default. +- [ ] Raw payloads are hidden by default. +- [ ] Provider secrets are not visible. +- [ ] Internal exception/debug text is not visible. +- [ ] No false immutability/certification/health/compliance claims are introduced. +- [ ] No false green success state is introduced. + +### Scope + +- [ ] Clean URLs are workspace-wide. +- [ ] Shell is Workspace-only. +- [ ] Environment filter uses `environment_id`. +- [ ] Visible Environment chip appears when filtered. +- [ ] Clear filter works. +- [ ] Reload after clear is safe. +- [ ] Legacy aliases do not create filter state. +- [ ] Cross-workspace Environment is rejected. + +### RBAC + +- [ ] Unauthorized user cannot access protected evidence/audit data. +- [ ] Unauthorized actions are hidden/disabled/unavailable. +- [ ] Evidence export/open respects capability. +- [ ] Audit detail access respects capability. +- [ ] Diagnostics/raw metadata access respects capability. +- [ ] OperationRun proof access respects capability. + +### UI / Visual + +- [ ] Layout uses Spec 325 direction without treating target images as runtime truth. +- [ ] Filament light mode remains readable. +- [ ] No heavy one-off CSS. +- [ ] Right-side proof/disclosure panel exists on desktop where suitable. +- [ ] Tables are not the only default experience. +- [ ] Page remains responsive enough for Filament shell. +- [ ] Native Filament components are preferred where suitable. + +### Tests / Validation + +- [ ] Repo truth map exists. +- [ ] Required Feature tests pass. +- [ ] Required Browser smoke passes. +- [ ] Relevant Spec 314-322 guards still pass. +- [ ] `pint --dirty` passes. +- [ ] `git diff --check` passes. +- [ ] No broad rebaseline. +- [ ] Full suite status is honestly reported if run/not run. + +## Success Criteria + +- **SC-001**: A reviewer can determine proof availability on Evidence Overview without opening raw artifact details. +- **SC-002**: A reviewer can identify actor/action/target/outcome/time on Audit Log before seeing technical metadata. +- **SC-003**: Browser smoke confirms clean, filtered, clear/reload, non-empty, empty, diagnostics-collapsed, table-secondary, and tenant-copy guard states. +- **SC-004**: Tests prove raw diagnostic strings are absent by default. +- **SC-005**: No migration, package, env var, queue, scheduler, storage, deployment asset, compatibility route, or legacy alias support is added. + +## Required Tests + +- `it('documents_evidence_audit_log_repo_truth_map')` +- `it('renders_evidence_overview_proof_first_layout')` +- `it('renders_audit_log_event_proof_first_layout')` +- `it('shows_evidence_path_without_raw_metadata_by_default')` +- `it('shows_audit_actor_action_target_outcome_time_before_raw_metadata')` +- `it('shows_export_or_report_availability_only_when_repo_supported')` +- `it('hides_evidence_and_audit_raw_diagnostics_by_default')` +- `it('respects_evidence_audit_and_diagnostics_capabilities')` +- `it('evidence_overview_supports_canonical_environment_filter')` +- `it('audit_log_supports_canonical_environment_filter')` +- `it('evidence_and_audit_reject_legacy_environment_aliases')` +- `it('evidence_and_audit_reject_cross_workspace_environment_filter')` +- `it('evidence_and_audit_do_not_use_tenant_as_platform_context_copy')` +- `tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php` + +## Browser Verification Required + +Screenshots may be saved under: + +```text +specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/ +``` + +Required screenshots: + +- `evidence-overview-proof-workbench.png` +- `evidence-overview-filtered.png` +- `audit-log-event-proof-workbench.png` +- `audit-log-filtered.png` + +Optional screenshots: + +- `evidence-overview-empty.png` +- `audit-log-empty.png` +- `evidence-overview-after-clear.png` +- `audit-log-after-clear.png` + +## Risks + +- Existing Audit Log route currently includes environment-context middleware; implementation must verify it does not force Environment shell ownership or remembered fallback for a workspace hub. +- Evidence route is duplicated in `routes/web.php`; implementation may leave it alone if harmless or document a bounded cleanup task if needed. +- Audit selected-event detail currently renders `Technical metadata` directly; moving it behind disclosure must preserve authorized proof inspection. +- Evidence proof path may not have all links for every environment. Unsupported links must render unavailable or be omitted. +- Browser smoke may need focused fixtures to avoid broad lane cost. + +## Assumptions + +- No production data migration compatibility is needed under the repo's pre-production posture. +- Evidence and audit data already persisted in the repo are sufficient for v1 productization. +- Existing policies/capabilities are authoritative; new capability strings are not expected. +- EN/DE localization is added only if implementation follows existing stable-copy localization patterns for these pages. + +## Open Questions + +No open question blocks implementation. Implementation must update this spec/plan/tasks first if repo truth shows a required backend, schema, capability, export, or route contract change. + +## Follow-Up Spec Candidates + +- Spec 330 - Environment Dashboard / Baseline Compare Productization. +- Spec 331 - Restore Safety Workflow Productization. +- Spec 332 - Provider Readiness Productization. + +Do not start these inside Spec 329. diff --git a/specs/329-evidence-audit-log-disclosure-productization/tasks.md b/specs/329-evidence-audit-log-disclosure-productization/tasks.md new file mode 100644 index 00000000..5e215209 --- /dev/null +++ b/specs/329-evidence-audit-log-disclosure-productization/tasks.md @@ -0,0 +1,205 @@ +# Tasks: Spec 329 - Evidence / Audit Log Disclosure Productization + +**Input**: Design documents from `/specs/329-evidence-audit-log-disclosure-productization/` +**Prerequisites**: `spec.md`, `plan.md`, `repo-truth-map.md` + +**Tests**: Required. This is a runtime UI/operator proof-disclosure Filament page productization with browser smoke. + +## Test Governance Checklist + +- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior. +- [x] New or changed tests stay in the smallest honest family, and the browser addition is explicit. +- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default. +- [x] Planned validation commands cover the change without pulling in unrelated lane cost. +- [x] The declared surface test profile (`global-context-shell`, `monitoring-state-page`, `shared-detail-family`) is explicit. +- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR. + +## Phase 1: Preparation And Repo Truth + +**Purpose**: Confirm runtime truth and prevent invented claims before page edits. + +- [x] T001 Re-read `specs/329-evidence-audit-log-disclosure-productization/spec.md`, `plan.md`, `tasks.md`, and `repo-truth-map.md`. +- [x] T002 Re-read related completed context only: Specs 314-328. Do not modify their artifacts. +- [x] T003 Verify current Evidence Overview route/class/view and existing tests before editing: `apps/platform/routes/web.php`, `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`, `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`, and `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`. +- [x] T004 Verify current Audit Log route/class/view/partial and existing tests before editing: `apps/platform/routes/web.php`, `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`, `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php`, `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php`, and `apps/platform/tests/Feature/Filament/AuditLogPageTest.php`. +- [x] T005 Verify source models and authorization paths: `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, `AuditLog`, `EvidenceSnapshotPolicy`, `ReviewPackPolicy`, `StoredReportResource`, `OperationRunLinks`, and capability resolvers. +- [x] T006 Update `repo-truth-map.md` with any newly discovered source, capability, fallback, or classification before runtime changes. +- [x] T007 Confirm no migration/package/env/queue/storage/deployment asset change is required; if one appears necessary, stop and update spec/plan first. +- [x] T008 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use. +- [x] T009 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`. +- [x] T010 Confirm related globally searchable resources stay disabled or have safe View/Edit pages; no global search change is expected. +- [x] T011 Verify the duplicated `/admin/evidence/overview` route and Audit Log environment-context middleware do not create scope/shell drift; document any bounded cleanup in spec artifacts before code changes if needed. + +## Phase 2: Feature Tests First + +**Purpose**: Lock proof-first layout, event-first-read, RBAC, scope, and diagnostics behavior before UI refactor. + +- [x] T012 Add or update a feature test asserting `specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md` exists and lists required Evidence Overview and Audit Log data areas. +- [x] T013 Add or update a Feature/Livewire/HTTP test for Evidence Overview layout text: `Evidence`, `What proof is available for this scope?`, `Evidence path`, `Review pack`, `Operation proof`, and `Diagnostics - Collapsed` in `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php` or a focused Spec 329 monitoring test. +- [x] T014 Add or update a Feature/Livewire/HTTP test for Audit Log layout text: `Audit Log`, `Which event proves what happened?`, `Actor`, `Action`, `Target`, `Outcome`, `Time`, and `Diagnostics - Collapsed`. +- [x] T015 Add or update a test asserting Evidence Overview shows `Evidence snapshot`, `Review pack`, `Operation proof`, and `Stored report / export` without default-visible raw metadata. +- [x] T016 Add or update a test asserting Audit Log shows actor/action/target/outcome/time before raw metadata for a selected event. +- [x] T017 Add or update a test asserting export/report availability uses only repo-supported states such as `Available`, `Unavailable`, `Not generated`, or `Not applicable`; no fake download/export action appears. +- [x] T018 Add or update a test asserting raw diagnostics are hidden by default on both pages: `raw payload`, `provider secret`, `stack trace`, `debug metadata`, `internal exception`, `provider response`, and raw OperationRun context must not appear. +- [x] T019 Add or update RBAC tests covering evidence snapshot open, review pack open/download, stored report open, audit event detail, operation proof, and raw diagnostics visibility where existing capabilities support coverage. +- [x] T020 Add or update canonical Evidence Overview environment filter tests for `?environment_id=`, visible chip, workspace shell only, filtered proof data, clear filter, and reload safety. +- [x] T021 Add or update canonical Audit Log environment filter tests for `?environment_id=`, visible chip, workspace shell only, filtered audit rows, selected-event normalization, clear filter, and reload safety. +- [x] T022 Add or update legacy alias rejection tests for Evidence Overview and Audit Log covering `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`. +- [x] T023 Add or update cross-workspace environment filter guard tests returning safe 404/no-access for both Evidence Overview and Audit Log. +- [x] T024 Add or update tenant-copy guard asserting platform-context copy such as `current tenant`, `tenant filter`, `entitled tenant`, `all tenants`, and `production tenant` is not visible on either page. + +## Phase 3: Evidence Overview Productization + +**Purpose**: Refactor Evidence Overview from table-first to proof-first without new backend foundation. + +- [x] T025 Update `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` to expose repo-truth-bounded payloads for scope, proof readiness, evidence path, export/report availability, proof links, unavailable states, and diagnostics disclosure. +- [x] T026 Update `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php` to render header/scope, proof readiness workbench, evidence path panel, export/report availability panel, secondary table, and collapsed diagnostics disclosure. +- [x] T027 Ensure Evidence Overview shows workspace-wide vs environment-filtered context and the shared Environment chip when filtered. +- [x] T028 Ensure the main proof workbench shows the stable question, evidence availability, freshness, snapshot state, review pack/export state, stored report/export state, operation proof state, and one dominant open-proof action when authorized. +- [x] T029 Ensure evidence path items show only honest states: available, unavailable, stale, not generated, not applicable, or omitted. +- [x] T030 Keep the existing Evidence Overview table available as secondary context; do not remove existing search/filter/sort/row navigation functionality. +- [x] T031 Ensure Evidence Overview diagnostics/raw metadata are collapsed, hidden, or capability-gated by default. + +## Phase 4: Audit Log Productization + +**Purpose**: Refactor Audit Log from summary/table-first to event-proof-first while preserving event history. + +- [x] T032 Update `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php` to expose repo-truth-bounded payloads for audit proof workbench, selected/latest event proof, related proof links, unavailable states, disclosure hierarchy, and diagnostics gating. +- [x] T033 Update `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php` to render header/scope, audit proof workbench, selected/latest event proof panel, secondary table, and collapsed diagnostics disclosure. +- [x] T034 Update `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php` so actor/action/target/outcome/time/scope and related proof are first-read, while technical metadata is behind collapsed/capability-aware disclosure. +- [x] T035 Ensure Audit Log shows workspace-wide vs environment-filtered context and the shared Environment chip when filtered. +- [x] T036 Ensure selected event proof normalizes against active filter/search/authorization and falls back safely when the event is invalid, inaccessible, or outside scope. +- [x] T037 Keep the existing Audit Log table available as secondary event history; do not remove existing filters/search/sort/inspect behavior. +- [x] T038 Ensure Audit Log diagnostics/raw metadata are collapsed, hidden, or capability-gated by default. + +## Phase 5: Data Binding And Honest States + +**Purpose**: Bind proof surfaces to repo-verified sources and avoid false claims. + +- [x] T039 Bind evidence snapshot display to `EvidenceSnapshot` fields, `ArtifactTruthPresenter`, and existing snapshot detail links only. +- [x] T040 Bind review pack state to existing `ReviewPack` fields/statuses and `ReviewPackResource`/download links only where authorized. +- [x] T041 Bind stored report state to existing `StoredReport` records, report-type capabilities, and `StoredReportResource` links only where authorized. +- [x] T042 Bind operation proof state only through existing `operationRun()` relations, `OperationRunLinks`, and authorized operation visibility. +- [x] T043 Bind audit event proof to `AuditLog` actor snapshot, action/action label, target snapshot, normalized outcome, recorded time, scope, readable context, operation relation, and related navigation resolver. +- [x] T044 Render unavailable/missing/not generated/not applicable states for unsupported proof paths rather than inventing backend capabilities. +- [x] T045 Ensure no generic green success state, immutable/certified/compliance-ready copy, or environment/governance health claim appears without exact repo proof. + +## Phase 6: Actions, RBAC, And Safety + +**Purpose**: Show only real, authorized actions and preserve read-first default behavior. + +- [x] T046 Keep primary actions singular and context-aware on each proof panel. +- [x] T047 Show open evidence snapshot, open review pack, download/open export artifact, open stored report, open operation proof, open audit event, or open related record only when route and authorization are repo-real. +- [x] T048 Ensure unauthorized actions are hidden or replaced with safe unavailable state without leaking sensitive details. +- [x] T049 Ensure raw diagnostics/metadata disclosure is unavailable without `support_diagnostics.view` or stricter existing raw/support capability. +- [x] T050 Verify no default action approves, rejects, accepts risk, deletes, restores, remediates, mutates provider state, or changes evidence/audit storage. +- [x] T051 If any high-impact action is unexpectedly required, update spec/plan first, then implement it with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests. + +## Phase 7: Workspace / Environment Scope Contract + +**Purpose**: Preserve Specs 314-322. + +- [x] T052 Verify clean `/admin/evidence/overview` and `/admin/audit-log` do not read remembered environment shell state or persisted table filters. +- [x] T053 Verify `/admin/evidence/overview?environment_id={id}` and `/admin/audit-log?environment_id={id}` filter only page data, show visible chip, and keep Workspace shell ownership. +- [x] T054 Verify clear filter redirects to clean workspace URL and remains safe after reload. +- [x] T055 Verify legacy aliases are removed/neutralized and do not set filter state. +- [x] T056 Verify cross-workspace or unauthorized `environment_id` returns safe no-access/404. +- [x] T057 Verify back/forward/reload behavior does not resurrect cleared environment filter state. +- [x] T058 Verify Audit Log route middleware does not force active Environment shell ownership or remembered fallback; if it does, apply the narrowest route/middleware correction in scope and cover it with tests. + +## Phase 8: Browser Smoke And Screenshots + +**Purpose**: Prove the user-facing contract in the integrated browser lane. + +- [x] T059 Create `apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php` using existing Pest Browser conventions. +- [x] T060 Browser Flow A: Evidence Overview clean workspace; assert Workspace shell only, no Environment chip, main proof question, proof workbench, evidence path, diagnostics collapsed, table secondary, screenshot. +- [x] T061 Browser Flow B: Evidence Overview filtered entry; assert visible Environment chip, filtered proof state, clear action, no active Environment shell, screenshot. +- [x] T062 Browser Flow C: Evidence clear filter and reload; assert clean URL, chip does not return, no active Environment shell. +- [x] T063 Browser Flow D: Evidence non-empty and empty proof states; assert available/unavailable/not generated states and no raw metadata. +- [x] T064 Browser Flow E: Audit Log clean workspace; assert Workspace shell only, no Environment chip, audit proof question, actor/action/target/outcome/time first-read, diagnostics collapsed, table secondary, screenshot. +- [x] T065 Browser Flow F: Audit Log filtered entry; assert visible Environment chip, filtered event proof, clear action, no active Environment shell, screenshot. +- [x] T066 Browser Flow G: Audit clear filter and reload; assert clean URL, chip does not return, no active Environment shell. +- [x] T067 Browser Flow H: Audit non-empty and empty event states; assert selected/latest event proof and no raw metadata. +- [x] T068 Browser Flow I: no platform-context tenant wording appears on either surface. +- [x] T069 Save screenshots under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/` when generated and ensure they contain no secrets. + +## Phase 9: UI Coverage And Documentation Artifacts + +**Purpose**: Satisfy UI-COV without unrelated docs churn. + +- [x] T070 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md`, `design-coverage-matrix.md`, page reports, or unresolved pages need an update. +- [x] T071 If coverage docs are not changed, add a close-out note explaining why existing UI-025/UI-044 rows plus Spec 325 target artifacts and Spec 329 package artifacts remain sufficient. +- [x] T072 Update `repo-truth-map.md` final classifications for implemented/empty/deferred elements. +- [x] T073 Do not create general documentation files outside required Spec Kit/UI coverage artifacts unless explicitly requested. +- [x] T081 Add the existing Evidence Overview route to the Workspace Monitoring sidebar through both workspace navigation paths with a concise area label and cover the navigation entry with existing workspace-hub sidebar regression tests. + +## Phase 10: Validation + +**Purpose**: Run narrow proof and report honestly. + +- [x] T074 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Evidence tests/Feature/Audit tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`. +- [x] T075 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php --compact`. +- [x] T076 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='Evidence|AuditLog|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`. +- [x] T077 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`. +- [x] T078 Run `git diff --check`. +- [x] T079 Report full-suite status honestly if not run. +- [x] T080 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy tenant alias support were added. +- [x] T082 Replace implementation-heavy empty-snapshot copy with product-safe proof copy and assert the old artifact-row wording is not visible. +- [x] T083 Add empty-primary-snapshot proof hierarchy coverage for `Proof incomplete`, reason, and impact. +- [x] T084 Keep dynamic display names containing `Tenant` allowed while rejecting static `Search tenant or next` copy. +- [x] T085 Prevent clipped Evidence Path badge labels in the right panel and cover `Empty`, `Ready`, and `Available` labels in Feature/Browser tests. + +### Validation Close-Out + +- Focused Spec 329 and impacted Feature tests passed: `./vendor/bin/sail artisan test tests/Feature/Monitoring/Spec329EvidenceAuditDisclosureProductizationTest.php tests/Feature/Filament/AuditLogPageTest.php tests/Feature/Filament/AuditLogDetailInspectionTest.php tests/Feature/Monitoring/AuditLogInspectFlowTest.php tests/Feature/Evidence/EvidenceOverviewPageTest.php --compact`. +- Spec 329 Browser smoke passed: `./vendor/bin/sail artisan test tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php --compact`. +- Neighboring navigation/browser contracts passed: Spec 198, Spec 322, Spec 321, WorkspaceHubEnvironmentFilter, and WorkspaceHubClearFilter targeted run. +- Filter-based validation passed: `./vendor/bin/sail artisan test --filter='Evidence|AuditLog|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact` with 352 passed, 1 skipped, 4,220 assertions. +- Broad Feature lane was run and had one unrelated pre-existing failure in `tests/Feature/Audit/ProviderConnectionIdentityAuditTest.php` (`getDefaultTestingSchemaName()` on null). The same test failed in isolation; no provider-connection audit code was changed for Spec 329. +- `./vendor/bin/sail pint --dirty` passed. +- `git diff --check` passed. +- Screenshots are stored under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`. + +## Dependencies + +- Phase 1 blocks all runtime implementation. +- Phase 2 should be written before or alongside implementation to lock behavior. +- Phase 3 and Phase 4 can be implemented in parallel only if write scopes stay disjoint: + - Evidence write scope: Evidence page class/view/tests. + - Audit write scope: Audit page class/view/partial/tests. +- Phase 5 and Phase 6 depend on Phases 3-4 payload shape. +- Phase 7 must be validated after both surfaces are changed. +- Phase 8 depends on user-facing runtime changes. +- Phase 10 is final validation. + +## Non-Goals Checklist + +- [x] NT001 Do not build a new evidence backend. +- [x] NT002 Do not build a new audit ingestion engine. +- [x] NT003 Do not build immutable/legal/certification/compliance attestation. +- [x] NT004 Do not build a new export/report generation engine. +- [x] NT005 Do not add AI summarization. +- [x] NT006 Do not redesign Customer Review Workspace, Governance Inbox, Operations Hub, Environment Dashboard, Baseline Compare, Restore Safety, or Provider Readiness. +- [x] NT007 Do not add migrations unless spec/plan are updated first with proof. +- [x] NT008 Do not rewrite completed Specs 314-328. +- [x] NT009 Do not add legacy tenant query alias support. +- [x] NT010 Do not expose raw diagnostics or provider payloads by default. + +## Required Final Report Content + +When implementation later completes, report: + +- Changed behavior. +- Evidence Overview proof surface. +- Audit Log event-proof surface. +- Disclosure / diagnostics default state. +- RBAC-visible/hidden actions. +- Repo-verified vs unavailable states. +- Files changed. +- Repo truth map status. +- Tests run and results. +- Browser verification and screenshots path. +- Known gaps. +- Remaining follow-ups. +- Full suite run/not run. +- Explicit no migrations/seeders/packages/env/queues/scheduler/storage/deployment assets/backcompat/legacy aliases statement.