, * summary_counts: array * } */ public function capture( ManagedEnvironment $tenant, ProviderConnection $providerConnection, OperationRun $operationRun, string $canonicalType, ExchangePowerShellInvocationResult $runnerResult, ): array { $canonicalType = trim($canonicalType); $eligibility = $this->eligibility->evaluate($tenant, $providerConnection, $operationRun, $canonicalType, $runnerResult); if (($eligibility['allowed'] ?? false) !== true) { $summaryCounts = $this->zeroSummaryCounts(); $this->recordSummaryCounts($operationRun, $summaryCounts); return $this->result( canonicalType: $canonicalType, outcome: $eligibility['outcome'] ?? CaptureOutcome::BlockedUnsupported, adapterOutcome: self::OUTCOME_PREREQUISITE_BLOCKED, itemCount: 0, reasonCode: $eligibility['reason_code'] ?? 'exchange_capture_prerequisite_blocked', sourceContractKey: null, evidenceIds: [], summaryCounts: $summaryCounts, ); } /** @var TenantConfigurationResourceType $resourceType */ $resourceType = $eligibility['resource_type']; /** @var CoverageSourceContractDecision $sourceDecision */ $sourceDecision = $eligibility['decision']; /** @var array $contract */ $contract = $eligibility['contract']; /** @var list> $items */ $items = $runnerResult->collection; if ($items === []) { $summaryCounts = $this->zeroSummaryCounts(); $this->recordSummaryCounts($operationRun, $summaryCounts); return $this->result( canonicalType: $canonicalType, outcome: CaptureOutcome::Captured, adapterOutcome: self::OUTCOME_EMPTY_COLLECTION, itemCount: 0, reasonCode: self::OUTCOME_EMPTY_COLLECTION, sourceContractKey: $sourceDecision->contractKey, evidenceIds: [], summaryCounts: $summaryCounts, ); } $decision = $this->capturableDecision($sourceDecision, $contract); $identity = $this->identityGate->evaluateCollection( tenant: $tenant, providerConnection: $providerConnection, resourceType: $resourceType, items: $items, sourceMetadata: $decision->sourceMetadata, ); if (($identity['allowed'] ?? false) !== true) { $summaryCounts = $this->zeroSummaryCounts(); $this->recordSummaryCounts($operationRun, $summaryCounts); return $this->result( canonicalType: $canonicalType, outcome: CaptureOutcome::BlockedUnsupported, adapterOutcome: self::OUTCOME_IDENTITY_BLOCKED, itemCount: 0, reasonCode: $identity['reason_code'] ?? self::OUTCOME_IDENTITY_BLOCKED, sourceContractKey: $sourceDecision->contractKey, evidenceIds: [], summaryCounts: $summaryCounts, ); } $evidenceIds = []; $volatileFields = $this->volatileFields($contract); $permissionContext = $this->permissionContext($providerConnection); foreach ($items as $item) { $normalizedPayload = $this->normalizedPayload($resourceType, $item, $volatileFields, $decision); $payloadHash = $this->genericNormalizer->payloadHash($normalizedPayload); $resource = $this->resourceUpserter->upsert( tenant: $tenant, providerConnection: $providerConnection, resourceType: $resourceType, payload: $item, sourceMetadata: $decision->sourceMetadata, ); $evidence = $this->evidenceWriter->append( resource: $resource, resourceType: $resourceType, providerConnection: $providerConnection, operationRun: $operationRun, decision: $decision, rawPayload: $item, normalizedPayload: $normalizedPayload, payloadHash: $payloadHash, permissionContext: $permissionContext, maximumCoverageLevel: $this->contentOnlyGuard->maximumCoverageLevel(), ); $this->contentOnlyGuard->assertContentOnly($evidence, $resource->refresh()); $evidenceIds[] = (int) $evidence->getKey(); } $summaryCounts = $this->successSummaryCounts(count($evidenceIds)); $this->recordSummaryCounts($operationRun, $summaryCounts); return $this->result( canonicalType: $canonicalType, outcome: CaptureOutcome::Captured, adapterOutcome: CaptureOutcome::Captured->value, itemCount: count($evidenceIds), reasonCode: null, sourceContractKey: $decision->contractKey, evidenceIds: $evidenceIds, summaryCounts: $summaryCounts, ); } /** * @param array $contract */ private function capturableDecision(CoverageSourceContractDecision $sourceDecision, array $contract): CoverageSourceContractDecision { $commandName = (string) $contract['command_name']; $sourceEndpoint = ExchangePowerShellCommandContracts::SOURCE_SURFACE.':'.$commandName; $sourceMetadata = array_filter([ ...$sourceDecision->sourceMetadata, 'source_endpoint' => $sourceEndpoint, 'source_contract_state' => CoverageSourceContractDecision::CONTRACT_VERIFIED_PENDING_CAPTURE, 'capture_adapter' => 'exchange_powershell_evidence_capture_adapter', 'capture_eligibility_state' => 'content_backed_only', 'content_level_maximum' => 'content_backed', 'evidence_promotion_allowed' => false, 'customer_claims_allowed' => false, 'restore_allowed' => false, 'certification_allowed' => false, ], static fn (mixed $value): bool => $value !== null && $value !== ''); return new CoverageSourceContractDecision( canonicalType: $sourceDecision->canonicalType, outcome: CaptureOutcome::Captured, contractKey: $sourceDecision->contractKey, sourceEndpoint: $sourceEndpoint, sourceVersion: $sourceDecision->sourceVersion, sourceSchemaHash: $sourceDecision->sourceSchemaHash, reasonCode: null, sourceContractState: CoverageSourceContractDecision::CONTRACT_VERIFIED_PENDING_CAPTURE, contract: $contract, sourceMetadata: $sourceMetadata, ); } /** * @param array $payload * @param list $volatileFields * @return array */ private function normalizedPayload( TenantConfigurationResourceType $resourceType, array $payload, array $volatileFields, CoverageSourceContractDecision $decision, ): array { $canonicalType = (string) $resourceType->canonical_type; if ($this->exchangeTeamsNormalizer->supports($canonicalType)) { $normalized = $this->redactedPayload($this->exchangeTeamsNormalizer->normalize($canonicalType, $payload, $volatileFields)); $source = is_array($normalized['source'] ?? null) ? $normalized['source'] : []; $normalized['source'] = array_filter([ ...$source, 'source_contract_key' => $decision->contractKey, 'source_endpoint' => $decision->sourceEndpoint, 'source_version' => $decision->sourceVersion, 'source_schema_hash' => $decision->sourceSchemaHash, 'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE, ], static fn (mixed $value): bool => $value !== null && $value !== ''); return $normalized; } $normalized = $this->genericNormalizer->normalize($payload, $volatileFields); $normalized['source'] = array_filter([ 'source_contract_key' => $decision->contractKey, 'source_endpoint' => $decision->sourceEndpoint, 'source_version' => $decision->sourceVersion, 'source_schema_hash' => $decision->sourceSchemaHash, 'source_surface' => ExchangePowerShellCommandContracts::SOURCE_SURFACE, ], static fn (mixed $value): bool => $value !== null && $value !== ''); return $this->redactedPayload($normalized); } /** * @param array $payload * @return array */ private function redactedPayload(array $payload): array { $redacted = $this->redactor->redact($payload); return is_array($redacted) ? $redacted : []; } /** * @param array $contract * @return list */ private function volatileFields(array $contract): array { $fields = data_get($contract, 'response_shape.volatile_fields', []); if (! is_array($fields)) { return []; } return array_values(array_filter( array_map(static fn (mixed $field): string => is_string($field) ? trim($field) : '', $fields), static fn (string $field): bool => $field !== '', )); } /** * @return array */ private function permissionContext(ProviderConnection $providerConnection): array { return $this->redactedPayload([ 'provider_connection_id' => (int) $providerConnection->getKey(), 'provider' => (string) $providerConnection->provider, 'connection_type' => $this->stringValue($providerConnection->connection_type), 'consent_status' => $this->stringValue($providerConnection->consent_status), 'verification_status' => $this->stringValue($providerConnection->verification_status), ]); } /** * @return array */ private function zeroSummaryCounts(): array { return [ 'total' => 0, 'processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 0, 'items' => 0, ]; } /** * @return array */ private function successSummaryCounts(int $count): array { $count = max(0, $count); return [ 'total' => $count, 'processed' => $count, 'succeeded' => $count, 'failed' => 0, 'skipped' => 0, 'items' => $count, ]; } /** * @param array $summaryCounts */ private function recordSummaryCounts(OperationRun $operationRun, array $summaryCounts): void { $operationRun->forceFill([ 'summary_counts' => SummaryCountsNormalizer::normalize($summaryCounts), ])->save(); } private function stringValue(mixed $value): ?string { if ($value instanceof \BackedEnum) { return (string) $value->value; } if (is_scalar($value)) { $value = trim((string) $value); return $value !== '' ? $value : null; } return null; } /** * @param list $evidenceIds * @param array $summaryCounts * @return array{ * canonical_type: string, * outcome: string, * adapter_outcome: string, * item_count: int, * reason_code?: string|null, * source_contract_key?: string|null, * evidence_ids: list, * summary_counts: array * } */ private function result( string $canonicalType, CaptureOutcome $outcome, string $adapterOutcome, int $itemCount, ?string $reasonCode, ?string $sourceContractKey, array $evidenceIds, array $summaryCounts, ): array { return [ 'canonical_type' => $canonicalType, 'outcome' => $outcome->value, 'adapter_outcome' => $adapterOutcome, 'item_count' => max(0, $itemCount), 'reason_code' => $reasonCode, 'source_contract_key' => $sourceContractKey, 'evidence_ids' => $evidenceIds, 'summary_counts' => $summaryCounts, ]; } }