feat: add exchange comparable payload builder
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m46s

This commit is contained in:
Ahmed Darrazi 2026-07-09 21:10:17 +02:00
parent bcec737cd4
commit f8c298632b
3 changed files with 1548 additions and 0 deletions

View File

@ -0,0 +1,674 @@
<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use BackedEnum;
final class ExchangePowerShellComparablePayloadBuilder
{
public const string PAYLOAD_SHAPE_VERSION = 'exchange-powershell-comparable-payload-v1';
/**
* @var list<string>
*/
private const SUPPORTED_RESOURCE_TYPES = [
'transportRule',
'remoteDomain',
'inboundConnector',
];
/**
* @var list<string>
*/
private const UNSAFE_REMOTE_DOMAIN_IDENTITY_FIELDS = [
'domainname',
'displayname',
'name',
];
/**
* Fields listed here may keep exact normalized values in target sections
* only when the value matches the expected safe scalar shape. All other
* values in those sections must already be redacted markers.
*
* @var array<string, array<string, array<string, string|array{enum: list<string>}>>>
*/
private const SAFE_EXACT_VALUE_POLICIES = [
'transportRule' => [
'material' => [
'enabled' => 'bool',
'mode' => ['enum' => ['enforce', 'audit', 'auditandnotify']],
'state' => ['enum' => ['enabled', 'disabled']],
],
'rule_order' => [
'priority' => 'non_negative_integer',
],
'conditions' => [],
'actions' => [
'delete_message' => 'bool',
'apply_html_disclaimer_fallback_action' => ['enum' => ['wrap', 'ignore', 'reject']],
'apply_html_disclaimer_location' => ['enum' => ['append', 'prepend']],
],
'exceptions' => [],
'protected_configuration' => [],
'sensitive_content' => [],
],
'remoteDomain' => [
'remote_domain_class' => [
'is_default' => 'bool',
'domain_name_is_identity' => 'bool',
],
'settings' => [
'auto_reply_enabled' => 'bool',
],
'protected_configuration' => [],
'sensitive_content' => [],
],
'inboundConnector' => [
'material' => [
'connector_type' => ['enum' => ['partner', 'onpremises']],
'enabled' => 'bool',
'require_tls' => 'bool',
],
'routing' => [
'connector_type' => ['enum' => ['partner', 'onpremises']],
'enabled' => 'bool',
'require_tls' => 'bool',
],
'protected_configuration' => [],
'sensitive_content' => [],
],
];
/**
* These sections are only read through explicit fields in target payload builders.
* Unknown keys in these sections are ignored by output and therefore do not need
* leakage blocking.
*
* @var array<string, list<string>>
*/
private const PARTIAL_EXACT_VALUE_SECTIONS = [
'transportRule' => [
'material',
'rule_order',
],
'inboundConnector' => [
'material',
],
];
/**
* @param array<string, mixed> $normalizedPayload
* @param array<string, mixed> $metadata
* @return array{comparable: bool, blocker: string|null, resource_type: string, payload: array<string, mixed>|null, comparable_hash?: string}
*/
public function build(
string $resourceType,
array $normalizedPayload,
?string $payloadHash = null,
array $metadata = [],
): array {
$resourceType = trim($resourceType);
if (! in_array($resourceType, self::SUPPORTED_RESOURCE_TYPES, true)) {
return $this->blocked($resourceType, 'unsupported_resource_type');
}
if ($normalizedPayload === []) {
return $this->blocked($resourceType, 'missing_normalized_payload');
}
if (($normalizedPayload['canonical_type'] ?? null) !== $resourceType) {
return $this->blocked($resourceType, 'normalized_payload_type_mismatch');
}
if (($normalizedPayload['payload_shape_version'] ?? null) !== ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION) {
return $this->blocked($resourceType, 'unsupported_normalized_payload_shape');
}
$contentBackedBlocker = $this->contentBackedBlocker($payloadHash, $metadata);
if ($contentBackedBlocker !== null) {
return $this->blocked($resourceType, $contentBackedBlocker);
}
$identity = $this->sourceIdentity($normalizedPayload);
if ($identity === null) {
return $this->blocked($resourceType, 'missing_stable_identity');
}
if ($resourceType === 'remoteDomain' && in_array($this->canonicalFieldKey($identity['field']), self::UNSAFE_REMOTE_DOMAIN_IDENTITY_FIELDS, true)) {
return $this->blocked($resourceType, 'unsafe_identity');
}
if ($this->identityHmacKey() === null) {
return $this->blocked($resourceType, 'missing_identity_hash_key');
}
if ($this->firstUnsafeExactValuePath($resourceType, $normalizedPayload) !== null) {
return $this->blocked($resourceType, 'unsafe_exact_value');
}
$payload = $this->sortAssociative([
'workload' => 'exchange',
'resource_type' => $resourceType,
'comparable_payload_shape_version' => self::PAYLOAD_SHAPE_VERSION,
'compare_input' => 'normalized_payload_only',
'identity' => $this->comparableIdentity($resourceType, $identity),
...$this->targetPayload($resourceType, $normalizedPayload),
'redaction' => $this->redactionSummary($normalizedPayload),
'source' => $this->sourceMetadata($normalizedPayload),
'provenance' => $this->provenance($payloadHash, $metadata),
]);
return [
'comparable' => true,
'blocker' => null,
'resource_type' => $resourceType,
'payload' => $payload,
'comparable_hash' => $this->hashComparablePayload($payload),
];
}
/**
* @return array{field: string, value: string}
*/
private function sourceIdentity(array $normalizedPayload): ?array
{
$identity = $normalizedPayload['source_identity'] ?? null;
if (! is_array($identity)) {
return null;
}
$field = $this->stringValue($identity['field'] ?? null);
$value = $this->stringValue($identity['value'] ?? null);
if ($field === null || $value === null) {
return null;
}
return [
'field' => $field,
'value' => $value,
];
}
/**
* @param array{field: string, value: string} $identity
* @return array<string, mixed>
*/
private function comparableIdentity(string $resourceType, array $identity): array
{
$valueHmac = $this->identityHmac($resourceType, $identity);
return [
'field' => $identity['field'],
'value_hmac' => $valueHmac,
'source_key' => $resourceType.':'.$identity['field'].':'.$valueHmac,
];
}
/**
* @param array<string, mixed> $metadata
*/
private function contentBackedBlocker(?string $payloadHash, array $metadata): ?string
{
$payloadHash = $this->stringValue($payloadHash);
if ($payloadHash === null) {
return 'missing_payload_hash';
}
if (! preg_match('/^[a-f0-9]{64}$/', $payloadHash)) {
return 'invalid_payload_hash';
}
if ($this->stringValue($metadata['evidence_state'] ?? null) !== EvidenceState::ContentBacked->value) {
return 'evidence_not_content_backed';
}
if ($this->stringValue($metadata['coverage_level'] ?? null) !== CoverageLevel::ContentBacked->value) {
return 'coverage_not_content_backed';
}
if ($this->stringValue($metadata['capture_outcome'] ?? null) !== CaptureOutcome::Captured->value) {
return 'capture_not_captured';
}
return null;
}
/**
* @param array{field: string, value: string} $identity
*/
private function identityHmac(string $resourceType, array $identity): string
{
return hash_hmac(
'sha256',
$resourceType."\0".$identity['field']."\0".$identity['value'],
(string) $this->identityHmacKey(),
);
}
private function identityHmacKey(): ?string
{
$key = $this->stringValue(config('app.key'));
if ($key === null) {
return null;
}
if (str_starts_with($key, 'base64:')) {
$decoded = base64_decode(substr($key, 7), true);
return $decoded !== false && $decoded !== '' ? $decoded : null;
}
return $key;
}
/**
* @param array<string, mixed> $normalizedPayload
*/
private function firstUnsafeExactValuePath(string $resourceType, array $normalizedPayload): ?string
{
foreach (self::SAFE_EXACT_VALUE_POLICIES[$resourceType] as $section => $safeFields) {
$sectionValue = $this->arrayValue($normalizedPayload[$section] ?? null);
if (in_array($section, self::PARTIAL_EXACT_VALUE_SECTIONS[$resourceType] ?? [], true)) {
foreach ($safeFields as $field => $policy) {
$value = $this->sectionValueByCanonicalField($sectionValue, (string) $field);
if ($value === null || $this->isRedactedMarker($value)) {
continue;
}
if (! $this->safeExactValue($policy, $value)) {
return $section.'.'.(string) $field;
}
}
continue;
}
foreach ($sectionValue as $field => $value) {
if ($value === null || $this->isRedactedMarker($value)) {
continue;
}
$policy = $this->exactValuePolicy($safeFields, (string) $field);
if ($policy === null || ! $this->safeExactValue($policy, $value)) {
return $section.'.'.(string) $field;
}
}
}
return null;
}
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
private function targetPayload(string $resourceType, array $normalizedPayload): array
{
return match ($resourceType) {
'transportRule' => $this->transportRulePayload($normalizedPayload),
'remoteDomain' => $this->remoteDomainPayload($normalizedPayload),
'inboundConnector' => $this->inboundConnectorPayload($normalizedPayload),
};
}
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
private function transportRulePayload(array $normalizedPayload): array
{
$material = $this->arrayValue($normalizedPayload['material'] ?? null);
return [
'material' => $this->sortAssociative([
'enabled' => $this->sanitizeComparableValue($material['Enabled'] ?? $material['enabled'] ?? null),
'mode' => $this->sanitizeComparableValue($material['Mode'] ?? $material['mode'] ?? null),
'state' => $this->sanitizeComparableValue($material['State'] ?? $material['state'] ?? null),
'priority' => $this->sanitizeComparableValue(data_get($normalizedPayload, 'rule_order.priority')),
]),
'conditions' => $this->sanitizeComparableMap($normalizedPayload['conditions'] ?? []),
'actions' => $this->sanitizeComparableMap($normalizedPayload['actions'] ?? []),
'exceptions' => $this->sanitizeComparableMap($normalizedPayload['exceptions'] ?? []),
'protected_values' => $this->sanitizeComparableMap($normalizedPayload['protected_configuration'] ?? []),
'sensitive_values' => $this->sanitizeComparableMap($normalizedPayload['sensitive_content'] ?? []),
];
}
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
private function remoteDomainPayload(array $normalizedPayload): array
{
return [
'remote_domain_class' => $this->sanitizeComparableMap($normalizedPayload['remote_domain_class'] ?? []),
'settings' => $this->sanitizeComparableMap($normalizedPayload['settings'] ?? []),
'protected_values' => $this->sanitizeComparableMap($normalizedPayload['protected_configuration'] ?? []),
'sensitive_values' => $this->sanitizeComparableMap($normalizedPayload['sensitive_content'] ?? []),
];
}
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
private function inboundConnectorPayload(array $normalizedPayload): array
{
$material = $this->arrayValue($normalizedPayload['material'] ?? null);
return [
'material' => $this->sortAssociative([
'connector_type' => $this->sanitizeComparableValue($material['ConnectorType'] ?? $material['connector_type'] ?? null),
'enabled' => $this->sanitizeComparableValue($material['Enabled'] ?? $material['enabled'] ?? null),
'require_tls' => $this->sanitizeComparableValue($material['RequireTls'] ?? $material['require_tls'] ?? null),
]),
'routing' => $this->sanitizeComparableMap($normalizedPayload['routing'] ?? []),
'protected_values' => $this->sanitizeComparableMap($normalizedPayload['protected_configuration'] ?? []),
'sensitive_values' => $this->sanitizeComparableMap($normalizedPayload['sensitive_content'] ?? []),
];
}
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
private function redactionSummary(array $normalizedPayload): array
{
return $this->sortAssociative([
'protected_fields' => $this->stringList(data_get($normalizedPayload, 'diagnostics.protected_fields', [])),
'sensitive_fields' => $this->stringList(data_get($normalizedPayload, 'diagnostics.sensitive_fields', [])),
'volatile_fields_ignored' => $this->stringList(data_get($normalizedPayload, 'diagnostics.volatile_fields', [])),
'protected_values_use_safe_markers' => true,
]);
}
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
private function sourceMetadata(array $normalizedPayload): array
{
$source = $this->arrayValue($normalizedPayload['source'] ?? null);
return $this->sortAssociative([
'source_surface' => $this->stringValue($source['source_surface'] ?? $normalizedPayload['source_surface'] ?? null),
'source_contract_key' => $this->stringValue($source['source_contract_key'] ?? null),
'command_contract_name' => $this->stringValue($source['command_contract_name'] ?? null),
'command_contract_version' => $this->stringValue($source['command_contract_version'] ?? null),
'payload_shape_version' => $this->stringValue($source['payload_shape_version'] ?? $normalizedPayload['payload_shape_version'] ?? null),
'normalizer_version' => $this->stringValue($source['normalizer_version'] ?? $normalizedPayload['normalizer_version'] ?? null),
]);
}
/**
* @param array<string, mixed> $metadata
* @return array<string, mixed>
*/
private function provenance(?string $payloadHash, array $metadata): array
{
return $this->sortAssociative([
'payload_hash' => $this->stringValue($payloadHash),
'source_contract_key' => $this->stringValue($metadata['source_contract_key'] ?? null),
'source_endpoint' => $this->stringValue($metadata['source_endpoint'] ?? null),
'source_version' => $this->stringValue($metadata['source_version'] ?? null),
'source_schema_hash' => $this->stringValue($metadata['source_schema_hash'] ?? null),
'evidence_state' => $this->stringValue($metadata['evidence_state'] ?? null),
'coverage_level' => $this->stringValue($metadata['coverage_level'] ?? null),
'capture_outcome' => $this->stringValue($metadata['capture_outcome'] ?? null),
]);
}
/**
* @param array<string, mixed> $payload
*/
private function hashComparablePayload(array $payload): string
{
unset($payload['provenance']);
return hash('sha256', json_encode($this->sortAssociative($payload), JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
}
private function sanitizeComparableValue(mixed $value): mixed
{
if ($this->isRedactedMarker($value)) {
$present = (bool) ($value['present'] ?? false);
$count = is_numeric($value['count'] ?? null) ? (int) $value['count'] : ($present ? 1 : 0);
return [
'redacted' => true,
'present' => $present,
'count' => max(0, $count),
'state' => $present ? ($count > 1 ? 'present_multiple' : 'present') : 'absent',
];
}
if (is_bool($value) || is_int($value) || is_float($value)) {
return $value;
}
if (is_string($value)) {
$value = trim($value);
return $value !== '' ? $value : null;
}
if (! is_array($value)) {
return null;
}
if (array_is_list($value)) {
$items = array_values(array_filter(
array_map(fn (mixed $item): mixed => $this->sanitizeComparableValue($item), $value),
static fn (mixed $item): bool => $item !== null && $item !== '',
));
usort($items, static fn (mixed $left, mixed $right): int => strcmp(
json_encode($left, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
json_encode($right, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
));
return $items;
}
return $this->sanitizeComparableMap($value);
}
/**
* @param mixed $value
* @return array<string, mixed>
*/
private function sanitizeComparableMap(mixed $value): array
{
$value = $this->arrayValue($value);
$sanitized = [];
foreach ($value as $key => $item) {
$sanitized[(string) $key] = $this->sanitizeComparableValue($item);
}
return $this->sortAssociative($sanitized);
}
private function isRedactedMarker(mixed $value): bool
{
if (! is_array($value) || array_is_list($value)) {
return false;
}
return ($value['value'] ?? null) === '[redacted]' || ($value['redacted'] ?? null) === true;
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function sortAssociative(array $payload): array
{
$payload = array_filter($payload, static fn (mixed $value): bool => $value !== null);
ksort($payload, SORT_NATURAL | SORT_FLAG_CASE);
foreach ($payload as $key => $value) {
if (is_array($value) && ! array_is_list($value)) {
$payload[$key] = $this->sortAssociative($value);
}
}
return $payload;
}
/**
* @return array<string, mixed>
*/
private function arrayValue(mixed $value): array
{
if (! is_array($value) || array_is_list($value)) {
return [];
}
return $value;
}
private function stringValue(mixed $value): ?string
{
if ($value instanceof BackedEnum) {
$value = $value->value;
}
if (! is_scalar($value)) {
return null;
}
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
/**
* @param array<string, mixed> $payload
*/
private function sectionValueByCanonicalField(array $payload, string $field): mixed
{
$field = $this->canonicalFieldKey($field);
foreach ($payload as $candidateField => $value) {
if ($this->canonicalFieldKey((string) $candidateField) === $field) {
return $value;
}
}
return null;
}
/**
* @param array<string, string|array{enum: list<string>}> $policies
* @return string|array{enum: list<string>}|null
*/
private function exactValuePolicy(array $policies, string $field): string|array|null
{
$field = $this->canonicalFieldKey($field);
foreach ($policies as $policyField => $policy) {
if ($this->canonicalFieldKey((string) $policyField) === $field) {
return $policy;
}
}
return null;
}
/**
* @param string|array{enum: list<string>} $policy
*/
private function safeExactValue(string|array $policy, mixed $value): bool
{
if ($policy === 'bool') {
return is_bool($value);
}
if ($policy === 'non_negative_integer') {
if (is_int($value)) {
return $value >= 0;
}
if (! is_string($value)) {
return false;
}
$value = trim($value);
return $value !== '' && ctype_digit($value);
}
if (is_array($policy) && array_key_exists('enum', $policy)) {
$value = $this->stringValue($value);
if ($value === null) {
return false;
}
$allowed = array_map(fn (string $item): string => $this->canonicalFieldKey($item), $policy['enum']);
return in_array($this->canonicalFieldKey($value), $allowed, true);
}
return false;
}
private function canonicalFieldKey(string $field): string
{
return strtolower(preg_replace('/[^A-Za-z0-9]+/', '', $field) ?? '');
}
/**
* @return list<string>
*/
private function stringList(mixed $value): array
{
if (! is_array($value)) {
return [];
}
$values = array_values(array_filter(
array_map(static fn (mixed $item): string => is_string($item) ? trim($item) : '', $value),
static fn (string $item): bool => $item !== '',
));
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return $values;
}
/**
* @return array{comparable: false, blocker: string, resource_type: string, payload: null}
*/
private function blocked(string $resourceType, string $blocker): array
{
return [
'comparable' => false,
'blocker' => $blocker,
'resource_type' => $resourceType,
'payload' => null,
];
}
}

View File

@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Models\TenantConfigurationResourceType;
use App\Services\TenantConfiguration\ExchangePowerShellComparablePayloadBuilder;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
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\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\IdentityState;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
beforeEach(function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
});
it('Spec437 derives comparable payloads without mutating evidence resource resource-type or OperationRun truth', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$resourceType = spec437FeatureResourceType('transportRule');
$operationRun = OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'context' => [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => ['transportRule'],
],
]);
$resource = TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'canonical_type' => 'transportRule',
'latest_evidence_state' => EvidenceState::ContentBacked->value,
'latest_identity_state' => IdentityState::Stable->value,
'latest_claim_state' => ClaimState::InternalOnly->value,
'latest_payload_hash' => spec437FeaturePayloadHash(),
]);
$evidence = TenantConfigurationResourceEvidence::factory()->create([
'resource_id' => (int) $resource->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'operation_run_id' => (int) $operationRun->getKey(),
'source_contract_key' => 'exchange_powershell.transportRule',
'source_endpoint' => 'exchange_online_powershell_rest:Get-TransportRule',
'source_metadata' => ['spec' => 437, 'source_identity' => 'must-not-override'],
'raw_payload' => ['Guid' => 'spec437-rule-guid-1', 'SubjectContainsWords' => ['secret-subject']],
'normalized_payload' => spec437FeatureTransportRuleNormalizedPayload(),
'payload_hash' => spec437FeaturePayloadHash(),
'permission_context' => ['provider_connection_id' => (int) $connection->getKey()],
'evidence_state' => EvidenceState::ContentBacked->value,
'coverage_level' => CoverageLevel::ContentBacked->value,
'capture_outcome' => CaptureOutcome::Captured->value,
]);
$resource->forceFill([
'latest_evidence_id' => (int) $evidence->getKey(),
'latest_payload_hash' => spec437FeaturePayloadHash(),
])->save();
$beforeEvidence = $evidence->fresh();
$beforeResource = $resource->fresh();
$beforeResourceType = $resourceType->fresh();
$beforeOperationRun = $operationRun->fresh();
$result = app(ExchangePowerShellComparablePayloadBuilder::class)->build(
resourceType: 'transportRule',
normalizedPayload: $beforeEvidence->normalized_payload,
payloadHash: $beforeEvidence->payload_hash,
metadata: [
'source_contract_key' => $beforeEvidence->source_contract_key,
'source_endpoint' => $beforeEvidence->source_endpoint,
'evidence_state' => $beforeEvidence->evidence_state->value,
'coverage_level' => $beforeEvidence->coverage_level->value,
'capture_outcome' => $beforeEvidence->capture_outcome->value,
'source_identity' => ['field' => 'metadata', 'value' => 'unsafe-override'],
],
);
$afterEvidence = $evidence->fresh();
$afterResource = $resource->fresh();
$afterResourceType = $resourceType->fresh();
$afterOperationRun = $operationRun->fresh();
expect($result['comparable'])->toBeTrue()
->and($result['payload']['provenance']['coverage_level'])->toBe(CoverageLevel::ContentBacked->value)
->and($result['payload']['identity']['field'])->toBe('Guid')
->and($afterEvidence->raw_payload)->toBe($beforeEvidence->raw_payload)
->and($afterEvidence->normalized_payload)->toBe($beforeEvidence->normalized_payload)
->and($afterEvidence->payload_hash)->toBe($beforeEvidence->payload_hash)
->and($afterEvidence->operation_run_id)->toBe($beforeEvidence->operation_run_id)
->and($afterEvidence->source_metadata)->toBe($beforeEvidence->source_metadata)
->and($afterEvidence->coverage_level)->toBe(CoverageLevel::ContentBacked)
->and($afterResource->latest_evidence_id)->toBe($beforeResource->latest_evidence_id)
->and($afterResource->latest_evidence_state)->toBe($beforeResource->latest_evidence_state)
->and($afterResource->latest_claim_state)->toBe($beforeResource->latest_claim_state)
->and($afterResource->latest_payload_hash)->toBe($beforeResource->latest_payload_hash)
->and($afterResourceType->default_coverage_level)->toBe($beforeResourceType->default_coverage_level)
->and($afterOperationRun->type)->toBe($beforeOperationRun->type)
->and($afterOperationRun->context)->toBe($beforeOperationRun->context);
});
it('Spec437 returns no comparable artifact for missing unsafe or unsupported evidence input', function (): void {
$builder = app(ExchangePowerShellComparablePayloadBuilder::class);
$missing = $builder->build('transportRule', []);
$unsafe = $builder->build('remoteDomain', array_replace_recursive(spec437FeatureRemoteDomainNormalizedPayload(), [
'source_identity' => ['field' => 'DomainName', 'value' => 'contoso.example'],
]), payloadHash: spec437FeaturePayloadHash(), metadata: spec437FeatureEvidenceMetadata());
$unsupported = $builder->build('outboundConnector', spec437FeatureRemoteDomainNormalizedPayload());
expect($missing['comparable'])->toBeFalse()
->and($missing['payload'])->toBeNull()
->and($unsafe['comparable'])->toBeFalse()
->and($unsafe['payload'])->toBeNull()
->and($unsupported['comparable'])->toBeFalse()
->and($unsupported['payload'])->toBeNull()
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec437 introduces no product surface trigger persisted compare table tenant-id ownership or OperationRun type', function (): void {
$builderSource = file_get_contents(app_path('Services/TenantConfiguration/ExchangePowerShellComparablePayloadBuilder.php')) ?: '';
$operationTypes = OperationRunType::values();
expect($builderSource)
->not->toContain('GenericPayloadNormalizer')
->not->toContain('ExchangeTeamsComparablePayloadNormalizer')
->not->toContain('CoverageEvidenceWriter')
->not->toContain('TenantConfigurationResourceEvidence::')
->and(Schema::hasColumn('tenant_configuration_resources', 'tenant_id'))->toBeFalse()
->and(Schema::hasColumn('tenant_configuration_resource_evidence', 'tenant_id'))->toBeFalse()
->and(glob(app_path('Filament/**/*ExchangePowerShellComparable*')) ?: [])->toBe([])
->and(glob(app_path('Livewire/**/*ExchangePowerShellComparable*')) ?: [])->toBe([])
->and(glob(base_path('routes/*exchange*comparable*')) ?: [])->toBe([])
->and(glob(resource_path('views/**/*ExchangePowerShellComparable*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*exchange*comparable*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*tenant*configuration*compare*')) ?: [])->toBe([])
->and(File::exists(app_path('Jobs/TenantConfiguration/BuildExchangePowerShellComparablePayloadJob.php')))->toBeFalse()
->and(File::exists(app_path('Console/Commands/BuildExchangePowerShellComparablePayload.php')))->toBeFalse()
->and($operationTypes)
->not->toContain('tenant_configuration.exchange_comparable')
->not->toContain('tenant_configuration.exchange_powershell_compare')
->not->toContain('tenant_configuration.compare');
});
function spec437FeatureResourceType(string $canonicalType): TenantConfigurationResourceType
{
return TenantConfigurationResourceType::query()
->where('canonical_type', $canonicalType)
->firstOrFail();
}
function spec437FeatureTransportRuleNormalizedPayload(): 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' => [
'Enabled' => true,
'Mode' => 'Enforce',
'State' => 'Enabled',
],
'rule_order' => ['priority' => '0'],
'conditions' => [
'sender_domain_is' => spec437FeatureRedactedMarker(count: 1),
'subject_contains_words' => spec437FeatureRedactedMarker(count: 1),
],
'actions' => [
'delete_message' => true,
'set_header_value' => spec437FeatureRedactedMarker(count: 1),
],
'exceptions' => [],
'protected_configuration' => [
'sender_domain_is' => spec437FeatureRedactedMarker(count: 1),
],
'sensitive_content' => [
'subject_contains_words' => spec437FeatureRedactedMarker(count: 1),
],
'diagnostics' => [
'volatile_fields' => ['WhenChanged', 'ExchangeVersion', 'RunspaceId'],
'protected_fields' => ['sender_domain_is'],
'sensitive_fields' => ['subject_contains_words'],
],
'source' => [
'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE,
'source_contract_key' => 'exchange_powershell.transportRule',
'command_contract_name' => 'Get-TransportRule',
'command_contract_version' => ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION,
'payload_shape_version' => ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
'normalizer_version' => 'exchange-transport-rule-readiness-v1',
],
];
}
function spec437FeatureRemoteDomainNormalizedPayload(): 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' => ['IsDefault' => false],
'remote_domain_class' => [
'domain_name_is_identity' => false,
'is_default' => false,
],
'settings' => [
'auto_reply_enabled' => true,
'auto_forward_enabled' => spec437FeatureRedactedMarker(count: 1),
],
'protected_configuration' => [
'domain_name' => spec437FeatureRedactedMarker(count: 1),
],
'sensitive_content' => [],
'source' => [
'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE,
'source_contract_key' => 'exchange_powershell.remoteDomain',
'command_contract_name' => 'Get-RemoteDomain',
'command_contract_version' => ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION,
'payload_shape_version' => ExchangePowerShellTargetEvidenceNormalizer::PAYLOAD_SHAPE_VERSION,
'normalizer_version' => 'exchange-remote-domain-readiness-v1',
],
];
}
function spec437FeatureRedactedMarker(int $count = 1, bool $present = true): array
{
return [
'value' => '[redacted]',
'present' => $present,
'count' => $count,
];
}
function spec437FeatureEvidenceMetadata(): array
{
return [
'evidence_state' => EvidenceState::ContentBacked->value,
'coverage_level' => CoverageLevel::ContentBacked->value,
'capture_outcome' => CaptureOutcome::Captured->value,
];
}
function spec437FeaturePayloadHash(): string
{
return str_repeat('b', 64);
}

View File

@ -0,0 +1,601 @@
<?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);
}