> */ public function make(EvidenceSnapshot $snapshot): array { $items = $snapshot->items->keyBy('dimension_key'); $findingsItem = $this->item($items, 'findings_summary'); $permissionItem = $this->item($items, 'permission_posture'); $rolesItem = $this->item($items, 'entra_admin_roles'); $baselineItem = $this->item($items, 'baseline_drift_posture'); $operationsItem = $this->item($items, 'operations_summary'); return [ $this->executiveSummarySection($snapshot, $findingsItem, $permissionItem, $rolesItem, $baselineItem, $operationsItem), $this->openRisksSection($findingsItem), $this->acceptedRisksSection($findingsItem), $this->permissionPostureSection($permissionItem, $rolesItem), $this->baselineDriftSection($baselineItem), $this->operationsHealthSection($operationsItem), ]; } private function executiveSummarySection( EvidenceSnapshot $snapshot, ?EvidenceSnapshotItem $findingsItem, ?EvidenceSnapshotItem $permissionItem, ?EvidenceSnapshotItem $rolesItem, ?EvidenceSnapshotItem $baselineItem, ?EvidenceSnapshotItem $operationsItem, ): array { $findingsSummary = $this->summary($findingsItem); $permissionSummary = $this->summary($permissionItem); $rolesSummary = $this->summary($rolesItem); $baselineSummary = $this->summary($baselineItem); $operationsSummary = $this->summary($operationsItem); $riskAcceptance = is_array($findingsSummary['risk_acceptance'] ?? null) ? $findingsSummary['risk_acceptance'] : []; $openCount = (int) ($findingsSummary['open_count'] ?? 0); $findingCount = (int) ($findingsSummary['count'] ?? 0); $driftCount = (int) ($baselineSummary['open_drift_count'] ?? 0); $postureScore = $permissionSummary['posture_score'] ?? null; $operationFailures = (int) ($operationsSummary['failed_count'] ?? 0); $partialOperations = (int) ($operationsSummary['partial_count'] ?? 0); $highlights = array_values(array_filter([ sprintf('%d open risks from %d tracked findings.', $openCount, $findingCount), $postureScore !== null ? sprintf('Permission posture score is %s.', $postureScore) : 'Permission posture report is unavailable.', sprintf('%d baseline drift findings remain open.', $driftCount), sprintf('%d recent operations failed and %d completed with warnings.', $operationFailures, $partialOperations), sprintf('%d risk-accepted findings are currently governed.', (int) ($riskAcceptance['valid_governed_count'] ?? 0)), sprintf('%d privileged Entra roles are captured in the evidence basis.', (int) ($rolesSummary['role_count'] ?? 0)), ])); return [ 'section_key' => 'executive_summary', 'title' => 'Executive summary', 'sort_order' => 10, 'required' => true, 'completeness_state' => $this->maxState([ $this->state($findingsItem), $this->state($permissionItem), $this->state($rolesItem), $this->state($baselineItem), $this->state($operationsItem), ])->value, 'source_snapshot_fingerprint' => (string) $snapshot->fingerprint, 'summary_payload' => [ 'finding_count' => $findingCount, 'open_risk_count' => $openCount, 'posture_score' => $postureScore, 'baseline_drift_count' => $driftCount, 'failed_operation_count' => $operationFailures, 'partial_operation_count' => $partialOperations, 'risk_acceptance' => $riskAcceptance, ], 'render_payload' => [ 'highlights' => $highlights, 'next_actions' => $this->nextActions( openCount: $openCount, driftCount: $driftCount, operationFailures: $operationFailures, postureScore: is_numeric($postureScore) ? (int) $postureScore : null, riskWarnings: (int) ($riskAcceptance['warning_count'] ?? 0), ), 'included_dimensions' => collect($snapshot->items) ->map(static fn (EvidenceSnapshotItem $item): array => [ 'key' => (string) $item->dimension_key, 'state' => (string) $item->state, 'required' => (bool) $item->required, ]) ->values() ->all(), ], 'measured_at' => $snapshot->generated_at, ]; } private function openRisksSection(?EvidenceSnapshotItem $findingsItem): array { $summary = $this->summary($findingsItem); $entries = collect(Arr::wrap($summary['entries'] ?? [])) ->filter(static fn (mixed $entry): bool => is_array($entry) && in_array((string) ($entry['status'] ?? ''), ['open', 'in_progress', 'acknowledged'], true)) ->sortByDesc(static fn (array $entry): int => match ((string) ($entry['severity'] ?? 'low')) { 'critical' => 4, 'high' => 3, 'medium' => 2, default => 1, }) ->take(5) ->values() ->all(); return [ 'section_key' => 'open_risks', 'title' => 'Open risk highlights', 'sort_order' => 20, 'required' => true, 'completeness_state' => $this->state($findingsItem)->value, 'source_snapshot_fingerprint' => $this->sourceFingerprint($findingsItem), 'summary_payload' => [ 'open_count' => (int) ($summary['open_count'] ?? 0), 'severity_counts' => is_array($summary['severity_counts'] ?? null) ? $summary['severity_counts'] : [], ], 'render_payload' => [ 'entries' => $entries, 'empty_state' => empty($entries) ? 'No open risks are recorded in the anchored evidence basis.' : null, ], 'measured_at' => $findingsItem?->measured_at, ]; } private function acceptedRisksSection(?EvidenceSnapshotItem $findingsItem): array { $summary = $this->summary($findingsItem); $entries = collect(Arr::wrap($summary['entries'] ?? [])) ->filter(static fn (mixed $entry): bool => is_array($entry) && (string) ($entry['status'] ?? '') === 'risk_accepted') ->take(5) ->values() ->all(); $riskAcceptance = is_array($summary['risk_acceptance'] ?? null) ? $summary['risk_acceptance'] : []; return [ 'section_key' => 'accepted_risks', 'title' => 'Accepted risk summary', 'sort_order' => 30, 'required' => true, 'completeness_state' => $this->state($findingsItem)->value, 'source_snapshot_fingerprint' => $this->sourceFingerprint($findingsItem), 'summary_payload' => [ 'status_marked_count' => (int) ($riskAcceptance['status_marked_count'] ?? 0), 'valid_governed_count' => (int) ($riskAcceptance['valid_governed_count'] ?? 0), 'warning_count' => (int) ($riskAcceptance['warning_count'] ?? 0), 'expired_count' => (int) ($riskAcceptance['expired_count'] ?? 0), 'revoked_count' => (int) ($riskAcceptance['revoked_count'] ?? 0), 'missing_exception_count' => (int) ($riskAcceptance['missing_exception_count'] ?? 0), ], 'render_payload' => [ 'entries' => $entries, 'disclosure' => (int) ($riskAcceptance['warning_count'] ?? 0) > 0 ? 'Some accepted risks need governance follow-up before stakeholder delivery.' : 'Accepted risks are governed by the anchored evidence basis.', ], 'measured_at' => $findingsItem?->measured_at, ]; } private function permissionPostureSection(?EvidenceSnapshotItem $permissionItem, ?EvidenceSnapshotItem $rolesItem): array { $permissionSummary = $this->summary($permissionItem); $rolesSummary = $this->summary($rolesItem); return [ 'section_key' => 'permission_posture', 'title' => 'Permission posture', 'sort_order' => 40, 'required' => true, 'completeness_state' => $this->maxState([ $this->state($permissionItem), $this->state($rolesItem), ])->value, 'source_snapshot_fingerprint' => $this->sourceFingerprint($permissionItem) ?? $this->sourceFingerprint($rolesItem), 'summary_payload' => [ 'posture_score' => $permissionSummary['posture_score'] ?? null, 'required_count' => (int) ($permissionSummary['required_count'] ?? 0), 'granted_count' => (int) ($permissionSummary['granted_count'] ?? 0), 'role_count' => (int) ($rolesSummary['role_count'] ?? 0), ], 'render_payload' => [ 'permission_payload' => is_array($permissionSummary['payload'] ?? null) ? $permissionSummary['payload'] : [], 'roles' => is_array($rolesSummary['roles'] ?? null) ? $rolesSummary['roles'] : [], ], 'measured_at' => $permissionItem?->measured_at ?? $rolesItem?->measured_at, ]; } private function baselineDriftSection(?EvidenceSnapshotItem $baselineItem): array { $summary = $this->summary($baselineItem); return [ 'section_key' => 'baseline_drift_posture', 'title' => 'Baseline drift posture', 'sort_order' => 50, 'required' => true, 'completeness_state' => $this->state($baselineItem)->value, 'source_snapshot_fingerprint' => $this->sourceFingerprint($baselineItem), 'summary_payload' => [ 'drift_count' => (int) ($summary['drift_count'] ?? 0), 'open_drift_count' => (int) ($summary['open_drift_count'] ?? 0), ], 'render_payload' => [ 'disclosure' => (int) ($summary['open_drift_count'] ?? 0) > 0 ? 'Baseline drift remains visible in this review and should be discussed as hardening work.' : 'No open baseline drift findings are present in the anchored evidence basis.', ], 'measured_at' => $baselineItem?->measured_at, ]; } private function operationsHealthSection(?EvidenceSnapshotItem $operationsItem): array { $summary = $this->summary($operationsItem); return [ 'section_key' => 'operations_health', 'title' => 'Operations health', 'sort_order' => 60, 'required' => true, 'completeness_state' => $this->state($operationsItem)->value, 'source_snapshot_fingerprint' => $this->sourceFingerprint($operationsItem), 'summary_payload' => [ 'operation_count' => (int) ($summary['operation_count'] ?? 0), 'failed_count' => (int) ($summary['failed_count'] ?? 0), 'partial_count' => (int) ($summary['partial_count'] ?? 0), ], 'render_payload' => [ 'entries' => array_values(array_slice(Arr::wrap($summary['entries'] ?? []), 0, 10)), ], 'measured_at' => $operationsItem?->measured_at, ]; } private function item(Collection $items, string $key): ?EvidenceSnapshotItem { $item = $items->get($key); return $item instanceof EvidenceSnapshotItem ? $item : null; } /** * @return array */ private function summary(?EvidenceSnapshotItem $item): array { return is_array($item?->summary_payload) ? $item->summary_payload : []; } private function state(?EvidenceSnapshotItem $item): TenantReviewCompletenessState { return TenantReviewCompletenessState::tryFrom((string) $item?->state) ?? TenantReviewCompletenessState::Missing; } private function sourceFingerprint(?EvidenceSnapshotItem $item): ?string { $fingerprint = $item?->source_fingerprint; return is_string($fingerprint) && $fingerprint !== '' ? $fingerprint : null; } /** * @param array $states */ private function maxState(array $states): TenantReviewCompletenessState { if (in_array(TenantReviewCompletenessState::Missing, $states, true)) { return TenantReviewCompletenessState::Missing; } if (in_array(TenantReviewCompletenessState::Stale, $states, true)) { return TenantReviewCompletenessState::Stale; } if (in_array(TenantReviewCompletenessState::Partial, $states, true)) { return TenantReviewCompletenessState::Partial; } return TenantReviewCompletenessState::Complete; } /** * @return list */ private function nextActions( int $openCount, int $driftCount, int $operationFailures, ?int $postureScore, int $riskWarnings, ): array { $actions = []; if ($openCount > 0) { $actions[] = 'Review the highest-severity open findings with the tenant and confirm ownership.'; } if ($riskWarnings > 0) { $actions[] = 'Reconcile accepted-risk governance records before external delivery.'; } if ($postureScore !== null && $postureScore < 80) { $actions[] = 'Prioritize missing permissions or posture controls that materially affect review confidence.'; } if ($driftCount > 0) { $actions[] = 'Schedule remediation for recurring baseline drift to reduce repeated review findings.'; } if ($operationFailures > 0) { $actions[] = 'Inspect recent failed operations to confirm tenant management workflows are stable.'; } if ($actions === []) { $actions[] = 'No immediate corrective action is required beyond the normal review cadence.'; } return $actions; } }