timedOut) { return ExchangePowerShellInvocationResult::failed( reasonCode: ProviderReasonCodes::NetworkUnreachable, failureCode: 'execution_failed_timeout', message: 'Exchange PowerShell process timed out.', summaryCounts: $this->failureSummaryCounts(), context: [ 'output_state' => 'timeout', 'timeout_cleanup_attempted' => true, 'duration_ms' => $result->durationMs, ], ); } if ($result->exceptionClass !== null) { return ExchangePowerShellInvocationResult::failed( reasonCode: ProviderReasonCodes::UnknownError, failureCode: 'execution_failed_unexpected_exception', message: 'Exchange PowerShell process failed safely.', summaryCounts: $this->failureSummaryCounts(), context: [ 'output_state' => 'executor_exception', 'duration_ms' => $result->durationMs, ], ); } if (strlen($result->stdout) > $policy->stdoutMaxBytes) { return $this->shapeFailure('execution_failed_stdout_oversized', 'stdout_oversized', $result->durationMs); } if (strlen($result->stderr) > $policy->stderrMaxBytes) { return $this->shapeFailure('execution_failed_stderr_oversized', 'stderr_oversized', $result->durationMs); } if (! $this->isSafeUtf8Text($result->stdout) || ! $this->isSafeUtf8Text($result->stderr)) { return $this->shapeFailure('execution_failed_output_encoding', 'non_utf8_or_binary', $result->durationMs); } if ($result->exitCode !== 0) { return ExchangePowerShellInvocationResult::failed( reasonCode: ProviderReasonCodes::ProviderConnectionInvalid, failureCode: 'execution_failed_nonzero_exit', message: 'Exchange PowerShell process exited unsuccessfully.', summaryCounts: $this->failureSummaryCounts(), context: [ 'output_state' => 'nonzero_exit', 'duration_ms' => $result->durationMs, ], ); } if (trim($result->stderr) !== '') { return $this->shapeFailure('execution_failed_stderr_present', 'stderr_present', $result->durationMs); } $stdout = trim($result->stdout); if ($stdout === '') { return ExchangePowerShellInvocationResult::succeeded([], context: [ 'output_state' => 'empty_collection', 'item_count' => 0, 'duration_ms' => $result->durationMs, ]); } if (preg_match('/\AWARNING:/i', $stdout) === 1) { return $this->shapeFailure('execution_failed_output_shape_unsafe', 'warning_prefixed', $result->durationMs); } $decoded = json_decode($stdout, true); if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded) || ! array_is_list($decoded)) { return $this->shapeFailure('execution_failed_shape_unsafe', 'malformed_json_collection', $result->durationMs); } $items = []; foreach ($decoded as $item) { if (! is_array($item)) { return $this->shapeFailure('execution_failed_shape_unsafe', 'malformed_collection_item', $result->durationMs); } $items[] = $item; } if (count($items) > $policy->itemLimit) { return $this->shapeFailure('execution_failed_item_limit_exceeded', 'item_limit_exceeded', $result->durationMs); } return ExchangePowerShellInvocationResult::succeeded($items, context: [ 'output_state' => 'structured_collection', 'item_count' => count($items), 'duration_ms' => $result->durationMs, ]); } private function shapeFailure(string $failureCode, string $outputState, int $durationMs): ExchangePowerShellInvocationResult { return ExchangePowerShellInvocationResult::failed( reasonCode: ProviderReasonCodes::ProviderConnectionInvalid, failureCode: $failureCode, message: 'Exchange PowerShell output was rejected safely.', summaryCounts: $this->failureSummaryCounts(), context: [ 'output_state' => $outputState, 'duration_ms' => $durationMs, ], ); } private function isSafeUtf8Text(string $value): bool { if ($value === '') { return true; } if (function_exists('mb_check_encoding') && ! mb_check_encoding($value, 'UTF-8')) { return false; } if (preg_match('//u', $value) !== 1) { return false; } return preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $value) !== 1; } /** * @return array */ private function failureSummaryCounts(): array { return [ 'total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0, ]; } }