, * status:'granted'|'missing'|'error', * details:array|null * } * * @param array> $permissions * @param array{fresh?:bool,reason_code?:string,message?:string}|null $inventory * @return array> */ public static function buildChecks(Tenant $tenant, array $permissions, ?array $inventory = null): array { $inventory = is_array($inventory) ? $inventory : []; $inventoryFresh = $inventory['fresh'] ?? true; $inventoryFresh = is_bool($inventoryFresh) ? $inventoryFresh : true; $inventoryReasonCode = $inventory['reason_code'] ?? null; $inventoryReasonCode = is_string($inventoryReasonCode) && $inventoryReasonCode !== '' ? $inventoryReasonCode : 'dependency_unreachable'; $inventoryMessage = $inventory['message'] ?? null; $inventoryMessage = is_string($inventoryMessage) && trim($inventoryMessage) !== '' ? trim($inventoryMessage) : 'Unable to refresh observed permissions inventory during this run. Retry verification.'; $inventoryEvidence = self::inventoryEvidence($inventory); /** @var array $rows */ $rows = collect($permissions) ->filter(fn (mixed $row): bool => is_array($row)) ->map(fn (array $row): array => self::normalizePermissionRow($row)) ->values() ->all(); $checks = []; foreach (self::definitions() as $definition) { $key = (string) ($definition['key'] ?? 'unknown'); $title = (string) ($definition['title'] ?? 'Check'); $clusterRows = array_values(array_filter($rows, fn (array $row): bool => self::matches($definition, $row))); $checks[] = self::buildCheck( tenant: $tenant, key: $key, title: $title, clusterRows: $clusterRows, inventoryFresh: $inventoryFresh, inventoryReasonCode: $inventoryReasonCode, inventoryMessage: $inventoryMessage, inventoryEvidence: $inventoryEvidence, ); } return $checks; } /** * @return array,keys?:array}> */ private static function definitions(): array { return [ [ 'key' => 'permissions.admin_consent', 'title' => 'Admin consent granted', 'mode' => 'type', 'type' => 'application', ], [ 'key' => 'permissions.directory_groups', 'title' => 'Directory & group read access', 'mode' => 'keys', 'keys' => [ 'Directory.Read.All', 'Group.Read.All', ], ], [ 'key' => 'permissions.intune_configuration', 'title' => 'Intune configuration access', 'mode' => 'prefixes', 'prefixes' => [ 'DeviceManagementConfiguration.', 'DeviceManagementServiceConfig.', ], ], [ 'key' => 'permissions.intune_apps', 'title' => 'Intune apps access', 'mode' => 'prefixes', 'prefixes' => [ 'DeviceManagementApps.', ], ], [ 'key' => 'permissions.intune_rbac_assignments', 'title' => 'Intune RBAC & assignments prerequisites', 'mode' => 'prefixes', 'prefixes' => [ 'DeviceManagementRBAC.', ], ], [ 'key' => 'permissions.scripts_remediations', 'title' => 'Scripts/remediations access', 'mode' => 'prefixes', 'prefixes' => [ 'DeviceManagementScripts.', ], ], ]; } /** * @param array{mode:string,prefixes?:array,keys?:array,type?:string} $definition * @param TenantPermissionRow $row */ private static function matches(array $definition, array $row): bool { $mode = (string) ($definition['mode'] ?? ''); $key = (string) ($row['key'] ?? ''); if ($mode === 'type') { return ($row['type'] ?? null) === ($definition['type'] ?? null); } if ($mode === 'keys') { $keys = $definition['keys'] ?? []; return is_array($keys) && in_array($key, $keys, true); } if ($mode === 'prefixes') { $prefixes = $definition['prefixes'] ?? []; if (! is_array($prefixes)) { return false; } foreach ($prefixes as $prefix) { if (is_string($prefix) && $prefix !== '' && str_starts_with($key, $prefix)) { return true; } } return false; } return false; } /** * @param array $clusterRows * @return array */ private static function buildCheck( Tenant $tenant, string $key, string $title, array $clusterRows, bool $inventoryFresh, string $inventoryReasonCode, string $inventoryMessage, array $inventoryEvidence, ): array { if (! $inventoryFresh) { return [ 'key' => $key, 'title' => $title, 'status' => VerificationCheckStatus::Warn->value, 'severity' => VerificationCheckSeverity::Medium->value, 'blocking' => false, 'reason_code' => $inventoryReasonCode, 'message' => $inventoryMessage, 'evidence' => $inventoryEvidence, 'next_steps' => [ [ 'label' => 'Open required permissions', 'url' => RequiredPermissionsLinks::requiredPermissions($tenant), ], ], ]; } if ($clusterRows === []) { return [ 'key' => $key, 'title' => $title, 'status' => VerificationCheckStatus::Skip->value, 'severity' => VerificationCheckSeverity::Info->value, 'blocking' => false, 'reason_code' => 'not_applicable', 'message' => 'Not applicable for this tenant.', 'evidence' => [], 'next_steps' => [], ]; } $missingApplication = array_values(array_filter( $clusterRows, static fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'application', )); $missingDelegated = array_values(array_filter( $clusterRows, static fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated', )); $errored = array_values(array_filter( $clusterRows, static fn (array $row): bool => $row['status'] === 'error', )); $evidence = array_values(array_unique(array_merge( self::evidence($missingApplication, $missingDelegated, $errored), $inventoryEvidence, ), SORT_REGULAR)); if ($missingApplication !== [] || $errored !== []) { $missingKeys = array_values(array_unique(array_merge( array_map(static fn (array $row): string => $row['key'], $missingApplication), array_map(static fn (array $row): string => $row['key'], $errored), ))); $message = $missingKeys !== [] ? sprintf('Missing required application permission(s): %s', implode(', ', array_slice($missingKeys, 0, 6))) : 'Missing required permissions.'; return [ 'key' => $key, 'title' => $title, 'status' => VerificationCheckStatus::Fail->value, 'severity' => VerificationCheckSeverity::Critical->value, 'blocking' => true, 'reason_code' => 'ext.missing_permission', 'message' => $message, 'evidence' => $evidence, 'next_steps' => [ [ 'label' => 'Open required permissions', 'url' => RequiredPermissionsLinks::requiredPermissions($tenant), ], ], ]; } if ($missingDelegated !== []) { $missingKeys = array_values(array_unique(array_map(static fn (array $row): string => $row['key'], $missingDelegated))); $message = sprintf('Missing delegated permission(s): %s', implode(', ', array_slice($missingKeys, 0, 6))); return [ 'key' => $key, 'title' => $title, 'status' => VerificationCheckStatus::Warn->value, 'severity' => VerificationCheckSeverity::Medium->value, 'blocking' => false, 'reason_code' => 'ext.missing_delegated_permission', 'message' => $message, 'evidence' => $evidence, 'next_steps' => [ [ 'label' => 'Open required permissions', 'url' => RequiredPermissionsLinks::requiredPermissions($tenant), ], ], ]; } return [ 'key' => $key, 'title' => $title, 'status' => VerificationCheckStatus::Pass->value, 'severity' => VerificationCheckSeverity::Info->value, 'blocking' => false, 'reason_code' => 'ok', 'message' => 'All required permissions are granted.', 'evidence' => [], 'next_steps' => [], ]; } /** * @param array $missingApplication * @param array $missingDelegated * @param array $errored * @return array */ private static function evidence(array $missingApplication, array $missingDelegated, array $errored): array { $pointers = []; foreach (array_merge($missingApplication, $missingDelegated, $errored) as $row) { $pointers[] = [ 'kind' => 'missing_permission', 'value' => (string) ($row['key'] ?? ''), ]; $pointers[] = [ 'kind' => 'permission_type', 'value' => (string) ($row['type'] ?? 'application'), ]; foreach (($row['features'] ?? []) as $feature) { if (! is_string($feature) || $feature === '') { continue; } $pointers[] = [ 'kind' => 'feature', 'value' => $feature, ]; } } $unique = []; foreach ($pointers as $pointer) { $key = $pointer['kind'].':'.(string) $pointer['value']; $unique[$key] = $pointer; } return array_values($unique); } /** * @param array $inventory * @return array */ private static function inventoryEvidence(array $inventory): array { $pointers = []; $appId = $inventory['app_id'] ?? null; if (is_string($appId) && $appId !== '') { $pointers[] = [ 'kind' => 'app_id', 'value' => $appId, ]; } $observedCount = $inventory['observed_permissions_count'] ?? null; if (is_int($observedCount) || (is_numeric($observedCount) && (string) (int) $observedCount === (string) $observedCount)) { $pointers[] = [ 'kind' => 'observed_permissions_count', 'value' => (int) $observedCount, ]; } return $pointers; } /** * @param array $row * @return TenantPermissionRow */ private static function normalizePermissionRow(array $row): array { $key = (string) ($row['key'] ?? ''); $type = (string) ($row['type'] ?? 'application'); $status = (string) ($row['status'] ?? 'missing'); $description = $row['description'] ?? null; $features = $row['features'] ?? []; $details = $row['details'] ?? null; if (! in_array($type, ['application', 'delegated'], true)) { $type = 'application'; } if (! in_array($status, ['granted', 'missing', 'error'], true)) { $status = 'missing'; } if (! is_string($description) || $description === '') { $description = null; } if (! is_array($features)) { $features = []; } $features = array_values(array_unique(array_filter(array_map('strval', $features)))); if (! is_array($details)) { $details = null; } return [ 'key' => $key, 'type' => $type, 'description' => $description, 'features' => $features, 'status' => $status, 'details' => $details, ]; } }