'missing', 'permissions' => [ ['key' => 'DeviceManagementConfiguration.ReadWrite.All', 'type' => 'application', 'status' => 'missing', 'features' => ['backup', 'restore']], ['key' => 'DeviceManagementApps.ReadWrite.All', 'type' => 'application', 'status' => 'granted', 'features' => ['app_management']], ['key' => 'DeviceManagementManagedDevices.ReadWrite.All', 'type' => 'application', 'status' => 'missing', 'features' => ['compliance']], ['key' => 'Group.Read.All', 'type' => 'application', 'status' => 'granted', 'features' => ['assignments']], ['key' => 'User.Read', 'type' => 'delegated', 'status' => 'granted', 'features' => []], ], 'last_refreshed_at' => now()->toIso8601String(), ]; $start = microtime(true); $job = new GeneratePermissionPostureFindingsJob($tenant->getKey(), $comparison); $job->handle( app(FindingGeneratorContract::class), app(\App\Services\OperationRunService::class), ); $elapsed = (microtime(true) - $start) * 1000; // --- Timing assertion --- expect($elapsed)->toBeLessThan(5000); // --- Findings --- $findings = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE) ->get(); $missingFindings = $findings->where('status', Finding::STATUS_NEW); expect($missingFindings)->toHaveCount(2); // 2 missing permissions // --- StoredReport --- $report = StoredReport::query() ->where('tenant_id', $tenant->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_PERMISSION_POSTURE) ->first(); expect($report)->not->toBeNull() ->and($report->payload['posture_score'])->toBe(60) // 3/5 = 60% ->and($report->payload['required_count'])->toBe(5) ->and($report->payload['granted_count'])->toBe(3) ->and($report->payload['permissions'])->toHaveCount(5); // --- OperationRun --- $run = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', OperationCatalog::TYPE_PERMISSION_POSTURE_CHECK) ->first(); expect($run)->not->toBeNull() ->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->summary_counts['findings_created'])->toBe(2) ->and($run->summary_counts['posture_score'])->toBe(60); }); it('second run auto-resolves findings for newly granted permissions', function (): void { [$user, $tenant] = createUserWithTenant(); $generator = app(FindingGeneratorContract::class); // First run: 2 missing permissions $comparison1 = [ 'overall_status' => 'missing', 'permissions' => [ ['key' => 'Perm.A', 'type' => 'application', 'status' => 'missing', 'features' => ['a']], ['key' => 'Perm.B', 'type' => 'application', 'status' => 'missing', 'features' => ['b']], ['key' => 'Perm.C', 'type' => 'application', 'status' => 'granted', 'features' => ['c']], ], 'last_refreshed_at' => now()->toIso8601String(), ]; $result1 = $generator->generate($tenant, $comparison1); expect($result1->findingsCreated)->toBe(2) ->and($result1->postureScore)->toBe(33); // Second run: Perm.A now granted $comparison2 = [ 'overall_status' => 'missing', 'permissions' => [ ['key' => 'Perm.A', 'type' => 'application', 'status' => 'granted', 'features' => ['a']], ['key' => 'Perm.B', 'type' => 'application', 'status' => 'missing', 'features' => ['b']], ['key' => 'Perm.C', 'type' => 'application', 'status' => 'granted', 'features' => ['c']], ], 'last_refreshed_at' => now()->toIso8601String(), ]; $result2 = $generator->generate($tenant, $comparison2); expect($result2->findingsResolved)->toBe(1) ->and($result2->findingsUnchanged)->toBe(1) ->and($result2->postureScore)->toBe(67); // Verify the resolved finding $resolvedFinding = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE) ->where('status', Finding::STATUS_RESOLVED) ->first(); expect($resolvedFinding)->not->toBeNull() ->and($resolvedFinding->resolved_reason)->toBe('permission_granted'); }); it('alert events are produced for new missing permission findings', function (): void { [$user, $tenant] = createUserWithTenant(); $generator = app(FindingGeneratorContract::class); $comparison = [ 'overall_status' => 'missing', 'permissions' => [ ['key' => 'Perm.Dangerous', 'type' => 'application', 'status' => 'missing', 'features' => ['backup', 'restore', 'sync']], ], 'last_refreshed_at' => now()->toIso8601String(), ]; $result = $generator->generate($tenant, $comparison); expect($result->findingsCreated)->toBe(1); // Verify the finding was created and has proper data for alert pipeline $finding = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE) ->where('status', Finding::STATUS_NEW) ->first(); expect($finding)->not->toBeNull() ->and($finding->evidence_jsonb)->toHaveKey('permission_key') ->and($finding->evidence_jsonb['permission_key'])->toBe('Perm.Dangerous'); });