From c852b80701208026521ab4f615dd9dcec194872e Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 3 Jan 2026 21:03:34 +0100 Subject: [PATCH] fix: sanitize endpointSecurityIntent updates --- config/graph_contracts.php | 5 + ...tSecurityIntentRestoreSanitizationTest.php | 102 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/Feature/EndpointSecurityIntentRestoreSanitizationTest.php diff --git a/config/graph_contracts.php b/config/graph_contracts.php index cd1a970..b6c35a4 100644 --- a/config/graph_contracts.php +++ b/config/graph_contracts.php @@ -534,6 +534,11 @@ 'update_method' => 'PATCH', 'id_field' => 'id', 'hydration' => 'properties', + 'update_strip_keys' => [ + 'isAssigned', + 'templateId', + 'isMigratingToConfigurationPolicy', + ], ], 'mobileApp' => [ 'resource' => 'deviceAppManagement/mobileApps', diff --git a/tests/Feature/EndpointSecurityIntentRestoreSanitizationTest.php b/tests/Feature/EndpointSecurityIntentRestoreSanitizationTest.php new file mode 100644 index 0000000..c19edbc --- /dev/null +++ b/tests/Feature/EndpointSecurityIntentRestoreSanitizationTest.php @@ -0,0 +1,102 @@ +}> */ + public array $applyPolicyCalls = []; + + public function listPolicies(string $policyType, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse + { + return new GraphResponse(true, ['payload' => []]); + } + + public function getOrganization(array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse + { + $this->applyPolicyCalls[] = [ + 'policyType' => $policyType, + 'policyId' => $policyId, + 'payload' => $payload, + 'options' => $options, + ]; + + return new GraphResponse(true, []); + } + + public function getServicePrincipalPermissions(array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } + + public function request(string $method, string $path, array $options = []): GraphResponse + { + return new GraphResponse(true, []); + } +} + +test('restore strips non-patchable fields from endpoint security intent updates', function () { + $client = new EndpointSecurityIntentRestoreGraphClient; + app()->instance(GraphClientInterface::class, $client); + + $tenant = Tenant::factory()->create(); + $backupSet = BackupSet::factory()->for($tenant)->create([ + 'status' => 'completed', + 'item_count' => 1, + ]); + + $backupItem = BackupItem::factory()->for($tenant)->for($backupSet)->create([ + 'policy_id' => null, + 'policy_identifier' => 'intent-1', + 'policy_type' => 'endpointSecurityIntent', + 'platform' => 'windows', + 'payload' => [ + 'id' => 'intent-1', + '@odata.type' => '#microsoft.graph.deviceManagementIntent', + 'displayName' => 'SPO Account Protection', + 'description' => 'Demo', + 'isAssigned' => false, + 'templateId' => '0f2b5d70-d4e9-4156-8c16-1397eb6c54a5', + 'isMigratingToConfigurationPolicy' => false, + ], + 'assignments' => null, + ]); + + $service = app(RestoreService::class); + $run = $service->execute( + tenant: $tenant, + backupSet: $backupSet, + selectedItemIds: [$backupItem->id], + dryRun: false, + ); + + expect($run->status)->toBe('completed'); + expect($client->applyPolicyCalls)->toHaveCount(1); + + $payload = $client->applyPolicyCalls[0]['payload'] ?? []; + expect($payload)->toBeArray(); + expect($payload)->toHaveKey('displayName'); + expect($payload)->toHaveKey('description'); + expect($payload)->not->toHaveKey('id'); + expect($payload)->not->toHaveKey('isAssigned'); + expect($payload)->not->toHaveKey('templateId'); + expect($payload)->not->toHaveKey('isMigratingToConfigurationPolicy'); +});