CIS_Benchmarks/nginx_audit/executor.py
2026-06-18 20:47:15 +03:00

85 lines
2.5 KiB
Python

"""Command executors: run shell commands locally or over SSH."""
from __future__ import annotations
import subprocess
from typing import Tuple
class ExecutorError(Exception):
pass
class LocalExecutor:
"""Run shell commands on the local machine."""
name = "localhost"
def run(self, cmd: str, timeout: int = 30) -> Tuple[str, int]:
"""
Execute *cmd* in a shell and return (combined_output, exit_code).
stdout and stderr are merged so callers get one string to inspect.
"""
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
timeout=timeout,
)
combined = (result.stdout + result.stderr).strip()
return combined, result.returncode
except subprocess.TimeoutExpired:
return f"[TIMEOUT after {timeout}s]", 1
except Exception as exc:
return f"[ERROR: {exc}]", 1
def close(self) -> None:
pass # nothing to clean up
class SSHExecutor:
"""Run shell commands on a remote host over SSH (requires paramiko)."""
def __init__(
self,
host: str,
port: int = 22,
username: str = "root",
password: str | None = None,
key_file: str | None = None,
) -> None:
try:
import paramiko # noqa: F401
except ImportError as exc:
raise ExecutorError(
"paramiko is required for SSH mode: pip install paramiko"
) from exc
import paramiko
self.name = host
self._client = paramiko.SSHClient()
self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self._client.connect(
host,
port=port,
username=username,
password=password,
key_filename=key_file,
timeout=10,
)
def run(self, cmd: str, timeout: int = 30) -> Tuple[str, int]:
"""Execute *cmd* on the remote host and return (combined_output, exit_code)."""
try:
_, stdout, stderr = self._client.exec_command(cmd, timeout=timeout)
exit_code = stdout.channel.recv_exit_status()
out = stdout.read().decode(errors="replace")
err = stderr.read().decode(errors="replace")
return (out + err).strip(), exit_code
except Exception as exc:
return f"[ERROR: {exc}]", 1
def close(self) -> None:
self._client.close()