212 lines
11 KiB
PHP
212 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\TenantConfigurationResourceEvidence;
|
|
use App\Services\TenantConfiguration\EntraCertifiedComparePackEvaluator;
|
|
use App\Services\TenantConfiguration\EntraCertifiedComparePackResult;
|
|
use App\Support\TenantConfiguration\CaptureOutcome;
|
|
use App\Support\TenantConfiguration\CoverageLevel;
|
|
use App\Support\TenantConfiguration\EvidenceState;
|
|
use App\Support\TenantConfiguration\IdentityState;
|
|
use Tests\Support\TenantConfiguration\Spec425Fixtures as Spec425;
|
|
|
|
it('Spec425 certifies only when both mandatory denominator types pass every criterion', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $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'],
|
|
]);
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'conditionalAccessPolicy', Spec425::fixture('conditional-access', 'no-change'));
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'securityDefaults', Spec425::fixture('security-defaults', 'no-change'));
|
|
bindFailHardGraphClient();
|
|
|
|
$result = assertNoOutboundHttp(fn (): EntraCertifiedComparePackResult => app(EntraCertifiedComparePackEvaluator::class)
|
|
->evaluate($environment, $connection));
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::PASSED)
|
|
->and($result->certified())->toBeTrue()
|
|
->and($result->denominator())->toBe(['conditionalAccessPolicy', 'securityDefaults'])
|
|
->and($result->blockers())->toBe([])
|
|
->and(json_encode($result->toArray(), JSON_THROW_ON_ERROR))
|
|
->not->toContain('raw_payload')
|
|
->not->toContain('permission_context')
|
|
->not->toContain('Policy.Read.All');
|
|
});
|
|
|
|
it('Spec425 blocks certification when either denominator item is missing and does not fallback to wrong-scope evidence', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
[, $foreignEnvironment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
]);
|
|
$foreignConnection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $foreignEnvironment->workspace_id,
|
|
'managed_environment_id' => (int) $foreignEnvironment->getKey(),
|
|
]);
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'conditionalAccessPolicy', Spec425::fixture('conditional-access', 'no-change'));
|
|
Spec425::createResourceWithEvidence($foreignEnvironment, $foreignConnection, 'securityDefaults', Spec425::fixture('security-defaults', 'no-change'));
|
|
|
|
$result = app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE)
|
|
->and($result->certified())->toBeFalse()
|
|
->and($result->blockers())->toContain(EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE);
|
|
});
|
|
|
|
it('Spec425 blocks stale or superseded latest evidence instead of falling back to first or latest implicitly', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
]);
|
|
$resource = Spec425::createResourceWithEvidence($environment, $connection, 'conditionalAccessPolicy', Spec425::fixture('conditional-access', 'no-change'));
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'securityDefaults', Spec425::fixture('security-defaults', 'no-change'));
|
|
$latestEvidence = $resource->latestEvidence()->firstOrFail();
|
|
|
|
TenantConfigurationResourceEvidence::factory()->create([
|
|
'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,
|
|
'operation_run_id' => (int) $latestEvidence->operation_run_id,
|
|
'source_contract_key' => 'conditionalAccessPolicy',
|
|
'source_endpoint' => '/identity/conditionalAccess/policies',
|
|
'source_version' => 'v1.0',
|
|
'raw_payload' => Spec425::fixture('conditional-access', 'state-change'),
|
|
'normalized_payload' => Spec425::normalizedPayload(Spec425::fixture('conditional-access', 'state-change')),
|
|
'payload_hash' => Spec425::payloadHash(Spec425::normalizedPayload(Spec425::fixture('conditional-access', 'state-change'))),
|
|
'evidence_state' => EvidenceState::ContentBacked->value,
|
|
'coverage_level' => CoverageLevel::Renderable->value,
|
|
'capture_outcome' => CaptureOutcome::Captured->value,
|
|
'captured_at' => $latestEvidence->captured_at->copy()->addMinute(),
|
|
]);
|
|
|
|
$result = app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $connection);
|
|
$conditionalAccess = collect($result->resourceResults())->firstWhere('canonical_type', 'conditionalAccessPolicy');
|
|
$firstResource = collect($conditionalAccess['resources'])->first();
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::BLOCKED_MISSING_EVIDENCE)
|
|
->and($firstResource['reasons'])->toContain('latest_evidence_not_current');
|
|
});
|
|
|
|
it('Spec425 blocks identity states that are not stable', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
]);
|
|
Spec425::createResourceWithEvidence(
|
|
$environment,
|
|
$connection,
|
|
'conditionalAccessPolicy',
|
|
Spec425::fixture('conditional-access', 'no-change'),
|
|
['latest_identity_state' => IdentityState::Derived->value],
|
|
);
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'securityDefaults', Spec425::fixture('security-defaults', 'no-change'));
|
|
|
|
$result = app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::BLOCKED_IDENTITY)
|
|
->and($result->blockers())->toContain(EntraCertifiedComparePackResult::BLOCKED_IDENTITY);
|
|
});
|
|
|
|
it('Spec425 blocks unsupported fields because they make certification ambiguous', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
]);
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'conditionalAccessPolicy', Spec425::fixture('conditional-access', 'unsupported-field'));
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'securityDefaults', Spec425::fixture('security-defaults', 'no-change'));
|
|
|
|
$result = app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::BLOCKED_COMPARE)
|
|
->and($result->blockers())->toContain(EntraCertifiedComparePackResult::BLOCKED_COMPARE);
|
|
});
|
|
|
|
it('Spec425 blocks non-renderable denominator evidence', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
]);
|
|
Spec425::createResourceWithEvidence(
|
|
$environment,
|
|
$connection,
|
|
'conditionalAccessPolicy',
|
|
Spec425::fixture('conditional-access', 'no-change'),
|
|
evidenceOverrides: ['coverage_level' => CoverageLevel::ContentBacked->value],
|
|
);
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'securityDefaults', Spec425::fixture('security-defaults', 'no-change'));
|
|
|
|
$result = app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::BLOCKED_RENDER)
|
|
->and($result->blockers())->toContain(EntraCertifiedComparePackResult::BLOCKED_RENDER);
|
|
});
|
|
|
|
it('Spec425 blocks certification output when redaction would leak a sensitive value', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
]);
|
|
$leakingPayload = array_replace(Spec425::fixture('security-defaults', 'no-change'), [
|
|
'clientSecret' => 'spec425-redaction-leak',
|
|
]);
|
|
$leakingNormalized = array_replace(Spec425::fixture('security-defaults', 'no-change'), [
|
|
'displayName' => 'spec425-redaction-leak',
|
|
]);
|
|
|
|
Spec425::createResourceWithEvidence($environment, $connection, 'conditionalAccessPolicy', Spec425::fixture('conditional-access', 'no-change'));
|
|
Spec425::createResourceWithEvidence(
|
|
$environment,
|
|
$connection,
|
|
'securityDefaults',
|
|
$leakingPayload,
|
|
evidenceOverrides: [
|
|
'normalized_payload' => $leakingNormalized,
|
|
'payload_hash' => Spec425::payloadHash($leakingNormalized),
|
|
],
|
|
);
|
|
|
|
$result = app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->state())->toBe(EntraCertifiedComparePackResult::BLOCKED_REDACTION)
|
|
->and($result->blockers())->toContain(EntraCertifiedComparePackResult::BLOCKED_REDACTION);
|
|
});
|
|
|
|
it('Spec425 rejects provider connections outside the managed environment scope', function (): void {
|
|
Spec425::syncDefaults();
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
[, $foreignEnvironment] = createMinimalUserWithTenant(role: 'owner');
|
|
$foreignConnection = ProviderConnection::factory()->withCredential()->create([
|
|
'workspace_id' => (int) $foreignEnvironment->workspace_id,
|
|
'managed_environment_id' => (int) $foreignEnvironment->getKey(),
|
|
]);
|
|
|
|
expect(fn () => app(EntraCertifiedComparePackEvaluator::class)->evaluate($environment, $foreignConnection))
|
|
->toThrow(InvalidArgumentException::class, 'Provider connection scope mismatch');
|
|
});
|