*/ private const REQUIRED_EVIDENCE_DIMENSIONS = [ 'findings_summary', 'permission_posture', 'entra_admin_roles', 'baseline_drift_posture', 'operations_summary', ]; /** * Create an OperationRun + ReviewPack and dispatch the generation job. * * @param array $options */ public function generate(ManagedEnvironment $tenant, User $user, array $options = []): ReviewPack { $this->assertReviewPackGenerationAllowed($tenant); $options = $this->normalizeOptions($options); $snapshot = $this->resolveSnapshot($tenant); $fingerprint = $this->computeFingerprintForSnapshot($snapshot, $options); $existing = $this->findExistingPack($tenant, $fingerprint); if ($existing instanceof ReviewPack) { $this->recordReviewPackRequestTelemetry($existing, $user, 'tenant'); return $existing; } $operationRun = $this->operationRunService->ensureRun( tenant: $tenant, type: OperationRunType::ReviewPackGenerate->value, inputs: [ 'include_pii' => $options['include_pii'], 'include_operations' => $options['include_operations'], 'evidence_snapshot_id' => (int) $snapshot->getKey(), ], initiator: $user, ); if (! $operationRun->wasRecentlyCreated) { $queuedPack = $this->findPackForRun($tenant, $operationRun); if ($queuedPack instanceof ReviewPack) { $this->recordReviewPackRequestTelemetry($queuedPack, $user, 'tenant'); return $queuedPack; } } $reviewPack = ReviewPack::create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'operation_run_id' => (int) $operationRun->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'status' => ReviewPackStatus::Queued->value, 'options' => $options, 'summary' => [ 'finding_outcomes' => is_array($snapshot->summary['finding_outcomes'] ?? null) ? $snapshot->summary['finding_outcomes'] : [], 'finding_report_buckets' => is_array($snapshot->summary['finding_report_buckets'] ?? null) ? $snapshot->summary['finding_report_buckets'] : [], 'risk_acceptance' => is_array($snapshot->summary['risk_acceptance'] ?? null) ? $snapshot->summary['risk_acceptance'] : [], 'evidence_resolution' => [ 'outcome' => 'resolved', 'snapshot_id' => (int) $snapshot->getKey(), 'snapshot_fingerprint' => (string) $snapshot->fingerprint, 'completeness_state' => (string) $snapshot->completeness_state, 'required_dimensions' => self::REQUIRED_EVIDENCE_DIMENSIONS, ], ], ]); $this->operationRunService->dispatchOrFail($operationRun, function () use ($reviewPack, $operationRun): void { GenerateReviewPackJob::dispatch( reviewPackId: (int) $reviewPack->getKey(), operationRunId: (int) $operationRun->getKey(), ); }); $this->recordReviewPackRequestTelemetry($reviewPack, $user, 'tenant'); return $reviewPack; } /** * Create a review-derived executive pack. * * @param array $options */ public function generateFromReview(EnvironmentReview $review, User $user, array $options = []): ReviewPack { $review->loadMissing(['tenant', 'evidenceSnapshot', 'sections']); $tenant = $review->tenant; $snapshot = $review->evidenceSnapshot; if (! $tenant instanceof ManagedEnvironment || ! $snapshot instanceof EvidenceSnapshot) { throw new \InvalidArgumentException('Review exports require an anchored evidence snapshot.'); } $this->assertReviewPackGenerationAllowed($tenant); $options = $this->normalizeOptions($options); $fingerprint = $this->computeFingerprintForReview($review, $options); $existing = $this->findExistingPackForReview($review, $fingerprint); if ($existing instanceof ReviewPack) { $this->logReviewExport($review, $user, $existing, 'reused'); $this->recordReviewPackRequestTelemetry($existing, $user, 'environment_review'); return $existing; } $operationRun = $this->operationRunService->ensureRun( tenant: $tenant, type: OperationRunType::ReviewPackGenerate->value, inputs: [ 'environment_review_id' => (int) $review->getKey(), 'include_pii' => $options['include_pii'], 'include_operations' => $options['include_operations'], 'evidence_snapshot_id' => (int) $snapshot->getKey(), ], initiator: $user, ); if (! $operationRun->wasRecentlyCreated) { $queuedPack = $this->findPackForRun($tenant, $operationRun); if ($queuedPack instanceof ReviewPack) { $this->logReviewExport($review, $user, $queuedPack, 'reused_active_run'); $this->recordReviewPackRequestTelemetry($queuedPack, $user, 'environment_review'); return $queuedPack; } } $reviewPack = ReviewPack::create([ 'managed_environment_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'environment_review_id' => (int) $review->getKey(), 'operation_run_id' => (int) $operationRun->getKey(), 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'status' => ReviewPackStatus::Queued->value, 'options' => $options, 'summary' => [ 'environment_review_id' => (int) $review->getKey(), 'review_status' => (string) $review->status, 'review_completeness_state' => (string) $review->completeness_state, 'section_count' => $review->sections->count(), 'delivery_bundle' => [ 'contract' => self::REVIEW_DERIVED_DELIVERY_CONTRACT, 'executive_entrypoint_file' => self::EXECUTIVE_ENTRYPOINT_FILENAME, 'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'], ], 'finding_outcomes' => is_array($review->summary['finding_outcomes'] ?? null) ? $review->summary['finding_outcomes'] : [], 'finding_report_buckets' => is_array($review->summary['finding_report_buckets'] ?? null) ? $review->summary['finding_report_buckets'] : [], 'evidence_resolution' => [ 'outcome' => 'resolved', 'snapshot_id' => (int) $snapshot->getKey(), 'snapshot_fingerprint' => (string) $snapshot->fingerprint, 'completeness_state' => (string) $snapshot->completeness_state, 'required_dimensions' => self::REQUIRED_EVIDENCE_DIMENSIONS, ], ], ]); $this->operationRunService->dispatchOrFail($operationRun, function () use ($reviewPack, $operationRun): void { GenerateReviewPackJob::dispatch( reviewPackId: (int) $reviewPack->getKey(), operationRunId: (int) $operationRun->getKey(), ); }); $this->logReviewExport($review, $user, $reviewPack, 'queued'); $this->recordReviewPackRequestTelemetry($reviewPack, $user, 'environment_review'); return $reviewPack; } /** * Compute a deterministic fingerprint for deduplication. * * @param array $options */ public function computeFingerprint(ManagedEnvironment $tenant, array $options): string { return $this->computeFingerprintForSnapshot($this->resolveSnapshot($tenant), $this->normalizeOptions($options)); } /** * Generate a signed download URL for a review pack. * * @param array $parameters */ public function generateDownloadUrl(ReviewPack $pack, array $parameters = []): string { $ttlMinutes = (int) config('tenantpilot.review_pack.download_url_ttl_minutes', 60); return URL::signedRoute( 'admin.review-packs.download', array_merge(['reviewPack' => $pack->getKey()], $parameters), now()->addMinutes($ttlMinutes), ); } /** * @return array{path: string, bytes: string, size: int, sha256: string}|null */ public function resolveDownloadableArtifact(ReviewPack $pack): ?array { if ($pack->status !== ReviewPackStatus::Ready->value) { return null; } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return null; } if ($pack->file_disk !== 'exports' || ! filled($pack->file_path)) { return null; } if ((int) $pack->file_size <= 0 || ! filled($pack->sha256)) { return null; } $path = (string) $pack->file_path; $disk = Storage::disk('exports'); try { if (! $disk->exists($path)) { return null; } $fileSize = (int) $disk->size($path); $fileBytes = $disk->get($path); } catch (Throwable) { return null; } if (! is_string($fileBytes)) { return null; } if ($fileSize <= 0 || $fileSize !== (int) $pack->file_size) { return null; } if (! hash_equals((string) $pack->sha256, hash('sha256', $fileBytes))) { return null; } return [ 'path' => $path, 'bytes' => $fileBytes, 'size' => $fileSize, 'sha256' => (string) $pack->sha256, ]; } public function hasDownloadableArtifact(ReviewPack $pack): bool { return $this->resolveDownloadableArtifact($pack) !== null; } public function expire(ReviewPack $reviewPack, User $actor, string $sourceSurface = 'review_pack'): ReviewPack { $reviewPack->loadMissing(['tenant.workspace']); $tenant = $reviewPack->tenant; if (! $tenant instanceof ManagedEnvironment || ! $tenant->workspace) { throw new AuthorizationException('Review pack scope is unavailable.'); } if (! $actor->canAccessTenant($tenant)) { throw new AuthorizationException('Review pack not found.'); } Gate::forUser($actor)->authorize(Capabilities::REVIEW_PACK_MANAGE, $tenant); $beforeStatus = (string) $reviewPack->status; if ($beforeStatus !== ReviewPackStatus::Ready->value) { throw new \InvalidArgumentException('Only ready review packs can be expired.'); } $fileDeleted = false; $fileWasPresent = false; $fileDisk = is_string($reviewPack->file_disk) ? $reviewPack->file_disk : null; $filePath = is_string($reviewPack->file_path) ? $reviewPack->file_path : null; if ($fileDisk === 'exports' && filled($filePath)) { $disk = Storage::disk('exports'); if ($disk->exists((string) $filePath)) { $fileWasPresent = true; if (! $disk->delete((string) $filePath) || $disk->exists((string) $filePath)) { throw new \RuntimeException('Unable to delete the review pack file before expiring it.'); } $fileDeleted = true; } } $reviewPack->forceFill([ 'status' => ReviewPackStatus::Expired->value, ])->save(); $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::ReviewPackExpired, context: [ 'metadata' => [ 'review_pack_id' => (int) $reviewPack->getKey(), 'environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'artifact_family' => 'review_pack', 'before_status' => $beforeStatus, 'after_status' => ReviewPackStatus::Expired->value, 'file_disk' => $fileDisk, 'file_path_present' => filled($filePath), 'file_present_before' => $fileWasPresent, 'file_deleted' => $fileDeleted, 'source_surface' => $sourceSurface, ], ], actor: $actor, resourceType: 'review_pack', resourceId: (string) $reviewPack->getKey(), targetLabel: sprintf('Review pack #%d', (int) $reviewPack->getKey()), tenant: $tenant, operationRunId: $reviewPack->operation_run_id, ); return $reviewPack->refresh(); } /** * Generate a signed rendered-report URL for a review pack. * * @param array $parameters */ public function generateRenderedReportUrl(ReviewPack $pack, array $parameters = []): string { $ttlMinutes = (int) config('tenantpilot.review_pack.download_url_ttl_minutes', 60); return URL::signedRoute( 'admin.review-packs.report', array_merge(['reviewPack' => $pack->getKey()], $parameters), now()->addMinutes($ttlMinutes), ); } /** * @return array */ public function reviewPackGenerationDecisionForTenant(ManagedEnvironment $tenant): array { $tenant->loadMissing('workspace'); $decision = $this->workspaceCommercialLifecycleResolver->reviewPackStartDecisionForTenant($tenant); $entitlementDecision = is_array($decision['entitlement_decision'] ?? null) ? $decision['entitlement_decision'] : []; return $decision + [ 'effective_value' => $entitlementDecision['effective_value'] ?? null, 'source' => $decision['source'] ?? null, 'current_usage' => $entitlementDecision['current_usage'] ?? null, 'remaining_capacity' => $entitlementDecision['remaining_capacity'] ?? null, ]; } private function recordReviewPackRequestTelemetry(ReviewPack $reviewPack, User $user, string $sourceSurface): void { $this->productTelemetryRecorder->record( eventName: ProductUsageEventCatalog::REVIEW_PACK_REQUESTED, workspaceId: (int) $reviewPack->workspace_id, tenantId: (int) $reviewPack->managed_environment_id, userId: (int) $user->getKey(), subjectType: 'review_pack', subjectId: (int) $reviewPack->getKey(), metadata: [ 'source_surface' => $sourceSurface, 'include_operations' => (bool) ($reviewPack->options['include_operations'] ?? false), 'include_pii' => (bool) ($reviewPack->options['include_pii'] ?? false), ], occurredAt: $reviewPack->created_at ?? now(), ); } /** * Find an existing ready, non-expired pack with the same fingerprint. */ public function findExistingPack(ManagedEnvironment $tenant, string $fingerprint): ?ReviewPack { return ReviewPack::query() ->forTenant((int) $tenant->getKey()) ->ready() ->where('fingerprint', $fingerprint) ->where('expires_at', '>', now()) ->first(); } public function findExistingPackForReview(EnvironmentReview $review, string $fingerprint): ?ReviewPack { return ReviewPack::query() ->where('environment_review_id', (int) $review->getKey()) ->ready() ->where('fingerprint', $fingerprint) ->where('expires_at', '>', now()) ->first(); } /** * Check if a generation run is currently active for this tenant. */ public function checkActiveRun(ManagedEnvironment $tenant): bool { return OperationRun::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->where('type', OperationRunType::ReviewPackGenerate->value) ->active() ->exists(); } public function checkActiveRunForReview(EnvironmentReview $review): bool { return OperationRun::query() ->where('managed_environment_id', (int) $review->managed_environment_id) ->where('type', OperationRunType::ReviewPackGenerate->value) ->whereJsonContains('context->environment_review_id', (int) $review->getKey()) ->active() ->exists(); } /** * @param array $options * @return array{include_pii: bool, include_operations: bool} */ private function normalizeOptions(array $options): array { return [ 'include_pii' => (bool) ($options['include_pii'] ?? config('tenantpilot.review_pack.include_pii_default', true)), 'include_operations' => (bool) ($options['include_operations'] ?? config('tenantpilot.review_pack.include_operations_default', true)), ]; } private function assertReviewPackGenerationAllowed(ManagedEnvironment $tenant): void { $decision = $this->reviewPackGenerationDecisionForTenant($tenant); if (! (bool) ($decision['is_blocked'] ?? false)) { return; } throw new WorkspaceEntitlementBlockedException($decision); } public function computeFingerprintForSnapshot(EvidenceSnapshot $snapshot, array $options): string { $data = [ 'managed_environment_id' => (int) $snapshot->managed_environment_id, 'evidence_snapshot_id' => (int) $snapshot->getKey(), 'evidence_fingerprint' => (string) $snapshot->fingerprint, 'include_pii' => (bool) ($options['include_pii'] ?? true), 'include_operations' => (bool) ($options['include_operations'] ?? true), ]; return hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); } public function computeFingerprintForReview(EnvironmentReview $review, array $options): string { $data = [ 'environment_review_id' => (int) $review->getKey(), 'review_fingerprint' => (string) $review->fingerprint, 'review_status' => (string) $review->status, 'delivery_contract' => self::REVIEW_DERIVED_DELIVERY_CONTRACT, 'include_pii' => (bool) ($options['include_pii'] ?? true), 'include_operations' => (bool) ($options['include_operations'] ?? true), ]; return hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); } private function resolveSnapshot(ManagedEnvironment $tenant): EvidenceSnapshot { $result = $this->snapshotResolver->resolve(new EvidenceResolutionRequest( workspaceId: (int) $tenant->workspace_id, tenantId: (int) $tenant->getKey(), requiredDimensions: self::REQUIRED_EVIDENCE_DIMENSIONS, )); if (! $result->isResolved()) { throw new ReviewPackEvidenceResolutionException($result); } return $result->snapshot; } private function findPackForRun(ManagedEnvironment $tenant, OperationRun $operationRun): ?ReviewPack { return ReviewPack::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->where('operation_run_id', (int) $operationRun->getKey()) ->latest('id') ->first(); } private function logReviewExport(EnvironmentReview $review, User $user, ReviewPack $reviewPack, string $mode): void { $tenant = $review->tenant; if (! $tenant instanceof ManagedEnvironment) { return; } $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::EnvironmentReviewExported, context: [ 'metadata' => [ 'review_id' => (int) $review->getKey(), 'review_pack_id' => (int) $reviewPack->getKey(), 'mode' => $mode, 'status' => (string) $reviewPack->status, ], ], actor: $user, resourceType: 'environment_review', resourceId: (string) $review->getKey(), targetLabel: sprintf('ManagedEnvironment review #%d', (int) $review->getKey()), operationRunId: $reviewPack->operation_run_id, tenant: $tenant, ); } }