feat: scripts policy normalization
This commit is contained in:
parent
2765b50eef
commit
52d6d016cd
@ -9,6 +9,7 @@
|
|||||||
use App\Services\Intune\CompliancePolicyNormalizer;
|
use App\Services\Intune\CompliancePolicyNormalizer;
|
||||||
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
|
use App\Services\Intune\DeviceConfigurationPolicyNormalizer;
|
||||||
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
|
use App\Services\Intune\GroupPolicyConfigurationNormalizer;
|
||||||
|
use App\Services\Intune\ScriptsPolicyNormalizer;
|
||||||
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
||||||
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||||
@ -42,6 +43,7 @@ public function register(): void
|
|||||||
CompliancePolicyNormalizer::class,
|
CompliancePolicyNormalizer::class,
|
||||||
DeviceConfigurationPolicyNormalizer::class,
|
DeviceConfigurationPolicyNormalizer::class,
|
||||||
GroupPolicyConfigurationNormalizer::class,
|
GroupPolicyConfigurationNormalizer::class,
|
||||||
|
ScriptsPolicyNormalizer::class,
|
||||||
SettingsCatalogPolicyNormalizer::class,
|
SettingsCatalogPolicyNormalizer::class,
|
||||||
WindowsFeatureUpdateProfileNormalizer::class,
|
WindowsFeatureUpdateProfileNormalizer::class,
|
||||||
WindowsQualityUpdateProfileNormalizer::class,
|
WindowsQualityUpdateProfileNormalizer::class,
|
||||||
|
|||||||
90
app/Services/Intune/ScriptsPolicyNormalizer.php
Normal file
90
app/Services/Intune/ScriptsPolicyNormalizer.php
Normal 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'] ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
specs/013-scripts-management/plan.md
Normal file
42
specs/013-scripts-management/plan.md
Normal 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.
|
||||||
20
specs/013-scripts-management/tasks.md
Normal file
20
specs/013-scripts-management/tasks.md
Normal 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.
|
||||||
@ -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'],
|
||||||
|
]);
|
||||||
55
tests/Unit/ScriptsPolicyNormalizerTest.php
Normal file
55
tests/Unit/ScriptsPolicyNormalizerTest.php
Normal 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();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user