feat: scripts policy normalization

This commit is contained in:
Ahmed Darrazi 2026-01-01 12:50:37 +01:00
parent 2765b50eef
commit 52d6d016cd
6 changed files with 260 additions and 0 deletions

View File

@ -9,6 +9,7 @@
use App\Services\Intune\CompliancePolicyNormalizer;
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
use App\Services\Intune\ScriptsPolicyNormalizer;
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
@ -42,6 +43,7 @@ public function register(): void
CompliancePolicyNormalizer::class,
DeviceConfigurationPolicyNormalizer::class,
GroupPolicyConfigurationNormalizer::class,
ScriptsPolicyNormalizer::class,
SettingsCatalogPolicyNormalizer::class,
WindowsFeatureUpdateProfileNormalizer::class,
WindowsQualityUpdateProfileNormalizer::class,

View File

@ -0,0 +1,90 @@
<?php
namespace App\Services\Intune;
use Illuminate\Support\Arr;
class ScriptsPolicyNormalizer implements PolicyTypeNormalizer
{
public function __construct(private readonly DefaultPolicyNormalizer $defaultNormalizer) {}
public function supports(string $policyType): bool
{
return in_array($policyType, [
'deviceManagementScript',
'deviceShellScript',
'deviceHealthScript',
], true);
}
/**
* @return array{status: string, settings: array<int, array<string, mixed>>, settings_table?: array<string, mixed>, warnings: array<int, string>}
*/
public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
{
$snapshot = is_array($snapshot) ? $snapshot : [];
$displayName = Arr::get($snapshot, 'displayName') ?? Arr::get($snapshot, 'name');
$description = Arr::get($snapshot, 'description');
$settings = [];
$settings[] = ['key' => 'Type', 'value' => $policyType];
if (is_string($displayName) && $displayName !== '') {
$settings[] = ['key' => 'Display name', 'value' => $displayName];
}
if (is_string($description) && $description !== '') {
$settings[] = ['key' => 'Description', 'value' => $description];
}
// Script content and large blobs should not dominate normalized output.
// Keep only safe summary fields if present.
$contentKeys = [
'scriptContent',
'scriptContentBase64',
'detectionScriptContent',
'remediationScriptContent',
];
foreach ($contentKeys as $key) {
$value = Arr::get($snapshot, $key);
if (is_string($value) && $value !== '') {
$settings[] = ['key' => $key, 'value' => sprintf('[content: %d chars]', strlen($value))];
}
}
$schedule = Arr::get($snapshot, 'runSchedule');
if (is_array($schedule) && $schedule !== []) {
$settings[] = ['key' => 'Run schedule', 'value' => Arr::except($schedule, ['@odata.type'])];
}
$frequency = Arr::get($snapshot, 'runFrequency');
if (is_string($frequency) && $frequency !== '') {
$settings[] = ['key' => 'Run frequency', 'value' => $frequency];
}
$roleScopeTagIds = Arr::get($snapshot, 'roleScopeTagIds');
if (is_array($roleScopeTagIds) && $roleScopeTagIds !== []) {
$settings[] = ['key' => 'Scope tag IDs', 'value' => array_values($roleScopeTagIds)];
}
return [
'status' => 'ok',
'settings' => $settings,
'warnings' => [],
];
}
/**
* @return array<string, mixed>
*/
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
{
$normalized = $this->normalize($snapshot, $policyType, $platform);
return $this->defaultNormalizer->flattenForDiff($normalized['settings'] ?? []);
}
}

View File

@ -0,0 +1,42 @@
# Plan: Scripts Management (013)
**Branch**: `013-scripts-management`
**Date**: 2026-01-01
**Input**: [spec.md](./spec.md)
## Goal
Provide end-to-end support for script policies (PowerShell scripts, macOS shell scripts, and proactive remediations) with readable normalized settings and safe restore behavior including assignments.
## Scope
### In scope
- Script policy types:
- `deviceManagementScript`
- `deviceShellScript`
- `deviceHealthScript`
- Readable “Normalized settings” output for the above types.
- Restore apply safety is preserved (type mismatch fails; preview vs execute follows existing system behavior).
- Assignment restore is supported (using existing assignment restore mechanisms and contract metadata).
### Out of scope
- Adding new UI flows or pages.
- Introducing new external services or background infrastructure.
- Changing how authentication/authorization works.
## Approach
1. Confirm contract entries exist and are correct for the three script policy types (resource, type families, assignment paths/payload keys).
2. Add a policy normalizer that supports the three script policy types and outputs a stable, readable structure.
3. Register the normalizer in the application normalizer tag.
4. Add tests:
- Normalized output shape/stability for each type.
- Filament “Normalized settings” tab renders without errors for a version of each type.
5. Run targeted tests and Pint.
## Risks & Mitigations
- Scripts may contain large content blobs: normalized view must be readable and avoid overwhelming output (truncate or summarize where needed).
- Platform-specific fields vary: normalizer must handle missing keys safely and remain stable.
## Success Criteria
- Normalized settings views are readable and stable for all three script policy types.
- Restore execution remains safe and assignment behavior is unchanged/regression-free.
- Tests cover the new normalizer behavior and basic UI render.

View File

@ -0,0 +1,20 @@
# Tasks: Scripts Management (013)
**Branch**: `013-scripts-management` | **Date**: 2026-01-01
**Input**: [spec.md](./spec.md), [plan.md](./plan.md)
## Phase 1: Contracts Review
- [ ] T001 Verify `config/graph_contracts.php` entries for `deviceManagementScript`, `deviceShellScript`, `deviceHealthScript` (resource, type_family, assignment payload key).
## Phase 2: UI Normalization
- [ ] T002 Add a `ScriptsPolicyNormalizer` (or equivalent) to produce readable normalized settings for the three script policy types.
- [ ] T003 Register the normalizer in `AppServiceProvider`.
## Phase 3: Tests + Verification
- [ ] T004 Add tests for normalized output (shape + stability) for each script policy type.
- [ ] T005 Add Filament render tests for “Normalized settings” tab for each script policy type.
- [ ] T006 Run targeted tests.
- [ ] T007 Run Pint (`./vendor/bin/pint --dirty`).
## Open TODOs (Follow-up)
- None yet.

View File

@ -0,0 +1,51 @@
<?php
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('renders policy version normalized settings for script policies', function (string $policyType, string $odataType) {
$originalEnv = getenv('INTUNE_TENANT_ID');
putenv('INTUNE_TENANT_ID=');
$this->actingAs(User::factory()->create());
$tenant = Tenant::factory()->create();
putenv('INTUNE_TENANT_ID='.$tenant->tenant_id);
$tenant->makeCurrent();
$policy = Policy::factory()->create([
'tenant_id' => $tenant->id,
'policy_type' => $policyType,
'platform' => 'all',
'display_name' => 'Script policy',
'external_id' => 'policy-1',
]);
PolicyVersion::factory()->create([
'policy_id' => $policy->id,
'tenant_id' => $tenant->id,
'policy_type' => $policyType,
'snapshot' => [
'@odata.type' => $odataType,
'displayName' => 'Script policy',
'description' => 'desc',
'scriptContent' => str_repeat('X', 20),
],
]);
$this->get(\App\Filament\Resources\PolicyVersionResource::getUrl('index'))
->assertSuccessful();
$originalEnv !== false
? putenv("INTUNE_TENANT_ID={$originalEnv}")
: putenv('INTUNE_TENANT_ID');
})->with([
['deviceManagementScript', '#microsoft.graph.deviceManagementScript'],
['deviceShellScript', '#microsoft.graph.deviceShellScript'],
['deviceHealthScript', '#microsoft.graph.deviceHealthScript'],
]);

View File

@ -0,0 +1,55 @@
<?php
use App\Services\Intune\PolicyNormalizer;
use Tests\TestCase;
uses(TestCase::class);
it('normalizes deviceManagementScript into readable settings', function () {
$normalizer = app(PolicyNormalizer::class);
$snapshot = [
'@odata.type' => '#microsoft.graph.deviceManagementScript',
'displayName' => 'My PS script',
'description' => 'Does a thing',
'scriptContent' => str_repeat('A', 10),
'runFrequency' => 'weekly',
];
$result = $normalizer->normalize($snapshot, 'deviceManagementScript', 'windows');
expect($result['status'])->toBe('ok');
expect($result['settings'])->toBeArray()->not->toBeEmpty();
expect(collect($result['settings'])->pluck('key')->all())->toContain('Display name');
});
it('normalizes deviceShellScript into readable settings', function () {
$normalizer = app(PolicyNormalizer::class);
$snapshot = [
'@odata.type' => '#microsoft.graph.deviceShellScript',
'displayName' => 'My macOS shell script',
'scriptContent' => str_repeat('B', 5),
];
$result = $normalizer->normalize($snapshot, 'deviceShellScript', 'macOS');
expect($result['status'])->toBe('ok');
expect($result['settings'])->toBeArray()->not->toBeEmpty();
});
it('normalizes deviceHealthScript into readable settings', function () {
$normalizer = app(PolicyNormalizer::class);
$snapshot = [
'@odata.type' => '#microsoft.graph.deviceHealthScript',
'displayName' => 'My remediation',
'detectionScriptContent' => str_repeat('C', 3),
'remediationScriptContent' => str_repeat('D', 4),
];
$result = $normalizer->normalize($snapshot, 'deviceHealthScript', 'windows');
expect($result['status'])->toBe('ok');
expect($result['settings'])->toBeArray()->not->toBeEmpty();
});