lib.winrm

This library collects some Microsoft WinRM related functions.

  1#!/usr/bin/env python3
  2# -*- coding: utf-8; py-indent-offset: 4 -*-
  3#
  4# Author:  Linuxfabrik GmbH, Zurich, Switzerland
  5# Contact: info (at) linuxfabrik (dot) ch
  6#          https://www.linuxfabrik.ch/
  7# License: The Unlicense, see LICENSE file.
  8
  9# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.rst
 10
 11"""This library collects some Microsoft WinRM related functions."""
 12
 13__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
 14__version__ = '2026030301'
 15
 16try:
 17    import winrm
 18
 19    HAVE_WINRM = True
 20except ImportError:
 21    HAVE_WINRM = False
 22
 23try:
 24    from pypsrp.client import Client
 25
 26    HAVE_JEA = True
 27except ImportError:
 28    HAVE_JEA = False
 29
 30from . import txt
 31
 32_AUTH_MAP = {
 33    'kerberos': 'kerberos',
 34    'negotiate': 'negotiate',
 35    'ntlm': 'ntlm',
 36    'credssp': 'credssp',
 37    'basic': 'basic',
 38    'plaintext': 'basic',
 39    'ssl': 'basic',
 40}
 41
 42
 43def _build_auth(args):
 44    """
 45    Build a `(username, password)` tuple from `args`.
 46
 47    For Kerberos/negotiate transports with missing credentials,
 48    returns `(None, None)` so the library falls back to the
 49    Kerberos credential cache (`kinit`). Otherwise prepends
 50    `WINRM_DOMAIN` to the username when set.
 51
 52    ### Parameters
 53    - **args**: Object with `WINRM_USERNAME`,
 54      `WINRM_PASSWORD`, `WINRM_TRANSPORT`, and optionally
 55      `WINRM_DOMAIN`.
 56
 57    ### Returns
 58    - **tuple**: `(username, password)` suitable for
 59      pypsrp or pywinrm.
 60    """
 61    username = getattr(args, 'WINRM_USERNAME', None)
 62    password = getattr(args, 'WINRM_PASSWORD', None)
 63    transport = (getattr(args, 'WINRM_TRANSPORT', None) or '').lower()
 64    if transport in ('kerberos', 'negotiate') and (not username or not password):
 65        return (None, None)
 66    if getattr(args, 'WINRM_DOMAIN', None):
 67        return (f'{username}@{args.WINRM_DOMAIN}', password)
 68    return (username, password)
 69
 70
 71def _map_transport(args):
 72    """
 73    Derive PSRP auth method, SSL flag, and port from `args`.
 74
 75    ### Parameters
 76    - **args**: Object with `WINRM_TRANSPORT`.
 77
 78    ### Returns
 79    - **tuple**: `(psrp_auth, use_ssl, port)`.
 80    """
 81    transport = (getattr(args, 'WINRM_TRANSPORT', None) or '').lower()
 82    psrp_auth = _AUTH_MAP.get(transport, 'negotiate')
 83    use_ssl = transport == 'ssl'
 84    port = 5986 if use_ssl else 5985
 85    return (psrp_auth, use_ssl, port)
 86
 87
 88def run_cmd(args, cmd, params=None):
 89    """
 90    Run a native command on a remote Windows host via WinRM/PSRP and return a
 91    normalized result dictionary.
 92
 93    Prefers **pypsrp (PSRP)** if available (for JEA/PowerShell Remoting
 94    compatibility); otherwise falls back to **pywinrm**. Authentication,
 95    transport and SSL/port selection are derived from the provided `args`.
 96
 97    ### Parameters
 98    - **args**: An object (e.g., `argparse.Namespace`) that provides at least:
 99        - `WINRM_HOSTNAME` (`str`): Target host or IP.
100        - `WINRM_USERNAME` (`str`, optional): Username. If `None` or empty when using
101          Kerberos transport, will use existing Kerberos credentials from credential cache
102          (e.g., obtained via `kinit`).
103        - `WINRM_PASSWORD` (`str`, optional): Password. If `None` or empty when using
104          Kerberos transport, will use existing Kerberos credentials from credential cache.
105        - `WINRM_TRANSPORT` (`str`, optional): Transport (e.g., `'negotiate'`, `'kerberos'`,
106          `'ntlm'`, `'credssp'`, `'basic'`, `'ssl'`). Defaults to `'negotiate'` if unset.
107        - `WINRM_DOMAIN` (`str`, optional): If set, username is sent as `user@domain`.
108      (Additional fields may be honored by the underlying libraries if present.)
109    - **cmd** (`str`): The executable/command to run remotely (native command, not a PowerShell
110       script block).
111    - **params** (`list[str]`, optional): Positional arguments passed to the command. Defaults
112       to `[]`.
113
114    ### Returns
115    - **dict**: A normalized result with:
116        - `retc` (`int`): Process return code (`0` on success).
117        - `stdout` (`str`): Captured standard output (text).
118        - `stderr` (`str`): Captured standard error (text).
119
120    ### Behavior
121    - If **pypsrp** is available, maps `WINRM_TRANSPORT` to an appropriate PSRP auth
122      and chooses SSL/port (5986 for SSL, 5985 otherwise), then executes the command
123      via `Client.execute_cmd()`.
124    - If pypsrp is unavailable but **pywinrm** is installed, executes via
125      `Session.run_cmd()`.
126    - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
127      (or are empty/None), the function will attempt to use existing Kerberos credentials
128      from the credential cache (obtained via `kinit`).
129    - On any exception, returns `{'retc': 1, 'stdout': '', 'stderr': <exception text>}`.
130    - If neither backend is present, returns an error indicating that no compatible
131      remoting library is available.
132
133    ### Example
134    >>> # With explicit credentials:
135    >>> run_cmd(args, 'ipconfig', ['/all'])
136    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
137    >>> # With Kerberos using kinit credentials (username/password can be None):
138    >>> run_cmd(args, 'ipconfig', ['/all'])
139    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
140    """
141    auth = _build_auth(args)
142    if params is None:
143        params = []
144
145    if HAVE_JEA:
146        try:
147            _psrp_auth, _use_ssl, _port = _map_transport(args)
148            session = Client(
149                server=args.WINRM_HOSTNAME,
150                username=auth[0],
151                password=auth[1],
152                auth=_psrp_auth,
153                ssl=_use_ssl,
154                port=_port,
155                cert_validation=True,
156            )
157
158            stdout, stderr, rc = session.execute_cmd(
159                cmd,
160                args=params,
161            )
162            return {
163                'retc': rc,
164                'stdout': txt.to_text(stdout),
165                'stderr': txt.to_text(stderr),
166            }
167        except Exception as e:
168            return {
169                'retc': 1,
170                'stdout': '',
171                'stderr': txt.exception2text(e),
172            }
173
174    if HAVE_WINRM:
175        try:
176            session = winrm.Session(
177                args.WINRM_HOSTNAME,
178                auth=auth,
179                transport=args.WINRM_TRANSPORT,
180            )
181
182            result = session.run_cmd(cmd, params)
183            return {
184                'retc': result.status_code,
185                'stdout': txt.to_text(result.std_out),
186                'stderr': txt.to_text(result.std_err),
187            }
188        except Exception as e:
189            return {
190                'retc': 1,
191                'stdout': '',
192                'stderr': txt.exception2text(e),
193            }
194
195    return {
196        'retc': 1,
197        'stdout': '',
198        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
199    }
200
201
202def _quote_ps_value(value):
203    """Escape a value for use in a PowerShell command string.
204
205    Single-quotes the value, doubling any embedded single
206    quotes (`'` becomes `''`).
207
208    ### Parameters
209    - **value**: The value to quote (converted to `str`).
210
211    ### Returns
212    - **str**: A safely quoted PowerShell string literal.
213    """
214    return "'{}'".format(str(value).replace("'", "''"))
215
216
217def run_ps(args, cmd, params=None):
218    """
219    Run PowerShell on a remote Windows host via WinRM/PSRP
220    and return a normalized result dictionary.
221
222    Prefers **pypsrp (PSRP)** if available (best for
223    JEA/PowerShell Remoting); otherwise falls back to
224    **pywinrm**.
225
226    ### Parameters
227    - **args**: Object (e.g. `argparse.Namespace`) with:
228        - `WINRM_HOSTNAME` (`str`): Target host or IP.
229        - `WINRM_USERNAME` (`str`, optional): Username.
230          If empty with Kerberos transport, the credential
231          cache (`kinit`) is used.
232        - `WINRM_PASSWORD` (`str`, optional): Password.
233          Same Kerberos fallback as username.
234        - `WINRM_TRANSPORT` (`str`, optional): Transport
235          (e.g. `'negotiate'`, `'kerberos'`, `'ntlm'`,
236          `'credssp'`, `'basic'`, `'ssl'`).
237          Defaults to `'negotiate'`.
238        - `WINRM_DOMAIN` (`str`, optional): If set,
239          username is sent as `user@domain`.
240        - `WINRM_CONFIGURATION_NAME` (`str`, optional):
241          JEA endpoint name. Defaults to
242          `'Microsoft.PowerShell'`. Only with **pypsrp**.
243    - **cmd** (`str`): What to execute remotely. Meaning
244      depends on `params`:
245        - `params is None` — `cmd` is an arbitrary
246          PowerShell script (pipelines, expressions, etc.)
247          executed via `add_script()`.
248        - `params` given (`list` or `dict`) — `cmd` is a
249          single cmdlet name executed via `add_cmdlet()`
250          (optimal for JEA allow/deny).
251    - **params** (`list[str]`, `dict`, or `None`):
252        - `None` (default) — no params; `cmd` is run as a
253          script.
254        - `list[str]` — positional arguments added via
255          `add_argument()`.
256        - `dict` — named parameters added via
257          `add_parameter(name, value)`.
258
259    ### Returns
260    - **dict**: Normalized result with:
261        - `retc` (`int`): `0` if no errors.
262        - `stdout` (`str`): Captured output.
263        - `stderr` (`str`): Error/diagnostic output.
264          For **pywinrm**: CLIXML progress noise is
265          suppressed when `retc == 0`.
266
267    ### Example
268    Pipeline (params=None, uses add_script):
269    >>> run_ps(args, 'Get-Process | Select -First 1')
270
271    Positional params (uses add_cmdlet + add_argument):
272    >>> run_ps(args, 'Get-Service', ['WinRM'])
273
274    Named params (uses add_cmdlet + add_parameter):
275    >>> run_ps(
276    ...     args,
277    ...     'Get-WmiObject',
278    ...     {'Class': 'Win32_OperatingSystem'},
279    ... )
280    """
281    auth = _build_auth(args)
282
283    configuration_name = getattr(
284        args,
285        'WINRM_CONFIGURATION_NAME',
286        None,
287    )
288    if configuration_name and not HAVE_JEA:
289        return {
290            'retc': 1,
291            'stdout': '',
292            'stderr': 'WINRM_CONFIGURATION_NAME requires '
293            'pypsrp (JEA). Install pypsrp or '
294            'unset '
295            '--winrm-configuration-name.',
296        }
297
298    if HAVE_JEA:
299        try:
300            from pypsrp.powershell import (
301                PowerShell,
302                RunspacePool,
303            )
304            from pypsrp.wsman import WSMan
305
306            _psrp_auth, _use_ssl, _port = _map_transport(args)
307
308            wsman = WSMan(
309                server=args.WINRM_HOSTNAME,
310                username=auth[0],
311                password=auth[1],
312                auth=_psrp_auth,
313                ssl=_use_ssl,
314                port=_port,
315                cert_validation=True,
316            )
317
318            with RunspacePool(
319                wsman,
320                configuration_name=(configuration_name or 'Microsoft.PowerShell'),
321            ) as pool:
322                ps = PowerShell(pool)
323                if params is not None:
324                    ps.add_cmdlet(cmd)
325                    if isinstance(params, dict):
326                        for name, value in params.items():
327                            ps.add_parameter(name, value)
328                    else:
329                        for param in params:
330                            ps.add_argument(param)
331                else:
332                    ps.add_script(cmd)
333                output = ps.invoke()
334
335            stdout = '\n'.join(str(o) for o in output)
336
337            stderr_lines = []
338            for err in ps.streams.error:
339                try:
340                    stderr_lines.append(
341                        err.to_string(),
342                    )
343                except Exception:
344                    msg = getattr(err, 'message', None) or str(err)
345                    stderr_lines.append(str(msg))
346            stderr = '\n'.join(stderr_lines)
347
348            return {
349                'retc': 0 if not ps.had_errors else 1,
350                'stdout': txt.to_text(stdout),
351                'stderr': txt.to_text(stderr),
352            }
353        except Exception as e:
354            return {
355                'retc': 1,
356                'stdout': '',
357                'stderr': txt.exception2text(e),
358            }
359
360    if HAVE_WINRM:
361        try:
362            session = winrm.Session(
363                args.WINRM_HOSTNAME,
364                auth=auth,
365                transport=args.WINRM_TRANSPORT,
366            )
367
368            if params is not None:
369                if isinstance(params, dict):
370                    param_str = ' '.join(
371                        f'-{k} {_quote_ps_value(v)}' for k, v in params.items()
372                    )
373                    ps_cmd = f'{cmd} {param_str}'
374                else:
375                    ps_cmd = (
376                        '{} {}'.format(
377                            cmd,
378                            ' '.join(str(p) for p in params),
379                        )
380                        if params
381                        else cmd
382                    )
383            else:
384                ps_cmd = cmd
385
386            result = session.run_ps(ps_cmd)
387
388            result = {
389                'retc': result.status_code,
390                'stdout': txt.to_text(result.std_out),
391                'stderr': txt.to_text(result.std_err),
392            }
393            if result['retc'] == 0 and result['stderr'].startswith(
394                '#< CLIXML',
395            ):
396                result['stderr'] = ''
397            return result
398        except Exception as e:
399            return {
400                'retc': 1,
401                'stdout': '',
402                'stderr': txt.exception2text(e),
403            }
404
405    return {
406        'retc': 1,
407        'stdout': '',
408        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
409    }
def run_cmd(args, cmd, params=None):
 89def run_cmd(args, cmd, params=None):
 90    """
 91    Run a native command on a remote Windows host via WinRM/PSRP and return a
 92    normalized result dictionary.
 93
 94    Prefers **pypsrp (PSRP)** if available (for JEA/PowerShell Remoting
 95    compatibility); otherwise falls back to **pywinrm**. Authentication,
 96    transport and SSL/port selection are derived from the provided `args`.
 97
 98    ### Parameters
 99    - **args**: An object (e.g., `argparse.Namespace`) that provides at least:
100        - `WINRM_HOSTNAME` (`str`): Target host or IP.
101        - `WINRM_USERNAME` (`str`, optional): Username. If `None` or empty when using
102          Kerberos transport, will use existing Kerberos credentials from credential cache
103          (e.g., obtained via `kinit`).
104        - `WINRM_PASSWORD` (`str`, optional): Password. If `None` or empty when using
105          Kerberos transport, will use existing Kerberos credentials from credential cache.
106        - `WINRM_TRANSPORT` (`str`, optional): Transport (e.g., `'negotiate'`, `'kerberos'`,
107          `'ntlm'`, `'credssp'`, `'basic'`, `'ssl'`). Defaults to `'negotiate'` if unset.
108        - `WINRM_DOMAIN` (`str`, optional): If set, username is sent as `user@domain`.
109      (Additional fields may be honored by the underlying libraries if present.)
110    - **cmd** (`str`): The executable/command to run remotely (native command, not a PowerShell
111       script block).
112    - **params** (`list[str]`, optional): Positional arguments passed to the command. Defaults
113       to `[]`.
114
115    ### Returns
116    - **dict**: A normalized result with:
117        - `retc` (`int`): Process return code (`0` on success).
118        - `stdout` (`str`): Captured standard output (text).
119        - `stderr` (`str`): Captured standard error (text).
120
121    ### Behavior
122    - If **pypsrp** is available, maps `WINRM_TRANSPORT` to an appropriate PSRP auth
123      and chooses SSL/port (5986 for SSL, 5985 otherwise), then executes the command
124      via `Client.execute_cmd()`.
125    - If pypsrp is unavailable but **pywinrm** is installed, executes via
126      `Session.run_cmd()`.
127    - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
128      (or are empty/None), the function will attempt to use existing Kerberos credentials
129      from the credential cache (obtained via `kinit`).
130    - On any exception, returns `{'retc': 1, 'stdout': '', 'stderr': <exception text>}`.
131    - If neither backend is present, returns an error indicating that no compatible
132      remoting library is available.
133
134    ### Example
135    >>> # With explicit credentials:
136    >>> run_cmd(args, 'ipconfig', ['/all'])
137    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
138    >>> # With Kerberos using kinit credentials (username/password can be None):
139    >>> run_cmd(args, 'ipconfig', ['/all'])
140    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
141    """
142    auth = _build_auth(args)
143    if params is None:
144        params = []
145
146    if HAVE_JEA:
147        try:
148            _psrp_auth, _use_ssl, _port = _map_transport(args)
149            session = Client(
150                server=args.WINRM_HOSTNAME,
151                username=auth[0],
152                password=auth[1],
153                auth=_psrp_auth,
154                ssl=_use_ssl,
155                port=_port,
156                cert_validation=True,
157            )
158
159            stdout, stderr, rc = session.execute_cmd(
160                cmd,
161                args=params,
162            )
163            return {
164                'retc': rc,
165                'stdout': txt.to_text(stdout),
166                'stderr': txt.to_text(stderr),
167            }
168        except Exception as e:
169            return {
170                'retc': 1,
171                'stdout': '',
172                'stderr': txt.exception2text(e),
173            }
174
175    if HAVE_WINRM:
176        try:
177            session = winrm.Session(
178                args.WINRM_HOSTNAME,
179                auth=auth,
180                transport=args.WINRM_TRANSPORT,
181            )
182
183            result = session.run_cmd(cmd, params)
184            return {
185                'retc': result.status_code,
186                'stdout': txt.to_text(result.std_out),
187                'stderr': txt.to_text(result.std_err),
188            }
189        except Exception as e:
190            return {
191                'retc': 1,
192                'stdout': '',
193                'stderr': txt.exception2text(e),
194            }
195
196    return {
197        'retc': 1,
198        'stdout': '',
199        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
200    }

Run a native command on a remote Windows host via WinRM/PSRP and return a normalized result dictionary.

Prefers pypsrp (PSRP) if available (for JEA/PowerShell Remoting compatibility); otherwise falls back to pywinrm. Authentication, transport and SSL/port selection are derived from the provided args.

Parameters

  • args: An object (e.g., argparse.Namespace) that provides at least:
    • WINRM_HOSTNAME (str): Target host or IP.
    • WINRM_USERNAME (str, optional): Username. If None or empty when using Kerberos transport, will use existing Kerberos credentials from credential cache (e.g., obtained via kinit).
    • WINRM_PASSWORD (str, optional): Password. If None or empty when using Kerberos transport, will use existing Kerberos credentials from credential cache.
    • WINRM_TRANSPORT (str, optional): Transport (e.g., 'negotiate', 'kerberos', 'ntlm', 'credssp', 'basic', 'ssl'). Defaults to 'negotiate' if unset.
    • WINRM_DOMAIN (str, optional): If set, username is sent as user@domain. (Additional fields may be honored by the underlying libraries if present.)
  • cmd (str): The executable/command to run remotely (native command, not a PowerShell script block).
  • params (list[str], optional): Positional arguments passed to the command. Defaults to [].

Returns

  • dict: A normalized result with:
    • retc (int): Process return code (0 on success).
    • stdout (str): Captured standard output (text).
    • stderr (str): Captured standard error (text).

Behavior

  • If pypsrp is available, maps WINRM_TRANSPORT to an appropriate PSRP auth and chooses SSL/port (5986 for SSL, 5985 otherwise), then executes the command via Client.execute_cmd().
  • If pypsrp is unavailable but pywinrm is installed, executes via Session.run_cmd().
  • For Kerberos authentication: if WINRM_USERNAME and WINRM_PASSWORD are not provided (or are empty/None), the function will attempt to use existing Kerberos credentials from the credential cache (obtained via kinit).
  • On any exception, returns {'retc': 1, 'stdout': '', 'stderr': <exception text>}.
  • If neither backend is present, returns an error indicating that no compatible remoting library is available.

Example

>>> # With explicit credentials:
>>> run_cmd(args, 'ipconfig', ['/all'])
{'retc': 0, 'stdout': 'Windows IP Configuration\r\n...','stderr': ''}
>>> # With Kerberos using kinit credentials (username/password can be None):
>>> run_cmd(args, 'ipconfig', ['/all'])
{'retc': 0, 'stdout': 'Windows IP Configuration\r\n...','stderr': ''}
def run_ps(args, cmd, params=None):
218def run_ps(args, cmd, params=None):
219    """
220    Run PowerShell on a remote Windows host via WinRM/PSRP
221    and return a normalized result dictionary.
222
223    Prefers **pypsrp (PSRP)** if available (best for
224    JEA/PowerShell Remoting); otherwise falls back to
225    **pywinrm**.
226
227    ### Parameters
228    - **args**: Object (e.g. `argparse.Namespace`) with:
229        - `WINRM_HOSTNAME` (`str`): Target host or IP.
230        - `WINRM_USERNAME` (`str`, optional): Username.
231          If empty with Kerberos transport, the credential
232          cache (`kinit`) is used.
233        - `WINRM_PASSWORD` (`str`, optional): Password.
234          Same Kerberos fallback as username.
235        - `WINRM_TRANSPORT` (`str`, optional): Transport
236          (e.g. `'negotiate'`, `'kerberos'`, `'ntlm'`,
237          `'credssp'`, `'basic'`, `'ssl'`).
238          Defaults to `'negotiate'`.
239        - `WINRM_DOMAIN` (`str`, optional): If set,
240          username is sent as `user@domain`.
241        - `WINRM_CONFIGURATION_NAME` (`str`, optional):
242          JEA endpoint name. Defaults to
243          `'Microsoft.PowerShell'`. Only with **pypsrp**.
244    - **cmd** (`str`): What to execute remotely. Meaning
245      depends on `params`:
246        - `params is None` — `cmd` is an arbitrary
247          PowerShell script (pipelines, expressions, etc.)
248          executed via `add_script()`.
249        - `params` given (`list` or `dict`) — `cmd` is a
250          single cmdlet name executed via `add_cmdlet()`
251          (optimal for JEA allow/deny).
252    - **params** (`list[str]`, `dict`, or `None`):
253        - `None` (default) — no params; `cmd` is run as a
254          script.
255        - `list[str]` — positional arguments added via
256          `add_argument()`.
257        - `dict` — named parameters added via
258          `add_parameter(name, value)`.
259
260    ### Returns
261    - **dict**: Normalized result with:
262        - `retc` (`int`): `0` if no errors.
263        - `stdout` (`str`): Captured output.
264        - `stderr` (`str`): Error/diagnostic output.
265          For **pywinrm**: CLIXML progress noise is
266          suppressed when `retc == 0`.
267
268    ### Example
269    Pipeline (params=None, uses add_script):
270    >>> run_ps(args, 'Get-Process | Select -First 1')
271
272    Positional params (uses add_cmdlet + add_argument):
273    >>> run_ps(args, 'Get-Service', ['WinRM'])
274
275    Named params (uses add_cmdlet + add_parameter):
276    >>> run_ps(
277    ...     args,
278    ...     'Get-WmiObject',
279    ...     {'Class': 'Win32_OperatingSystem'},
280    ... )
281    """
282    auth = _build_auth(args)
283
284    configuration_name = getattr(
285        args,
286        'WINRM_CONFIGURATION_NAME',
287        None,
288    )
289    if configuration_name and not HAVE_JEA:
290        return {
291            'retc': 1,
292            'stdout': '',
293            'stderr': 'WINRM_CONFIGURATION_NAME requires '
294            'pypsrp (JEA). Install pypsrp or '
295            'unset '
296            '--winrm-configuration-name.',
297        }
298
299    if HAVE_JEA:
300        try:
301            from pypsrp.powershell import (
302                PowerShell,
303                RunspacePool,
304            )
305            from pypsrp.wsman import WSMan
306
307            _psrp_auth, _use_ssl, _port = _map_transport(args)
308
309            wsman = WSMan(
310                server=args.WINRM_HOSTNAME,
311                username=auth[0],
312                password=auth[1],
313                auth=_psrp_auth,
314                ssl=_use_ssl,
315                port=_port,
316                cert_validation=True,
317            )
318
319            with RunspacePool(
320                wsman,
321                configuration_name=(configuration_name or 'Microsoft.PowerShell'),
322            ) as pool:
323                ps = PowerShell(pool)
324                if params is not None:
325                    ps.add_cmdlet(cmd)
326                    if isinstance(params, dict):
327                        for name, value in params.items():
328                            ps.add_parameter(name, value)
329                    else:
330                        for param in params:
331                            ps.add_argument(param)
332                else:
333                    ps.add_script(cmd)
334                output = ps.invoke()
335
336            stdout = '\n'.join(str(o) for o in output)
337
338            stderr_lines = []
339            for err in ps.streams.error:
340                try:
341                    stderr_lines.append(
342                        err.to_string(),
343                    )
344                except Exception:
345                    msg = getattr(err, 'message', None) or str(err)
346                    stderr_lines.append(str(msg))
347            stderr = '\n'.join(stderr_lines)
348
349            return {
350                'retc': 0 if not ps.had_errors else 1,
351                'stdout': txt.to_text(stdout),
352                'stderr': txt.to_text(stderr),
353            }
354        except Exception as e:
355            return {
356                'retc': 1,
357                'stdout': '',
358                'stderr': txt.exception2text(e),
359            }
360
361    if HAVE_WINRM:
362        try:
363            session = winrm.Session(
364                args.WINRM_HOSTNAME,
365                auth=auth,
366                transport=args.WINRM_TRANSPORT,
367            )
368
369            if params is not None:
370                if isinstance(params, dict):
371                    param_str = ' '.join(
372                        f'-{k} {_quote_ps_value(v)}' for k, v in params.items()
373                    )
374                    ps_cmd = f'{cmd} {param_str}'
375                else:
376                    ps_cmd = (
377                        '{} {}'.format(
378                            cmd,
379                            ' '.join(str(p) for p in params),
380                        )
381                        if params
382                        else cmd
383                    )
384            else:
385                ps_cmd = cmd
386
387            result = session.run_ps(ps_cmd)
388
389            result = {
390                'retc': result.status_code,
391                'stdout': txt.to_text(result.std_out),
392                'stderr': txt.to_text(result.std_err),
393            }
394            if result['retc'] == 0 and result['stderr'].startswith(
395                '#< CLIXML',
396            ):
397                result['stderr'] = ''
398            return result
399        except Exception as e:
400            return {
401                'retc': 1,
402                'stdout': '',
403                'stderr': txt.exception2text(e),
404            }
405
406    return {
407        'retc': 1,
408        'stdout': '',
409        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
410    }

Run PowerShell on a remote Windows host via WinRM/PSRP and return a normalized result dictionary.

Prefers pypsrp (PSRP) if available (best for JEA/PowerShell Remoting); otherwise falls back to pywinrm.

Parameters

  • args: Object (e.g. argparse.Namespace) with:
    • WINRM_HOSTNAME (str): Target host or IP.
    • WINRM_USERNAME (str, optional): Username. If empty with Kerberos transport, the credential cache (kinit) is used.
    • WINRM_PASSWORD (str, optional): Password. Same Kerberos fallback as username.
    • WINRM_TRANSPORT (str, optional): Transport (e.g. 'negotiate', 'kerberos', 'ntlm', 'credssp', 'basic', 'ssl'). Defaults to 'negotiate'.
    • WINRM_DOMAIN (str, optional): If set, username is sent as user@domain.
    • WINRM_CONFIGURATION_NAME (str, optional): JEA endpoint name. Defaults to 'Microsoft.PowerShell'. Only with pypsrp.
  • cmd (str): What to execute remotely. Meaning depends on params:
    • params is Nonecmd is an arbitrary PowerShell script (pipelines, expressions, etc.) executed via add_script().
    • params given (list or dict) — cmd is a single cmdlet name executed via add_cmdlet() (optimal for JEA allow/deny).
  • params (list[str], dict, or None):
    • None (default) — no params; cmd is run as a script.
    • list[str] — positional arguments added via add_argument().
    • dict — named parameters added via add_parameter(name, value).

Returns

  • dict: Normalized result with:
    • retc (int): 0 if no errors.
    • stdout (str): Captured output.
    • stderr (str): Error/diagnostic output. For pywinrm: CLIXML progress noise is suppressed when retc == 0.

Example

Pipeline (params=None, uses add_script):

>>> run_ps(args, 'Get-Process | Select -First 1')

Positional params (uses add_cmdlet + add_argument):

>>> run_ps(args, 'Get-Service', ['WinRM'])

Named params (uses add_cmdlet + add_parameter):

>>> run_ps(
...     args,
...     'Get-WmiObject',
...     {'Class': 'Win32_OperatingSystem'},
... )