*/ public const DENOMINATOR = [ 'conditionalAccessPolicy', 'securityDefaults', ]; /** * @var array */ private const EXPECTED_CONTRACT_KEYS = [ 'conditionalAccessPolicy' => 'conditionalAccessPolicy', 'securityDefaults' => 'securityDefaults', ]; /** * @var list */ private const BLOCKER_PRIORITY = [ EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE, EntraCertifiedComparePackResult::BLOCKED_IDENTITY, EntraCertifiedComparePackResult::BLOCKED_COMPARE, EntraCertifiedComparePackResult::BLOCKED_RENDER, EntraCertifiedComparePackResult::BLOCKED_REDACTION, EntraCertifiedComparePackResult::BLOCKED_CLAIM_GUARD, ]; /** * @var list */ private const SENSITIVE_KEY_PARTS = [ 'access_token', 'authorization', 'bearer', 'certificate', 'client_secret', 'cookie', 'credential', 'id_token', 'password', 'private_key', 'refresh_token', 'secret', 'set-cookie', 'token', ]; public function __construct( private readonly SupportedScopeResolver $supportedScopes, private readonly EntraCoverageComparator $comparator, private readonly EntraRenderableSummaryBuilder $summaryBuilder, private readonly CoveragePayloadRedactor $redactor, private readonly ClaimGuard $claimGuard, ) {} public function evaluate( ManagedEnvironment $environment, ProviderConnection $providerConnection, ): EntraCertifiedComparePackResult { $this->assertSameScope($environment, $providerConnection); $scope = $this->supportedScopes->findActive(self::SCOPE_KEY); if (! $scope instanceof TenantConfigurationSupportedScope) { return new EntraCertifiedComparePackResult( scopeKey: self::SCOPE_KEY, denominator: self::DENOMINATOR, state: EntraCertifiedComparePackResult::NOT_EVALUATED, blockers: ['supported_scope_missing'], ); } $scopeBlockers = $this->scopeBlockers($scope); if ($scopeBlockers !== []) { return new EntraCertifiedComparePackResult( scopeKey: self::SCOPE_KEY, denominator: self::DENOMINATOR, state: EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE, blockers: $scopeBlockers, ); } $resourceResults = []; $blockers = []; foreach (self::DENOMINATOR as $canonicalType) { $typeResult = $this->evaluateCanonicalType($canonicalType, $environment, $providerConnection); $resourceResults[] = $typeResult; $blockers = [...$blockers, ...$typeResult['blockers']]; } $blockers = $this->uniqueStrings($blockers); $claimState = null; if ($blockers === []) { $claimState = $this->claimGuard->evaluateCertifiedComparePackStatement( claim: self::CLAIM_LABEL, packPassed: true, internalOperatorOnly: true, ); if ($claimState !== ClaimState::InternalOnly) { $blockers[] = EntraCertifiedComparePackResult::BLOCKED_CLAIM_GUARD; } } return new EntraCertifiedComparePackResult( scopeKey: self::SCOPE_KEY, denominator: self::DENOMINATOR, state: $this->overallState($blockers), resourceResults: $resourceResults, blockers: $this->uniqueStrings($blockers), claimState: $claimState, ); } /** * @return array */ private function evaluateCanonicalType( string $canonicalType, ManagedEnvironment $environment, ProviderConnection $providerConnection, ): array { $resourceType = TenantConfigurationResourceType::query() ->active() ->where('canonical_type', $canonicalType) ->orderBy('source_class') ->first(); if (! $resourceType instanceof TenantConfigurationResourceType) { return $this->blockedTypeResult($canonicalType, ['resource_type_missing']); } $resources = TenantConfigurationResource::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->where('provider_connection_id', (int) $providerConnection->getKey()) ->where('resource_type_id', (int) $resourceType->getKey()) ->where('canonical_type', $canonicalType) ->with([ 'latestEvidence.operationRun:id,workspace_id,managed_environment_id', ]) ->orderBy('canonical_resource_key') ->get(); if ($resources->isEmpty()) { return $this->blockedTypeResult($canonicalType, ['current_same_scope_resource_missing']); } $resourceResults = $resources ->map(fn (TenantConfigurationResource $resource): array => $this->evaluateResource($resource, $canonicalType)) ->values(); $blockers = $this->uniqueStrings($resourceResults ->flatMap(fn (array $result): array => $result['blockers']) ->all()); return [ 'canonical_type' => $canonicalType, 'resource_count' => $resources->count(), 'criteria' => [ 'evidence' => $resourceResults->every(fn (array $result): bool => (bool) $result['criteria']['evidence']), 'identity' => $resourceResults->every(fn (array $result): bool => (bool) $result['criteria']['identity']), 'compare' => $resourceResults->every(fn (array $result): bool => (bool) $result['criteria']['compare']), 'render' => $resourceResults->every(fn (array $result): bool => (bool) $result['criteria']['render']), 'redaction' => $resourceResults->every(fn (array $result): bool => (bool) $result['criteria']['redaction']), ], 'certified' => $blockers === [], 'blockers' => $blockers, 'resources' => $resourceResults->all(), ]; } /** * @return array */ private function evaluateResource(TenantConfigurationResource $resource, string $canonicalType): array { $evidence = $resource->latestEvidence; $blockers = []; $criteria = [ 'evidence' => false, 'identity' => false, 'compare' => false, 'render' => false, 'redaction' => false, ]; $reasons = []; [$criteria['evidence'], $evidenceReasons] = $this->evidencePasses($resource, $evidence, $canonicalType); $reasons = [...$reasons, ...$evidenceReasons]; if (! $criteria['evidence']) { $blockers[] = EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE; } [$criteria['identity'], $identityReasons] = $this->identityPasses($resource); $reasons = [...$reasons, ...$identityReasons]; if (! $criteria['identity']) { $blockers[] = EntraCertifiedComparePackResult::BLOCKED_IDENTITY; } $renderSummary = null; if ($evidence instanceof TenantConfigurationResourceEvidence && is_array($evidence->normalized_payload)) { [$criteria['compare'], $compareReasons] = $this->comparePasses($canonicalType, $evidence); [$criteria['render'], $renderSummary, $renderReasons] = $this->renderPasses($resource, $canonicalType, $evidence); [$criteria['redaction'], $redactionReasons] = $this->redactionPasses($evidence, $renderSummary); $reasons = [...$reasons, ...$compareReasons, ...$renderReasons, ...$redactionReasons]; } if (! $criteria['compare']) { $blockers[] = EntraCertifiedComparePackResult::BLOCKED_COMPARE; } if (! $criteria['render']) { $blockers[] = EntraCertifiedComparePackResult::BLOCKED_RENDER; } if (! $criteria['redaction']) { $blockers[] = EntraCertifiedComparePackResult::BLOCKED_REDACTION; } return [ 'resource_id' => (int) $resource->getKey(), 'canonical_resource_key' => (string) $resource->canonical_resource_key, 'criteria' => $criteria, 'certified' => $blockers === [], 'blockers' => $this->uniqueStrings($blockers), 'reasons' => $this->uniqueStrings($reasons), ]; } /** * @return array{0: bool, 1: list} */ private function evidencePasses( TenantConfigurationResource $resource, mixed $evidence, string $canonicalType, ): array { $reasons = []; if (! $evidence instanceof TenantConfigurationResourceEvidence) { return [false, ['latest_evidence_missing']]; } if ((int) $resource->latest_evidence_id !== (int) $evidence->getKey()) { $reasons[] = 'latest_evidence_pointer_mismatch'; } foreach ([ 'resource_id' => (int) $resource->getKey(), 'workspace_id' => (int) $resource->workspace_id, 'managed_environment_id' => (int) $resource->managed_environment_id, 'provider_connection_id' => (int) $resource->provider_connection_id, 'resource_type_id' => (int) $resource->resource_type_id, ] as $column => $expected) { if ((int) $evidence->{$column} !== $expected) { $reasons[] = $column.'_scope_mismatch'; } } if ($evidence->evidence_state !== EvidenceState::ContentBacked) { $reasons[] = 'evidence_not_content_backed'; } if ($evidence->capture_outcome !== CaptureOutcome::Captured) { $reasons[] = 'evidence_not_captured'; } if (! is_array($evidence->raw_payload) || $evidence->raw_payload === []) { $reasons[] = 'raw_payload_missing'; } if (! is_array($evidence->normalized_payload) || $evidence->normalized_payload === []) { $reasons[] = 'normalized_payload_missing'; } if (! is_string($evidence->payload_hash) || ! preg_match('/^[a-f0-9]{64}$/', $evidence->payload_hash)) { $reasons[] = 'payload_hash_invalid'; } if ((string) $resource->latest_payload_hash !== (string) $evidence->payload_hash) { $reasons[] = 'latest_payload_hash_mismatch'; } if ((string) $evidence->source_contract_key !== self::EXPECTED_CONTRACT_KEYS[$canonicalType]) { $reasons[] = 'source_contract_mismatch'; } foreach (['source_endpoint', 'source_version'] as $field) { if (! is_string($evidence->{$field}) || trim($evidence->{$field}) === '') { $reasons[] = $field.'_missing'; } } if (! $evidence->captured_at instanceof CarbonInterface) { $reasons[] = 'captured_at_missing'; } if ((int) $evidence->operation_run_id <= 0 || ! $evidence->operationRun) { $reasons[] = 'operation_run_link_missing'; } elseif ((int) $evidence->operationRun->workspace_id !== (int) $resource->workspace_id || (int) $evidence->operationRun->managed_environment_id !== (int) $resource->managed_environment_id ) { $reasons[] = 'operation_run_scope_mismatch'; } if ($this->hasNewerEvidence($resource, $evidence)) { $reasons[] = 'latest_evidence_not_current'; } return [$reasons === [], $reasons]; } /** * @return array{0: bool, 1: list} */ private function identityPasses(TenantConfigurationResource $resource): array { return $resource->latest_identity_state === IdentityState::Stable ? [true, []] : [false, ['identity_state_'.$this->stateValue($resource->latest_identity_state)]]; } /** * @return array{0: bool, 1: list} */ private function comparePasses(string $canonicalType, TenantConfigurationResourceEvidence $evidence): array { $normalizedPayload = is_array($evidence->normalized_payload) ? $evidence->normalized_payload : []; $result = $this->comparator->compare($canonicalType, $normalizedPayload, $normalizedPayload); $changes = collect(is_array($result['changes'] ?? null) ? $result['changes'] : []); $unsupportedFields = data_get($normalizedPayload, 'diagnostics.unsupported_fields', []); $reasons = []; if (($result['supported'] ?? false) !== true) { $reasons[] = 'compare_not_supported'; } if (($result['classification'] ?? null) !== 'unchanged' || ($result['changed'] ?? true) !== false) { $reasons[] = 'compare_not_deterministic'; } if ($changes->contains(fn (mixed $change): bool => is_array($change) && ($change['classification'] ?? null) === 'unsupported_field') || (is_array($unsupportedFields) && $unsupportedFields !== []) ) { $reasons[] = 'unsupported_fields_present'; } return [$reasons === [], $reasons]; } /** * @return array{0: bool, 1: array|null, 2: list} */ private function renderPasses( TenantConfigurationResource $resource, string $canonicalType, TenantConfigurationResourceEvidence $evidence, ): array { $reasons = []; $coverageLevel = $this->coverageLevel($evidence->coverage_level); if (! $coverageLevel?->meets(CoverageLevel::Renderable)) { $reasons[] = 'coverage_not_renderable'; } $summary = $this->summaryBuilder->build($canonicalType, $evidence->normalized_payload, [ 'claim_state' => $resource->latest_claim_state, 'identity_state' => $resource->latest_identity_state, 'evidence_state' => $resource->latest_evidence_state, 'coverage_level' => $evidence->coverage_level, 'last_captured' => $resource->latest_captured_at?->toDayDateTimeString(), 'source_version' => $evidence->source_version, 'source_schema_hash' => $evidence->source_schema_hash, ]); if (! is_array($summary) || $summary === []) { $reasons[] = 'render_summary_missing'; } return [$reasons === [], $summary, $reasons]; } /** * @param array|null $summary * @return array{0: bool, 1: list} */ private function redactionPasses(TenantConfigurationResourceEvidence $evidence, ?array $summary): array { $reasons = []; $redactedRaw = $this->redactor->redact($evidence->raw_payload); $sensitiveValues = $this->sensitiveValues($evidence->raw_payload, $redactedRaw); $safeOutput = [ 'normalized_payload' => $evidence->normalized_payload, 'render_summary' => $summary, 'claim_label' => self::CLAIM_LABEL, ]; $encoded = json_encode($safeOutput, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $lowerEncoded = strtolower($encoded); foreach (['raw_payload', 'raw graph response', 'raw_graph_response', 'permission_context'] as $forbiddenToken) { if (str_contains($lowerEncoded, $forbiddenToken)) { $reasons[] = 'forbidden_output_token_'.$forbiddenToken; } } foreach ($sensitiveValues as $value) { if ($value !== '' && str_contains($encoded, $value)) { $reasons[] = 'sensitive_value_leaked'; break; } } return [$reasons === [], $reasons]; } /** * @return list */ private function scopeBlockers(TenantConfigurationSupportedScope $scope): array { $metadata = is_array($scope->metadata) ? $scope->metadata : []; $reasons = []; try { $resolved = $this->supportedScopes->resolveDefinition( $scope, TenantConfigurationResourceType::query() ->active() ->get(['canonical_type', 'source_class']), ); } catch (Throwable) { return ['supported_scope_unresolvable']; } if ($resolved['included_resource_types'] !== self::DENOMINATOR) { $reasons[] = 'supported_scope_denominator_mismatch'; } if ($resolved['minimum_coverage_level'] !== CoverageLevel::Certified) { $reasons[] = 'supported_scope_minimum_not_certified'; } if ($resolved['allow_beta'] !== false) { $reasons[] = 'supported_scope_beta_allowed'; } if ($resolved['customer_claims_allowed'] !== false) { $reasons[] = 'supported_scope_customer_claims_allowed'; } if (($metadata['graph_fallback_allowlist'] ?? null) !== ['securityDefaults']) { $reasons[] = 'supported_scope_graph_fallback_allowlist_mismatch'; } if (($metadata['resource_type_denominator'] ?? null) !== self::DENOMINATOR) { $reasons[] = 'supported_scope_metadata_denominator_mismatch'; } return $reasons; } /** * @param list $reasons * @return array */ private function blockedTypeResult(string $canonicalType, array $reasons): array { return [ 'canonical_type' => $canonicalType, 'resource_count' => 0, 'criteria' => [ 'evidence' => false, 'identity' => false, 'compare' => false, 'render' => false, 'redaction' => false, ], 'certified' => false, 'blockers' => [EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE], 'resources' => [], 'reasons' => $reasons, ]; } private function assertSameScope(ManagedEnvironment $environment, ProviderConnection $providerConnection): void { if ((int) $providerConnection->workspace_id !== (int) $environment->workspace_id || (int) $providerConnection->managed_environment_id !== (int) $environment->getKey() ) { throw new InvalidArgumentException('Provider connection scope mismatch while evaluating Entra certified compare pack.'); } } private function hasNewerEvidence( TenantConfigurationResource $resource, TenantConfigurationResourceEvidence $evidence, ): bool { if (! $evidence->captured_at instanceof CarbonInterface) { return true; } return TenantConfigurationResourceEvidence::query() ->where('resource_id', (int) $resource->getKey()) ->where('workspace_id', (int) $resource->workspace_id) ->where('managed_environment_id', (int) $resource->managed_environment_id) ->where('provider_connection_id', (int) $resource->provider_connection_id) ->where('resource_type_id', (int) $resource->resource_type_id) ->where('id', '<>', (int) $evidence->getKey()) ->where('evidence_state', EvidenceState::ContentBacked->value) ->where('capture_outcome', CaptureOutcome::Captured->value) ->where(function ($query) use ($evidence): void { $query->where('captured_at', '>', $evidence->captured_at) ->orWhere(function ($query) use ($evidence): void { $query->where('captured_at', '=', $evidence->captured_at) ->where('id', '>', (int) $evidence->getKey()); }); }) ->exists(); } private function overallState(array $blockers): string { $blockers = $this->uniqueStrings($blockers); if ($blockers === []) { return EntraCertifiedComparePackResult::PASSED; } foreach (self::BLOCKER_PRIORITY as $blocker) { if (in_array($blocker, $blockers, true)) { return $blocker; } } return EntraCertifiedComparePackResult::NOT_EVALUATED; } private function coverageLevel(mixed $value): ?CoverageLevel { if ($value instanceof CoverageLevel) { return $value; } return is_string($value) ? CoverageLevel::tryFrom($value) : null; } /** * @return list */ private function uniqueStrings(array $values): array { return array_values(array_unique(array_filter( array_map(static fn (mixed $value): string => is_scalar($value) ? trim((string) $value) : '', $values), static fn (string $value): bool => $value !== '', ))); } private function stateValue(mixed $state): string { return $state instanceof \BackedEnum ? (string) $state->value : (string) $state; } /** * @return list */ private function sensitiveValues(mixed $raw, mixed $redacted): array { $values = []; if (! is_array($raw)) { return []; } foreach ($raw as $key => $value) { $key = (string) $key; $redactedValue = is_array($redacted) ? ($redacted[$key] ?? null) : null; if ($redactedValue === '[redacted]' && is_scalar($value)) { $values[] = (string) $value; continue; } if ($this->isSensitiveKey($key) && is_scalar($value)) { $values[] = (string) $value; } if (is_array($value)) { $values = [...$values, ...$this->sensitiveValues($value, $redactedValue)]; } } return $this->uniqueStrings($values); } private function isSensitiveKey(string $key): bool { $normalized = strtolower($key); $compact = str_replace(['_', '-', ' '], '', $normalized); foreach (self::SENSITIVE_KEY_PARTS as $part) { $compactPart = str_replace(['_', '-', ' '], '', $part); if (str_contains($normalized, $part) || str_contains($compact, $compactPart)) { return true; } } return false; } }