Skip to content
Merged
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
15 changes: 12 additions & 3 deletions capabilities/network-ops/capability.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -51,14 +51,23 @@ 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.
Comment on lines +56 to +58

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — addressed in d914025. network-ops now declares $HOME/.local/bin in dependencies.path, mirroring web-security/capability.yaml, so the directory is appended to PATH for the preflight checks, MCP subprocesses, and the agent's shell tools (including off-platform under dreadnode serve, where the image PATH doesn't apply). This also covers the existing nxc install, which lands in the same directory.

# Runtime-fetched assets apt/pip can't provide: the NetExec binary, wordlists,
# 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
Expand Down
29 changes: 29 additions & 0 deletions capabilities/network-ops/scripts/install_coercion_tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +106 to +107

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This case is a long-lived sandbox that already has the broken certipy-ad 4.8.2 on PATH. Remediating pre-existing broken copies is deliberately out of scope here — this change fixes fresh provisioning. The skip-if-present guard is also intended behavior: a runtime that already has a working tool should make no outbound install request.

On a fresh runtime there is no pre-existing certipy, so the guard triggers the isolated install as intended, and with $HOME/.local/bin now declared in dependencies.path the isolated executable is the one on PATH. Leaving the guard as-is.

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
Expand Down
25 changes: 23 additions & 2 deletions capabilities/network-ops/tests/test_provisioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions capabilities/network-ops/tools/bloodyad.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion capabilities/network-ops/tools/certipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Loading