'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); }