Skip to content

Commit 0ca00c6

Browse files
committed
refactor(winrm.py): execute ps jea friendly
1 parent 12c2592 commit 0ca00c6

1 file changed

Lines changed: 46 additions & 24 deletions

File tree

winrm.py

100644100755
Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
1515
__version__ = '2025111402'
1616

17-
18-
import shlex
19-
2017
try:
2118
import winrm
2219
HAVE_WINRM = True
@@ -31,7 +28,6 @@
3128

3229
from . import txt
3330

34-
3531
def 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

Comments
 (0)