178 lines
6.5 KiB
PHP
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,
|
|
];
|
|
}
|