TenantAtlas/apps/platform/tests/Unit/Support/TenantConfiguration/Spec437ExchangeComparablePayloadBuilderTest.php
Ahmed Darrazi f8c298632b
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m46s
feat: add exchange comparable payload builder
2026-07-09 21:10:17 +02:00

602 lines
24 KiB
PHP

<?php
declare(strict_types=1);
use App\Services\TenantConfiguration\ExchangePowerShellComparablePayloadBuilder;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
use App\Services\TenantConfiguration\ExchangePowerShellTargetEvidenceNormalizer;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
it('Spec437 builds deterministic comparable payloads from normalized payloads for each Exchange target', function (
string $canonicalType,
array $normalizedPayload,
array $expectedPaths,
array $forbiddenValues,
): void {
$normalizedPayload['raw_payload'] = ['secret' => 'spec437-raw-provider-secret'];
$normalizedPayload['raw_stdout'] = 'spec437-raw-stdout-secret';
$normalizedPayload['raw_stderr'] = 'spec437-raw-stderr-secret';
$normalizedPayload['transcript'] = 'spec437-transcript-secret';
$normalizedPayload['operation_run_context'] = ['token' => 'spec437-run-context-token'];
$result = app(ExchangePowerShellComparablePayloadBuilder::class)->build(
resourceType: $canonicalType,
normalizedPayload: $normalizedPayload,
payloadHash: spec437UnitPayloadHash(),
metadata: [
...spec437UnitEvidenceMetadata(),
'source_identity' => ['field' => 'metadata', 'value' => 'spec437-metadata-override'],
'source_endpoint' => 'exchange_online_powershell_rest:'.$canonicalType,
],
);
expect($result['comparable'])->toBeTrue()
->and($result['blocker'])->toBeNull()
->and($result['payload']['workload'])->toBe('exchange')
->and($result['payload']['resource_type'])->toBe($canonicalType)
->and($result['payload']['compare_input'])->toBe('normalized_payload_only')
->and($result['payload']['identity']['source_key'])->toBe(
$canonicalType.':'.data_get($normalizedPayload, 'source_identity.field').':'.spec437UnitIdentityHmac(
$canonicalType,
(string) data_get($normalizedPayload, 'source_identity.field'),
(string) data_get($normalizedPayload, 'source_identity.value'),
),
)
->and($result['payload']['identity']['value_hmac'])->toBe(spec437UnitIdentityHmac(
$canonicalType,
(string) data_get($normalizedPayload, 'source_identity.field'),
(string) data_get($normalizedPayload, 'source_identity.value'),
))
->and($result['payload']['provenance']['payload_hash'])->toBe(spec437UnitPayloadHash())
->and($result['payload']['source']['source_surface'])->toBe(ExchangePowerShellCommandContracts::SOURCE_SURFACE)
->and($result['payload']['source']['payload_shape_version'])->toBe(ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION);
foreach ($expectedPaths as $path => $value) {
expect(data_get($result['payload'], $path))->toBe($value);
}
$encoded = json_encode($result, JSON_THROW_ON_ERROR);
foreach ($forbiddenValues as $forbiddenValue) {
expect($encoded)->not->toContain($forbiddenValue);
}
expect($encoded)
->not->toContain('spec437-raw-provider-secret')
->not->toContain('spec437-raw-stdout-secret')
->not->toContain('spec437-raw-stderr-secret')
->not->toContain('spec437-transcript-secret')
->not->toContain('spec437-run-context-token')
->not->toContain('spec437-metadata-override')
->not->toContain((string) data_get($normalizedPayload, 'source_identity.value'));
})->with([
'transportRule' => [
'transportRule',
spec437UnitTransportRuleNormalizedPayload(),
[
'material.enabled' => true,
'material.mode' => 'Enforce',
'material.state' => 'Enabled',
'material.priority' => '0',
'conditions.sender_domain_is.redacted' => true,
'conditions.sender_domain_is.count' => 2,
'actions.delete_message' => true,
'actions.set_header_value.redacted' => true,
'exceptions.except_if_from.redacted' => true,
],
[
'contoso.example',
'fabrikam.example',
'subject-secret',
'header-secret',
'except-secret@example.com',
'spec437-rule-guid-1',
],
],
'remoteDomain' => [
'remoteDomain',
spec437UnitRemoteDomainNormalizedPayload(),
[
'remote_domain_class.is_default' => false,
'settings.auto_reply_enabled' => true,
'settings.auto_forward_enabled.redacted' => true,
'settings.mail_tips_access_scope.redacted' => true,
'protected_values.domain_name.redacted' => true,
],
[
'contoso.example',
'mailtips-secret',
'spec437-remote-domain-1',
],
],
'inboundConnector' => [
'inboundConnector',
spec437UnitInboundConnectorNormalizedPayload(),
[
'material.connector_type' => 'Partner',
'material.enabled' => true,
'material.require_tls' => true,
'routing.sender_ip_addresses.redacted' => true,
'routing.sender_ip_addresses.count' => 1,
'routing.smart_hosts.redacted' => true,
'routing.tls_sender_certificate_name.redacted' => true,
'routing.comment.redacted' => true,
],
[
'203.0.113.10',
'mail.contoso.example',
'CN=secret-cert',
'sensitive routing note',
'spec437-inbound-connector-1',
],
],
]);
it('Spec437 blocks unsupported targets missing normalized identity and unsafe remote-domain identity', function (
string $resourceType,
array $normalizedPayload,
string $expectedBlocker,
): void {
$result = app(ExchangePowerShellComparablePayloadBuilder::class)->build(
$resourceType,
$normalizedPayload,
payloadHash: spec437UnitPayloadHash(),
metadata: spec437UnitEvidenceMetadata(),
);
expect($result['comparable'])->toBeFalse()
->and($result['blocker'])->toBe($expectedBlocker)
->and($result['payload'])->toBeNull();
})->with([
'unsupported target' => ['acceptedDomain', spec437UnitTransportRuleNormalizedPayload(), 'unsupported_resource_type'],
'missing normalized payload' => ['transportRule', [], 'missing_normalized_payload'],
'wrong canonical type' => ['transportRule', spec437UnitRemoteDomainNormalizedPayload(), 'normalized_payload_type_mismatch'],
'missing source identity' => ['transportRule', array_diff_key(spec437UnitTransportRuleNormalizedPayload(), ['source_identity' => true]), 'missing_stable_identity'],
'remote domain domain-only identity' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'source_identity' => ['field' => 'DomainName', 'value' => 'contoso.example'],
]),
'unsafe_identity',
],
'remote domain display-only identity' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'source_identity' => ['field' => 'DisplayName', 'value' => 'Contoso remote domain'],
]),
'unsafe_identity',
],
'remote domain snake-case domain identity' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'source_identity' => ['field' => 'domain_name', 'value' => 'contoso.example'],
]),
'unsafe_identity',
],
'remote domain uppercase display identity' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'source_identity' => ['field' => 'DISPLAY_NAME', 'value' => 'Contoso remote domain'],
]),
'unsafe_identity',
],
]);
it('Spec437 blocks non content-backed or uncaptured evidence provenance', function (
?string $payloadHash,
array $metadata,
string $expectedBlocker,
): void {
$result = app(ExchangePowerShellComparablePayloadBuilder::class)->build(
'transportRule',
spec437UnitTransportRuleNormalizedPayload(),
payloadHash: $payloadHash,
metadata: $metadata,
);
expect($result['comparable'])->toBeFalse()
->and($result['blocker'])->toBe($expectedBlocker)
->and($result['payload'])->toBeNull();
})->with([
'missing payload hash' => [null, spec437UnitEvidenceMetadata(), 'missing_payload_hash'],
'invalid payload hash' => ['spec437-source-payload-hash', spec437UnitEvidenceMetadata(), 'invalid_payload_hash'],
'wrong evidence state' => [
spec437UnitPayloadHash(),
[...spec437UnitEvidenceMetadata(), 'evidence_state' => EvidenceState::NotCaptured->value],
'evidence_not_content_backed',
],
'wrong coverage level' => [
spec437UnitPayloadHash(),
[...spec437UnitEvidenceMetadata(), 'coverage_level' => CoverageLevel::Detected->value],
'coverage_not_content_backed',
],
'wrong capture outcome' => [
spec437UnitPayloadHash(),
[...spec437UnitEvidenceMetadata(), 'capture_outcome' => CaptureOutcome::BlockedUnsupported->value],
'capture_not_captured',
],
]);
it('Spec437 blocks unsafe exact values in protected comparable sections', function (
string $resourceType,
array $normalizedPayload,
string $forbiddenValue,
): void {
$result = app(ExchangePowerShellComparablePayloadBuilder::class)->build(
$resourceType,
$normalizedPayload,
payloadHash: spec437UnitPayloadHash(),
metadata: spec437UnitEvidenceMetadata(),
);
$encoded = json_encode($result, JSON_THROW_ON_ERROR);
expect($result['comparable'])->toBeFalse()
->and($result['blocker'])->toBe('unsafe_exact_value')
->and($result['payload'])->toBeNull()
->and($encoded)->not->toContain($forbiddenValue);
})->with([
'transportRule raw condition' => [
'transportRule',
array_replace_recursive(spec437UnitTransportRuleNormalizedPayload(), [
'conditions' => ['sender_domain_is' => 'contoso.example'],
]),
'contoso.example',
],
'transportRule raw action' => [
'transportRule',
array_replace_recursive(spec437UnitTransportRuleNormalizedPayload(), [
'actions' => ['set_header_value' => 'header-secret'],
]),
'header-secret',
],
'remoteDomain raw setting' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'settings' => ['mail_tips_access_scope' => 'mailtips-secret'],
]),
'mailtips-secret',
],
'remoteDomain raw protected value' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'protected_configuration' => ['domain_name' => 'contoso.example'],
]),
'contoso.example',
],
'inboundConnector raw routing' => [
'inboundConnector',
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'routing' => ['sender_ip_addresses' => '203.0.113.10'],
]),
'203.0.113.10',
],
'inboundConnector raw protected value' => [
'inboundConnector',
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'protected_configuration' => ['sender_ip_addresses' => '203.0.113.10'],
]),
'203.0.113.10',
],
]);
it('Spec437 blocks malformed exact values on allowlisted comparable fields', function (
string $resourceType,
array $normalizedPayload,
string $forbiddenValue,
): void {
$result = app(ExchangePowerShellComparablePayloadBuilder::class)->build(
$resourceType,
$normalizedPayload,
payloadHash: spec437UnitPayloadHash(),
metadata: spec437UnitEvidenceMetadata(),
);
$encoded = json_encode($result, JSON_THROW_ON_ERROR);
expect($result['comparable'])->toBeFalse()
->and($result['blocker'])->toBe('unsafe_exact_value')
->and($result['payload'])->toBeNull()
->and($encoded)->not->toContain($forbiddenValue);
})->with([
'transportRule malformed bool action' => [
'transportRule',
array_replace_recursive(spec437UnitTransportRuleNormalizedPayload(), [
'actions' => ['delete_message' => 'header-secret'],
]),
'header-secret',
],
'transportRule malformed enum action' => [
'transportRule',
array_replace_recursive(spec437UnitTransportRuleNormalizedPayload(), [
'actions' => ['apply_html_disclaimer_fallback_action' => 'secret-fallback'],
]),
'secret-fallback',
],
'transportRule malformed priority' => [
'transportRule',
array_replace_recursive(spec437UnitTransportRuleNormalizedPayload(), [
'rule_order' => ['priority' => 'priority-secret'],
]),
'priority-secret',
],
'remoteDomain malformed bool setting' => [
'remoteDomain',
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'settings' => ['auto_reply_enabled' => 'mailtips-secret'],
]),
'mailtips-secret',
],
'inboundConnector malformed enum routing' => [
'inboundConnector',
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'routing' => ['connector_type' => 'mail.contoso.example'],
]),
'mail.contoso.example',
],
'inboundConnector malformed bool material' => [
'inboundConnector',
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'material' => ['RequireTls' => 'tls-secret'],
]),
'tls-secret',
],
]);
it('Spec437 comparable hash changes only for material or safe redacted-state changes', function (): void {
$builder = app(ExchangePowerShellComparablePayloadBuilder::class);
$base = spec437UnitTransportRuleNormalizedPayload();
$volatileOnly = array_replace_recursive($base, [
'diagnostics' => [
'volatile_fields' => ['RunspaceId', 'WhenChanged', 'ExchangeVersion'],
],
'runtime_context' => [
'WhenChanged' => '2026-07-09T10:00:00Z',
],
]);
$materialChanged = array_replace_recursive($base, [
'material' => [
'Enabled' => false,
],
]);
$redactedExactChangedSameCount = array_replace_recursive($base, [
'conditions' => [
'sender_domain_is' => spec437UnitRedactedMarker(count: 2),
],
]);
$redactedCountChanged = array_replace_recursive($base, [
'conditions' => [
'sender_domain_is' => spec437UnitRedactedMarker(count: 3),
],
]);
$baseResult = spec437UnitBuild($builder, 'transportRule', $base);
expect($baseResult['comparable_hash'])->toBe(spec437UnitBuild($builder, 'transportRule', $volatileOnly)['comparable_hash'])
->and($baseResult['comparable_hash'])->not->toBe(spec437UnitBuild($builder, 'transportRule', $materialChanged)['comparable_hash'])
->and($baseResult['comparable_hash'])->toBe(spec437UnitBuild($builder, 'transportRule', $redactedExactChangedSameCount)['comparable_hash'])
->and($baseResult['comparable_hash'])->not->toBe(spec437UnitBuild($builder, 'transportRule', $redactedCountChanged)['comparable_hash']);
});
it('Spec437 comparable hash behavior is deterministic for remoteDomain and inboundConnector targets', function (
string $resourceType,
array $base,
array $volatileOnly,
array $materialChanged,
array $redactedCountChanged,
): void {
$builder = app(ExchangePowerShellComparablePayloadBuilder::class);
$baseResult = spec437UnitBuild($builder, $resourceType, $base);
expect($baseResult['comparable_hash'])->toBe(spec437UnitBuild($builder, $resourceType, $volatileOnly)['comparable_hash'])
->and($baseResult['comparable_hash'])->not->toBe(spec437UnitBuild($builder, $resourceType, $materialChanged)['comparable_hash'])
->and($baseResult['comparable_hash'])->not->toBe(spec437UnitBuild($builder, $resourceType, $redactedCountChanged)['comparable_hash']);
})->with([
'remoteDomain' => [
'remoteDomain',
spec437UnitRemoteDomainNormalizedPayload(),
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'diagnostics' => ['volatile_fields' => ['RunspaceId', 'WhenChanged', 'ExchangeVersion']],
'runtime_context' => ['WhenChanged' => '2026-07-09T10:00:00Z'],
]),
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'settings' => ['auto_reply_enabled' => false],
]),
array_replace_recursive(spec437UnitRemoteDomainNormalizedPayload(), [
'protected_configuration' => ['domain_name' => spec437UnitRedactedMarker(count: 2)],
]),
],
'inboundConnector' => [
'inboundConnector',
spec437UnitInboundConnectorNormalizedPayload(),
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'diagnostics' => ['volatile_fields' => ['RunspaceId', 'WhenChanged', 'ExchangeVersion']],
'runtime_context' => ['WhenChanged' => '2026-07-09T10:00:00Z'],
]),
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'material' => ['RequireTls' => false],
'routing' => ['require_tls' => false],
]),
array_replace_recursive(spec437UnitInboundConnectorNormalizedPayload(), [
'routing' => ['sender_ip_addresses' => spec437UnitRedactedMarker(count: 2)],
'protected_configuration' => ['sender_ip_addresses' => spec437UnitRedactedMarker(count: 2)],
]),
],
]);
function spec437UnitTransportRuleNormalizedPayload(): array
{
return [
'canonical_type' => 'transportRule',
'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE,
'payload_shape_version' => ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
'normalizer_version' => 'exchange-transport-rule-readiness-v1',
'source_identity' => ['field' => 'Guid', 'value' => 'spec437-rule-guid-1'],
'material' => [
'DisplayName' => 'Spec 437 transport rule',
'Enabled' => true,
'Mode' => 'Enforce',
'State' => 'Enabled',
],
'rule_order' => [
'priority' => '0',
'order_is_identity' => false,
],
'conditions' => [
'sender_domain_is' => spec437UnitRedactedMarker(count: 2),
'subject_contains_words' => spec437UnitRedactedMarker(count: 1),
],
'actions' => [
'delete_message' => true,
'set_header_value' => spec437UnitRedactedMarker(count: 1),
],
'exceptions' => [
'except_if_from' => spec437UnitRedactedMarker(count: 1),
],
'protected_configuration' => [
'sender_domain_is' => spec437UnitRedactedMarker(count: 2),
],
'sensitive_content' => [
'subject_contains_words' => spec437UnitRedactedMarker(count: 1),
],
'diagnostics' => [
'volatile_fields' => ['WhenChanged', 'ExchangeVersion', 'RunspaceId'],
'protected_fields' => ['sender_domain_is'],
'sensitive_fields' => ['subject_contains_words'],
],
'source' => spec437UnitSourceMetadata('transportRule', 'Get-TransportRule', 'exchange-transport-rule-readiness-v1'),
];
}
function spec437UnitRemoteDomainNormalizedPayload(): array
{
return [
'canonical_type' => 'remoteDomain',
'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE,
'payload_shape_version' => ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
'normalizer_version' => 'exchange-remote-domain-readiness-v1',
'source_identity' => ['field' => 'Identity', 'value' => 'spec437-remote-domain-1'],
'material' => [
'Identity' => 'spec437-remote-domain-1',
'IsDefault' => false,
],
'remote_domain_class' => [
'domain_name_is_identity' => false,
'is_default' => false,
],
'settings' => [
'auto_reply_enabled' => true,
'auto_forward_enabled' => spec437UnitRedactedMarker(count: 1),
'mail_tips_access_scope' => spec437UnitRedactedMarker(count: 1),
],
'protected_configuration' => [
'domain_name' => spec437UnitRedactedMarker(count: 1),
],
'sensitive_content' => [
'mail_tips_access_scope' => spec437UnitRedactedMarker(count: 1),
],
'diagnostics' => [
'volatile_fields' => ['WhenChanged', 'ExchangeVersion', 'RunspaceId'],
'protected_fields' => ['domain_name'],
'sensitive_fields' => ['mail_tips_access_scope'],
],
'source' => spec437UnitSourceMetadata('remoteDomain', 'Get-RemoteDomain', 'exchange-remote-domain-readiness-v1'),
];
}
function spec437UnitInboundConnectorNormalizedPayload(): array
{
return [
'canonical_type' => 'inboundConnector',
'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE,
'payload_shape_version' => ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
'normalizer_version' => 'exchange-inbound-connector-readiness-v1',
'source_identity' => ['field' => 'ConnectorId', 'value' => 'spec437-inbound-connector-1'],
'material' => [
'ConnectorType' => 'Partner',
'Enabled' => true,
'RequireTls' => true,
],
'routing' => [
'connector_type' => 'Partner',
'enabled' => true,
'require_tls' => true,
'sender_ip_addresses' => spec437UnitRedactedMarker(count: 1),
'smart_hosts' => spec437UnitRedactedMarker(count: 1),
'tls_sender_certificate_name' => spec437UnitRedactedMarker(count: 1),
'comment' => spec437UnitRedactedMarker(count: 1),
],
'protected_configuration' => [
'sender_ip_addresses' => spec437UnitRedactedMarker(count: 1),
],
'sensitive_content' => [
'smart_hosts' => spec437UnitRedactedMarker(count: 1),
'tls_sender_certificate_name' => spec437UnitRedactedMarker(count: 1),
'comment' => spec437UnitRedactedMarker(count: 1),
],
'diagnostics' => [
'volatile_fields' => ['WhenChanged', 'ExchangeVersion', 'RunspaceId'],
'protected_fields' => ['sender_ip_addresses'],
'sensitive_fields' => ['smart_hosts', 'tls_sender_certificate_name', 'comment'],
],
'source' => spec437UnitSourceMetadata('inboundConnector', 'Get-InboundConnector', 'exchange-inbound-connector-readiness-v1'),
];
}
function spec437UnitRedactedMarker(int $count = 1, bool $present = true): array
{
return [
'value' => '[redacted]',
'present' => $present,
'count' => $count,
];
}
function spec437UnitSourceMetadata(string $canonicalType, string $commandName, string $normalizerVersion): array
{
return [
'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE,
'source_contract_key' => 'exchange_powershell.'.$canonicalType,
'command_contract_name' => $commandName,
'command_contract_version' => ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION,
'payload_shape_version' => ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
'normalizer_version' => $normalizerVersion,
];
}
function spec437UnitEvidenceMetadata(): array
{
return [
'evidence_state' => EvidenceState::ContentBacked->value,
'coverage_level' => CoverageLevel::ContentBacked->value,
'capture_outcome' => CaptureOutcome::Captured->value,
];
}
function spec437UnitBuild(ExchangePowerShellComparablePayloadBuilder $builder, string $resourceType, array $normalizedPayload): array
{
return $builder->build(
$resourceType,
$normalizedPayload,
payloadHash: spec437UnitPayloadHash(),
metadata: spec437UnitEvidenceMetadata(),
);
}
function spec437UnitPayloadHash(): string
{
return str_repeat('a', 64);
}
function spec437UnitIdentityHmac(string $resourceType, string $field, string $value): string
{
$key = (string) config('app.key');
if (str_starts_with($key, 'base64:')) {
$decoded = base64_decode(substr($key, 7), true);
$key = $decoded !== false ? $decoded : '';
}
return hash_hmac('sha256', $resourceType."\0".$field."\0".$value, $key);
}