From 0f554e245e3a98076061d89ea230dcdea1af0e28 Mon Sep 17 00:00:00 2001 From: Brian Greunke Date: Tue, 20 Oct 2026 08:29:03 -0500 Subject: [PATCH 1/2] fix(network-ops): isolate certipy-ad and bloodyAD into uv tool envs (ENG-8935) Remove certipy-ad and bloodyAD from the shared dependencies.python list and install each into its own isolated `uv tool` environment via install_coercion_tools.sh instead, avoiding dependency conflicts with the shared capability virtualenv. Each install is skipped when the tool is already present, and is non-fatal when `uv` is missing or the install fails. Updates the "not found in PATH" warnings in tools/certipy.py and tools/bloodyad.py to point at the new `uv tool install` commands. Bumps network-ops 3.0.1 -> 3.0.2. Co-Authored-By: Claude Opus 4.8 --- capabilities/network-ops/capability.yaml | 7 +++-- .../scripts/install_coercion_tools.sh | 29 +++++++++++++++++++ .../network-ops/tests/test_provisioning.py | 25 ++++++++++++++-- capabilities/network-ops/tools/bloodyad.py | 5 ++-- capabilities/network-ops/tools/certipy.py | 3 +- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/capabilities/network-ops/capability.yaml b/capabilities/network-ops/capability.yaml index c6e4890..ff876d0 100644 --- a/capabilities/network-ops/capability.yaml +++ b/capabilities/network-ops/capability.yaml @@ -1,6 +1,6 @@ schema: 1 name: network-ops -version: "3.0.1" +version: "3.0.2" description: > Network operations and Active Directory exploitation. Multi-agent pipeline for autonomous red teaming with Nmap scanning, Netexec @@ -51,10 +51,11 @@ dependencies: python: - "loguru>=0.7.0" - "impacket>=0.12.0" - - "certipy-ad" - - "bloodyAD" # NetExec (nxc) is deliberately absent here: it is not published on PyPI. The # install script below fetches the project's prebuilt standalone binary. + # certipy-ad and bloodyAD are also installed by the script below, each into + # its own isolated `uv tool` environment, to avoid dependency conflicts with + # the shared python deps above. # Runtime-fetched assets apt/pip can't provide: the NetExec binary, wordlists, # hashcat rules, and the NTLM coercion scripts. scripts: diff --git a/capabilities/network-ops/scripts/install_coercion_tools.sh b/capabilities/network-ops/scripts/install_coercion_tools.sh index eec21df..f167eea 100755 --- a/capabilities/network-ops/scripts/install_coercion_tools.sh +++ b/capabilities/network-ops/scripts/install_coercion_tools.sh @@ -98,6 +98,35 @@ else rm -rf /tmp/nxc.zip /tmp/nxc-extract 2>/dev/null || true fi +# -- certipy-ad --------------------------------------------------------------- +# Installed into its own isolated `uv tool` environment (not declared in +# dependencies.python) to avoid dependency conflicts with the shared +# capability virtualenv. Lands the `certipy`/`certipy-ad` command in +# $HOME/.local/bin. +if command -v certipy >/dev/null 2>&1 || command -v certipy-ad >/dev/null 2>&1; then + echo "[*] certipy already present, skipping" +elif ! command -v uv >/dev/null 2>&1; then + echo "[!] uv not found, cannot install certipy-ad (non-fatal)" +else + echo "[+] Installing certipy-ad 5.1.0 (uv tool)" + uv tool install certipy-ad==5.1.0 \ + || echo "[!] Failed to install certipy-ad (non-fatal)" +fi + +# -- bloodyAD ------------------------------------------------------------- +# Installed into its own isolated `uv tool` environment (not declared in +# dependencies.python) to avoid dependency conflicts with the shared +# capability virtualenv. Lands the `bloodyAD` command in $HOME/.local/bin. +if command -v bloodyAD >/dev/null 2>&1; then + echo "[*] bloodyAD already present, skipping" +elif ! command -v uv >/dev/null 2>&1; then + echo "[!] uv not found, cannot install bloodyAD (non-fatal)" +else + echo "[+] Installing bloodyAD 2.5.5 (uv tool)" + uv tool install bloodyAD==2.5.5 \ + || echo "[!] Failed to install bloodyAD (non-fatal)" +fi + # -- Coercion scripts ------------------------------------------------------- # These /opt paths are hardcoded in three places that must stay in sync: here, # the coercion-script checks: in capability.yaml, and the path defaults in the diff --git a/capabilities/network-ops/tests/test_provisioning.py b/capabilities/network-ops/tests/test_provisioning.py index 98994a5..20cce37 100644 --- a/capabilities/network-ops/tests/test_provisioning.py +++ b/capabilities/network-ops/tests/test_provisioning.py @@ -32,13 +32,34 @@ def test_apt_tools_declared() -> None: def test_pip_tools_declared() -> None: python = _manifest()["dependencies"].get("python", []) joined = " ".join(python) - for pkg in ("impacket", "certipy-ad", "bloodyAD"): + for pkg in ("loguru", "impacket"): assert pkg in joined, f"{pkg} must be declared in dependencies.python" + # certipy-ad and bloodyAD are installed into isolated `uv tool` + # environments by the install script instead, to avoid dependency + # conflicts with the shared capability virtualenv. They must stay out of + # the shared pip install. + assert "certipy-ad" not in joined, "certipy-ad must not be declared in dependencies.python" + assert "bloodyAD" not in joined, "bloodyAD must not be declared in dependencies.python" # NetExec is not on PyPI; declaring it here hard-fails the SDK's combined # pip install and takes the whole capability down. It must stay out. assert "netexec" not in joined.lower(), "netexec is not pip-installable; provision it in the image" +def test_certipy_and_bloodyad_installed_via_isolated_uv_tool_envs() -> None: + # certipy-ad and bloodyAD get their own `uv tool` environments (not the + # shared capability virtualenv) to avoid dependency conflicts, and each + # install is skipped when the tool is already present. + script = INSTALL_SCRIPT.read_text() + for pin, guard in ( + ("uv tool install certipy-ad==5.1.0", "command -v certipy"), + ("uv tool install bloodyAD==2.5.5", "command -v bloodyAD"), + ): + assert pin in script, f"script must install {pin}" + pin_index = script.index(pin) + guard_index = script.rfind(guard, 0, pin_index) + assert guard_index != -1, f"{pin} must be preceded by a `{guard}` guard" + + def test_every_invoked_binary_has_a_check() -> None: check_names = {c["name"] for c in _manifest().get("checks", [])} # Binaries the toolsets shell out to: a missing one must show up in @@ -112,7 +133,7 @@ def test_script_is_non_fatal_when_every_privileged_op_fails(tmp_path: Path) -> N """ stub_dir = tmp_path / "stubs" stub_dir.mkdir() - for name in ("mkdir", "sudo", "git", "curl", "gunzip"): + for name in ("mkdir", "sudo", "git", "curl", "gunzip", "uv"): stub = stub_dir / name stub.write_text("#!/bin/sh\nexit 1\n") stub.chmod(stub.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) diff --git a/capabilities/network-ops/tools/bloodyad.py b/capabilities/network-ops/tools/bloodyad.py index 3a7fe64..8244319 100644 --- a/capabilities/network-ops/tools/bloodyad.py +++ b/capabilities/network-ops/tools/bloodyad.py @@ -20,7 +20,7 @@ class BloodyAD(Toolset): cleartext passwords, pass-the-hash, pass-the-ticket, and certificates. Repository: https://github.com/CravateRouge/bloodyAD - Install: pip install bloodyAD + Install: uv tool install bloodyAD==2.5.5 """ timeout: int = Config(default=60) @@ -34,7 +34,8 @@ async def __aenter__(self): # Check if bloodyAD command is available if not shutil.which(self.bloodyad_cmd): logger.warning( - f"bloodyAD command '{self.bloodyad_cmd}' not found in PATH. Install it with: pip install bloodyAD" + f"bloodyAD command '{self.bloodyad_cmd}' not found in PATH. " + "Install it with: uv tool install bloodyAD==2.5.5" ) return self diff --git a/capabilities/network-ops/tools/certipy.py b/capabilities/network-ops/tools/certipy.py index de36cbe..61216d7 100644 --- a/capabilities/network-ops/tools/certipy.py +++ b/capabilities/network-ops/tools/certipy.py @@ -65,7 +65,8 @@ async def __aenter__(self): """Initialize Certipy toolset and verify certipy is installed.""" if not shutil.which(self.certipy_cmd): logger.warning( - f"certipy command '{self.certipy_cmd}' not found in PATH. " "Install with: pip install certipy-ad" + f"certipy command '{self.certipy_cmd}' not found in PATH. " + "Install with: uv tool install certipy-ad==5.1.0" ) else: logger.info(f"Certipy toolset initialized, using command: {self.certipy_cmd}") From d914025d67f2914e28615097f26bf9b6b6bd5605 Mon Sep 17 00:00:00 2001 From: Brian Greunke Date: Tue, 20 Oct 2026 08:59:26 -0500 Subject: [PATCH 2/2] fix(network-ops): declare $HOME/.local/bin in dependencies.path (ENG-8935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install script drops nxc, certipy, and bloodyAD into $HOME/.local/bin. Declare it in dependencies.path so the runtime adds it to PATH for preflight checks, MCP subprocesses, and the agent's shell tools — including off-platform under `dreadnode serve`, where the image PATH does not apply. Mirrors web-security. Co-Authored-By: Claude Opus 4.8 --- capabilities/network-ops/capability.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/capabilities/network-ops/capability.yaml b/capabilities/network-ops/capability.yaml index ff876d0..26e24ef 100644 --- a/capabilities/network-ops/capability.yaml +++ b/capabilities/network-ops/capability.yaml @@ -60,6 +60,14 @@ dependencies: # hashcat rules, and the NTLM coercion scripts. scripts: - scripts/install_coercion_tools.sh + # Directory the install script drops CLI tools into (nxc, certipy, bloodyAD). + # The runtime appends these to PATH (after the image's own PATH, so the + # platform's verified tools win) for install scripts, preflight checks, MCP + # subprocesses, and the agent's shell tools — including under `dreadnode + # serve` off-platform, where the image PATH does not apply. Mirrors + # web-security/capability.yaml. + path: + - $HOME/.local/bin checks: - name: nmap