Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions contents/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import os


def check_is_file(destination):
Expand Down Expand Up @@ -64,3 +65,29 @@ def conditionalReplace( aMatch ) :
result = '"' + result + '"'

return result+' '

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add a credential redaction helper here to ensure we don't leak proxy passwords into the Rundeck logs.

def _redact_proxy_url(url):
    """Redact userinfo from proxy URL for safe logging."""
    if not url:
        return url
    try:
        try:
            from urllib.parse import urlparse, urlunparse
        except ImportError:
            from urlparse import urlparse, urlunparse
        parsed = urlparse(url)
        if parsed.username or parsed.password:
            netloc = parsed.hostname or ''
            if parsed.port:
                netloc += ':%s' % parsed.port
            return urlunparse(parsed._replace(netloc=netloc))
    except Exception:
        return '[REDACTED]'
    return url


def configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log):
from requests.utils import should_bypass_proxies

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import will cause a runtime crash on environments running requests < 2.14.0. We need to wrap this in a try/except block to make the plugin stable for older systems.

Suggested change
from requests.utils import should_bypass_proxies
try:
from requests.utils import should_bypass_proxies
except (ImportError, AttributeError):
log.warning("requests.utils.should_bypass_proxies not available; NO_PROXY requires requests >= 2.14.0")
if winrmproxy:
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY (%s)" % (endpoint, _redact_proxy_url(winrmproxy)))
return arguments


if winrmproxy:
if winrmnoproxy:
# Delegate to requests via env vars so NO_PROXY matching works
os.environ['HTTP_PROXY'] = winrmproxy
os.environ['HTTPS_PROXY'] = winrmproxy
os.environ['NO_PROXY'] = winrmnoproxy
log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid logging the raw proxy URL here to prevent leaking plain-text passwords in the debug logs.

Suggested change
log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy))
log.debug("Proxy via env vars: HTTP(S)_PROXY set, NO_PROXY=%s" % winrmnoproxy)


if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
Comment on lines +81 to +83
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log message says "matched NO_PROXY" but prints the entire winrmnoproxy list, not the specific entry/pattern that matched. This is misleading (and differs from the PR description examples). Consider rewording to avoid implying a single match, or implement logic to identify which entry matched before logging it.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this intentionally so the customer can now see whether the proxy connection is using DIRECT or PROXY. It's just an INFO log.

log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the _redact_proxy_url helper here. This safely hides the password but still shows the proxy host in the logs

Suggested change
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
log.info("Connecting to %s via PROXY (%s)" % (endpoint, _redact_proxy_url(winrmproxy)))

else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
Comment on lines +84 to +88
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The INFO log message includes the full proxy URL, which may contain embedded credentials (e.g., http://user:pass@host:port per the plugin config). This would leak secrets into Rundeck logs. Consider redacting userinfo before logging (and likewise for any debug logs).

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +88
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

log.debug("Proxy via env vars: HTTP(S)_PROXY=%s...") can also leak proxy credentials when the proxy URL contains user:pass. Even though it's debug-level, Rundeck debug logs are often collected centrally; please redact credentials (or avoid logging the full URL).

Suggested change
log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy))
if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
log.debug("Proxy via env vars: HTTP(S)_PROXY set, NO_PROXY=%s" % winrmnoproxy)
if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
log.info("Connecting to %s via PROXY" % endpoint)
else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY" % endpoint)

Copilot uses AI. Check for mistakes.
else:
if winrmnoproxy:
log.warning("noproxy is set but no proxy configured; noproxy ignored")
log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch logs "DIRECTLY (no proxy configured)", but the request may still go through a proxy if the Rundeck process already has HTTP(S)_PROXY set in its environment (requests/pywinrm will honor env proxies by default). Consider either (a) adjusting the message to clarify it's only about plugin config (not actual routing), or (b) detecting existing env proxies and logging the effective routing more accurately.

Suggested change
log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint)
# No proxy configured via plugin; check for environment proxies that requests/pywinrm may still use
http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
if http_proxy or https_proxy:
log.info(
"Connecting to %s via PROXY from environment (HTTP_PROXY=%s, HTTPS_PROXY=%s)"
% (endpoint, http_proxy, https_proxy)
)
else:
log.info(
"Connecting to %s DIRECTLY (no proxy configured in plugin or environment)" % endpoint
)

Copilot uses AI. Check for mistakes.
return arguments
12 changes: 12 additions & 0 deletions contents/winrm-check.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import colored_formatter
from colored_formatter import ColoredFormatter
import kerberosauth
import common


#checking and importing dependencies
Expand Down Expand Up @@ -105,6 +106,8 @@
parser.add_argument('--debug', help='debug',default="False")
parser.add_argument('--certpath', help='certpath')
parser.add_argument('--krb5config', help='krb5config',default="/etc/krb5.conf")
parser.add_argument('--proxy', help='proxy', default=None)
parser.add_argument('--noproxy', help='noproxy patterns', default=None)


args = parser.parse_args()
Expand Down Expand Up @@ -161,6 +164,13 @@
if args.certpath:
certpath = args.certpath

winrmproxy = None
winrmnoproxy = None
if args.proxy and args.proxy not in ("None", ""):
winrmproxy = args.proxy
if args.noproxy and args.noproxy not in ("None", ""):
winrmnoproxy = args.noproxy

if not hostname:
print("hostname is required")
sys.exit(1)
Expand Down Expand Up @@ -243,6 +253,8 @@

arguments["credssp_disable_tlsv1_2"] = diabletls12

common.configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log)

if authentication == "kerberos":
k5bConfig = kerberosauth.KerberosAuth(krb5config=krb5config, log=log, kinit_command=kinit,username=username, password=password)
k5bConfig.get_ticket()
Expand Down
8 changes: 6 additions & 2 deletions contents/winrm-exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def httpclient_log(*args):
retryconnectiondelay = 0
username = None
winrmproxy = None
winrmnoproxy = None

if "RD_CONFIG_AUTHTYPE" in os.environ:
authentication = os.getenv("RD_CONFIG_AUTHTYPE")
Expand Down Expand Up @@ -189,6 +190,10 @@ def httpclient_log(*args):
winrmproxy = os.getenv("RD_CONFIG_WINRMPROXY")
log.debug("winrmproxy: " + str(winrmproxy))

if "RD_CONFIG_WINRMNOPROXY" in os.environ:
winrmnoproxy = os.getenv("RD_CONFIG_WINRMNOPROXY")
log.debug("winrmnoproxy: " + str(winrmnoproxy))

if "RD_CONFIG_ENABLEDHTTPDEBUG" in os.environ:
if os.getenv("RD_CONFIG_ENABLEDHTTPDEBUG") == "true":
enabledHttpDebug = True
Expand Down Expand Up @@ -311,8 +316,7 @@ def httpclient_log(*args):
if(readtimeout):
arguments["read_timeout_sec"] = readtimeout

if(winrmproxy):
arguments["proxy"] = winrmproxy
common.configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log)

if(operationtimeout):
arguments["operation_timeout_sec"] = operationtimeout
Expand Down
7 changes: 5 additions & 2 deletions contents/winrm-filecopier.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ def winrm_upload(self,
certpath = None
username = None
winrmproxy = None
winrmnoproxy = None

if os.environ.get('RD_CONFIG_OVERRIDE') == 'true':
override = True
Expand Down Expand Up @@ -299,6 +300,9 @@ def winrm_upload(self,
if "RD_CONFIG_WINRMPROXY" in os.environ:
winrmproxy = os.getenv("RD_CONFIG_WINRMPROXY")

if "RD_CONFIG_WINRMNOPROXY" in os.environ:
winrmnoproxy = os.getenv("RD_CONFIG_WINRMNOPROXY")

if "RD_OPTION_USERNAME" in os.environ and os.getenv("RD_OPTION_USERNAME"):
#take user from job
username = os.getenv("RD_OPTION_USERNAME").strip('\'')
Expand Down Expand Up @@ -367,8 +371,7 @@ def winrm_upload(self,

arguments["credssp_disable_tlsv1_2"] = diabletls12

if(winrmproxy):
arguments["proxy"] = winrmproxy
common.configure_proxy(arguments, winrmproxy, winrmnoproxy, endpoint, log)

if(readtimeout):
arguments["read_timeout_sec"] = readtimeout
Expand Down
32 changes: 30 additions & 2 deletions plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ providers:
renderingOptions:
groupName: Connection
instance-scope-node-attribute: "winrm-proxy"
- name: winrmnoproxy
title: No Proxy List
description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`"
type: String
Comment on lines +113 to +116
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar/wording: "It can be overwriting at node level" should be "It can be overridden at node level" (and similarly for any other occurrences).

Copilot uses AI. Check for mistakes.
required: false
scope: Instance
renderingOptions:
groupName: Connection
instance-scope-node-attribute: "winrm-noproxy"
- name: operationtimeout
title: operation timeout
description: "maximum allowed time in seconds for any single wsman HTTP operation (default 20). Note that operation timeouts while receiving output (the only wsman operation that should take any significant time, and where these timeouts are expected) will be silently retried indefinitely. It can be overwriting at node level using `winrm-operationtimeout`"
Expand Down Expand Up @@ -380,6 +389,15 @@ providers:
renderingOptions:
groupName: Connection
instance-scope-node-attribute: "winrm-proxy"
- name: winrmnoproxy
title: No Proxy List
description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`"
type: String
Comment on lines +392 to +395
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar/wording: "It can be overwriting at node level" should be "It can be overridden at node level".

Copilot uses AI. Check for mistakes.
required: false
scope: Instance
renderingOptions:
groupName: Connection
instance-scope-node-attribute: "winrm-noproxy"
- name: enabledhttpdebug
title: Enable HTTP logging in debug mode
description: "Print extra http logging in debug mode"
Expand Down Expand Up @@ -417,7 +435,7 @@ providers:
plugin-type: script
script-interpreter: ${config.interpreter} -u
script-file: winrm-check.py
script-args: --username ${config.username} --hostname ${config.hostname} --password ${config.password_storage_path} --authentication ${config.authtype} --transport ${config.winrmtransport} --port ${config.winrmport} --nossl ${config.nossl} --debug ${config.debug} --certpath ${config.certpath}
script-args: --username ${config.username} --hostname ${config.hostname} --password ${config.password_storage_path} --authentication ${config.authtype} --transport ${config.winrmtransport} --port ${config.winrmport} --nossl ${config.nossl} --debug ${config.debug} --certpath ${config.certpath} --proxy ${config.winrmproxy} --noproxy ${config.winrmnoproxy}
config:
- name: interpreter
title: Python Interpreter
Expand Down Expand Up @@ -512,4 +530,14 @@ providers:
default: "false"
required: false
renderingOptions:
groupName: Kerberos
groupName: Kerberos
- name: winrmproxy
title: Proxy
description: "Specify a proxy address for communicating with Windows nodes. Example HTTP proxy strings are http://server:port and http://user:pass@server:port. An example SOCKS5 proxy string is socks5://user:pass@server:port."
type: String
required: false
- name: winrmnoproxy
title: No Proxy List
description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*)."
type: String
required: false
Loading