Centralize SSH/clush command building and extract host resolution uti…#2149
Centralize SSH/clush command building and extract host resolution uti…#2149
Conversation
…lities Move host resolution functions (resolve_hostname_to_ip, get_primary_ipv4_from_netbox, resolve_host_with_fallback, get_hosts_from_group, select_host_from_list) from console.py into new osism/utils/hosts.py module. Add build_ssh_command() and build_clush_command() builder functions to osism/utils/ssh.py to eliminate duplicated SSH option boilerplate across console, log, report, compose, and container commands. This also removes shell=True subprocess calls in compose, container, and log commands. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
There was a problem hiding this comment.
Hey - I've found 15 security issues, and 1 other issue
Security issues:
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="osism/utils/ssh.py" line_range="74-71" />
<code_context>
+def build_clush_command(
</code_context>
<issue_to_address>
**issue (bug_risk):** Explicitly validate that either `hosts` or `group` is provided (and not both) when building the clush command.
If both `hosts` and `group` are `None`, `build_clush_command` returns `[CLUSH_BINARY, "-l", user]`, which will likely fail at runtime in a non-obvious way. If both are provided, `hosts` silently wins. Please have the function: (1) raise `ValueError` when neither is provided, and (2) either raise or otherwise clearly enforce/document that `hosts` and `group` are mutually exclusive, so misuse fails fast and call-site behavior is predictable.
</issue_to_address>
### Comment 2
<location path="osism/commands/compose.py" line_range="44" />
<code_context>
subprocess.call(build_ssh_command(host, remote_command=remote_command))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 3
<location path="osism/commands/console.py" line_range="73" />
<code_context>
subprocess.call(build_clush_command(group=host))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 4
<location path="osism/commands/console.py" line_range="89" />
<code_context>
subprocess.call(build_ssh_command(resolved_host))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 5
<location path="osism/commands/console.py" line_range="100-102" />
<code_context>
subprocess.call(
build_ssh_command(resolved_host, remote_command=ssh_command)
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 6
<location path="osism/commands/console.py" line_range="113-119" />
<code_context>
subprocess.call(
build_ssh_command(
resolved_target_host,
remote_command=ssh_command,
request_tty=True,
)
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 7
<location path="osism/commands/container.py" line_range="47" />
<code_context>
subprocess.call(build_ssh_command(host, remote_command=remote_command))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 8
<location path="osism/commands/container.py" line_range="52" />
<code_context>
subprocess.call(build_ssh_command(host, remote_command=remote_command))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 9
<location path="osism/commands/log.py" line_range="73" />
<code_context>
subprocess.call(build_ssh_command(host, remote_command=remote_command))
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 10
<location path="osism/commands/log.py" line_range="136-138" />
<code_context>
rc = subprocess.call(
build_clush_command(hosts=group_hosts, remote_command=tail_command)
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 11
<location path="osism/commands/log.py" line_range="156-158" />
<code_context>
rc = subprocess.call(
build_ssh_command(resolved_host, remote_command=tail_command)
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 12
<location path="osism/commands/report.py" line_range="83-91" />
<code_context>
memory_result = subprocess.run(
build_ssh_command(
resolved_host,
remote_command=dmidecode_command,
connect_timeout=10,
),
capture_output=True,
text=True,
timeout=30,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 13
<location path="osism/commands/report.py" line_range="101-109" />
<code_context>
uuid_result = subprocess.run(
build_ssh_command(
resolved_host,
remote_command=uuid_command,
connect_timeout=10,
),
capture_output=True,
text=True,
timeout=30,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 14
<location path="osism/commands/report.py" line_range="203-211" />
<code_context>
lldp_result = subprocess.run(
build_ssh_command(
resolved_host,
remote_command=lldp_command,
connect_timeout=10,
),
capture_output=True,
text=True,
timeout=30,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 15
<location path="osism/commands/report.py" line_range="359-367" />
<code_context>
bgp_result = subprocess.run(
build_ssh_command(
resolved_host,
remote_command=bgp_command,
connect_timeout=10,
),
capture_output=True,
text=True,
timeout=30,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>
### Comment 16
<location path="osism/commands/report.py" line_range="527-535" />
<code_context>
fact_result = subprocess.run(
build_ssh_command(
resolved_host,
remote_command=fact_command,
connect_timeout=10,
),
capture_output=True,
text=True,
timeout=30,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| if remote_command: | ||
| cmd.append(remote_command) | ||
|
|
||
| return cmd |
There was a problem hiding this comment.
issue (bug_risk): Explicitly validate that either hosts or group is provided (and not both) when building the clush command.
If both hosts and group are None, build_clush_command returns [CLUSH_BINARY, "-l", user], which will likely fail at runtime in a non-obvious way. If both are provided, hosts silently wins. Please have the function: (1) raise ValueError when neither is provided, and (2) either raise or otherwise clearly enforce/document that hosts and group are mutually exclusive, so misuse fails fast and call-site behavior is predictable.
| f"/usr/bin/ssh -i /ansible/secrets/id_rsa.operator {ssh_options} {settings.OPERATOR_USER}@{host} '{ssh_command}'", | ||
| shell=True, | ||
| ) | ||
| subprocess.call(build_ssh_command(host, remote_command=remote_command)) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| host, | ||
| ] | ||
| ) | ||
| subprocess.call(build_clush_command(group=host)) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| ] | ||
| ) | ||
| subprocess.call(build_ssh_command(resolved_host)) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| subprocess.call( | ||
| [ | ||
| "/usr/bin/ssh", | ||
| "-i", | ||
| "/ansible/secrets/id_rsa.operator", | ||
| *ssh_options, | ||
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| ssh_command, | ||
| ] | ||
| build_ssh_command(resolved_host, remote_command=ssh_command) | ||
| ) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'call' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| memory_result = subprocess.run( | ||
| [ | ||
| *ssh_base, | ||
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| dmidecode_command, | ||
| ], | ||
| build_ssh_command( | ||
| resolved_host, | ||
| remote_command=dmidecode_command, | ||
| connect_timeout=10, | ||
| ), | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| uuid_result = subprocess.run( | ||
| [ | ||
| *ssh_base, | ||
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| uuid_command, | ||
| ], | ||
| build_ssh_command( | ||
| resolved_host, | ||
| remote_command=uuid_command, | ||
| connect_timeout=10, | ||
| ), | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| lldp_result = subprocess.run( | ||
| [ | ||
| *ssh_base, | ||
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| lldp_command, | ||
| ], | ||
| build_ssh_command( | ||
| resolved_host, | ||
| remote_command=lldp_command, | ||
| connect_timeout=10, | ||
| ), | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| bgp_result = subprocess.run( | ||
| [ | ||
| *ssh_base, | ||
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| bgp_command, | ||
| ], | ||
| build_ssh_command( | ||
| resolved_host, | ||
| remote_command=bgp_command, | ||
| connect_timeout=10, | ||
| ), | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
| fact_result = subprocess.run( | ||
| [ | ||
| *ssh_base, | ||
| f"{settings.OPERATOR_USER}@{resolved_host}", | ||
| fact_command, | ||
| ], | ||
| build_ssh_command( | ||
| resolved_host, | ||
| remote_command=fact_command, | ||
| connect_timeout=10, | ||
| ), | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
…lities
Move host resolution functions (resolve_hostname_to_ip, get_primary_ipv4_from_netbox, resolve_host_with_fallback, get_hosts_from_group, select_host_from_list) from console.py into new osism/utils/hosts.py module. Add build_ssh_command() and build_clush_command() builder functions to osism/utils/ssh.py to eliminate duplicated SSH option boilerplate across console, log, report, compose, and container commands. This also removes shell=True subprocess calls in compose, container, and log commands.
AI-assisted: Claude Code