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
 14__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
 15__version__ = '2025111201'
 16
 17try:
 18    import winrm
 19    HAVE_WINRM = True
 20except ImportError:
 21    HAVE_WINRM = False
 22
 23try:
 24    from pypsrp.client import Client
 25    HAVE_JEA = True
 26except ImportError:
 27    HAVE_JEA = False
 28
 29from . import txt
 30
 31
 32def run_cmd(args, cmd, params=None):
 33    """
 34    Run a native command on a remote Windows host via WinRM/PSRP and return a
 35    normalized result dictionary.
 36
 37    Prefers **pypsrp (PSRP)** if available (for JEA/PowerShell Remoting
 38    compatibility); otherwise falls back to **pywinrm**. Authentication,
 39    transport and SSL/port selection are derived from the provided `args`.
 40
 41    ### Parameters
 42    - **args**: An object (e.g., `argparse.Namespace`) that provides at least:
 43        - `WINRM_HOSTNAME` (`str`): Target host or IP.
 44        - `WINRM_USERNAME` (`str`, optional): Username. If `None` or empty when using
 45          Kerberos transport, will use existing Kerberos credentials from credential cache
 46          (e.g., obtained via `kinit`).
 47        - `WINRM_PASSWORD` (`str`, optional): Password. If `None` or empty when using
 48          Kerberos transport, will use existing Kerberos credentials from credential cache.
 49        - `WINRM_TRANSPORT` (`str`, optional): Transport (e.g., `'negotiate'`, `'kerberos'`,
 50          `'ntlm'`, `'credssp'`, `'basic'`, `'ssl'`). Defaults to `'negotiate'` if unset.
 51        - `WINRM_DOMAIN` (`str`, optional): If set, username is sent as `user@domain`.
 52      (Additional fields may be honored by the underlying libraries if present.)
 53    - **cmd** (`str`): The executable/command to run remotely (native command, not a PowerShell
 54       script block).
 55    - **params** (`list[str]`, optional): Positional arguments passed to the command. Defaults
 56       to `[]`.
 57
 58    ### Returns
 59    - **dict**: A normalized result with:
 60        - `retc` (`int`): Process return code (`0` on success).
 61        - `stdout` (`str`): Captured standard output (text).
 62        - `stderr` (`str`): Captured standard error (text).
 63
 64    ### Behavior
 65    - If **pypsrp** is available, maps `WINRM_TRANSPORT` to an appropriate PSRP auth
 66      and chooses SSL/port (5986 for SSL, 5985 otherwise), then executes the command
 67      via `Client.execute_cmd()`.
 68    - If pypsrp is unavailable but **pywinrm** is installed, executes via
 69      `Session.run_cmd()`.
 70    - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
 71      (or are empty/None), the function will attempt to use existing Kerberos credentials
 72      from the credential cache (obtained via `kinit`).
 73    - On any exception, returns `{'retc': 1, 'stdout': '', 'stderr': <exception text>}`.
 74    - If neither backend is present, returns an error indicating that no compatible
 75      remoting library is available.
 76
 77    ### Example
 78    >>> # With explicit credentials:
 79    >>> run_cmd(args, "ipconfig", ["/all"])
 80    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
 81    >>> # With Kerberos using kinit credentials (username/password can be None):
 82    >>> run_cmd(args, "ipconfig", ["/all"])
 83    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
 84    """
 85    # Determine authentication credentials
 86    # For Kerberos, allow using existing credentials from kinit
 87    username = getattr(args, 'WINRM_USERNAME', None)
 88    password = getattr(args, 'WINRM_PASSWORD', None)
 89
 90    # Check if we should use Kerberos with existing credentials
 91    _transport = (getattr(args, 'WINRM_TRANSPORT', None) or '').lower()
 92    use_kerberos_cache = (_transport in ['kerberos', 'negotiate']) and (not username or not password)
 93
 94    if use_kerberos_cache:
 95        # Use None for username/password to let Kerberos use credential cache
 96        auth = (None, None)
 97    else:
 98        # Use provided credentials
 99        auth = (username, password)
100        if getattr(args, 'WINRM_DOMAIN', None):
101            auth = (f'{username}@{args.WINRM_DOMAIN}', password)
102
103    if params is None:
104        params = []
105
106    if HAVE_JEA:
107        try:
108            # translate pywinrm transport -> pypsrp auth/ssl/port
109            _auth_map = {
110                'kerberos': 'kerberos',
111                'negotiate': 'negotiate',
112                'ntlm': 'negotiate',   # NTLM is negotiated under "negotiate"
113                'credssp': 'credssp',
114                'basic': 'basic',
115                'plaintext': 'basic',  # basic over HTTP
116                'ssl': 'basic',        # basic over HTTPS
117            }
118            _psrp_auth = _auth_map.get(_transport, 'negotiate')
119            _use_ssl = (_transport == 'ssl')
120            _port = 5986 if _use_ssl else 5985
121
122            # create PSRP client
123            session = Client(
124                server=args.WINRM_HOSTNAME,
125                username=auth[0],
126                password=auth[1],
127                auth=_psrp_auth,
128                ssl=_use_ssl,
129                port=_port,
130                cert_validation=True,
131            )
132
133            # run native command (not PowerShell script)
134            stdout, stderr, rc = session.execute_cmd(cmd, params)
135
136            return {
137                'retc': rc,
138                'stdout': txt.to_text(stdout),
139                'stderr': txt.to_text(stderr),
140            }
141        except Exception as e:
142            return {
143                'retc': 1,
144                'stdout': '',
145                'stderr': txt.exception2text(e),
146            }
147
148    if HAVE_WINRM:
149        try:
150            session = winrm.Session(
151                args.WINRM_HOSTNAME,
152                auth=auth,
153                transport=args.WINRM_TRANSPORT,
154            )
155
156            result = session.run_cmd(cmd, params)
157            return {
158                'retc': result.status_code,
159                'stdout': txt.to_text(result.std_out),
160                'stderr': txt.to_text(result.std_err),
161            }
162        except Exception as e:
163            return {
164                'retc': 1,
165                'stdout': '',
166                'stderr': txt.exception2text(e),
167            }
168
169    # Neither pypsrp nor pywinrm is available
170    return {
171        'retc': 1,
172        'stdout': '',
173        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
174    }
175
176
177def run_ps(args, cmd):
178    """
179    Run a PowerShell script/string on a remote Windows host via WinRM/PSRP and
180    return a normalized result dictionary.
181
182    Prefers **pypsrp (PSRP)** if available (best for JEA/PowerShell Remoting);
183    otherwise falls back to **pywinrm**. Authentication, transport, and SSL/port
184    are derived from the provided `args`.
185
186    ### Parameters
187    - **args**: An object (e.g., `argparse.Namespace`) that provides at least:
188        - `WINRM_HOSTNAME` (`str`): Target host or IP.
189        - `WINRM_USERNAME` (`str`, optional): Username. If `None` or empty when using
190          Kerberos transport, will use existing Kerberos credentials from credential cache
191          (e.g., obtained via `kinit`).
192        - `WINRM_PASSWORD` (`str`, optional): Password. If `None` or empty when using
193          Kerberos transport, will use existing Kerberos credentials from credential cache.
194        - `WINRM_TRANSPORT` (`str`, optional): Transport (`'negotiate'`, `'kerberos'`,
195          `'ntlm'`, `'credssp'`, `'basic'`, `'ssl'`, etc.). Defaults to `'negotiate'`
196          if unset.
197        - `WINRM_DOMAIN` (`str`, optional): If set, username is sent as `user@domain`.
198      (Additional attributes may be honored by the underlying libraries if present.)
199    - **cmd** (`str`): PowerShell scriptblock/string to execute remotely.
200
201    ### Returns
202    - **dict**: A normalized result with:
203        - `retc` (`int`): Return code (`0` if no PowerShell errors were reported).
204        - `stdout` (`str`): Captured standard output/text from the script.
205        - `stderr` (`str`): Aggregated error/diagnostic output.
206          - For **PSRP**: collects entries from the PowerShell *Error* stream
207            (human-readable via `to_string()` when available).
208          - For **pywinrm**: uses `std_err`; if `retc == 0` and stderr begins with
209            `#< CLIXML`, it is suppressed as benign progress noise.
210
211    ### Behavior
212    - Maps `WINRM_TRANSPORT` to PSRP auth (`kerberos`, `negotiate`, `credssp`, `basic`)
213      and decides SSL/port (5986 for SSL, 5985 otherwise) when using **pypsrp**,
214      then executes via `Client.execute_ps()`.
215    - Falls back to **pywinrm** and executes via `Session.run_ps()` if pypsrp is not available.
216    - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
217      (or are empty/None), the function will attempt to use existing Kerberos credentials
218      from the credential cache (obtained via `kinit`).
219    - On any exception, returns `{'retc': 1, 'stdout': '', 'stderr': <exception text>}`.
220    - If neither backend is installed, returns an error indicating that no compatible
221      remoting library is available.
222
223    ### Example
224    >>> # With explicit credentials:
225    >>> run_ps(args, "Get-Process | Select-Object -First 1 | Format-Table Name,Id -AutoSize")
226    {'retc': 0, 'stdout': 'Name    Id\\r\\n----    --\\r\\n...\\r\\n', 'stderr': ''}
227    >>> # With Kerberos using kinit credentials (username/password can be None):
228    >>> run_ps(args, "Get-Process | Select-Object -First 1 | Format-Table Name,Id -AutoSize")
229    {'retc': 0, 'stdout': 'Name    Id\\r\\n----    --\\r\\n...\\r\\n', 'stderr': ''}
230    """
231    # Determine authentication credentials
232    # For Kerberos, allow using existing credentials from kinit
233    username = getattr(args, 'WINRM_USERNAME', None)
234    password = getattr(args, 'WINRM_PASSWORD', None)
235
236    # Check if we should use Kerberos with existing credentials
237    _transport = (getattr(args, 'WINRM_TRANSPORT', None) or '').lower()
238    use_kerberos_cache = (_transport in ['kerberos', 'negotiate']) and (not username or not password)
239
240    if use_kerberos_cache:
241        # Use None for username/password to let Kerberos use credential cache
242        auth = (None, None)
243    else:
244        # Use provided credentials
245        auth = (username, password)
246        if getattr(args, 'WINRM_DOMAIN', None):
247            auth = (f'{username}@{args.WINRM_DOMAIN}', password)
248
249    if HAVE_JEA:
250        try:
251            # translate pywinrm transport -> pypsrp auth/ssl/port
252            _auth_map = {
253                'kerberos': 'kerberos',
254                'negotiate': 'negotiate',
255                'ntlm': 'negotiate',   # NTLM is negotiated under "negotiate"
256                'credssp': 'credssp',
257                'basic': 'basic',
258                'plaintext': 'basic',  # basic over HTTP
259                'ssl': 'basic',        # basic over HTTPS
260            }
261            _psrp_auth = _auth_map.get(_transport, 'negotiate')
262            _use_ssl = (_transport == 'ssl')
263            _port = 5986 if _use_ssl else 5985
264
265            # create PSRP client (like in winrm.Session)
266            session = Client(
267                server=args.WINRM_HOSTNAME,
268                username=auth[0],
269                password=auth[1],
270                auth=_psrp_auth,
271                ssl=_use_ssl,
272                port=_port,
273                cert_validation=True,
274            )
275
276            # run PowerShell
277            stdout, streams, had_errors = session.execute_ps(cmd)
278
279            # stdout is already a string; stderr from PSRP error stream(s)
280            stderr_lines = []
281            for err in getattr(streams, 'error', []):
282                # err.to_string() gives a readable message with category/position if available
283                try:
284                    stderr_lines.append(err.to_string())
285                except Exception:
286                    # fallback to message text
287                    msg = getattr(err, 'message', None) or str(err)
288                    stderr_lines.append(str(msg))
289            stderr = '\n'.join(stderr_lines)
290
291            result = {
292                'retc': 0 if not had_errors else 1,
293                'stdout': txt.to_text(stdout),
294                'stderr': txt.to_text(stderr),
295            }
296            return result
297        except Exception as e:
298            return {
299                'retc': 1,
300                'stdout': '',
301                'stderr': txt.exception2text(e),
302            }
303
304    if HAVE_WINRM:
305        try:
306            session = winrm.Session(
307                args.WINRM_HOSTNAME,
308                auth=auth,
309                transport=args.WINRM_TRANSPORT,
310            )
311
312            # run PowerShell
313            result = session.run_ps(cmd)
314
315            result = {
316                'retc': result.status_code,
317                'stdout': txt.to_text(result.std_out),
318                'stderr': txt.to_text(result.std_err),
319            }
320            # if `result.status_code == 0`, ignore stderr that starts with `#< CLIXML`
321            # (it's just progress noise)
322            if result['retc'] == 0 and result['stderr'].startswith('#< CLIXML'):
323                result['stderr'] = ''
324            return result
325        except Exception as e:
326            return {
327                'retc': 1,
328                'stdout': '',
329                'stderr': txt.exception2text(e),
330            }
331
332    # Neither pypsrp nor pywinrm is available
333    return {
334        'retc': 1,
335        'stdout': '',
336        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
337    }
def run_cmd(args, cmd, params=None):
 33def run_cmd(args, cmd, params=None):
 34    """
 35    Run a native command on a remote Windows host via WinRM/PSRP and return a
 36    normalized result dictionary.
 37
 38    Prefers **pypsrp (PSRP)** if available (for JEA/PowerShell Remoting
 39    compatibility); otherwise falls back to **pywinrm**. Authentication,
 40    transport and SSL/port selection are derived from the provided `args`.
 41
 42    ### Parameters
 43    - **args**: An object (e.g., `argparse.Namespace`) that provides at least:
 44        - `WINRM_HOSTNAME` (`str`): Target host or IP.
 45        - `WINRM_USERNAME` (`str`, optional): Username. If `None` or empty when using
 46          Kerberos transport, will use existing Kerberos credentials from credential cache
 47          (e.g., obtained via `kinit`).
 48        - `WINRM_PASSWORD` (`str`, optional): Password. If `None` or empty when using
 49          Kerberos transport, will use existing Kerberos credentials from credential cache.
 50        - `WINRM_TRANSPORT` (`str`, optional): Transport (e.g., `'negotiate'`, `'kerberos'`,
 51          `'ntlm'`, `'credssp'`, `'basic'`, `'ssl'`). Defaults to `'negotiate'` if unset.
 52        - `WINRM_DOMAIN` (`str`, optional): If set, username is sent as `user@domain`.
 53      (Additional fields may be honored by the underlying libraries if present.)
 54    - **cmd** (`str`): The executable/command to run remotely (native command, not a PowerShell
 55       script block).
 56    - **params** (`list[str]`, optional): Positional arguments passed to the command. Defaults
 57       to `[]`.
 58
 59    ### Returns
 60    - **dict**: A normalized result with:
 61        - `retc` (`int`): Process return code (`0` on success).
 62        - `stdout` (`str`): Captured standard output (text).
 63        - `stderr` (`str`): Captured standard error (text).
 64
 65    ### Behavior
 66    - If **pypsrp** is available, maps `WINRM_TRANSPORT` to an appropriate PSRP auth
 67      and chooses SSL/port (5986 for SSL, 5985 otherwise), then executes the command
 68      via `Client.execute_cmd()`.
 69    - If pypsrp is unavailable but **pywinrm** is installed, executes via
 70      `Session.run_cmd()`.
 71    - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
 72      (or are empty/None), the function will attempt to use existing Kerberos credentials
 73      from the credential cache (obtained via `kinit`).
 74    - On any exception, returns `{'retc': 1, 'stdout': '', 'stderr': <exception text>}`.
 75    - If neither backend is present, returns an error indicating that no compatible
 76      remoting library is available.
 77
 78    ### Example
 79    >>> # With explicit credentials:
 80    >>> run_cmd(args, "ipconfig", ["/all"])
 81    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
 82    >>> # With Kerberos using kinit credentials (username/password can be None):
 83    >>> run_cmd(args, "ipconfig", ["/all"])
 84    {'retc': 0, 'stdout': 'Windows IP Configuration\\r\\n...','stderr': ''}
 85    """
 86    # Determine authentication credentials
 87    # For Kerberos, allow using existing credentials from kinit
 88    username = getattr(args, 'WINRM_USERNAME', None)
 89    password = getattr(args, 'WINRM_PASSWORD', None)
 90
 91    # Check if we should use Kerberos with existing credentials
 92    _transport = (getattr(args, 'WINRM_TRANSPORT', None) or '').lower()
 93    use_kerberos_cache = (_transport in ['kerberos', 'negotiate']) and (not username or not password)
 94
 95    if use_kerberos_cache:
 96        # Use None for username/password to let Kerberos use credential cache
 97        auth = (None, None)
 98    else:
 99        # Use provided credentials
100        auth = (username, password)
101        if getattr(args, 'WINRM_DOMAIN', None):
102            auth = (f'{username}@{args.WINRM_DOMAIN}', password)
103
104    if params is None:
105        params = []
106
107    if HAVE_JEA:
108        try:
109            # translate pywinrm transport -> pypsrp auth/ssl/port
110            _auth_map = {
111                'kerberos': 'kerberos',
112                'negotiate': 'negotiate',
113                'ntlm': 'negotiate',   # NTLM is negotiated under "negotiate"
114                'credssp': 'credssp',
115                'basic': 'basic',
116                'plaintext': 'basic',  # basic over HTTP
117                'ssl': 'basic',        # basic over HTTPS
118            }
119            _psrp_auth = _auth_map.get(_transport, 'negotiate')
120            _use_ssl = (_transport == 'ssl')
121            _port = 5986 if _use_ssl else 5985
122
123            # create PSRP client
124            session = Client(
125                server=args.WINRM_HOSTNAME,
126                username=auth[0],
127                password=auth[1],
128                auth=_psrp_auth,
129                ssl=_use_ssl,
130                port=_port,
131                cert_validation=True,
132            )
133
134            # run native command (not PowerShell script)
135            stdout, stderr, rc = session.execute_cmd(cmd, params)
136
137            return {
138                'retc': rc,
139                'stdout': txt.to_text(stdout),
140                'stderr': txt.to_text(stderr),
141            }
142        except Exception as e:
143            return {
144                'retc': 1,
145                'stdout': '',
146                'stderr': txt.exception2text(e),
147            }
148
149    if HAVE_WINRM:
150        try:
151            session = winrm.Session(
152                args.WINRM_HOSTNAME,
153                auth=auth,
154                transport=args.WINRM_TRANSPORT,
155            )
156
157            result = session.run_cmd(cmd, params)
158            return {
159                'retc': result.status_code,
160                'stdout': txt.to_text(result.std_out),
161                'stderr': txt.to_text(result.std_err),
162            }
163        except Exception as e:
164            return {
165                'retc': 1,
166                'stdout': '',
167                'stderr': txt.exception2text(e),
168            }
169
170    # Neither pypsrp nor pywinrm is available
171    return {
172        'retc': 1,
173        'stdout': '',
174        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
175    }

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):
178def run_ps(args, cmd):
179    """
180    Run a PowerShell script/string on a remote Windows host via WinRM/PSRP and
181    return a normalized result dictionary.
182
183    Prefers **pypsrp (PSRP)** if available (best for JEA/PowerShell Remoting);
184    otherwise falls back to **pywinrm**. Authentication, transport, and SSL/port
185    are derived from the provided `args`.
186
187    ### Parameters
188    - **args**: An object (e.g., `argparse.Namespace`) that provides at least:
189        - `WINRM_HOSTNAME` (`str`): Target host or IP.
190        - `WINRM_USERNAME` (`str`, optional): Username. If `None` or empty when using
191          Kerberos transport, will use existing Kerberos credentials from credential cache
192          (e.g., obtained via `kinit`).
193        - `WINRM_PASSWORD` (`str`, optional): Password. If `None` or empty when using
194          Kerberos transport, will use existing Kerberos credentials from credential cache.
195        - `WINRM_TRANSPORT` (`str`, optional): Transport (`'negotiate'`, `'kerberos'`,
196          `'ntlm'`, `'credssp'`, `'basic'`, `'ssl'`, etc.). Defaults to `'negotiate'`
197          if unset.
198        - `WINRM_DOMAIN` (`str`, optional): If set, username is sent as `user@domain`.
199      (Additional attributes may be honored by the underlying libraries if present.)
200    - **cmd** (`str`): PowerShell scriptblock/string to execute remotely.
201
202    ### Returns
203    - **dict**: A normalized result with:
204        - `retc` (`int`): Return code (`0` if no PowerShell errors were reported).
205        - `stdout` (`str`): Captured standard output/text from the script.
206        - `stderr` (`str`): Aggregated error/diagnostic output.
207          - For **PSRP**: collects entries from the PowerShell *Error* stream
208            (human-readable via `to_string()` when available).
209          - For **pywinrm**: uses `std_err`; if `retc == 0` and stderr begins with
210            `#< CLIXML`, it is suppressed as benign progress noise.
211
212    ### Behavior
213    - Maps `WINRM_TRANSPORT` to PSRP auth (`kerberos`, `negotiate`, `credssp`, `basic`)
214      and decides SSL/port (5986 for SSL, 5985 otherwise) when using **pypsrp**,
215      then executes via `Client.execute_ps()`.
216    - Falls back to **pywinrm** and executes via `Session.run_ps()` if pypsrp is not available.
217    - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
218      (or are empty/None), the function will attempt to use existing Kerberos credentials
219      from the credential cache (obtained via `kinit`).
220    - On any exception, returns `{'retc': 1, 'stdout': '', 'stderr': <exception text>}`.
221    - If neither backend is installed, returns an error indicating that no compatible
222      remoting library is available.
223
224    ### Example
225    >>> # With explicit credentials:
226    >>> run_ps(args, "Get-Process | Select-Object -First 1 | Format-Table Name,Id -AutoSize")
227    {'retc': 0, 'stdout': 'Name    Id\\r\\n----    --\\r\\n...\\r\\n', 'stderr': ''}
228    >>> # With Kerberos using kinit credentials (username/password can be None):
229    >>> run_ps(args, "Get-Process | Select-Object -First 1 | Format-Table Name,Id -AutoSize")
230    {'retc': 0, 'stdout': 'Name    Id\\r\\n----    --\\r\\n...\\r\\n', 'stderr': ''}
231    """
232    # Determine authentication credentials
233    # For Kerberos, allow using existing credentials from kinit
234    username = getattr(args, 'WINRM_USERNAME', None)
235    password = getattr(args, 'WINRM_PASSWORD', None)
236
237    # Check if we should use Kerberos with existing credentials
238    _transport = (getattr(args, 'WINRM_TRANSPORT', None) or '').lower()
239    use_kerberos_cache = (_transport in ['kerberos', 'negotiate']) and (not username or not password)
240
241    if use_kerberos_cache:
242        # Use None for username/password to let Kerberos use credential cache
243        auth = (None, None)
244    else:
245        # Use provided credentials
246        auth = (username, password)
247        if getattr(args, 'WINRM_DOMAIN', None):
248            auth = (f'{username}@{args.WINRM_DOMAIN}', password)
249
250    if HAVE_JEA:
251        try:
252            # translate pywinrm transport -> pypsrp auth/ssl/port
253            _auth_map = {
254                'kerberos': 'kerberos',
255                'negotiate': 'negotiate',
256                'ntlm': 'negotiate',   # NTLM is negotiated under "negotiate"
257                'credssp': 'credssp',
258                'basic': 'basic',
259                'plaintext': 'basic',  # basic over HTTP
260                'ssl': 'basic',        # basic over HTTPS
261            }
262            _psrp_auth = _auth_map.get(_transport, 'negotiate')
263            _use_ssl = (_transport == 'ssl')
264            _port = 5986 if _use_ssl else 5985
265
266            # create PSRP client (like in winrm.Session)
267            session = Client(
268                server=args.WINRM_HOSTNAME,
269                username=auth[0],
270                password=auth[1],
271                auth=_psrp_auth,
272                ssl=_use_ssl,
273                port=_port,
274                cert_validation=True,
275            )
276
277            # run PowerShell
278            stdout, streams, had_errors = session.execute_ps(cmd)
279
280            # stdout is already a string; stderr from PSRP error stream(s)
281            stderr_lines = []
282            for err in getattr(streams, 'error', []):
283                # err.to_string() gives a readable message with category/position if available
284                try:
285                    stderr_lines.append(err.to_string())
286                except Exception:
287                    # fallback to message text
288                    msg = getattr(err, 'message', None) or str(err)
289                    stderr_lines.append(str(msg))
290            stderr = '\n'.join(stderr_lines)
291
292            result = {
293                'retc': 0 if not had_errors else 1,
294                'stdout': txt.to_text(stdout),
295                'stderr': txt.to_text(stderr),
296            }
297            return result
298        except Exception as e:
299            return {
300                'retc': 1,
301                'stdout': '',
302                'stderr': txt.exception2text(e),
303            }
304
305    if HAVE_WINRM:
306        try:
307            session = winrm.Session(
308                args.WINRM_HOSTNAME,
309                auth=auth,
310                transport=args.WINRM_TRANSPORT,
311            )
312
313            # run PowerShell
314            result = session.run_ps(cmd)
315
316            result = {
317                'retc': result.status_code,
318                'stdout': txt.to_text(result.std_out),
319                'stderr': txt.to_text(result.std_err),
320            }
321            # if `result.status_code == 0`, ignore stderr that starts with `#< CLIXML`
322            # (it's just progress noise)
323            if result['retc'] == 0 and result['stderr'].startswith('#< CLIXML'):
324                result['stderr'] = ''
325            return result
326        except Exception as e:
327            return {
328                'retc': 1,
329                'stdout': '',
330                'stderr': txt.exception2text(e),
331            }
332
333    # Neither pypsrp nor pywinrm is available
334    return {
335        'retc': 1,
336        'stdout': '',
337        'stderr': 'No compatible remoting library available (pypsrp or pywinrm).',
338    }

Run a PowerShell script/string 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. Authentication, transport, and SSL/port 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 ('negotiate', 'kerberos', 'ntlm', 'credssp', 'basic', 'ssl', etc.). Defaults to 'negotiate' if unset.
    • WINRM_DOMAIN (str, optional): If set, username is sent as user@domain. (Additional attributes may be honored by the underlying libraries if present.)
  • cmd (str): PowerShell scriptblock/string to execute remotely.

Returns

  • dict: A normalized result with:
    • retc (int): Return code (0 if no PowerShell errors were reported).
    • stdout (str): Captured standard output/text from the script.
    • stderr (str): Aggregated error/diagnostic output.
      • For PSRP: collects entries from the PowerShell Error stream (human-readable via to_string() when available).
      • For pywinrm: uses std_err; if retc == 0 and stderr begins with #< CLIXML, it is suppressed as benign progress noise.

Behavior

  • Maps WINRM_TRANSPORT to PSRP auth (kerberos, negotiate, credssp, basic) and decides SSL/port (5986 for SSL, 5985 otherwise) when using pypsrp, then executes via Client.execute_ps().
  • Falls back to pywinrm and executes via Session.run_ps() if pypsrp is not available.
  • 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 installed, returns an error indicating that no compatible remoting library is available.

Example

>>> # With explicit credentials:
>>> run_ps(args, "Get-Process | Select-Object -First 1 | Format-Table Name,Id -AutoSize")
{'retc': 0, 'stdout': 'Name    Id\r\n----    --\r\n...\r\n', 'stderr': ''}
>>> # With Kerberos using kinit credentials (username/password can be None):
>>> run_ps(args, "Get-Process | Select-Object -First 1 | Format-Table Name,Id -AutoSize")
{'retc': 0, 'stdout': 'Name    Id\r\n----    --\r\n...\r\n', 'stderr': ''}