IDs of policies synced or created */ public function syncPolicies(Tenant $tenant, ?array $supportedTypes = null): array { $result = $this->syncPoliciesWithReport($tenant, $supportedTypes); return $result['synced']; } /** * Sync supported policies for a tenant from Microsoft Graph. * * @param array|null $supportedTypes * @return array{synced: array, failures: array} */ public function syncPoliciesWithReport(Tenant $tenant, ?array $supportedTypes = null): array { if (! $tenant->isActive()) { throw new \RuntimeException('Tenant is archived or inactive.'); } $types = $supportedTypes ?? config('tenantpilot.supported_policy_types', []); $synced = []; $failures = []; $tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id; foreach ($types as $typeConfig) { $policyType = $typeConfig['type']; $platform = $typeConfig['platform'] ?? null; $filter = $typeConfig['filter'] ?? null; $this->graphLogger->logRequest('list_policies', [ 'tenant' => $tenantIdentifier, 'policy_type' => $policyType, 'platform' => $platform, 'filter' => $filter, ]); try { $response = $this->graphClient->listPolicies($policyType, [ 'tenant' => $tenantIdentifier, 'client_id' => $tenant->app_client_id, 'client_secret' => $tenant->app_client_secret, 'platform' => $platform, 'filter' => $filter, ]); } catch (Throwable $throwable) { throw GraphErrorMapper::fromThrowable($throwable, [ 'policy_type' => $policyType, 'tenant_id' => $tenant->id, 'tenant_identifier' => $tenantIdentifier, ]); } $this->graphLogger->logResponse('list_policies', $response, [ 'policy_type' => $policyType, 'tenant_id' => $tenant->id, 'tenant' => $tenantIdentifier, ]); if ($response->failed()) { $failures[] = [ 'policy_type' => $policyType, 'status' => $response->status, 'errors' => $response->errors, 'meta' => $response->meta, ]; continue; } foreach ($response->data as $policyData) { $externalId = $policyData['id'] ?? $policyData['external_id'] ?? null; if ($externalId === null) { continue; } $canonicalPolicyType = $this->resolveCanonicalPolicyType($policyType, $policyData); if ($canonicalPolicyType !== $policyType) { continue; } if ($policyType === 'appProtectionPolicy') { $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null; if (is_string($odataType) && strtolower($odataType) === '#microsoft.graph.targetedmanagedappconfiguration') { Policy::query() ->where('tenant_id', $tenant->id) ->where('external_id', $externalId) ->where('policy_type', $policyType) ->whereNull('ignored_at') ->update(['ignored_at' => now()]); continue; } } $displayName = $policyData['displayName'] ?? $policyData['name'] ?? 'Unnamed policy'; $policyPlatform = $platform ?? ($policyData['platform'] ?? null); $this->reclassifyEnrollmentConfigurationPoliciesIfNeeded( tenantId: $tenant->id, externalId: $externalId, policyType: $policyType, ); $this->reclassifyConfigurationPoliciesIfNeeded( tenantId: $tenant->id, externalId: $externalId, policyType: $policyType, ); $policy = Policy::updateOrCreate( [ 'tenant_id' => $tenant->id, 'external_id' => $externalId, 'policy_type' => $policyType, ], [ 'display_name' => $displayName, 'platform' => $policyPlatform, 'last_synced_at' => now(), 'ignored_at' => null, 'metadata' => Arr::except($policyData, ['id', 'external_id', 'displayName', 'name', 'platform']), ] ); $synced[] = $policy->id; } } return [ 'synced' => $synced, 'failures' => $failures, ]; } private function resolveCanonicalPolicyType(string $policyType, array $policyData): string { if (in_array($policyType, ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy'], true)) { return $this->resolveConfigurationPolicyType($policyData); } if (! in_array($policyType, ['enrollmentRestriction', 'windowsEnrollmentStatusPage'], true)) { return $policyType; } if ($this->isEnrollmentStatusPageItem($policyData)) { return 'windowsEnrollmentStatusPage'; } return 'enrollmentRestriction'; } private function resolveConfigurationPolicyType(array $policyData): string { if ($this->isSecurityBaselineConfigurationPolicy($policyData)) { return 'securityBaselinePolicy'; } if ($this->isEndpointSecurityConfigurationPolicy($policyData)) { return 'endpointSecurityPolicy'; } return 'settingsCatalogPolicy'; } private function isEndpointSecurityConfigurationPolicy(array $policyData): bool { $technologies = $policyData['technologies'] ?? null; if (is_string($technologies)) { if (strcasecmp(trim($technologies), 'endpointSecurity') === 0) { return true; } } if (is_array($technologies)) { foreach ($technologies as $technology) { if (is_string($technology) && strcasecmp(trim($technology), 'endpointSecurity') === 0) { return true; } } } $templateReference = $policyData['templateReference'] ?? null; if (! is_array($templateReference)) { return false; } foreach ($templateReference as $value) { if (is_string($value) && stripos($value, 'endpoint') !== false) { return true; } } return false; } private function isSecurityBaselineConfigurationPolicy(array $policyData): bool { $templateReference = $policyData['templateReference'] ?? null; if (! is_array($templateReference)) { return false; } $templateFamily = $templateReference['templateFamily'] ?? null; if (is_string($templateFamily) && stripos($templateFamily, 'baseline') !== false) { return true; } foreach ($templateReference as $value) { if (is_string($value) && stripos($value, 'baseline') !== false) { return true; } } return false; } private function isEnrollmentStatusPageItem(array $policyData): bool { $odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null; $configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null; return (is_string($odataType) && strcasecmp($odataType, '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration') === 0) || (is_string($configurationType) && $configurationType === 'windows10EnrollmentCompletionPageConfiguration'); } private function reclassifyEnrollmentConfigurationPoliciesIfNeeded(int $tenantId, string $externalId, string $policyType): void { if (! in_array($policyType, ['enrollmentRestriction', 'windowsEnrollmentStatusPage'], true)) { return; } $enrollmentTypes = ['enrollmentRestriction', 'windowsEnrollmentStatusPage']; $existingCorrect = Policy::query() ->where('tenant_id', $tenantId) ->where('external_id', $externalId) ->where('policy_type', $policyType) ->first(); if ($existingCorrect) { Policy::query() ->where('tenant_id', $tenantId) ->where('external_id', $externalId) ->whereIn('policy_type', $enrollmentTypes) ->where('policy_type', '!=', $policyType) ->whereNull('ignored_at') ->update(['ignored_at' => now()]); return; } $existingWrong = Policy::query() ->where('tenant_id', $tenantId) ->where('external_id', $externalId) ->whereIn('policy_type', $enrollmentTypes) ->where('policy_type', '!=', $policyType) ->whereNull('ignored_at') ->first(); if (! $existingWrong) { return; } $existingWrong->forceFill([ 'policy_type' => $policyType, ])->save(); PolicyVersion::query() ->where('policy_id', $existingWrong->id) ->update(['policy_type' => $policyType]); } private function reclassifyConfigurationPoliciesIfNeeded(int $tenantId, string $externalId, string $policyType): void { $configurationTypes = ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy']; if (! in_array($policyType, $configurationTypes, true)) { return; } $existingCorrect = Policy::query() ->where('tenant_id', $tenantId) ->where('external_id', $externalId) ->where('policy_type', $policyType) ->first(); if ($existingCorrect) { Policy::query() ->where('tenant_id', $tenantId) ->where('external_id', $externalId) ->whereIn('policy_type', $configurationTypes) ->where('policy_type', '!=', $policyType) ->whereNull('ignored_at') ->update(['ignored_at' => now()]); return; } $existingWrong = Policy::query() ->where('tenant_id', $tenantId) ->where('external_id', $externalId) ->whereIn('policy_type', $configurationTypes) ->where('policy_type', '!=', $policyType) ->whereNull('ignored_at') ->first(); if (! $existingWrong) { return; } $existingWrong->forceFill([ 'policy_type' => $policyType, ])->save(); PolicyVersion::query() ->where('policy_id', $existingWrong->id) ->update(['policy_type' => $policyType]); } /** * Re-fetch a single policy from Graph and update local metadata. */ public function syncPolicy(Tenant $tenant, Policy $policy): void { if (! $tenant->isActive()) { throw new RuntimeException('Tenant is archived or inactive.'); } $tenantIdentifier = $tenant->tenant_id ?? $tenant->external_id; $this->graphLogger->logRequest('get_policy', [ 'tenant' => $tenantIdentifier, 'policy_type' => $policy->policy_type, 'policy_id' => $policy->external_id, 'platform' => $policy->platform, ]); try { $response = $this->graphClient->getPolicy($policy->policy_type, $policy->external_id, [ 'tenant' => $tenantIdentifier, 'client_id' => $tenant->app_client_id, 'client_secret' => $tenant->app_client_secret, 'platform' => $policy->platform, ]); } catch (Throwable $throwable) { throw GraphErrorMapper::fromThrowable($throwable, [ 'policy_type' => $policy->policy_type, 'policy_id' => $policy->external_id, 'tenant_id' => $tenant->id, 'tenant_identifier' => $tenantIdentifier, ]); } $this->graphLogger->logResponse('get_policy', $response, [ 'tenant_id' => $tenant->id, 'tenant' => $tenantIdentifier, 'policy_type' => $policy->policy_type, 'policy_id' => $policy->external_id, ]); if ($response->failed()) { $message = $response->errors[0]['message'] ?? $response->data['error']['message'] ?? 'Graph request failed.'; throw new RuntimeException($message); } $payload = $response->data['payload'] ?? $response->data; if (! is_array($payload)) { throw new RuntimeException('Invalid Graph response payload.'); } $displayName = $payload['displayName'] ?? $payload['name'] ?? $policy->display_name; $platform = $payload['platform'] ?? $policy->platform; $policy->forceFill([ 'display_name' => $displayName, 'platform' => $platform, 'last_synced_at' => now(), 'metadata' => Arr::except($payload, ['id', 'external_id', 'displayName', 'name', 'platform']), ])->save(); } }