From 07c0c0e8618b830d83b7ec72f3e2ff4eae030341 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sun, 28 Dec 2025 17:37:55 +0100 Subject: [PATCH] fix: hydrate compliance notifications in snapshots --- .../Intune/CompliancePolicyNormalizer.php | 1 + app/Services/Intune/PolicySnapshotService.php | 64 +++++++++++ tests/Unit/CompliancePolicyNormalizerTest.php | 10 ++ tests/Unit/PolicySnapshotServiceTest.php | 105 ++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 tests/Unit/PolicySnapshotServiceTest.php diff --git a/app/Services/Intune/CompliancePolicyNormalizer.php b/app/Services/Intune/CompliancePolicyNormalizer.php index be66042..a87ea65 100644 --- a/app/Services/Intune/CompliancePolicyNormalizer.php +++ b/app/Services/Intune/CompliancePolicyNormalizer.php @@ -282,6 +282,7 @@ private function ignoredKeys(): array 'settingCount', 'settingsCount', 'templateReference', + 'scheduledActionsForRule', ]; } diff --git a/app/Services/Intune/PolicySnapshotService.php b/app/Services/Intune/PolicySnapshotService.php index 87c9d6c..6ad2a60 100644 --- a/app/Services/Intune/PolicySnapshotService.php +++ b/app/Services/Intune/PolicySnapshotService.php @@ -73,6 +73,16 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null ); } + if ($policy->policy_type === 'deviceCompliancePolicy') { + [$payload, $metadata] = $this->hydrateComplianceActions( + tenantIdentifier: $tenantIdentifier, + tenant: $tenant, + policyId: $policy->external_id, + payload: is_array($payload) ? $payload : [], + metadata: $metadata + ); + } + if ($response->failed()) { $reason = $response->warnings[0] ?? 'Graph request failed'; $failure = [ @@ -174,6 +184,60 @@ private function hydrateSettingsCatalog(string $tenantIdentifier, Tenant $tenant return [$payload, $metadata]; } + /** + * Hydrate compliance policies with scheduled actions (notification templates). + * + * @return array{0:array,1:array} + */ + private function hydrateComplianceActions(string $tenantIdentifier, Tenant $tenant, string $policyId, array $payload, array $metadata): array + { + $path = sprintf('deviceManagement/deviceCompliancePolicies/%s/scheduledActionsForRule', urlencode($policyId)); + $options = [ + 'tenant' => $tenantIdentifier, + 'client_id' => $tenant->app_client_id, + 'client_secret' => $tenant->app_client_secret, + ]; + + $actions = []; + $nextPath = $path; + $hydrationStatus = 'complete'; + + while ($nextPath) { + $response = $this->graphClient->request('GET', $nextPath, $options); + + if ($response->failed()) { + $hydrationStatus = 'failed'; + + break; + } + + $data = $response->data; + $pageItems = $data['value'] ?? (is_array($data) ? $data : []); + + foreach ($pageItems as $item) { + if (is_array($item)) { + $actions[] = $item; + } + } + + $nextLink = $data['@odata.nextLink'] ?? null; + + if (! $nextLink) { + break; + } + + $nextPath = $this->stripGraphBaseUrl((string) $nextLink); + } + + if (! empty($actions)) { + $payload['scheduledActionsForRule'] = $actions; + } + + $metadata['compliance_actions_hydration'] = $hydrationStatus; + + return [$payload, $metadata]; + } + /** * Extract all settingDefinitionId from settings array, including nested children. */ diff --git a/tests/Unit/CompliancePolicyNormalizerTest.php b/tests/Unit/CompliancePolicyNormalizerTest.php index a2c5bca..d788e4f 100644 --- a/tests/Unit/CompliancePolicyNormalizerTest.php +++ b/tests/Unit/CompliancePolicyNormalizerTest.php @@ -15,6 +15,14 @@ 'bitLockerEnabled' => false, 'osMinimumVersion' => '10.0.19045', 'activeFirewallRequired' => true, + 'scheduledActionsForRule' => [ + [ + 'ruleName' => 'Default rule', + 'scheduledActionConfigurations' => [ + ['actionType' => 'notification'], + ], + ], + ], 'customSetting' => 'Custom value', ]; @@ -31,6 +39,8 @@ expect($additionalBlock)->not->toBeNull(); expect(collect($additionalBlock['rows'])->pluck('label')->all()) ->toContain('Custom Setting'); + expect(collect($additionalBlock['rows'])->pluck('label')->all()) + ->not->toContain('Scheduled Actions For Rule'); expect($settings->pluck('title')->all())->not->toContain('General'); }); diff --git a/tests/Unit/PolicySnapshotServiceTest.php b/tests/Unit/PolicySnapshotServiceTest.php new file mode 100644 index 0000000..eaa9a02 --- /dev/null +++ b/tests/Unit/PolicySnapshotServiceTest.php @@ -0,0 +1,105 @@ +requests[] = ['getPolicy', $policyType, $policyId]; + + return new GraphResponse(success: true, data: [ + 'payload' => [ + 'id' => $policyId, + 'displayName' => 'Compliance Alpha', + '@odata.type' => '#microsoft.graph.windows10CompliancePolicy', + ], + ]); + } + + public function getOrganization(array $options = []): GraphResponse + { + return new GraphResponse(success: true, data: []); + } + + public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse + { + return new GraphResponse(success: true, data: []); + } + + public function getServicePrincipalPermissions(array $options = []): GraphResponse + { + return new GraphResponse(success: true, data: []); + } + + public function request(string $method, string $path, array $options = []): GraphResponse + { + $this->requests[] = [$method, $path]; + + if (str_contains($path, 'scheduledActionsForRule')) { + return new GraphResponse(success: true, data: [ + 'value' => [ + [ + 'ruleName' => 'Default rule', + 'scheduledActionConfigurations' => [ + [ + 'actionType' => 'notification', + 'notificationTemplateId' => 'template-123', + ], + ], + ], + ], + ]); + } + + return new GraphResponse(success: true, data: []); + } +} + +it('hydrates compliance policy scheduled actions into snapshots', function () { + $client = new PolicySnapshotGraphClient; + app()->instance(GraphClientInterface::class, $client); + + $tenant = Tenant::factory()->create([ + 'tenant_id' => 'tenant-compliance', + 'app_client_id' => 'client-123', + 'app_client_secret' => 'secret-123', + 'is_current' => true, + ]); + $tenant->makeCurrent(); + + $policy = Policy::factory()->create([ + 'tenant_id' => $tenant->id, + 'external_id' => 'compliance-123', + 'policy_type' => 'deviceCompliancePolicy', + 'display_name' => 'Compliance Alpha', + 'platform' => 'windows', + ]); + + $service = app(PolicySnapshotService::class); + $result = $service->fetch($tenant, $policy); + + expect($result)->toHaveKey('payload'); + expect($result['payload'])->toHaveKey('scheduledActionsForRule'); + expect($result['payload']['scheduledActionsForRule'])->toHaveCount(1); + expect($result['payload']['scheduledActionsForRule'][0]['scheduledActionConfigurations'][0]['notificationTemplateId']) + ->toBe('template-123'); + expect($result['metadata']['compliance_actions_hydration'])->toBe('complete'); + expect($client->requests)->toContain(['getPolicy', 'deviceCompliancePolicy', 'compliance-123']); +});