syncDefaults(); config()->set('graph_contracts.types.conditionalAccessPolicy.volatile_fields', ['modifiedDateTime']); [$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', 'Directory.Read.All'], ]); $graph = spec420CaptureGraphClient([ 'conditionalAccessPolicy' => [ [ 'id' => 'cap-1', 'displayName' => 'Require MFA', 'state' => 'enabled', 'modifiedDateTime' => '2026-06-27T10:00:00Z', 'clientSecret' => 'spec420-provider-client-secret', 'tokenClaims' => [ 'access_token' => 'spec420-provider-access-token', ], 'conditions' => ['users' => ['includeUsers' => ['All']]], ], ], ]); app()->instance(GraphClientInterface::class, $graph); $run = spec420CaptureRun($user, $environment, $connection, [ 'conditionalAccessPolicy', 'acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy', ]); $result = app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: $run, canonicalTypes: ['conditionalAccessPolicy', 'acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy'], ); expect($graph->calls)->toHaveCount(1) ->and($graph->calls[0]['policy_type'])->toBe('conditionalAccessPolicy') ->and($result['summary_counts'])->toMatchArray([ 'total' => 4, 'processed' => 4, 'succeeded' => 1, 'skipped' => 3, 'failed' => 0, 'errors_recorded' => 0, ]) ->and($result['run_outcome'])->toBe(OperationRunOutcome::Succeeded->value); $outcomes = collect($result['outcomes'])->keyBy('canonical_type'); expect($outcomes['conditionalAccessPolicy']['outcome'])->toBe(CaptureOutcome::Captured->value) ->and($outcomes['conditionalAccessPolicy']['source_contract_key'])->toBe('conditionalAccessPolicy') ->and($outcomes['acceptedDomain']['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value) ->and($outcomes['appPermissionPolicy']['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value) ->and($outcomes['dlpCompliancePolicy']['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value); $resource = TenantConfigurationResource::query()->sole(); $evidence = TenantConfigurationResourceEvidence::query()->sole(); expect($resource->canonical_type)->toBe('conditionalAccessPolicy') ->and($resource->workspace_id)->toBe((int) $environment->workspace_id) ->and($resource->managed_environment_id)->toBe((int) $environment->getKey()) ->and($resource->provider_connection_id)->toBe((int) $connection->getKey()) ->and($resource->canonical_key_kind)->toBe(CanonicalKeyKind::GraphObjectId) ->and($resource->latest_identity_state)->toBe(IdentityState::Stable) ->and($resource->latest_claim_state)->toBe(ClaimState::InternalOnly) ->and($resource->source_identity['strategy_identifier'])->toBe('graph.conditional_access_policy.v1') ->and($resource->source_metadata['source_contract_key'])->toBe('conditionalAccessPolicy') ->and($resource->source_metadata['registry_source_class'])->toBe('tcm') ->and($resource->source_metadata['registry_support_state'])->toBe('out_of_scope'); expect($evidence->resource_id)->toBe((int) $resource->getKey()) ->and($evidence->operation_run_id)->toBe((int) $run->getKey()) ->and($evidence->source_contract_key)->toBe('conditionalAccessPolicy') ->and($evidence->source_endpoint)->toBe('/identity/conditionalAccess/policies') ->and($evidence->coverage_level)->toBe(CoverageLevel::ContentBacked) ->and($evidence->evidence_state)->toBe(EvidenceState::ContentBacked) ->and($evidence->capture_outcome)->toBe(CaptureOutcome::Captured) ->and($evidence->raw_payload['id'])->toBe('cap-1') ->and($evidence->raw_payload['clientSecret'])->toBe('spec420-provider-client-secret') ->and($evidence->normalized_payload)->not->toHaveKey('modifiedDateTime') ->and($evidence->normalized_payload['clientSecret'])->toBe('[redacted]') ->and($evidence->normalized_payload['tokenClaims'])->toBe('[redacted]') ->and(json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR))->not->toContain('spec420-provider-client-secret') ->and(json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR))->not->toContain('spec420-provider-access-token') ->and($evidence->payload_hash)->toBeString()->toHaveLength(64) ->and($evidence->permission_context['scopes_granted'])->toBe(['Policy.Read.All', 'Directory.Read.All']); }); it('Spec420 creates no fake evidence rows for selected missing-contract types', 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 = spec420CaptureGraphClient([]); app()->instance(GraphClientInterface::class, $graph); $run = spec420CaptureRun($user, $environment, $connection, [ 'acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy', ]); $result = app(GenericContentEvidenceCaptureService::class)->capture( tenant: $environment, providerConnection: $connection, operationRun: $run, canonicalTypes: ['acceptedDomain', 'appPermissionPolicy', 'dlpCompliancePolicy'], ); expect($graph->calls)->toBe([]) ->and(TenantConfigurationResource::query()->count())->toBe(0) ->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0) ->and($result['summary_counts'])->toMatchArray([ 'total' => 3, 'processed' => 3, 'succeeded' => 0, 'skipped' => 3, 'failed' => 0, ]) ->and($result['run_outcome'])->toBe(OperationRunOutcome::Blocked->value); }); function spec420CaptureRun($user, $environment, ProviderConnection $connection, array $resourceTypes): OperationRun { return OperationRun::factory()->withUser($user)->forTenant($environment)->create([ 'type' => OperationRunType::TenantConfigurationCapture->value, 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, 'context' => [ 'target_scope' => [ 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'provider_connection_id' => (int) $connection->getKey(), ], 'resource_types' => $resourceTypes, 'required_capability' => 'evidence.manage', ], ]); } function spec420CaptureGraphClient(array $responses): GraphClientInterface { return new class($responses) implements GraphClientInterface { public array $calls = []; public function __construct(private readonly array $responses) {} public function listPolicies(string $policyType, array $options = []): GraphResponse { $this->calls[] = ['policy_type' => $policyType, 'options' => $options]; return new GraphResponse(true, $this->responses[$policyType] ?? []); } 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); } }; }