TenantAtlas/apps/platform/tests/Unit/Governance/CanonicalControlResolverTest.php
2026-04-24 14:15:50 +02:00

178 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Support\Governance\Controls\CanonicalControlCatalog;
use App\Support\Governance\Controls\CanonicalControlResolutionRequest;
use App\Support\Governance\Controls\CanonicalControlResolver;
it('resolves multiple Microsoft subject families to one stable canonical control identity', function (): void {
$resolver = app(CanonicalControlResolver::class);
$configurationResult = $resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
subjectFamilyKey: 'deviceConfiguration',
workload: 'intune',
signalKey: 'intune.device_configuration_drift',
));
$complianceResult = $resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'review',
subjectFamilyKey: 'deviceCompliancePolicy',
workload: 'intune',
signalKey: 'intune.device_compliance_policy',
));
expect($configurationResult->toArray()['control']['control_key'])->toBe('endpoint_hardening_compliance')
->and($complianceResult->toArray()['control']['control_key'])->toBe('endpoint_hardening_compliance')
->and($configurationResult->toArray()['control']['name'])->toBe($complianceResult->toArray()['control']['name']);
});
it('uses supplied signal context instead of letting workload labels become primary identity', function (): void {
$resolver = app(CanonicalControlResolver::class);
$strongAuthentication = $resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
subjectFamilyKey: 'conditional_access_policy',
workload: 'entra',
signalKey: 'conditional_access.require_mfa',
));
$accessEnforcement = $resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
subjectFamilyKey: 'conditional_access_policy',
workload: 'entra',
signalKey: 'conditional_access.policy_state',
));
expect($strongAuthentication->toArray()['control']['control_key'])->toBe('strong_authentication')
->and($accessEnforcement->toArray()['control']['control_key'])->toBe('conditional_access_enforcement');
});
it('returns explicit unresolved reason codes instead of fallback labels', function (): void {
$resolver = app(CanonicalControlResolver::class);
expect($resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'unknown',
consumerContext: 'evidence',
subjectFamilyKey: 'deviceConfiguration',
))->toArray())->toMatchArray([
'status' => 'unresolved',
'reason_code' => 'unsupported_provider',
]);
expect($resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
))->toArray())->toMatchArray([
'status' => 'unresolved',
'reason_code' => 'insufficient_context',
]);
expect($resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
subjectFamilyKey: 'not_bound',
))->toArray())->toMatchArray([
'status' => 'unresolved',
'reason_code' => 'missing_binding',
]);
});
it('fails deterministically when a binding context is ambiguous', function (): void {
$resolver = new CanonicalControlResolver(new CanonicalControlCatalog([
spec236ControlDefinition('first_control', [
'microsoft_bindings' => [
spec236Binding('shared_subject', primary: false),
],
]),
spec236ControlDefinition('second_control', [
'microsoft_bindings' => [
spec236Binding('shared_subject', primary: false),
],
]),
]));
$result = $resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'evidence',
subjectFamilyKey: 'shared_subject',
workload: 'entra',
signalKey: 'shared.signal',
))->toArray();
expect($result['status'])->toBe('ambiguous')
->and($result['reason_code'])->toBe('ambiguous_binding')
->and($result['candidate_control_keys'])->toBe(['first_control', 'second_control']);
});
it('keeps retired controls resolvable for historical references', function (): void {
$resolver = new CanonicalControlResolver(new CanonicalControlCatalog([
spec236ControlDefinition('retired_control', [
'historical_status' => 'retired',
'microsoft_bindings' => [
spec236Binding('retired_subject'),
],
]),
]));
$result = $resolver->resolve(new CanonicalControlResolutionRequest(
provider: 'microsoft',
consumerContext: 'review',
subjectFamilyKey: 'retired_subject',
workload: 'entra',
signalKey: 'shared.signal',
))->toArray();
expect($result['status'])->toBe('resolved')
->and($result['control']['control_key'])->toBe('retired_control')
->and($result['control']['historical_status'])->toBe('retired');
});
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function spec236ControlDefinition(string $controlKey, array $overrides = []): array
{
return array_replace_recursive([
'control_key' => $controlKey,
'name' => str_replace('_', ' ', ucfirst($controlKey)),
'domain_key' => 'identity_access',
'subdomain_key' => 'test_subjects',
'control_class' => 'preventive',
'summary' => 'Test summary.',
'operator_description' => 'Test operator description.',
'detectability_class' => 'direct_technical',
'evaluation_strategy' => 'state_evaluated',
'evidence_archetypes' => ['configuration_snapshot'],
'artifact_suitability' => [
'baseline' => true,
'drift' => true,
'finding' => true,
'exception' => true,
'evidence' => true,
'review' => true,
'report' => true,
],
'historical_status' => 'active',
'microsoft_bindings' => [],
], $overrides);
}
/**
* @return array<string, mixed>
*/
function spec236Binding(string $subjectFamilyKey, bool $primary = true): array
{
return [
'subject_family_key' => $subjectFamilyKey,
'workload' => 'entra',
'signal_keys' => ['shared.signal'],
'supported_contexts' => ['evidence', 'review'],
'primary' => $primary,
];
}