computeFingerprint($tenant); $existing = $this->findExistingSnapshot($tenant, $fingerprint); if ($existing instanceof EvidenceSnapshot) { return $existing; } $operationRun = $this->operationRuns->ensureRunWithIdentity( tenant: $tenant, type: OperationRunType::EvidenceSnapshotGenerate->value, identityInputs: [ 'tenant_id' => (int) $tenant->getKey(), 'fingerprint' => $fingerprint, ], context: [ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'allow_stale' => $allowStale, 'fingerprint' => $fingerprint, ], initiator: $user, ); $snapshot = EvidenceSnapshot::query()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'operation_run_id' => (int) $operationRun->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'fingerprint' => $fingerprint, 'status' => EvidenceSnapshotStatus::Queued->value, 'completeness_state' => EvidenceCompletenessState::Missing->value, 'summary' => [ 'allow_stale' => $allowStale, 'requested_at' => now()->toIso8601String(), ], ]); $this->operationRuns->dispatchOrFail($operationRun, function () use ($snapshot, $operationRun): void { GenerateEvidenceSnapshotJob::dispatch( snapshotId: (int) $snapshot->getKey(), operationRunId: (int) $operationRun->getKey(), ); }); $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::EvidenceSnapshotCreated, context: [ 'metadata' => [ 'status' => EvidenceSnapshotStatus::Queued->value, ], ], actor: $user, resourceType: 'evidence_snapshot', resourceId: (string) $snapshot->getKey(), targetLabel: sprintf('Evidence snapshot #%d', (int) $snapshot->getKey()), operationRunId: (int) $operationRun->getKey(), tenant: $tenant, ); return $snapshot; } public function refresh(EvidenceSnapshot $snapshot, User $user): EvidenceSnapshot { $tenant = $snapshot->tenant; if (! $tenant instanceof Tenant) { throw new InvalidArgumentException('Snapshot tenant could not be resolved.'); } $refreshed = $this->generate($tenant, $user); $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::EvidenceSnapshotRefreshed, context: [ 'metadata' => [ 'previous_snapshot_id' => (int) $snapshot->getKey(), 'new_snapshot_id' => (int) $refreshed->getKey(), ], ], actor: $user, resourceType: 'evidence_snapshot', resourceId: (string) $refreshed->getKey(), targetLabel: sprintf('Evidence snapshot #%d', (int) $refreshed->getKey()), operationRunId: $refreshed->operation_run_id, tenant: $tenant, ); return $refreshed; } public function expire(EvidenceSnapshot $snapshot, User $user): EvidenceSnapshot { $snapshot->forceFill([ 'status' => EvidenceSnapshotStatus::Expired->value, 'expires_at' => now(), ])->save(); $tenant = $snapshot->tenant; if ($tenant instanceof Tenant) { $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::EvidenceSnapshotExpired, context: [ 'metadata' => [ 'before_status' => EvidenceSnapshotStatus::Active->value, 'after_status' => EvidenceSnapshotStatus::Expired->value, ], ], actor: $user, resourceType: 'evidence_snapshot', resourceId: (string) $snapshot->getKey(), targetLabel: sprintf('Evidence snapshot #%d', (int) $snapshot->getKey()), tenant: $tenant, ); } return $snapshot; } /** * @return list */ public function providers(): array { return [ app(FindingsSummarySource::class), app(PermissionPostureSource::class), app(EntraAdminRolesSource::class), app(BaselineDriftPostureSource::class), app(OperationsSummarySource::class), ]; } /** * @return array{items: list>, fingerprint: string, completeness: string, summary: array} */ public function buildSnapshotPayload(Tenant $tenant): array { $items = []; $fingerprintPayload = []; foreach ($this->providers() as $provider) { $item = $provider->collect($tenant); $items[] = $item; $fingerprintPayload[$provider->key()] = $item['fingerprint_payload']; } $completeness = $this->completenessEvaluator->evaluate(array_map( static fn (array $item): array => [ 'state' => (string) $item['state'], 'required' => (bool) $item['required'], ], $items, )); $itemsByKey = collect($items)->keyBy('dimension_key'); $findingsSummary = is_array($itemsByKey->get('findings_summary')['summary_payload'] ?? null) ? $itemsByKey->get('findings_summary')['summary_payload'] : []; $operationsSummary = is_array($itemsByKey->get('operations_summary')['summary_payload'] ?? null) ? $itemsByKey->get('operations_summary')['summary_payload'] : []; $summary = [ 'dimension_count' => count($items), 'finding_count' => (int) ($findingsSummary['count'] ?? 0), 'report_count' => count(array_filter($items, static fn (array $item): bool => in_array($item['dimension_key'], ['permission_posture', 'entra_admin_roles'], true) && $item['source_record_id'] !== null)), 'operation_count' => (int) ($operationsSummary['operation_count'] ?? 0), 'missing_dimensions' => count(array_filter($items, static fn (array $item): bool => $item['state'] === EvidenceCompletenessState::Missing->value)), 'stale_dimensions' => count(array_filter($items, static fn (array $item): bool => $item['state'] === EvidenceCompletenessState::Stale->value)), 'dimensions' => array_map(static fn (array $item): array => [ 'key' => $item['dimension_key'], 'state' => $item['state'], 'required' => $item['required'], ], $items), 'risk_acceptance' => is_array($findingsSummary['risk_acceptance'] ?? null) ? $findingsSummary['risk_acceptance'] : [ 'status_marked_count' => 0, 'valid_governed_count' => 0, 'warning_count' => 0, 'expired_count' => 0, 'revoked_count' => 0, 'missing_exception_count' => 0, ], 'hardening' => [ 'rbac_last_checked_at' => $tenant->rbac_last_checked_at?->toIso8601String(), 'rbac_last_setup_at' => $tenant->rbac_last_setup_at?->toIso8601String(), 'rbac_canary_results' => $tenant->rbac_canary_results, 'rbac_last_warnings' => $tenant->rbac_last_warnings, 'rbac_scope_mode' => $tenant->rbac_scope_mode, ], ]; return [ 'items' => $items, 'fingerprint' => EvidenceSnapshotFingerprint::hash($fingerprintPayload), 'completeness' => $completeness->value, 'summary' => $summary, ]; } public function computeFingerprint(Tenant $tenant): string { return $this->buildSnapshotPayload($tenant)['fingerprint']; } public function checkActiveRun(Tenant $tenant): bool { return $this->operationRuns->findCanonicalRunWithIdentity( tenant: $tenant, type: OperationRunType::EvidenceSnapshotGenerate->value, identityInputs: [ 'tenant_id' => (int) $tenant->getKey(), 'fingerprint' => $this->computeFingerprint($tenant), ], ) !== null; } private function findExistingSnapshot(Tenant $tenant, string $fingerprint): ?EvidenceSnapshot { return EvidenceSnapshot::query() ->forTenant((int) $tenant->getKey()) ->where('workspace_id', (int) $tenant->workspace_id) ->where('fingerprint', $fingerprint) ->where('status', EvidenceSnapshotStatus::Active->value) ->where(function ($query): void { $query->whereNull('expires_at')->orWhere('expires_at', '>', now()); }) ->first(); } }