TenantAtlas/apps/platform/app/Services/TenantConfiguration/ClaimGuard.php
ahmido 33e496c182 feat: complete spec 425 enta certified compare pack (#492)
Implements spec 425 with Entra certified compare pack support, coverage, guards, evaluator, fixtures, and tests.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #492
2026-07-01 23:27:16 +00:00

424 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\IdentityState;
use App\Support\TenantConfiguration\RestoreTier;
use App\Support\TenantConfiguration\SourceClass;
final class ClaimGuard
{
private const ENTRA_CERTIFIED_COMPARE_PACK_CLAIMS = [
'certified entra core compare pack conditional access and security defaults',
'certified compare support for conditional access and security defaults',
'certified compare render support for the entra core denominator conditional access and security defaults',
];
public function evaluateStatement(string $claim, bool $internalOperatorOnly = false): ClaimState
{
$tokens = $this->claimTokens($claim);
$registryScoped = $this->isRegistryScopedStatement($tokens);
if ($this->hasUnsafeBroadCoverageClaim($tokens, $registryScoped)) {
return ClaimState::ClaimBlocked;
}
if ($internalOperatorOnly && $this->hasScopedInternalComparableRenderableReadinessClaim($tokens)) {
return ClaimState::InternalOnly;
}
if ($internalOperatorOnly && $registryScoped) {
return ClaimState::InternalOnly;
}
return $registryScoped ? ClaimState::ClaimBlocked : ClaimState::ClaimLimited;
}
public function evaluateCertifiedComparePackStatement(
string $claim,
bool $packPassed,
bool $internalOperatorOnly,
): ClaimState {
$tokens = $this->claimTokens($claim);
$exactDenominatorClaim = $this->hasExactEntraCertifiedComparePackDenominator($tokens)
&& in_array(implode(' ', $tokens), self::ENTRA_CERTIFIED_COMPARE_PACK_CLAIMS, true);
if ($packPassed && $internalOperatorOnly && $exactDenominatorClaim) {
return ClaimState::InternalOnly;
}
if ($this->hasUnsafeBroadCoverageClaim($tokens, registryScoped: false)) {
return ClaimState::ClaimBlocked;
}
if (! $packPassed || ! $internalOperatorOnly) {
return ClaimState::ClaimBlocked;
}
return ClaimState::ClaimBlocked;
}
public function evaluate(
?string $scopeKey,
CoverageLevel|string $requestedLevel,
CoverageLevel|string $actualLevel,
bool $scopeComplete,
bool $customerFacing = false,
bool $customerClaimsAllowed = true,
bool $unscoped = false,
?int $percentage = null,
SourceClass|string|null $sourceClass = null,
RestoreTier|string|null $restoreTier = null,
IdentityState|string|null $identityState = null,
bool $restoreClaim = false,
bool $allowsBetaClaims = false,
bool $allowsCertifiedClaims = false,
bool $allowsDerivedIdentityClaims = false,
): ClaimState {
$requested = $this->coverageLevel($requestedLevel);
$actual = $this->coverageLevel($actualLevel);
$source = $this->sourceClass($sourceClass);
$restore = $this->restoreTier($restoreTier);
$identity = $this->identityState($identityState);
if (($scopeKey === null || $unscoped) && $percentage === 100) {
return ClaimState::ClaimBlocked;
}
if (in_array($identity, [
IdentityState::IdentityConflict,
IdentityState::MissingExternalId,
IdentityState::UnsupportedIdentity,
], true)) {
return ClaimState::ClaimBlocked;
}
if ($identity === IdentityState::Derived && ! $allowsDerivedIdentityClaims) {
return $customerFacing ? ClaimState::ClaimBlocked : ClaimState::ClaimLimited;
}
if ($source?->isBetaExperimental() === true) {
if (! $allowsBetaClaims) {
return ClaimState::ClaimBlocked;
}
if ($requested === CoverageLevel::Certified && ! $allowsCertifiedClaims) {
return ClaimState::ClaimBlocked;
}
}
if (($restoreClaim || $requested->meets(CoverageLevel::Restorable)) && $restore !== RestoreTier::Restorable) {
return ClaimState::ClaimBlocked;
}
if ($customerFacing && (! $scopeComplete || ! $customerClaimsAllowed)) {
return ClaimState::ClaimBlocked;
}
if ($scopeKey === null || ! $actual->meets($requested)) {
return ClaimState::ClaimLimited;
}
return ClaimState::ClaimAllowed;
}
private function coverageLevel(CoverageLevel|string $level): CoverageLevel
{
return $level instanceof CoverageLevel ? $level : CoverageLevel::from($level);
}
private function sourceClass(SourceClass|string|null $sourceClass): ?SourceClass
{
if ($sourceClass === null || $sourceClass instanceof SourceClass) {
return $sourceClass;
}
return SourceClass::from($sourceClass);
}
private function restoreTier(RestoreTier|string|null $restoreTier): ?RestoreTier
{
if ($restoreTier === null || $restoreTier instanceof RestoreTier) {
return $restoreTier;
}
return RestoreTier::from($restoreTier);
}
private function identityState(IdentityState|string|null $identityState): ?IdentityState
{
if ($identityState === null || $identityState instanceof IdentityState) {
return $identityState;
}
return IdentityState::from($identityState);
}
/**
* @return list<string>
*/
private function claimTokens(string $claim): array
{
$normalized = strtolower($claim);
$normalized = str_replace('%', ' percent ', $normalized);
$normalized = (string) preg_replace('/[^a-z0-9]+/', ' ', $normalized);
$normalized = (string) preg_replace('/\s+/', ' ', trim($normalized));
if ($normalized === '') {
return [];
}
return explode(' ', $normalized);
}
/**
* @param list<string> $tokens
*/
private function isRegistryScopedStatement(array $tokens): bool
{
return $this->hasToken($tokens, 'registry')
&& $this->hasToken($tokens, 'coverage')
&& $this->hasToken($tokens, 'seeded')
&& $this->hasToken($tokens, 'resource')
&& $this->hasToken($tokens, 'type')
&& $this->hasToken($tokens, 'entries');
}
/**
* @param list<string> $tokens
*/
private function hasUnsafeBroadCoverageClaim(array $tokens, bool $registryScoped): bool
{
$hasCoverageSurface = $this->hasAnyToken($tokens, [
'coverage',
'resource',
'resources',
'support',
'supported',
'tenant',
]);
$hasWorkloadReference = $this->hasMicrosoft365Reference($tokens)
|| $this->hasAnyToken($tokens, [
'entra',
'exchange',
'teams',
'defender',
'purview',
'tcm',
'dlp',
'retention',
'label',
'labels',
'compliance',
])
|| ($this->hasToken($tokens, 'security') && $this->hasToken($tokens, 'compliance'))
|| ($this->hasToken($tokens, 'security') && $this->hasToken($tokens, 'defaults'));
if ($this->hasHundredPercent($tokens) && ! $registryScoped) {
return true;
}
if ($this->hasCertificationTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasRestoreReadyTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasApplyReadyTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasCustomerReadyTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasLegalOrRegulatoryTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $this->hasToken($tokens, 'compliance'))) {
return true;
}
if ($this->hasReviewOutputTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) {
return true;
}
if ($this->hasComparableRenderableTerm($tokens)
&& ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)
&& ! $this->hasScopedInternalComparableRenderableReadinessClaim($tokens)
) {
return true;
}
if ($hasWorkloadReference
&& $hasCoverageSurface
&& ! $registryScoped
&& ! $this->hasScopedInternalComparableRenderableReadinessClaim($tokens)
) {
return true;
}
if ($this->hasAnyToken($tokens, ['full', 'complete', 'all'])
&& ($hasCoverageSurface || $hasWorkloadReference || $registryScoped || $this->hasToken($tokens, 'tenant'))
) {
return true;
}
return false;
}
/**
* @param list<string> $tokens
*/
private function hasScopedInternalComparableRenderableReadinessClaim(array $tokens): bool
{
return $this->hasToken($tokens, 'selected')
&& $this->hasScopedWorkloadReference($tokens)
&& ($this->hasComparableRenderableTerm($tokens) || $this->hasReadyForOperatorReviewTerm($tokens))
&& ($this->hasToken($tokens, 'internal') || $this->hasToken($tokens, 'operator'));
}
/**
* @param list<string> $tokens
*/
private function hasScopedWorkloadReference(array $tokens): bool
{
return $this->hasAnyToken($tokens, ['entra', 'exchange', 'teams'])
|| ($this->hasToken($tokens, 'security') && $this->hasToken($tokens, 'compliance'))
|| ($this->hasToken($tokens, 'security') && $this->hasToken($tokens, 'defaults'))
|| ($this->hasToken($tokens, 'retention') && $this->hasToken($tokens, 'compliance'))
|| ($this->hasToken($tokens, 'dlp') && $this->hasToken($tokens, 'compliance'))
|| $this->hasAnyToken($tokens, ['label', 'labels']);
}
/**
* @param list<string> $tokens
*/
private function hasExactEntraCertifiedComparePackDenominator(array $tokens): bool
{
return $this->hasToken($tokens, 'conditional')
&& $this->hasToken($tokens, 'access')
&& $this->hasToken($tokens, 'security')
&& $this->hasToken($tokens, 'defaults');
}
/**
* @param list<string> $tokens
*/
private function hasMicrosoft365Reference(array $tokens): bool
{
return $this->hasToken($tokens, 'm365')
|| ($this->hasToken($tokens, 'microsoft') && $this->hasToken($tokens, '365'));
}
/**
* @param list<string> $tokens
*/
private function hasHundredPercent(array $tokens): bool
{
return $this->hasToken($tokens, '100')
&& $this->hasToken($tokens, 'percent');
}
/**
* @param list<string> $tokens
*/
private function hasCertificationTerm(array $tokens): bool
{
return $this->hasAnyToken($tokens, [
'certified',
'certification',
'certify',
'certifies',
]);
}
/**
* @param list<string> $tokens
*/
private function hasRestoreReadyTerm(array $tokens): bool
{
return $this->hasToken($tokens, 'restorable')
|| ($this->hasToken($tokens, 'restore') && $this->hasAnyToken($tokens, ['ready', 'readiness']))
|| ($this->hasToken($tokens, 'restore') && $this->hasToken($tokens, 'coverage'));
}
/**
* @param list<string> $tokens
*/
private function hasApplyReadyTerm(array $tokens): bool
{
return ($this->hasToken($tokens, 'apply') && $this->hasAnyToken($tokens, ['ready', 'readiness']))
|| $this->hasToken($tokens, 'applyready');
}
/**
* @param list<string> $tokens
*/
private function hasCustomerReadyTerm(array $tokens): bool
{
return $this->hasToken($tokens, 'customer')
&& $this->hasAnyToken($tokens, ['ready', 'readiness', 'proof']);
}
/**
* @param list<string> $tokens
*/
private function hasLegalOrRegulatoryTerm(array $tokens): bool
{
return $this->hasAnyToken($tokens, ['legal', 'regulatory', 'attestation', 'verified']);
}
/**
* @param list<string> $tokens
*/
private function hasReviewOutputTerm(array $tokens): bool
{
return $this->hasToken($tokens, 'review')
&& $this->hasToken($tokens, 'pack');
}
/**
* @param list<string> $tokens
*/
private function hasComparableRenderableTerm(array $tokens): bool
{
return $this->hasAnyToken($tokens, ['comparable', 'renderable']);
}
/**
* @param list<string> $tokens
*/
private function hasReadyForOperatorReviewTerm(array $tokens): bool
{
return $this->hasToken($tokens, 'ready')
&& $this->hasToken($tokens, 'operator')
&& $this->hasToken($tokens, 'review');
}
/**
* @param list<string> $tokens
*/
private function hasAnyToken(array $tokens, array $expectedTokens): bool
{
foreach ($expectedTokens as $token) {
if ($this->hasToken($tokens, $token)) {
return true;
}
}
return false;
}
/**
* @param list<string> $tokens
*/
private function hasToken(array $tokens, string $expectedToken): bool
{
return in_array($expectedToken, $tokens, true);
}
}