TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec436ExchangeContentBackedEvidenceFeatureTest.php
ahmido 30a6733f78 Spec 436: Exchange content-backed evidence promotion (#503)
Summary: Wire Exchange PowerShell evidence capture through Spec 435 structured envelopes, target normalizers, and hash builder for transportRule, remoteDomain, and inboundConnector; preserve append-only internal content-backed evidence with fail-closed readiness, output, identity, and redaction behavior; add Spec 436 artifacts and focused coverage. Validation: php artisan test --filter=Spec436 --compact PASS, 28 tests / 307 assertions; git diff --cached --check PASS before commit. Product/Ops: Livewire v4 unchanged; provider registration unchanged under apps/platform/bootstrap/providers.php; global search unchanged; no destructive/high-impact actions; no assets/browser/UI/deployment impact.
Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #503
2026-07-09 14:03:10 +00:00

727 lines
32 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentPermission;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
use App\Services\TenantConfiguration\ExchangePowerShellEvidenceCaptureAdapter;
use App\Services\TenantConfiguration\ExchangePowerShellHashInputBuilder;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationGate;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationResult;
use App\Services\TenantConfiguration\ExchangePowerShellTargetEvidenceNormalizer;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\Providers\ProviderCredentialKind;
use App\Support\Providers\ProviderCredentialSource;
use App\Support\Providers\ProviderVerificationStatus;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
beforeEach(function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
'tenantpilot.exchange_powershell.permission_evidence.currentness_days' => 30,
]);
});
it('Spec436 appends content-backed Exchange PowerShell evidence through target normalizers and hash builder', function (
string $canonicalType,
string $commandName,
string $normalizerVersion,
array $payload,
): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec436ReadyProviderConnection($environment);
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec436CaptureRun($environment, $user, $connection, resourceTypes: [$canonicalType]);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: $canonicalType,
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[$payload],
context: ['output_state' => 'structured_collection', 'runner_mode' => 'fake'],
),
);
$resource = TenantConfigurationResource::query()->sole();
$evidence = TenantConfigurationResourceEvidence::query()->sole();
$expectedHash = app(ExchangePowerShellHashInputBuilder::class)->hash(
app(ExchangePowerShellHashInputBuilder::class)->build(
resourceType: $canonicalType,
sourceSurface: ExchangePowerShellCommandContracts::SOURCE_SURFACE,
commandContractName: $commandName,
commandContractVersion: ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION,
payloadShapeVersion: ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
normalizerVersion: $normalizerVersion,
normalizedPayload: $evidence->normalized_payload,
),
);
expect($result)
->toMatchArray([
'canonical_type' => $canonicalType,
'outcome' => CaptureOutcome::Captured->value,
'adapter_outcome' => CaptureOutcome::Captured->value,
'item_count' => 1,
'reason_code' => null,
'source_contract_key' => 'exchange_powershell.'.$canonicalType,
])
->and($result['evidence_ids'])->toBe([(int) $evidence->getKey()])
->and($resource->workspace_id)->toBe((int) $environment->workspace_id)
->and($resource->managed_environment_id)->toBe((int) $environment->getKey())
->and($resource->provider_connection_id)->toBe((int) $connection->getKey())
->and($resource->latest_evidence_id)->toBe((int) $evidence->getKey())
->and($resource->latest_evidence_state)->toBe(EvidenceState::ContentBacked)
->and($resource->latest_claim_state)->toBe(ClaimState::InternalOnly)
->and($evidence->operation_run_id)->toBe((int) $run->getKey())
->and($evidence->workspace_id)->toBe((int) $environment->workspace_id)
->and($evidence->managed_environment_id)->toBe((int) $environment->getKey())
->and($evidence->provider_connection_id)->toBe((int) $connection->getKey())
->and($evidence->coverage_level)->toBe(CoverageLevel::ContentBacked)
->and($evidence->evidence_state)->toBe(EvidenceState::ContentBacked)
->and($evidence->capture_outcome)->toBe(CaptureOutcome::Captured)
->and($evidence->source_endpoint)->toBe(ExchangePowerShellCommandContracts::SOURCE_SURFACE.':'.$commandName)
->and($evidence->raw_payload)->toMatchArray($payload)
->and($evidence->normalized_payload['canonical_type'])->toBe($canonicalType)
->and($evidence->normalized_payload['source_surface'])->toBe(ExchangePowerShellCommandContracts::SOURCE_SURFACE)
->and($evidence->normalized_payload['payload_shape_version'])->toBe(ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION)
->and($evidence->normalized_payload['normalizer_version'])->toBe($normalizerVersion)
->and($evidence->normalized_payload['source']['source_contract_key'])->toBe('exchange_powershell.'.$canonicalType)
->and($evidence->normalized_payload['source']['command_contract_name'])->toBe($commandName)
->and($evidence->payload_hash)->toBe($expectedHash)
->and($evidence->permission_context['provider_connection_id'])->toBe((int) $connection->getKey());
$runJson = json_encode([
'context' => $run->refresh()->context,
'summary_counts' => $run->summary_counts,
'failure_summary' => $run->failure_summary,
], JSON_THROW_ON_ERROR);
expect($runJson)
->not->toContain('raw_stdout')
->not->toContain('raw_stderr')
->not->toContain('normalizer_preview')
->not->toContain('hash_preview')
->not->toContain('client_secret')
->not->toContain('access_token');
})->with([
'transportRule' => [
'transportRule',
'Get-TransportRule',
'exchange-transport-rule-readiness-v1',
spec436TransportRulePayload(),
],
'remoteDomain' => [
'remoteDomain',
'Get-RemoteDomain',
'exchange-remote-domain-readiness-v1',
spec436RemoteDomainPayload(),
],
'inboundConnector' => [
'inboundConnector',
'Get-InboundConnector',
'exchange-inbound-connector-readiness-v1',
spec436InboundConnectorPayload(),
],
]);
it('Spec436 blocks unsafe target-normalizer identity states before writer use', function (
string $canonicalType,
array $items,
string $expectedReasonCode,
): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec436ReadyProviderConnection($environment);
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec436CaptureRun($environment, $user, $connection, resourceTypes: [$canonicalType]);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: $canonicalType,
runnerResult: ExchangePowerShellInvocationResult::succeeded(
$items,
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_IDENTITY_BLOCKED)
->and($result['reason_code'])->toBe($expectedReasonCode)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'remoteDomain domain-only' => [
'remoteDomain',
[['DomainName' => 'contoso.example']],
'derived_identity_blocked',
],
'remoteDomain display-only' => [
'remoteDomain',
[['DisplayName' => 'Contoso remote domain']],
'display_name_only',
],
'remoteDomain duplicate identity' => [
'remoteDomain',
[
spec436RemoteDomainPayload(['Guid' => 'duplicate', 'RemoteDomainId' => 'duplicate', 'Identity' => 'duplicate']),
spec436RemoteDomainPayload(['Guid' => 'duplicate', 'RemoteDomainId' => 'duplicate', 'Identity' => 'duplicate']),
],
'duplicate_stable_identity',
],
'remoteDomain conflicting aliases' => [
'remoteDomain',
[spec436RemoteDomainPayload(['Guid' => 'guid-a', 'RemoteDomainId' => 'guid-b', 'Identity' => 'guid-a'])],
'identity_conflict',
],
'inboundConnector missing identity' => [
'inboundConnector',
[['ConnectorType' => 'Partner', 'Enabled' => true]],
'missing_stable_external_id',
],
]);
it('Spec436 keeps protected inbound connector configuration out of OperationRun while persisting raw payload only in evidence', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec436ReadyProviderConnection($environment);
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec436CaptureRun($environment, $user, $connection, resourceTypes: ['inboundConnector']);
app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'inboundConnector',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec436InboundConnectorPayload([
'SenderIPAddresses' => ['203.0.113.10'],
'SmartHosts' => ['mail.contoso.example'],
'TlsSenderCertificateName' => 'CN=secret-cert',
'Comment' => 'sensitive routing note',
])],
context: ['output_state' => 'structured_collection'],
),
);
$evidence = TenantConfigurationResourceEvidence::query()->sole();
$runJson = json_encode([
'context' => $run->refresh()->context,
'summary_counts' => $run->summary_counts,
'failure_summary' => $run->failure_summary,
], JSON_THROW_ON_ERROR);
$normalizedJson = json_encode($evidence->normalized_payload, JSON_THROW_ON_ERROR);
expect($evidence->raw_payload['SenderIPAddresses'])->toBe(['203.0.113.10'])
->and(data_get($evidence->normalized_payload, 'routing.sender_ip_addresses.value'))->toBe('[redacted]')
->and(data_get($evidence->normalized_payload, 'routing.smart_hosts.value'))->toBe('[redacted]')
->and(data_get($evidence->normalized_payload, 'routing.tls_sender_certificate_name.value'))->toBe('[redacted]')
->and(data_get($evidence->normalized_payload, 'routing.comment.value'))->toBe('[redacted]')
->and($runJson)
->not->toContain('203.0.113.10')
->not->toContain('mail.contoso.example')
->not->toContain('secret-cert')
->not->toContain('sensitive routing note')
->and($normalizedJson)
->not->toContain('203.0.113.10')
->not->toContain('mail.contoso.example')
->not->toContain('secret-cert')
->not->toContain('sensitive routing note');
});
it('Spec436 appends evidence without mutating prior payloads and updates the latest pointer', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec436ReadyProviderConnection($environment);
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec436CaptureRun($environment, $user, $connection);
$adapter = app(ExchangePowerShellEvidenceCaptureAdapter::class);
$adapter->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec436TransportRulePayload(['Enabled' => true, 'WhenChanged' => '2026-07-08T10:00:00Z'])],
context: ['output_state' => 'structured_collection'],
),
);
$firstEvidence = TenantConfigurationResourceEvidence::query()->sole();
$firstHash = $firstEvidence->payload_hash;
$firstPayload = $firstEvidence->normalized_payload;
$adapter->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec436TransportRulePayload(['Enabled' => false, 'WhenChanged' => '2026-07-09T10:00:00Z'])],
context: ['output_state' => 'structured_collection'],
),
);
$resource = TenantConfigurationResource::query()->sole();
$evidenceRows = TenantConfigurationResourceEvidence::query()->orderBy('id')->get();
expect($evidenceRows)->toHaveCount(2)
->and($resource->latest_evidence_id)->toBe((int) $evidenceRows[1]->getKey())
->and($evidenceRows[0]->payload_hash)->toBe($firstHash)
->and($evidenceRows[0]->normalized_payload)->toBe($firstPayload)
->and($evidenceRows[1]->payload_hash)->not->toBe($firstHash);
});
it('Spec436 treats empty collections as zero item summaries without fake evidence', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec436ReadyProviderConnection($environment);
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec436CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[],
context: ['output_state' => 'empty_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_EMPTY_COLLECTION)
->and($result['outcome'])->toBe(CaptureOutcome::Captured->value)
->and($result['item_count'])->toBe(0)
->and($result['summary_counts']['total'])->toBe(0)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec436 readiness and output failures fail closed with no evidence', function (
string $case,
callable $arrange,
ExchangePowerShellInvocationResult $runnerResult,
): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec436ReadyProviderConnection($environment);
$arrange($environment, $connection);
$run = spec436CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: $runnerResult,
);
expect($case)->not->toBe('')
->and($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_PREREQUISITE_BLOCKED)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'missing credential' => [
'missing credential',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {},
ExchangePowerShellInvocationResult::succeeded([spec436TransportRulePayload()], context: ['output_state' => 'structured_collection']),
],
'client secret credential' => [
'client secret credential',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::ClientSecret->value);
},
ExchangePowerShellInvocationResult::succeeded([spec436TransportRulePayload()], context: ['output_state' => 'structured_collection']),
],
'stale permission evidence' => [
'stale permission evidence',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
spec436SeedExchangePowerShellPermission($environment, $connection, [
'last_checked_at' => now()->subDays(31),
'details' => [
'observed_at' => now()->subDays(31)->toJSON(),
'verified_at' => now()->subDays(31)->toJSON(),
],
]);
},
ExchangePowerShellInvocationResult::succeeded([spec436TransportRulePayload()], context: ['output_state' => 'structured_collection']),
],
'missing permission evidence' => [
'missing permission evidence',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
ManagedEnvironmentPermission::query()
->where('managed_environment_id', $environment->getKey())
->where('permission_key', 'Exchange.ManageAsApp')
->delete();
},
ExchangePowerShellInvocationResult::succeeded([spec436TransportRulePayload()], context: ['output_state' => 'structured_collection']),
],
'wrong-scope permission evidence' => [
'wrong-scope permission evidence',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
spec436SeedExchangePowerShellPermission($environment, $connection, [
'details' => [
'provider_connection_id' => 999999,
],
]);
},
ExchangePowerShellInvocationResult::succeeded([spec436TransportRulePayload()], context: ['output_state' => 'structured_collection']),
],
'warning-prefixed output' => [
'warning-prefixed output',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::succeeded([spec436TransportRulePayload()], context: ['output_state' => 'warning_prefixed']),
],
'authentication failure' => [
'authentication failure',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::failed(
reasonCode: 'authentication_failed',
failureCode: 'authentication_failed',
message: 'Authentication failed safely.',
context: ['output_state' => 'authentication_failed'],
),
],
'authorization permission failure' => [
'authorization permission failure',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::failed(
reasonCode: 'permission_denied',
failureCode: 'permission_denied',
message: 'Permission denied safely.',
context: ['output_state' => 'permission_denied'],
),
],
'source unavailable' => [
'source unavailable',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::failed(
reasonCode: 'source_unavailable',
failureCode: 'source_unavailable',
message: 'Source unavailable safely.',
context: ['output_state' => 'source_unavailable'],
),
],
'malformed output' => [
'malformed output',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::succeededPayload('not-json', context: ['output_state' => 'malformed_json_collection']),
],
'scalar output' => [
'scalar output',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::succeededPayload(42, context: ['output_state' => 'scalar_output']),
],
'binary output' => [
'binary output',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::succeededPayload([], context: ['output_state' => 'non_utf8_or_binary']),
],
'oversized output' => [
'oversized output',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::succeededPayload([], context: ['output_state' => 'stdout_oversized']),
],
'non-zero exit' => [
'non-zero exit',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::failed(
reasonCode: 'provider_failed',
failureCode: 'nonzero_exit',
message: 'Non-zero exit safely blocked.',
context: ['output_state' => 'nonzero_exit'],
),
],
'partial output' => [
'partial output',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::failed(
reasonCode: 'partial_output',
failureCode: 'partial_output',
message: 'Partial output safely blocked.',
context: ['output_state' => 'partial_output'],
),
],
'runner timeout' => [
'runner timeout',
function (ManagedEnvironment $environment, ProviderConnection $connection): void {
spec436Credential($connection, ProviderCredentialKind::Certificate->value);
},
ExchangePowerShellInvocationResult::failed(
reasonCode: 'provider_failed',
failureCode: 'timeout',
message: 'Timed out safely.',
context: ['output_state' => 'timeout'],
),
],
]);
it('Spec436 introduces no generic promotion path product surface trigger tenant-id ownership or mini-platform', function (): void {
$adapterSource = file_get_contents(app_path('Services/TenantConfiguration/ExchangePowerShellEvidenceCaptureAdapter.php')) ?: '';
$runtimeFiles = [
app_path('Services/TenantConfiguration/ExchangePowerShellEvidenceCaptureAdapter.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellEvidenceNormalizer.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellTargetEvidenceNormalizer.php'),
app_path('Services/TenantConfiguration/ExchangeTransportRuleEvidenceNormalizer.php'),
app_path('Services/TenantConfiguration/ExchangeRemoteDomainEvidenceNormalizer.php'),
app_path('Services/TenantConfiguration/ExchangeInboundConnectorEvidenceNormalizer.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellHashInputBuilder.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellStructuredOutputEnvelope.php'),
];
$runtimeSource = collect($runtimeFiles)
->map(fn (string $file): string => file_get_contents($file) ?: '')
->implode("\n");
$captureOutcomeValues = array_map(
static fn (CaptureOutcome $outcome): string => $outcome->value,
CaptureOutcome::cases(),
);
expect($adapterSource)
->not->toContain('GenericPayloadNormalizer')
->not->toContain('ExchangeTeamsComparablePayloadNormalizer')
->and($runtimeSource)
->not->toContain('ProviderGateway')
->not->toContain('fake_empty_success')
->not->toContain('customer_ready')
->not->toContain('restore_ready')
->and(preg_match('/\btenant_id\b/', $runtimeSource))->toBe(0)
->and(Schema::hasColumn('tenant_configuration_resources', 'tenant_id'))->toBeFalse()
->and(Schema::hasColumn('tenant_configuration_resource_evidence', 'tenant_id'))->toBeFalse()
->and(glob(app_path('Filament/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(app_path('Livewire/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(base_path('routes/*exchange*')) ?: [])->toBe([])
->and(glob(resource_path('views/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*exchange*evidence*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*tenant*configuration*exchange*')) ?: [])->toBe([])
->and(File::exists(app_path('Jobs/TenantConfiguration/CaptureExchangePowerShellEvidenceJob.php')))->toBeFalse()
->and(File::exists(app_path('Console/Commands/CaptureExchangePowerShellEvidence.php')))->toBeFalse()
->and($captureOutcomeValues)
->toContain(CaptureOutcome::Captured->value)
->not->toContain('customer_ready')
->not->toContain('certified')
->not->toContain('renderable')
->not->toContain('comparable');
});
/**
* @param array<string, mixed> $overrides
*/
function spec436ReadyProviderConnection(
ManagedEnvironment $environment,
array $overrides = [],
bool $seedExchangePowerShellPermission = true,
): ProviderConnection {
$connection = ProviderConnection::factory()
->dedicated()
->consentGranted()
->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'entra_tenant_id' => $environment->providerTenantContext(),
'is_default' => true,
'verification_status' => ProviderVerificationStatus::Healthy->value,
'last_health_check_at' => now(),
...$overrides,
]);
if ($seedExchangePowerShellPermission) {
spec436SeedExchangePowerShellPermission($environment, $connection);
}
return $connection;
}
/**
* @param array<string, mixed> $overrides
*/
function spec436SeedExchangePowerShellPermission(
ManagedEnvironment $environment,
ProviderConnection $connection,
array $overrides = [],
): ManagedEnvironmentPermission {
$details = [
'source' => 'provider_verification',
'observed_at' => now()->toJSON(),
'verified_at' => now()->toJSON(),
'evaluator' => 'exchange_powershell_permission_evidence',
'evaluator_version' => 'v1',
'workspace_id' => (int) $connection->workspace_id,
'managed_environment_id' => (int) $connection->managed_environment_id,
'provider' => (string) $connection->provider,
'provider_connection_id' => (int) $connection->getKey(),
];
if (is_array($overrides['details'] ?? null)) {
$details = array_replace($details, $overrides['details']);
}
unset($overrides['details']);
return ManagedEnvironmentPermission::query()->updateOrCreate(
[
'managed_environment_id' => (int) $environment->getKey(),
'permission_key' => 'Exchange.ManageAsApp',
'workspace_id' => (int) $environment->workspace_id,
],
[
'status' => 'granted',
'details' => $details,
'last_checked_at' => now(),
...$overrides,
],
);
}
/**
* @param array<string, mixed> $overrides
*/
function spec436Credential(ProviderConnection $connection, string $kind, array $overrides = []): ProviderCredential
{
return ProviderCredential::factory()->create([
'provider_connection_id' => (int) $connection->getKey(),
'type' => $kind,
'credential_kind' => $kind,
'source' => ProviderCredentialSource::DedicatedManual->value,
'last_rotated_at' => now(),
'expires_at' => now()->addYear(),
...$overrides,
]);
}
/**
* @param array<string, mixed> $overrides
* @param list<string> $resourceTypes
*/
function spec436CaptureRun(
ManagedEnvironment $environment,
App\Models\User $user,
ProviderConnection $connection,
array $overrides = [],
array $resourceTypes = ['transportRule'],
): OperationRun {
$context = [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => $resourceTypes,
'required_capability' => 'exchange_powershell_invoke',
'runner_mode' => 'fake',
];
if (is_array($overrides['context'] ?? null)) {
$context = array_replace_recursive($context, $overrides['context']);
}
unset($overrides['context']);
return OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => $context,
...$overrides,
]);
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function spec436TransportRulePayload(array $overrides = []): array
{
return [
'RuleId' => 'spec436-rule-1',
'Guid' => 'spec436-rule-1',
'DisplayName' => 'Spec 436 transport rule',
'Name' => 'Spec 436 transport rule',
'Priority' => 0,
'Mode' => 'Enforce',
'State' => 'Enabled',
'Enabled' => true,
'Conditions' => ['SenderDomainIs' => ['contoso.example']],
'Actions' => ['RedirectMessageTo' => ['security@contoso.example']],
'WhenChanged' => 'volatile',
...$overrides,
];
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function spec436RemoteDomainPayload(array $overrides = []): array
{
return [
'RemoteDomainId' => 'spec436-remote-domain-1',
'Guid' => 'spec436-remote-domain-1',
'Identity' => 'spec436-remote-domain-1',
'DomainName' => 'contoso.example',
'DisplayName' => 'Contoso remote domain',
'IsDefault' => false,
'AllowedOOFType' => 'External',
'AutoReplyEnabled' => true,
...$overrides,
];
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function spec436InboundConnectorPayload(array $overrides = []): array
{
return [
'ConnectorId' => 'spec436-inbound-connector-1',
'Guid' => 'spec436-inbound-connector-1',
'Identity' => 'spec436-inbound-connector-1',
'DisplayName' => 'Spec 436 inbound connector',
'Name' => 'Spec 436 inbound connector',
'ConnectorType' => 'Partner',
'Enabled' => true,
'RequireTls' => true,
...$overrides,
];
}