*/ function automationBaselineCompareDriftItem(int $baselineProfileId, int $compareOperationRunId, string $subjectKey): array { return [ 'change_type' => 'different_version', 'severity' => Finding::SEVERITY_MEDIUM, 'subject_type' => 'policy', 'subject_external_id' => $subjectKey, 'subject_key' => $subjectKey, 'policy_type' => 'deviceConfiguration', 'baseline_hash' => 'baseline', 'current_hash' => 'current', 'evidence_fidelity' => 'meta', 'evidence' => [ 'change_type' => 'different_version', 'policy_type' => 'deviceConfiguration', 'subject_key' => $subjectKey, 'summary' => ['kind' => 'policy_snapshot'], 'baseline' => ['policy_version_id' => null, 'hash' => 'baseline'], 'current' => ['policy_version_id' => null, 'hash' => 'current'], 'fidelity' => 'meta', 'provenance' => [ 'baseline_profile_id' => $baselineProfileId, 'baseline_snapshot_id' => 1, 'compare_operation_run_id' => $compareOperationRunId, 'inventory_sync_run_id' => null, ], ], ]; } } if (! function_exists('invokeAutomationBaselineCompareUpsertFindings')) { /** * @param array> $driftResults * @return array{processed_count:int,created_count:int,reopened_count:int,unchanged_count:int,seen_fingerprints:array} */ function invokeAutomationBaselineCompareUpsertFindings( CompareBaselineToTenantJob $job, \App\Models\Tenant $tenant, BaselineProfile $profile, string $scopeKey, array $driftResults, ): array { $reflection = new ReflectionMethod($job, 'upsertFindings'); /** @var array{processed_count:int,created_count:int,reopened_count:int,unchanged_count:int,seen_fingerprints:array} $result */ $result = $reflection->invoke($job, $tenant, $profile, $scopeKey, $driftResults); return $result; } } it('writes a system-origin audit row when stale drift findings auto-resolve', function (): void { [, $tenant] = createUserWithTenant(role: 'manager'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $scopeKey = 'baseline_profile:'.$profile->getKey(); $run = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_compare', ]); $observedAt = CarbonImmutable::parse('2026-03-18T09:00:00Z'); CarbonImmutable::setTestNow($observedAt); $finding = Finding::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'finding_type' => Finding::FINDING_TYPE_DRIFT, 'source' => 'baseline.compare', 'scope_key' => $scopeKey, 'fingerprint' => 'drift-stale-audit', 'recurrence_key' => 'drift-stale-audit', 'status' => Finding::STATUS_NEW, ]); expect(app(BaselineAutoCloseService::class)->resolveStaleFindings( tenant: $tenant, baselineProfileId: (int) $profile->getKey(), seenFingerprints: [], currentOperationRunId: (int) $run->getKey(), ))->toBe(1); $audit = AuditLog::query() ->where('resource_type', 'finding') ->where('resource_id', (string) $finding->getKey()) ->where('action', AuditActionId::FindingResolved->value) ->latest('id') ->first(); expect($audit)->not->toBeNull() ->and($audit->actorSnapshot()->type)->toBe(AuditActorType::System) ->and(data_get($audit->metadata, 'system_origin'))->toBeTrue() ->and(data_get($audit->metadata, 'resolved_reason'))->toBe('no_longer_drifting'); }); it('writes system-origin audit rows for permission posture auto-resolve and recurrence reopen', function (): void { [, $tenant] = createUserWithTenant(); CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-03-18T10:00:00Z')); $generator = app(PermissionPostureFindingGenerator::class); $generator->generate($tenant, [ 'overall_status' => 'missing', 'permissions' => [[ 'key' => 'Perm.Automation', 'type' => 'application', 'status' => 'missing', 'features' => ['policy-sync'], ]], 'last_refreshed_at' => now()->toIso8601String(), ]); CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-03-18T11:00:00Z')); $generator->generate($tenant, [ 'overall_status' => 'granted', 'permissions' => [[ 'key' => 'Perm.Automation', 'type' => 'application', 'status' => 'granted', 'features' => ['policy-sync'], ]], 'last_refreshed_at' => now()->toIso8601String(), ]); CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-03-18T12:00:00Z')); $generator->generate($tenant, [ 'overall_status' => 'missing', 'permissions' => [[ 'key' => 'Perm.Automation', 'type' => 'application', 'status' => 'missing', 'features' => ['policy-sync'], ]], 'last_refreshed_at' => now()->toIso8601String(), ]); $finding = Finding::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE) ->firstOrFail(); $resolvedAudit = AuditLog::query() ->where('resource_type', 'finding') ->where('resource_id', (string) $finding->getKey()) ->where('action', AuditActionId::FindingResolved->value) ->latest('id') ->first(); $reopenedAudit = AuditLog::query() ->where('resource_type', 'finding') ->where('resource_id', (string) $finding->getKey()) ->where('action', AuditActionId::FindingReopened->value) ->latest('id') ->first(); expect($resolvedAudit)->not->toBeNull() ->and($resolvedAudit->actorSnapshot()->type)->toBe(AuditActorType::System) ->and(data_get($resolvedAudit->metadata, 'resolved_reason'))->toBe('permission_granted') ->and($reopenedAudit)->not->toBeNull() ->and($reopenedAudit->actorSnapshot()->type)->toBe(AuditActorType::System) ->and(data_get($reopenedAudit->metadata, 'after_status'))->toBe(Finding::STATUS_REOPENED); }); it('writes system-origin audit rows for entra admin role auto-resolve and recurrence reopen', function (): void { [, $tenant] = createUserWithTenant(); $generator = new EntraAdminRolesFindingGenerator(new HighPrivilegeRoleCatalog); $generator->generate($tenant, [ 'measured_at' => '2026-03-18T10:00:00Z', 'role_definitions' => [[ 'id' => 'def-ga', 'displayName' => 'Global Administrator', 'templateId' => '62e90394-69f5-4237-9190-012177145e10', 'isBuiltIn' => true, ]], 'role_assignments' => [[ 'id' => 'a1', 'roleDefinitionId' => 'def-ga', 'principalId' => 'user-1', 'directoryScopeId' => '/', 'principal' => [ '@odata.type' => '#microsoft.graph.user', 'displayName' => 'Alice', ], ]], ]); $generator->generate($tenant, [ 'measured_at' => '2026-03-18T11:00:00Z', 'role_definitions' => [[ 'id' => 'def-ga', 'displayName' => 'Global Administrator', 'templateId' => '62e90394-69f5-4237-9190-012177145e10', 'isBuiltIn' => true, ]], 'role_assignments' => [], ]); $generator->generate($tenant, [ 'measured_at' => '2026-03-18T12:00:00Z', 'role_definitions' => [[ 'id' => 'def-ga', 'displayName' => 'Global Administrator', 'templateId' => '62e90394-69f5-4237-9190-012177145e10', 'isBuiltIn' => true, ]], 'role_assignments' => [[ 'id' => 'a2', 'roleDefinitionId' => 'def-ga', 'principalId' => 'user-1', 'directoryScopeId' => '/', 'principal' => [ '@odata.type' => '#microsoft.graph.user', 'displayName' => 'Alice Reactivated', ], ]], ]); $finding = Finding::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES) ->where('subject_external_id', 'user-1:def-ga') ->firstOrFail(); expect(AuditLog::query() ->where('resource_type', 'finding') ->where('resource_id', (string) $finding->getKey()) ->where('action', AuditActionId::FindingResolved->value) ->latest('id') ->first()?->actorSnapshot()->type)->toBe(AuditActorType::System) ->and(AuditLog::query() ->where('resource_type', 'finding') ->where('resource_id', (string) $finding->getKey()) ->where('action', AuditActionId::FindingReopened->value) ->latest('id') ->first()?->actorSnapshot()->type)->toBe(AuditActorType::System); }); it('writes a system-origin reopen audit row for baseline recurrence handling', function (): void { [, $tenant] = createUserWithTenant(role: 'manager'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, ]); $scopeKey = 'baseline_profile:'.$profile->getKey(); $run1 = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_compare', ]); CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-03-18T08:00:00Z')); invokeAutomationBaselineCompareUpsertFindings( new CompareBaselineToTenantJob($run1), $tenant, $profile, $scopeKey, [automationBaselineCompareDriftItem((int) $profile->getKey(), (int) $run1->getKey(), 'policy-audit-recur')], ); $finding = Finding::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('source', 'baseline.compare') ->where('subject_external_id', 'policy-audit-recur') ->firstOrFail(); $finding->forceFill([ 'status' => Finding::STATUS_RESOLVED, 'resolved_at' => CarbonImmutable::parse('2026-03-18T09:00:00Z'), 'resolved_reason' => 'fixed', ])->save(); $run2 = OperationRun::factory()->create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'baseline_compare', ]); CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-03-18T10:00:00Z')); invokeAutomationBaselineCompareUpsertFindings( new CompareBaselineToTenantJob($run2), $tenant, $profile, $scopeKey, [automationBaselineCompareDriftItem((int) $profile->getKey(), (int) $run2->getKey(), 'policy-audit-recur')], ); $audit = AuditLog::query() ->where('resource_type', 'finding') ->where('resource_id', (string) $finding->getKey()) ->where('action', AuditActionId::FindingReopened->value) ->latest('id') ->first(); expect($audit)->not->toBeNull() ->and($audit->actorSnapshot()->type)->toBe(AuditActorType::System) ->and(data_get($audit->metadata, 'system_origin'))->toBeTrue(); });