graphOptionsResolver->resolveForTenant($tenant); $roleDefinitions = $this->fetchRoleDefinitions($graphOptions); $roleAssignments = $this->fetchRoleAssignments($graphOptions); $payload = $this->buildPayload($roleDefinitions, $roleAssignments); $fingerprint = $this->computeFingerprint($roleDefinitions, $roleAssignments); $latestReport = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES) ->orderByDesc('created_at') ->orderByDesc('id') ->first(); if ($latestReport instanceof StoredReport && $latestReport->fingerprint === $fingerprint) { return new EntraAdminRolesReportResult( created: false, storedReportId: (int) $latestReport->getKey(), fingerprint: $fingerprint, payload: $payload, ); } $report = StoredReport::create([ 'tenant_id' => (int) $tenant->getKey(), 'report_type' => StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES, 'payload' => $payload, 'fingerprint' => $fingerprint, 'previous_fingerprint' => $latestReport?->fingerprint, ]); return new EntraAdminRolesReportResult( created: true, storedReportId: (int) $report->getKey(), fingerprint: $fingerprint, payload: $payload, ); } /** * @return array> */ private function fetchRoleDefinitions(array $graphOptions): array { $response = $this->graphClient->listPolicies('entraRoleDefinitions', $graphOptions); if ($response->failed()) { throw new RuntimeException('Failed to fetch Entra role definitions from Graph API.'); } return $response->data; } /** * @return array> */ private function fetchRoleAssignments(array $graphOptions): array { $response = $this->graphClient->listPolicies('entraRoleAssignments', array_merge($graphOptions, [ 'expand' => 'principal', ])); if ($response->failed()) { throw new RuntimeException('Failed to fetch Entra role assignments from Graph API.'); } return $response->data; } /** * @return array */ private function buildPayload(array $roleDefinitions, array $roleAssignments): array { $roleDefMap = []; foreach ($roleDefinitions as $def) { $id = (string) ($def['id'] ?? ''); if ($id !== '') { $roleDefMap[$id] = $def; } } $highPrivilegeAssignments = []; foreach ($roleAssignments as $assignment) { $roleDefId = (string) ($assignment['roleDefinitionId'] ?? ''); $roleDef = $roleDefMap[$roleDefId] ?? null; $templateId = (string) ($roleDef['templateId'] ?? $roleDefId); $displayName = $roleDef !== null ? (string) ($roleDef['displayName'] ?? '') : null; $severity = $this->catalog->classify($templateId, $displayName); if ($severity !== null) { $principal = $assignment['principal'] ?? []; $highPrivilegeAssignments[] = [ 'role_definition_id' => $roleDefId, 'role_template_id' => (string) ($roleDef['templateId'] ?? ''), 'role_display_name' => (string) ($roleDef['displayName'] ?? 'Unknown'), 'is_built_in' => (bool) ($roleDef['isBuiltIn'] ?? false), 'principal_id' => (string) ($assignment['principalId'] ?? ''), 'principal_display_name' => (string) ($principal['displayName'] ?? 'Unknown'), 'principal_type' => $this->resolvePrincipalType($principal), 'directory_scope_id' => (string) ($assignment['directoryScopeId'] ?? '/'), 'severity' => $severity, ]; } } return [ 'provider_key' => 'microsoft', 'domain' => 'entra.admin_roles', 'measured_at' => CarbonImmutable::now('UTC')->toIso8601String(), 'role_definitions' => $roleDefinitions, 'role_assignments' => $roleAssignments, 'totals' => [ 'roles_total' => count($roleDefinitions), 'assignments_total' => count($roleAssignments), 'high_privilege_assignments' => count($highPrivilegeAssignments), ], 'high_privilege' => $highPrivilegeAssignments, ]; } private function computeFingerprint(array $roleDefinitions, array $roleAssignments): string { $roleDefMap = []; foreach ($roleDefinitions as $def) { $id = (string) ($def['id'] ?? ''); if ($id !== '') { $roleDefMap[$id] = $def; } } $tuples = []; foreach ($roleAssignments as $assignment) { $roleDefId = (string) ($assignment['roleDefinitionId'] ?? ''); $roleDef = $roleDefMap[$roleDefId] ?? null; $templateId = (string) ($roleDef['templateId'] ?? $roleDefId); $principalId = (string) ($assignment['principalId'] ?? ''); $scopeId = (string) ($assignment['directoryScopeId'] ?? '/'); $tuples[] = "{$templateId}:{$principalId}:{$scopeId}"; } sort($tuples); return hash('sha256', implode("\n", $tuples)); } private function resolvePrincipalType(array $principal): string { $odataType = (string) ($principal['@odata.type'] ?? ''); return match (true) { str_contains($odataType, 'user') => 'user', str_contains($odataType, 'group') => 'group', str_contains($odataType, 'servicePrincipal') => 'servicePrincipal', default => 'unknown', }; } }