Implements Spec 119 (Drift Golden Master Cutover): - Baseline Compare is the only drift writer (`source = baseline.compare`). - Drift findings now store diff-compatible `evidence_jsonb` (summary.kind, baseline/current policy_version_id refs, fidelity + provenance). - Findings UI renders one-sided diffs for `missing_policy`/`unexpected_policy` when a single ref exists; otherwise shows explicit “diff unavailable”. - Removes legacy drift generator runtime (jobs/services/UI) and related tests. - Adds one-time migration to delete legacy drift findings (`finding_type=drift` where source is null or != baseline.compare). - Scopes baseline capture & landing duplicate warnings to latest completed inventory sync. - Canonicalizes compliance `scheduledActionsForRule` drift signal and keeps legacy snapshots comparable. Tests: - `vendor/bin/sail artisan test --compact` (full suite per tasks) - Focused pack: BaselinePolicyVersionResolverTest, BaselineCompareDriftEvidenceContractTest, DriftFindingDiffUnavailableTest, LegacyDriftFindingsCleanupMigrationTest, ComplianceNoncomplianceActionsDriftTest Notes: - Livewire v4+ / Filament v5 compatible (no legacy APIs). - No new external dependencies. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #144
158 lines
6.8 KiB
PHP
158 lines
6.8 KiB
PHP
<?php
|
|
|
|
use App\Services\Intune\CompliancePolicyNormalizer;
|
|
|
|
it('groups compliance policy fields into structured blocks', function () {
|
|
$normalizer = app(CompliancePolicyNormalizer::class);
|
|
|
|
$snapshot = [
|
|
'@odata.type' => '#microsoft.graph.windows10CompliancePolicy',
|
|
'passwordRequired' => true,
|
|
'passwordMinimumLength' => 8,
|
|
'defenderEnabled' => true,
|
|
'bitLockerEnabled' => false,
|
|
'osMinimumVersion' => '10.0.19045',
|
|
'activeFirewallRequired' => true,
|
|
'scheduledActionsForRule' => [
|
|
[
|
|
'ruleName' => 'Default rule',
|
|
'scheduledActionConfigurations' => [
|
|
['actionType' => 'notification'],
|
|
],
|
|
],
|
|
],
|
|
'scheduledActionsForRule@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/deviceCompliancePolicies',
|
|
'customSetting' => 'Custom value',
|
|
];
|
|
|
|
$normalized = $normalizer->normalize($snapshot, 'deviceCompliancePolicy', 'windows');
|
|
|
|
$settings = collect($normalized['settings']);
|
|
|
|
$passwordBlock = $settings->firstWhere('title', 'Password & Access');
|
|
expect($passwordBlock)->not->toBeNull();
|
|
expect(collect($passwordBlock['rows'])->pluck('label')->all())
|
|
->toContain('Password required', 'Password minimum length');
|
|
|
|
$additionalBlock = $settings->firstWhere('title', 'Additional Settings');
|
|
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(collect($additionalBlock['rows'])->pluck('label')->all())
|
|
->not->toContain('Scheduled Actions For Rule@Odata.context');
|
|
|
|
expect($settings->pluck('title')->all())->not->toContain('General');
|
|
});
|
|
|
|
it('flattens compliance noncompliance actions into a canonical diff signal', function () {
|
|
$normalizer = app(CompliancePolicyNormalizer::class);
|
|
|
|
$snapshot = [
|
|
'@odata.type' => '#microsoft.graph.windows10CompliancePolicy',
|
|
'passwordRequired' => true,
|
|
'scheduledActionsForRule' => [
|
|
[
|
|
'ruleName' => null,
|
|
'scheduledActionConfigurations' => [
|
|
[
|
|
'actionType' => 'block',
|
|
'gracePeriodHours' => 240,
|
|
'notificationTemplateId' => '00000000-0000-0000-0000-000000000000',
|
|
],
|
|
[
|
|
'actionType' => 'notification',
|
|
'gracePeriodHours' => 48,
|
|
'notificationTemplateId' => 'template-123',
|
|
],
|
|
[
|
|
'actionType' => 'retire',
|
|
'gracePeriodHours' => 2664,
|
|
'notificationTemplateId' => 'template-retire',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
'scheduledActionsForRule@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/deviceCompliancePolicies',
|
|
];
|
|
|
|
$flat = $normalizer->flattenForDiff($snapshot, 'deviceCompliancePolicy', 'windows');
|
|
|
|
expect($flat)->toHaveKey('Password & Access > Password required');
|
|
expect($flat['Password & Access > Password required'])->toBeTrue();
|
|
|
|
expect($flat)->toHaveKey('Actions for noncompliance > Mark device noncompliant > Grace period');
|
|
expect($flat['Actions for noncompliance > Mark device noncompliant > Grace period'])->toBe('10 days (240 hours)');
|
|
expect($flat)->toHaveKey('Actions for noncompliance > Mark device noncompliant > Notification template ID');
|
|
expect($flat['Actions for noncompliance > Mark device noncompliant > Notification template ID'])->toBeNull();
|
|
|
|
expect($flat)->toHaveKey('Actions for noncompliance > Send notification > Grace period');
|
|
expect($flat['Actions for noncompliance > Send notification > Grace period'])->toBe('2 days (48 hours)');
|
|
expect($flat['Actions for noncompliance > Send notification > Notification template ID'])->toBe('template-123');
|
|
|
|
expect($flat)->toHaveKey('Actions for noncompliance > Add device to retire list > Grace period');
|
|
expect($flat['Actions for noncompliance > Add device to retire list > Grace period'])->toBe('111 days (2664 hours)');
|
|
expect($flat['Actions for noncompliance > Add device to retire list > Notification template ID'])->toBe('template-retire');
|
|
|
|
expect(array_keys($flat))->not->toContain('scheduledActionsForRule');
|
|
expect(array_keys($flat))->not->toContain('scheduledActionsForRule@odata.context');
|
|
});
|
|
|
|
it('ignores noncompliance action ids and ordering when flattening for diff', function () {
|
|
$normalizer = app(CompliancePolicyNormalizer::class);
|
|
|
|
$firstSnapshot = [
|
|
'@odata.type' => '#microsoft.graph.windows10CompliancePolicy',
|
|
'scheduledActionsForRule' => [
|
|
[
|
|
'ruleName' => null,
|
|
'scheduledActionConfigurations' => [
|
|
[
|
|
'id' => 'config-a',
|
|
'actionType' => 'notification',
|
|
'gracePeriodHours' => 48,
|
|
'notificationTemplateId' => 'template-b',
|
|
],
|
|
[
|
|
'id' => 'config-b',
|
|
'actionType' => 'notification',
|
|
'gracePeriodHours' => 24,
|
|
'notificationTemplateId' => 'template-a',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$secondSnapshot = [
|
|
'@odata.type' => '#microsoft.graph.windows10CompliancePolicy',
|
|
'scheduledActionsForRule' => [
|
|
[
|
|
'ruleName' => null,
|
|
'scheduledActionConfigurations' => [
|
|
[
|
|
'id' => 'config-z',
|
|
'actionType' => 'notification',
|
|
'gracePeriodHours' => 24,
|
|
'notificationTemplateId' => 'template-a',
|
|
],
|
|
[
|
|
'id' => 'config-y',
|
|
'actionType' => 'notification',
|
|
'gracePeriodHours' => 48,
|
|
'notificationTemplateId' => 'template-b',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
$firstFlat = $normalizer->flattenForDiff($firstSnapshot, 'deviceCompliancePolicy', 'windows');
|
|
$secondFlat = $normalizer->flattenForDiff($secondSnapshot, 'deviceCompliancePolicy', 'windows');
|
|
|
|
expect($firstFlat)->toBe($secondFlat);
|
|
expect($firstFlat)->toHaveKey('Actions for noncompliance > Send notification #1 > Grace period');
|
|
expect($firstFlat)->toHaveKey('Actions for noncompliance > Send notification #2 > Grace period');
|
|
});
|