1414__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
1515__version__ = '2025111402'
1616
17-
18- import shlex
19-
2017try :
2118 import winrm
2219 HAVE_WINRM = True
3128
3229from . import txt
3330
34-
3531def run_cmd (args , cmd , params = None ):
3632 """
3733 Run a native command on a remote Windows host via WinRM/PSRP and return a
@@ -133,9 +129,7 @@ def run_cmd(args, cmd, params=None):
133129 cert_validation = True ,
134130 )
135131
136- full_command = shlex .join ([cmd ] + params )
137- stdout , stderr , rc = session .execute_cmd (full_command )
138-
132+ stdout , stderr , rc = session .execute_cmd (cmd , args = params )
139133 return {
140134 'retc' : rc ,
141135 'stdout' : txt .to_text (stdout ),
@@ -177,9 +171,9 @@ def run_cmd(args, cmd, params=None):
177171 }
178172
179173
180- def run_ps (args , cmd ):
174+ def run_ps (args , cmd , params = None ):
181175 """
182- Run a PowerShell script/string on a remote Windows host via WinRM/PSRP and
176+ Run a PowerShell cmdlet on a remote Windows host via WinRM/PSRP and
183177 return a normalized result dictionary.
184178
185179 Prefers **pypsrp (PSRP)** if available (best for JEA/PowerShell Remoting);
@@ -202,7 +196,12 @@ def run_ps(args, cmd):
202196 name (JEA endpoint). Defaults to `'Microsoft.PowerShell'` if unset.
203197 Only supported with **pypsrp**.
204198 (Additional attributes may be honored by the underlying libraries if present.)
205- - **cmd** (`str`): PowerShell scriptblock/string to execute remotely.
199+ - **cmd** (`str`): PowerShell cmdlet name to execute remotely (e.g. `'Get-Service'`).
200+ When using pypsrp, this is passed directly as a cmdlet to the pipeline so JEA
201+ can properly allow/deny it. When falling back to pywinrm, it is passed as a
202+ scriptblock string.
203+ - **params** (`list[str]`, optional): Positional arguments passed to the cmdlet.
204+ Only used with pypsrp. Defaults to `[]`.
206205
207206 ### Returns
208207 - **dict**: A normalized result with:
@@ -217,7 +216,8 @@ def run_ps(args, cmd):
217216 ### Behavior
218217 - Maps `WINRM_TRANSPORT` to PSRP auth (`kerberos`, `negotiate`, `credssp`, `basic`)
219218 and decides SSL/port (5986 for SSL, 5985 otherwise) when using **pypsrp**,
220- then executes via `Client.execute_ps()`.
219+ then executes via a direct PSRP pipeline (RunspacePool + PowerShell) to avoid
220+ Invoke-Expression wrapping, ensuring JEA can enforce allow/deny on the cmdlet name.
221221 - Falls back to **pywinrm** and executes via `Session.run_ps()` if pypsrp is not available.
222222 - For Kerberos authentication: if `WINRM_USERNAME` and `WINRM_PASSWORD` are not provided
223223 (or are empty/None), the function will attempt to use existing Kerberos credentials
@@ -235,7 +235,7 @@ def run_ps(args, cmd):
235235 {'retc': 0, 'stdout': 'Name Id\\ r\\ n---- --\\ r\\ n...\\ r\\ n', 'stderr': ''}
236236 >>> # With custom configuration name (JEA endpoint):
237237 >>> args.WINRM_CONFIGURATION_NAME = 'MyJEAEndpoint'
238- >>> run_ps(args, "Get-Service", [])
238+ >>> run_ps(args, "Get-Service", ["servicename" ])
239239 {'retc': 0, 'stdout': '...','stderr': ''}
240240 """
241241 # Determine authentication credentials
@@ -256,8 +256,22 @@ def run_ps(args, cmd):
256256 if getattr (args , 'WINRM_DOMAIN' , None ):
257257 auth = (f'{ username } @{ args .WINRM_DOMAIN } ' , password )
258258
259+ if params is None :
260+ params = []
261+
262+ configuration_name = getattr (args , 'WINRM_CONFIGURATION_NAME' , None )
263+ if configuration_name and not HAVE_JEA :
264+ return {
265+ 'retc' : 1 ,
266+ 'stdout' : '' ,
267+ 'stderr' : 'WINRM_CONFIGURATION_NAME requires pypsrp (JEA). Install pypsrp or unset --winrm-configuration-name.' ,
268+ }
269+
259270 if HAVE_JEA :
260271 try :
272+ from pypsrp .powershell import PowerShell , RunspacePool
273+ from pypsrp .wsman import WSMan
274+
261275 # translate pywinrm transport -> pypsrp auth/ssl/port
262276 _auth_map = {
263277 'kerberos' : 'kerberos' ,
@@ -272,8 +286,7 @@ def run_ps(args, cmd):
272286 _use_ssl = (_transport == 'ssl' )
273287 _port = 5986 if _use_ssl else 5985
274288
275- # create PSRP client (like in winrm.Session)
276- session = Client (
289+ wsman = WSMan (
277290 server = args .WINRM_HOSTNAME ,
278291 username = auth [0 ],
279292 password = auth [1 ],
@@ -283,16 +296,25 @@ def run_ps(args, cmd):
283296 cert_validation = True ,
284297 )
285298
286- # run PowerShell
287- _configuration_name = getattr (args , 'WINRM_CONFIGURATION_NAME' , None )
288- if _configuration_name :
289- stdout , streams , had_errors = session .execute_ps (cmd , configuration_name = _configuration_name )
290- else :
291- stdout , streams , had_errors = session .execute_ps (cmd )
299+ if not params :
300+ parts = cmd .split ()
301+ cmd = parts [0 ]
302+ params = parts [1 :]
292303
293- # stdout is already a string; stderr from PSRP error stream(s)
304+ # Use RunspacePool + PowerShell directly to avoid Invoke-Expression wrapping, so JEA can properly allow/deny
305+ # the cmdlet by name rather than seeing a raw string blob.
306+ with RunspacePool (wsman , configuration_name = configuration_name or 'Microsoft.PowerShell' ) as pool :
307+ ps = PowerShell (pool )
308+ ps .add_cmdlet (cmd )
309+ for param in params :
310+ ps .add_argument (param )
311+ output = ps .invoke ()
312+
313+ stdout = '\n ' .join ([str (o ) for o in output ])
314+
315+ # stderr from PSRP error stream(s)
294316 stderr_lines = []
295- for err in getattr ( streams , ' error' , []) :
317+ for err in ps . streams . error :
296318 # err.to_string() gives a readable message with category/position if available
297319 try :
298320 stderr_lines .append (err .to_string ())
@@ -303,7 +325,7 @@ def run_ps(args, cmd):
303325 stderr = '\n ' .join (stderr_lines )
304326
305327 result = {
306- 'retc' : 0 if not had_errors else 1 ,
328+ 'retc' : 0 if not ps . had_errors else 1 ,
307329 'stdout' : txt .to_text (stdout ),
308330 'stderr' : txt .to_text (stderr ),
309331 }
@@ -348,4 +370,4 @@ def run_ps(args, cmd):
348370 'retc' : 1 ,
349371 'stdout' : '' ,
350372 'stderr' : 'No compatible remoting library available (pypsrp or pywinrm).' ,
351- }
373+ }
0 commit comments