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
29 changes: 1 addition & 28 deletions capabilities/web-security/capability.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema: 1
name: web-security
version: "2.0.1"
version: "2.0.2"
description: >
Web application penetration testing with 83 attack technique playbooks
covering HTTP desync/request smuggling, cache poisoning, SSRF, SSTI, DOM
Expand Down Expand Up @@ -181,33 +181,6 @@ checks:
command: 'command -v interactsh-client >/dev/null 2>&1 || test -x "$HOME/go/bin/interactsh-client"'
- name: protoscope
command: 'command -v protoscope >/dev/null 2>&1 || test -x "$HOME/go/bin/protoscope"'
# The capability talks to Caido over CAIDO_URL using the Python client, not
# by driving the local binary (see mcp/caido.py). Asserting only the binary
# reports a correctly configured deployment as degraded — self-hosted
# installs may not ship caido-cli at all and instead point at a Caido the
# operator runs. Local binary or reachable endpoint, either satisfies it.
- name: caido-cli
command: 'command -v caido-cli >/dev/null 2>&1 || curl -fsS --max-time 3 -o /dev/null "${CAIDO_URL:-http://localhost:8080}"'
- name: caido-mcp-server
command: 'command -v caido-mcp-server >/dev/null 2>&1 || test -x "$HOME/bin/caido-mcp-server"'
- name: caido-mode
command: 'test -f skills/caido-mode/caido-client.ts && test -d skills/caido-mode/node_modules'
- name: burp
command: test -f /opt/burp/burpsuite.jar
- name: waymore
command: command -v waymore
- name: pacu
command: command -v pacu
- name: fireprox
command: 'test -f "$HOME/git/fireprox/fire.py"'
- name: archivealchemist
command: 'test -f "$HOME/git/archivealchemist/archive-alchemist.py"'
- name: exiftool
command: command -v exiftool
- name: ast-grep
command: command -v ast-grep
- name: wrangler
command: command -v wrangler

author:
name: Dreadnode
Expand Down
28 changes: 21 additions & 7 deletions capabilities/web-security/scripts/install_tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,26 @@ failed_stages=()
run_stage() {
local name="$1" status
shift
# Sealed images supply their tools at build time. Validate what is present
# without entering any installer (including retry loops and package managers).
if [ "${DREADNODE_CAPABILITY_INSTALL:-}" = "sealed" ] && [ "$name" != validation ]; then
return 0
fi
echo "web-security: starting $name" >&2
set +e
( set -e; "$@" )
status=$?
set -e
if [ "$status" -ne 0 ]; then
failed_stages+=("$name")
echo "web-security: $name failed (exit $status)" >&2
case "$name" in
caido_cli|caido_mcp|burp|exiftool|browser|caido_mode|wrangler|ast_grep|waymore|pacu|fireprox|archivealchemist)
echo "WARN: optional stage $name failed (exit $status); continuing with available tools" >&2
;;
*)
failed_stages+=("$name")
echo "web-security: $name failed (exit $status)" >&2
;;
esac
fi
}

Expand Down Expand Up @@ -462,13 +474,15 @@ validate_tools() {
for tool in nuclei httpx subfinder naabu dnsx uncover alterx tlsx asnmap; do
have_pd_tool "$tool" || { echo "Missing required tool: $tool" >&2; missing=1; }
done
for tool in katana protoscope interactsh-client 2fa kr caido-cli caido-mcp-server \
burp exiftool agent-browser wrangler ast-grep waymore pacu; do
for tool in katana protoscope interactsh-client 2fa kr; do
have "$tool" || { echo "Missing required tool: $tool" >&2; missing=1; }
done
for tool in caido-cli caido-mcp-server burp exiftool agent-browser wrangler ast-grep waymore pacu; do
have "$tool" || echo "WARN: optional tool unavailable: $tool" >&2
done
for artifact in /opt/burp/burpsuite.jar "$HOME/git/fireprox/fire.py" \
"$HOME/git/archivealchemist/archive-alchemist.py"; do
[ -s "$artifact" ] || { echo "Missing required artifact: $artifact" >&2; missing=1; }
[ -s "$artifact" ] || echo "WARN: optional artifact unavailable: $artifact" >&2
done
return "$missing"
}
Expand All @@ -479,8 +493,8 @@ if [ "${#failed_stages[@]}" -gt 0 ]; then
exit 1
fi
# Retain downloaded modules after a failed pass so the next pass can reuse them.
if [ "$need_go" = true ]; then
if [ "${DREADNODE_CAPABILITY_INSTALL:-}" != "sealed" ] && [ "$need_go" = true ]; then
go clean -cache -modcache 2>/dev/null || true
fi

echo "web-security tools installed successfully"
echo "web-security required tools installed successfully; optional tools may be unavailable"
11 changes: 4 additions & 7 deletions capabilities/web-security/tests/test_caido_go_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,10 @@ def test_coexists_with_python_caido_server(self) -> None:


class TestCaidoGoCheck:
def test_presence_check_registered(self) -> None:
checks = {c["name"]: c["command"] for c in MANIFEST["checks"]}
assert "caido-mcp-server" in checks
cmd = checks["caido-mcp-server"]
# Accept a PATH install or the c0tton-fluff install.sh default (~/bin).
assert "command -v caido-mcp-server" in cmd
assert "$HOME/bin/caido-mcp-server" in cmd
def test_optional_tool_is_not_health_checked(self) -> None:
# Optional tools may be absent at runtime, so their presence is not asserted.
checks = {c["name"] for c in MANIFEST["checks"]}
assert "caido-mcp-server" not in checks


# =============================================================================
Expand Down
17 changes: 7 additions & 10 deletions capabilities/web-security/tests/test_caido_mode_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,10 @@ def test_node_is_available_before_skill_install(self) -> None:
skill_install = INSTALL_SCRIPT.index("CAIDO_MODE_DIR=")
assert node_setup < skill_install

def test_presence_check_registered(self) -> None:
checks = {c["name"]: c["command"] for c in MANIFEST["checks"]}
assert "caido-mode" in checks
command = checks["caido-mode"]
# Both the entrypoint and the installed deps — either alone is a
# false green.
assert "skills/caido-mode/caido-client.ts" in command
assert "skills/caido-mode/node_modules" in command
def test_optional_tool_is_not_health_checked(self) -> None:
# Optional tools may be absent at runtime, so their presence is not asserted.
checks = {c["name"] for c in MANIFEST["checks"]}
assert "caido-mode" not in checks


# =============================================================================
Expand All @@ -260,9 +256,10 @@ def test_presence_check_registered(self) -> None:
class TestCaidoSurfacesCoexist:
def test_all_four_surfaces_are_declared(self) -> None:
servers = MANIFEST["mcp"]["servers"]
checks = {c["name"] for c in MANIFEST["checks"]}
assert "caido" in servers and "caido-go" in servers
assert {"caido-cli", "caido-mcp-server", "caido-mode"} <= checks
# Optional tools may be absent at runtime, so their presence is not asserted.
checks = {c["name"] for c in MANIFEST["checks"]}
assert not ({"caido-cli", "caido-mcp-server", "caido-mode"} & checks)

def test_sibling_caido_skills_present(self) -> None:
for name in ("caido-sdk", "caido-proxy"):
Expand Down
54 changes: 47 additions & 7 deletions capabilities/web-security/tests/test_install_tools_offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,8 @@ def test_partial_npm_directory_does_not_prevent_repair(
script = _installer_fixture(tmp_path)
(tmp_path / "skills/caido-mode/node_modules/complete").unlink()
first = _run_installer(script, NPM_FAIL="1")
assert first.returncode == 1
assert "failed stages: caido_mode" in first.stderr
assert first.returncode == 0, first.stderr
assert "optional stage caido_mode failed" in first.stderr
second = _run_installer(script)
assert second.returncode == 0, second.stderr
assert (tmp_path / "skills/caido-mode/node_modules/complete").is_file()
Expand All @@ -551,8 +551,8 @@ def test_fireprox_requirements_retry_after_successful_clone(tmp_path: Path) -> N
script = _installer_fixture(tmp_path)
(tmp_path / "git/fireprox/.dreadnode-deps-installed").unlink()
first = _run_installer(script)
assert first.returncode == 1
assert "failed stages: fireprox" in first.stderr
assert first.returncode == 0, first.stderr
assert "optional stage fireprox failed" in first.stderr
assert not (tmp_path / "git/fireprox/.dreadnode-deps-installed").exists()
_command(tmp_path / "bin/uv", "exit 0")
second = _run_installer(script)
Expand All @@ -574,7 +574,8 @@ def test_failed_burp_download_is_not_published_and_recovers(tmp_path: Path) -> N
""",
)
first = _run_installer(script, DOWNLOAD_FAIL="1")
assert first.returncode == 1
assert first.returncode == 0, first.stderr
assert "optional stage burp failed" in first.stderr
assert not jar.exists()
assert len((tmp_path / "downloads").read_text().splitlines()) == 3
second = _run_installer(script)
Expand All @@ -585,9 +586,10 @@ def test_failed_burp_download_is_not_published_and_recovers(tmp_path: Path) -> N
def test_sealed_install_does_not_download_missing_browser(tmp_path: Path) -> None:
script = _installer_fixture(tmp_path)
(tmp_path / ".cache/agent-browser").rename(tmp_path / "saved-browser")
_command(tmp_path / "bin/nuclei", "exit 0")
result = _run_installer(script, DREADNODE_CAPABILITY_INSTALL="sealed")
assert result.returncode == 0, result.stderr
assert "browser-install" not in (tmp_path / "commands").read_text()
assert not (tmp_path / "commands").exists()


def test_clone_retry_uses_fresh_staging_after_partial_failure(tmp_path: Path) -> None:
Expand Down Expand Up @@ -697,13 +699,15 @@ def test_python_probe_failure_does_not_attempt_install(tmp_path: Path) -> None:
@pytest.mark.parametrize("sealed", [True, False])
def test_existing_browser_cache_needs_no_marker(tmp_path: Path, sealed: bool) -> None:
script = _installer_fixture(tmp_path)
_command(tmp_path / "bin/nuclei", "exit 0")
result = _run_installer(
script,
BROWSER_FAIL="1",
DREADNODE_CAPABILITY_INSTALL="sealed" if sealed else "",
)
assert result.returncode == 0, result.stderr
assert "browser-install" not in (tmp_path / "commands").read_text()
commands = tmp_path / "commands"
assert not commands.exists() or "browser-install" not in commands.read_text()


def test_optional_browser_download_failure_does_not_fail_install(
Expand Down Expand Up @@ -771,3 +775,39 @@ def test_python_probe_checks_install_destinations(
else:
assert result.returncode != 0
assert "installed" not in result.stdout


@pytest.mark.parametrize("missing_required", [False, True])
def test_sealed_install_checks_local_tools_without_fetches_or_retries(
tmp_path: Path, missing_required: bool
) -> None:
script = _installer_fixture(tmp_path)
if not missing_required:
_command(tmp_path / "bin/nuclei", "exit 0")
for name in ("caido-cli", "caido-mcp-server", "burp", "waymore", "pacu"):
(tmp_path / "bin" / name).unlink()
for relative in (
"opt/burp",
"git/fireprox",
"git/archivealchemist",
"skills/caido-mode/node_modules",
):
(tmp_path / relative).rename(tmp_path / Path(relative).name)
_command(tmp_path / "bin/node", "echo 18")
_command(tmp_path / "bin/sleep", 'echo sleep >> "$HOME/commands"; exit 97')
result = _run_installer(script, DREADNODE_CAPABILITY_INSTALL="sealed")
assert result.returncode == (1 if missing_required else 0), result.stderr
assert not (tmp_path / "commands").exists()
assert not (tmp_path / "go-attempts").exists()
for name in (
"caido-cli",
"caido-mcp-server",
"burp",
"wrangler",
"waymore",
"pacu",
):
assert f"WARN: optional tool unavailable: {name}" in result.stderr
assert "WARN: optional artifact unavailable:" in result.stderr
if missing_required:
assert "Missing required tool: nuclei" in result.stderr
Loading