diff --git a/capabilities/network-ops/tools/impacket.py b/capabilities/network-ops/tools/impacket.py index f3bd0c2..8bbee03 100644 --- a/capabilities/network-ops/tools/impacket.py +++ b/capabilities/network-ops/tools/impacket.py @@ -61,45 +61,62 @@ def _extract_real_path_from_wrapper(wrapper_path: Path) -> Path | None: def _ensure_impacket_installed() -> None: - """Install impacket into the running Python if it's not importable. + """Ensure impacket is importable by subprocesses using sys.executable. - The runtime may not process ``dependencies.python`` from - ``capability.yaml``, so we do a best-effort pip install at import - time as a fallback. + The in-process ``import impacket`` may succeed (via inherited + PYTHONPATH or global site-packages) while subprocesses spawned with + ``sys.executable`` fail because they run in an isolated venv. We + check the *subprocess* environment specifically, since that's what + ``_build_script_command`` uses to run impacket scripts. Set ``DREADNODE_SKIP_AUTO_INSTALL=1`` to disable (useful in tests/CI). """ + import subprocess + if os.environ.get("DREADNODE_SKIP_AUTO_INSTALL", "").strip() in ("1", "true", "yes"): return + # Check if sys.executable can import impacket as a subprocess — + # this matches how _build_script_command actually runs scripts. try: - import impacket as _ # noqa: F401 - except ImportError: - import subprocess + subprocess.run( + [sys.executable, "-c", "import impacket"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + timeout=10, + ) + return # sys.executable can import impacket — nothing to do + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError): + pass - logger.warning("impacket not importable — attempting pip install") - for extra_args in ([], ["--break-system-packages"]): - try: - subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "--quiet", - "impacket>=0.12.0", - *extra_args, - ], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=True, - timeout=120, - ) - logger.info("impacket installed successfully") - return - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError): - continue - logger.error("Failed to install impacket — impacket tools will not work") + logger.warning( + f"impacket not importable by {sys.executable} — attempting pip install" + ) + for extra_args in ([], ["--break-system-packages"]): + try: + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--quiet", + "impacket>=0.12.0", + *extra_args, + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + timeout=120, + ) + logger.info("impacket installed successfully") + return + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError): + continue + logger.error( + f"Failed to install impacket into {sys.executable} — impacket tools will not work" + ) _ensure_impacket_installed() @@ -112,13 +129,34 @@ def _get_impacket_script_path() -> Path: Returns a directory containing real Python impacket scripts (not shell wrappers) so they can be invoked via sys.executable. - Tries multiple common locations: - 1. Global PATH: resolve the found script's directory, following + Tries multiple common locations, preferring site-packages (which + matches sys.executable) over PATH (which may contain pip entry + point wrappers installed for a different Python version): + 1. pip-installed: site-packages/impacket/examples/ (same Python as sys.executable) + 2. apt-installed: /usr/share/doc/python3-impacket/examples/ + 3. Global PATH: resolve the found script's directory, following shell wrappers to the real scripts if needed - 2. pip-installed: site-packages/impacket/examples/ - 3. apt-installed: /usr/share/doc/python3-impacket/examples/ """ - # Check if scripts are on PATH (pipx / manual install) + # Prefer site-packages — these scripts are guaranteed to work with + # sys.executable since they live in the same Python installation. + # PATH entries (e.g. ~/.local/bin/) are pip entry point wrappers + # that may be installed for a different Python version. + try: + import impacket + + impacket_pkg_path = Path(impacket.__file__).parent + examples_path = impacket_pkg_path / "examples" + if examples_path.exists() and (examples_path / "secretsdump.py").exists(): + return examples_path + except ImportError: + pass + + # Fall back to apt installation path + apt_path = Path("/usr/share/doc/python3-impacket/examples/") + if apt_path.exists() and (apt_path / "secretsdump.py").exists(): + return apt_path + + # Last resort: check PATH (pipx / manual install) found = shutil.which("secretsdump.py") if found is not None: found_path = Path(found).resolve() @@ -138,23 +176,6 @@ def _get_impacket_script_path() -> Path: f"Impacket script at {found_path} is a shell wrapper, skipping PATH discovery" ) - # Try to find impacket in site-packages (pip install) - try: - import impacket - - impacket_pkg_path = Path(impacket.__file__).parent - examples_path = impacket_pkg_path / "examples" - # Check that actual script files exist, not just the directory - if examples_path.exists() and (examples_path / "secretsdump.py").exists(): - return examples_path - except ImportError: - pass - - # Fall back to apt installation path - apt_path = Path("/usr/share/doc/python3-impacket/examples/") - if apt_path.exists() and (apt_path / "secretsdump.py").exists(): - return apt_path - # Default fallback return Path("/usr/share/doc/python3-impacket/examples/")