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\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,
|
||||
|
||||
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