syncDefaults(); $rows = TenantConfigurationResourceType::query() ->active() ->where('canonical_type', 'securityDefaults') ->get(); $type = $rows->sole(); expect($rows)->toHaveCount(1) ->and($type->source_class)->toBe(SourceClass::GraphV1Fallback) ->and($type->support_state)->toBe(SupportState::FallbackSupported) ->and($type->default_coverage_level)->toBe(CoverageLevel::Renderable) ->and($type->default_evidence_state)->toBe(EvidenceState::NotCaptured) ->and($type->default_identity_state)->toBe(IdentityState::Stable) ->and($type->default_claim_state)->toBe(ClaimState::InternalOnly) ->and($type->restore_tier)->toBe(RestoreTier::NotRestorable) ->and((bool) $type->allows_beta_claims)->toBeFalse() ->and((bool) $type->allows_graph_fallback_claims)->toBeTrue() ->and((bool) $type->allows_certified_claims)->toBeFalse() ->and($type->metadata['registry_only'])->toBeFalse() ->and($type->metadata['source_contract_key'])->toBe('securityDefaults') ->and($type->metadata['source_version'])->toBe('v1.0') ->and($type->metadata['customer_claims_allowed'])->toBeFalse() ->and($type->metadata['certification_allowed'])->toBeFalse() ->and($type->metadata['restore_allowed'])->toBeFalse(); expect(TenantConfigurationResourceType::query() ->active() ->where('canonical_type', 'securityDefaults') ->where('source_class', SourceClass::Tcm->value) ->count())->toBe(0); }); it('Spec424 default sync command deactivates stale Security Defaults TCM planning rows', function (): void { TenantConfigurationResourceType::query()->updateOrCreate( [ 'canonical_type' => 'securityDefaults', 'source_class' => SourceClass::Tcm->value, ], [ 'display_name' => 'Security defaults legacy planning row', 'description' => 'Legacy Security Defaults planning row.', 'workload' => 'entra', 'resource_class' => 'configuration', 'support_state' => SupportState::OutOfScope->value, 'default_coverage_level' => CoverageLevel::Detected->value, 'default_evidence_state' => EvidenceState::NotCaptured->value, 'default_identity_state' => IdentityState::Derived->value, 'default_claim_state' => ClaimState::InternalOnly->value, 'restore_tier' => RestoreTier::NotRestorable->value, 'allows_beta_claims' => false, 'allows_graph_fallback_claims' => false, 'allows_certified_claims' => false, 'is_active' => true, 'metadata' => ['catalog_import_batch' => 'spec_419_seeded_representative_manifest'], ], ); $this->artisan('tenant-configuration:sync-defaults') ->assertSuccessful(); $activeRows = TenantConfigurationResourceType::query() ->active() ->where('canonical_type', 'securityDefaults') ->get(); $activeType = $activeRows->sole(); expect($activeRows)->toHaveCount(1) ->and($activeType->source_class)->toBe(SourceClass::GraphV1Fallback) ->and(TenantConfigurationResourceType::query() ->where('canonical_type', 'securityDefaults') ->where('source_class', SourceClass::Tcm->value) ->where('is_active', true) ->exists())->toBeFalse(); }); it('Spec424 captures singleton Security Defaults evidence with internal-only renderable posture', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'scopes_granted' => ['Policy.Read.All'], ]); $graph = spec424CaptureGraphClient([ spec424CaptureSecurityDefaultsPayload([ '@odata.context' => 'https://graph.microsoft.com/v1.0/$metadata#policies/identitySecurityDefaultsEnforcementPolicy/$entity', 'clientSecret' => 'spec424-provider-secret', ]), ]); app()->instance(GraphClientInterface::class, $graph); $result = app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: spec424CaptureRun($user, $environment, $connection), canonicalTypes: ['securityDefaults'], ); expect($graph->calls)->toHaveCount(1) ->and($graph->calls[0]['policy_type'])->toBe('securityDefaults') ->and($graph->calls[0]['options'])->toHaveKey('client_request_id') ->and($graph->calls[0]['options'])->not->toHaveKey('top') ->and($result['summary_counts'])->toMatchArray([ 'total' => 1, 'processed' => 1, 'succeeded' => 1, 'skipped' => 0, 'failed' => 0, ]) ->and($result['outcomes'][0]['outcome'])->toBe(CaptureOutcome::Captured->value) ->and($result['outcomes'][0]['item_count'])->toBe(1) ->and($result['outcomes'][0]['source_contract_key'])->toBe('securityDefaults'); $resource = TenantConfigurationResource::query()->sole(); $evidence = TenantConfigurationResourceEvidence::query()->sole(); expect($resource->canonical_type)->toBe('securityDefaults') ->and($resource->source_class)->toBe(SourceClass::GraphV1Fallback) ->and($resource->canonical_key_kind)->toBe(CanonicalKeyKind::GraphObjectId) ->and($resource->source_resource_id)->toBe('securityDefaults') ->and($resource->source_display_name)->toBe('Security Defaults') ->and($resource->latest_identity_state)->toBe(IdentityState::Stable) ->and($resource->latest_claim_state)->toBe(ClaimState::InternalOnly) ->and($resource->source_identity['strategy_identifier'])->toBe('graph.security_defaults.v1') ->and($resource->source_metadata['source_contract_key'])->toBe('securityDefaults') ->and($resource->source_metadata['registry_source_class'])->toBe('graph_v1_fallback') ->and($resource->source_metadata['registry_support_state'])->toBe('fallback_supported'); expect($evidence->source_contract_key)->toBe('securityDefaults') ->and($evidence->source_endpoint)->toBe('/policies/identitySecurityDefaultsEnforcementPolicy') ->and($evidence->source_version)->toBe('v1.0') ->and($evidence->coverage_level)->toBe(CoverageLevel::Renderable) ->and($evidence->evidence_state)->toBe(EvidenceState::ContentBacked) ->and($evidence->capture_outcome)->toBe(CaptureOutcome::Captured) ->and($evidence->raw_payload['clientSecret'])->toBe('spec424-provider-secret') ->and($evidence->normalized_payload)->not->toHaveKey('@odata.context') ->and($evidence->normalized_payload['clientSecret'])->toBe('[redacted]') ->and($evidence->permission_context['scopes_granted'])->toBe(['Policy.Read.All']) ->and($evidence->payload_hash)->toBeString()->toHaveLength(64); }); it('Spec424 creates no fake evidence when Security Defaults capture is permission blocked', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'scopes_granted' => [], ]); $graph = spec424CaptureGraphClient([ new GraphResponse(false, [], 403, [['message' => 'Forbidden']]), ]); app()->instance(GraphClientInterface::class, $graph); $result = app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: spec424CaptureRun($user, $environment, $connection), canonicalTypes: ['securityDefaults'], ); expect($graph->calls)->toHaveCount(1) ->and($result['outcomes'][0]['outcome'])->toBe(CaptureOutcome::BlockedPermission->value) ->and($result['outcomes'][0]['reason_code'])->toBe('graph_permission_blocked') ->and(TenantConfigurationResource::query()->count())->toBe(0) ->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0); }); it('Spec424 blocks mismatched operation run scope before provider work', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); $graph = spec424CaptureGraphClient([ spec424CaptureSecurityDefaultsPayload(), ]); app()->instance(GraphClientInterface::class, $graph); $run = spec424CaptureRun($user, $environment, $connection, [ 'securityDefaults', ], [ 'provider_connection_id' => (int) $connection->getKey() + 1000, ]); expect(fn () => app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: $run, canonicalTypes: ['securityDefaults'], ))->toThrow(InvalidArgumentException::class, 'target scope'); expect($graph->calls)->toBe([]) ->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0); }); it('Spec424 renders Security Defaults inspect summaries and compare details from DB only', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'scopes_granted' => ['Policy.Read.All'], ]); app()->instance(GraphClientInterface::class, spec424CaptureGraphClient([ spec424CaptureSecurityDefaultsPayload(['isEnabled' => false]), spec424CaptureSecurityDefaultsPayload(['isEnabled' => true, 'clientSecret' => 'spec424-render-secret']), ])); app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: spec424CaptureRun($user, $environment, $connection), canonicalTypes: ['securityDefaults'], ); app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: spec424CaptureRun($user, $environment, $connection), canonicalTypes: ['securityDefaults'], ); $resource = TenantConfigurationResource::query()->sole(); app()->instance(GraphClientInterface::class, spec424FailingGraphClient()); $details = assertNoOutboundHttp(fn (): array => app(CoverageV2ReadinessReadModel::class) ->inspectDetails($resource, $environment, $user)); $summary = $details['typed_render_summary'] ?? null; $encodedSummary = json_encode($summary, JSON_THROW_ON_ERROR); expect($summary)->toBeArray() ->and($summary['resource_type'])->toBe('Security Defaults') ->and($summary['state'])->toBe('Enabled') ->and($summary['compare_summary']['status'])->toBe('Material changes detected') ->and($summary['compare_summary']['changed'])->toBeTrue() ->and(collect($summary['compare_summary']['changes'])->pluck('label')->all())->toContain('Enabled State') ->and($encodedSummary)->not->toContain('spec424-render-secret') ->and($encodedSummary)->not->toContain('source_endpoint') ->and($encodedSummary)->not->toContain('identitySecurityDefaultsEnforcementPolicy'); }); it('Spec424 denies Security Defaults inspect and page access outside the actor scope', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); [, $foreignEnvironment] = createMinimalUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->withCredential()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); app()->instance(GraphClientInterface::class, spec424CaptureGraphClient([ spec424CaptureSecurityDefaultsPayload(), ])); app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: spec424CaptureRun($user, $environment, $connection), canonicalTypes: ['securityDefaults'], ); $resource = TenantConfigurationResource::query()->sole(); $outsider = User::factory()->create(); $outsiderDetails = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource, $environment, $outsider); expect(app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource, $foreignEnvironment, $user))->toBe([]) ->and($outsiderDetails['operation_run_url'] ?? null)->toBeNull(); $this->actingAs($outsider) ->get(CoverageV2Readiness::getUrl(tenant: $environment)) ->assertNotFound(); }); it('Spec424 returns forbidden for Security Defaults readiness when view capability is denied', function (): void { app(ResourceTypeRegistry::class)->syncDefaults(); [$user, $environment] = createMinimalUserWithTenant(role: 'owner'); spec424CoverageActingAs($user, $environment); app()->instance(ManagedEnvironmentAccessScopeResolver::class, new class { public function decision(User $user, $environment, ?string $requiredCapability = null): ManagedEnvironmentAccessDecision { return new ManagedEnvironmentAccessDecision( workspaceId: (int) $environment->workspace_id, managedEnvironmentId: (int) $environment->getKey(), userId: (int) $user->getKey(), workspaceMember: true, workspaceRole: 'owner', explicitScopeRowsPresent: false, managedEnvironmentAllowed: true, failedBoundary: 'capability', requiredCapability: $requiredCapability, capabilityAllowed: false, denialHttpStatus: 403, ); } }); try { $this->get(CoverageV2Readiness::getUrl(tenant: $environment)) ->assertForbidden(); } finally { app()->forgetInstance(ManagedEnvironmentAccessScopeResolver::class); } }); it('Spec424 readonly users cannot start Security Defaults capture', function (): void { Queue::fake(); [$user, $environment] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly'); $connection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), ]); expect(fn () => app(StartTenantConfigurationCapture::class)->start($environment, $connection, $user, [ 'securityDefaults', ]))->toThrow(AuthorizationException::class); Queue::assertNothingPushed(); }); it('Spec424 adds no tenant id ownership, mini-platform, route, Filament resource, or customer output file', function (): void { $newPlatformFiles = collect([ 'apps/platform/app/Filament', 'apps/platform/app/Models', 'apps/platform/routes', 'apps/platform/database/migrations', ]) ->flatMap(fn (string $path): array => glob(repo_path($path).'/**/*424*') ?: []) ->map(fn (string $path): string => str_replace(repo_path().DIRECTORY_SEPARATOR, '', $path)) ->values() ->all(); $runtimeFiles = [ app_path('Services/TenantConfiguration/ClaimGuard.php'), app_path('Services/TenantConfiguration/CoverageIdentityStrategyRegistry.php'), app_path('Services/TenantConfiguration/CoverageSourceContractResolver.php'), app_path('Services/TenantConfiguration/EntraComparablePayloadNormalizer.php'), app_path('Services/TenantConfiguration/EntraCoverageComparator.php'), app_path('Services/TenantConfiguration/EntraRenderableSummaryBuilder.php'), app_path('Services/TenantConfiguration/ResourceTypeRegistry.php'), app_path('Services/Graph/MicrosoftGraphClient.php'), config_path('graph_contracts.php'), ]; $joinedRuntime = implode("\n", array_map(static fn (string $path): string => file_get_contents($path) ?: '', $runtimeFiles)); expect($newPlatformFiles)->toBe([]) ->and($joinedRuntime)->not->toContain('tenant_id') ->and($joinedRuntime)->not->toContain('ReviewPack') ->and($joinedRuntime)->not->toContain('customer-ready') ->and($joinedRuntime)->not->toContain('certification-ready') ->and($joinedRuntime)->not->toContain('restore-ready'); }); function spec424CaptureRun( $user, $environment, ProviderConnection $connection, array $resourceTypes = ['securityDefaults'], array $targetScopeOverrides = [], ): OperationRun { return OperationRun::factory()->withUser($user)->forTenant($environment)->create([ 'type' => OperationRunType::TenantConfigurationCapture->value, 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'target_scope' => array_replace([ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), ], $targetScopeOverrides), 'resource_types' => $resourceTypes, 'required_capability' => 'evidence.manage', ], ]); } function spec424CaptureGraphClient(array $responses): GraphClientInterface { return new class($responses) implements GraphClientInterface { public array $calls = []; public function __construct(private array $responses) {} public function listPolicies(string $policyType, array $options = []): GraphResponse { $this->calls[] = ['policy_type' => $policyType, 'options' => $options]; $response = array_shift($this->responses); if ($response instanceof GraphResponse) { return $response; } return new GraphResponse(true, is_array($response) ? $response : []); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getOrganization(array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } public function request(string $method, string $path, array $options = []): GraphResponse { return new GraphResponse(false, [], 501); } }; } function spec424CoverageActingAs(User $user, $environment): void { test()->actingAs($user); $environment->makeCurrent(); Filament::setTenant($environment, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id); } function spec424CaptureSecurityDefaultsPayload(array $overrides = []): array { return array_replace([ 'id' => 'securityDefaults', 'displayName' => 'Security Defaults', 'description' => 'Tenant-wide Security Defaults policy.', 'isEnabled' => true, ], $overrides); } function spec424FailingGraphClient(): GraphClientInterface { return new class implements GraphClientInterface { public function listPolicies(string $policyType, array $options = []): GraphResponse { throw new RuntimeException('Spec424 render path must not call provider clients.'); } public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse { throw new RuntimeException('Spec424 render path must not call provider clients.'); } public function getOrganization(array $options = []): GraphResponse { throw new RuntimeException('Spec424 render path must not call provider clients.'); } public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse { throw new RuntimeException('Spec424 render path must not call provider clients.'); } public function getServicePrincipalPermissions(array $options = []): GraphResponse { throw new RuntimeException('Spec424 render path must not call provider clients.'); } public function request(string $method, string $path, array $options = []): GraphResponse { throw new RuntimeException('Spec424 render path must not call provider clients.'); } }; }