diff --git a/capabilities/web-security/capability.yaml b/capabilities/web-security/capability.yaml index 60a1e1c..6a5888f 100644 --- a/capabilities/web-security/capability.yaml +++ b/capabilities/web-security/capability.yaml @@ -1,6 +1,6 @@ schema: 1 name: web-security -version: "1.4.0" +version: "1.5.0" description: > Web application penetration testing with 70+ attack technique playbooks covering request smuggling, cache poisoning, SSRF, SSTI, DOM @@ -36,6 +36,22 @@ mcp: - "run" - "${CAPABILITY_ROOT}/mcp/caido.py" init_timeout: 60 + caido-go: + # Upstream Go MCP server (c0tton-fluff/caido-mcp-server), installed by + # scripts/install_tools.sh. Exposes the full 60+ tool surface (batch + # send, race-window, tamper rules, intercept, environments, websocket + # streams) that the lightweight Python `caido` wrapper does not. Both + # target the same Caido instance via CAIDO_URL. Auth resolves from + # CAIDO_ACCESS_TOKEN (or the deprecated CAIDO_PAT alias), otherwise the + # shared ~/.caido-mcp/token.json created by `caido-mcp-server login`. + command: "caido-mcp-server" + args: + - "serve" + env: + CAIDO_URL: "${CAIDO_URL:-http://127.0.0.1:8080}" + CAIDO_ACCESS_TOKEN: "${CAIDO_ACCESS_TOKEN:-}" + CAIDO_ALLOW_SENSITIVE_HEADERS: "${CAIDO_ALLOW_SENSITIVE_HEADERS:-}" + init_timeout: 60 jxscout: command: "uv" args: @@ -149,6 +165,8 @@ checks: command: 'command -v protoscope >/dev/null 2>&1 || test -x "$HOME/go/bin/protoscope"' - name: caido-cli command: command -v caido-cli + - name: caido-mcp-server + command: 'command -v caido-mcp-server >/dev/null 2>&1 || test -x "$HOME/bin/caido-mcp-server"' - name: burp command: test -f /opt/burp/burpsuite.jar - name: waymore @@ -215,3 +233,4 @@ keywords: - interactsh - oob-callbacks - securitycontext + - caido diff --git a/capabilities/web-security/scripts/install_tools.sh b/capabilities/web-security/scripts/install_tools.sh index 739cdbf..4f71d7c 100755 --- a/capabilities/web-security/scripts/install_tools.sh +++ b/capabilities/web-security/scripts/install_tools.sh @@ -78,6 +78,36 @@ if ! command -v caido-cli &>/dev/null; then || echo "WARN: Caido CLI install failed (check version), skipping" fi +# -- Caido MCP server (Go, c0tton-fluff/caido-mcp-server) ------------------- +# Full-surface Caido MCP server wired into capability.yaml as `caido-go`. +# Pinned to a release with SHA-256 verification. Installed to /usr/local/bin +# so it resolves on PATH for the MCP `command: caido-mcp-server`. +if ! command -v caido-mcp-server &>/dev/null; then + CAIDO_MCP_VERSION="4.3.0" + case "$ARCH" in + aarch64|arm64) + CAIDO_MCP_ARCH="arm64" + CAIDO_MCP_SHA256="7b8d6a89f6b404345715a25d8201a0fbe37db9a0f23b8b1868d01c68b110071b" + ;; + *) + CAIDO_MCP_ARCH="amd64" + CAIDO_MCP_SHA256="5236620c693f973d5725133c660ca0ac852796dd75e02ce1993bd66202d0b04c" + ;; + esac + CAIDO_MCP_URL="https://github.com/c0tton-fluff/caido-mcp-server/releases/download/v${CAIDO_MCP_VERSION}/caido-mcp-server-linux-${CAIDO_MCP_ARCH}" + if curl -fsSL "$CAIDO_MCP_URL" -o /tmp/caido-mcp-server; then + if echo "${CAIDO_MCP_SHA256} /tmp/caido-mcp-server" | sha256sum -c - >/dev/null 2>&1; then + install -m 0755 /tmp/caido-mcp-server /usr/local/bin/caido-mcp-server + echo "caido-mcp-server v${CAIDO_MCP_VERSION} installed" + else + echo "WARN: caido-mcp-server checksum mismatch, skipping install" >&2 + fi + rm -f /tmp/caido-mcp-server + else + echo "WARN: caido-mcp-server download failed (check version), skipping" >&2 + fi +fi + # -- Burp Suite Community (headless) ---------------------------------------- # Downloads the Burp Suite Community JAR for headless scanning. # Pro features require BURP_LICENSE_KEY at runtime. diff --git a/capabilities/web-security/tests/test_caido_go_mcp.py b/capabilities/web-security/tests/test_caido_go_mcp.py new file mode 100644 index 0000000..8bbc8bf --- /dev/null +++ b/capabilities/web-security/tests/test_caido_go_mcp.py @@ -0,0 +1,138 @@ +"""Tests for the upstream Caido Go MCP server wiring (`caido-go`). + +The web-security capability ships two Caido MCP servers: + + * ``caido`` — the lightweight Python wrapper over ``caido-sdk-client``. + * ``caido-go`` — the full-surface upstream Go binary + (c0tton-fluff/caido-mcp-server), installed by + ``scripts/install_tools.sh``. + +These tests lock the manifest wiring and the install-script contract so the +Go server is declared as a stdio MCP server, targets the local Caido +instance, is checked for presence, and is installed pinned + checksum +verified. They are pure static assertions over the manifest and script — no +network or binary required. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = yaml.safe_load((ROOT / "capability.yaml").read_text(encoding="utf-8")) +INSTALL_SCRIPT = (ROOT / "scripts" / "install_tools.sh").read_text(encoding="utf-8") + +# Keep this in lock-step with scripts/install_tools.sh. If the pinned release +# is bumped, both the script and this constant must change together. +PINNED_VERSION = "4.3.0" + + +# ============================================================================= +# Manifest wiring +# ============================================================================= + + +class TestCaidoGoManifest: + def test_caido_go_server_declared(self) -> None: + servers = MANIFEST["mcp"]["servers"] + assert "caido-go" in servers, "caido-go MCP server must be declared" + + def test_is_stdio_server_running_the_binary(self) -> None: + server = MANIFEST["mcp"]["servers"]["caido-go"] + # stdio transport is inferred from `command:`; `url:` would make it HTTP. + assert server["command"] == "caido-mcp-server" + assert "url" not in server + assert server["args"] == ["serve"] + + def test_targets_local_caido_with_overridable_default(self) -> None: + env = MANIFEST["mcp"]["servers"]["caido-go"]["env"] + # Connect-time interpolation with a sane default so it works with no + # extra config but can be redirected via CAIDO_URL. + assert env["CAIDO_URL"] == "${CAIDO_URL:-http://127.0.0.1:8080}" + + def test_auth_and_redaction_env_are_optional(self) -> None: + env = MANIFEST["mcp"]["servers"]["caido-go"]["env"] + # Optional token + redaction toggle: default-empty so absence never + # raises at connect time (the server falls back to the token file). + assert env["CAIDO_ACCESS_TOKEN"] == "${CAIDO_ACCESS_TOKEN:-}" + assert ( + env["CAIDO_ALLOW_SENSITIVE_HEADERS"] + == "${CAIDO_ALLOW_SENSITIVE_HEADERS:-}" + ) + + def test_has_init_timeout(self) -> None: + assert MANIFEST["mcp"]["servers"]["caido-go"]["init_timeout"] == 60 + + def test_coexists_with_python_caido_server(self) -> None: + servers = MANIFEST["mcp"]["servers"] + # Both servers ship together; the Python one is unchanged. + assert servers["caido"]["command"] == "uv" + assert servers["caido"]["args"] == ["run", "${CAPABILITY_ROOT}/mcp/caido.py"] + + +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 + + +# ============================================================================= +# Install-script contract +# ============================================================================= + + +class TestCaidoGoInstall: + def test_installs_pinned_version(self) -> None: + assert f'CAIDO_MCP_VERSION="{PINNED_VERSION}"' in INSTALL_SCRIPT + + def test_downloads_from_official_release_url(self) -> None: + assert ( + "https://github.com/c0tton-fluff/caido-mcp-server/releases/download/" + "v${CAIDO_MCP_VERSION}/caido-mcp-server-linux-${CAIDO_MCP_ARCH}" + ) in INSTALL_SCRIPT + + def test_pins_both_arch_checksums(self) -> None: + # arm64 + amd64 SHA-256 digests must both be present (64 hex chars). + arm = re.search( + r'CAIDO_MCP_ARCH="arm64"\s*\n\s*CAIDO_MCP_SHA256="([0-9a-f]{64})"', + INSTALL_SCRIPT, + ) + amd = re.search( + r'CAIDO_MCP_ARCH="amd64"\s*\n\s*CAIDO_MCP_SHA256="([0-9a-f]{64})"', + INSTALL_SCRIPT, + ) + assert arm is not None, "arm64 checksum pin missing" + assert amd is not None, "amd64 checksum pin missing" + assert arm.group(1) != amd.group(1), "arch checksums must differ" + + def test_verifies_checksum_before_install(self) -> None: + # The download must be checksum-verified with sha256sum -c before it is + # placed on PATH — never install an unverified binary. + assert "sha256sum -c -" in INSTALL_SCRIPT + verify = INSTALL_SCRIPT.index("sha256sum -c -") + install = INSTALL_SCRIPT.index("install -m 0755 /tmp/caido-mcp-server") + assert verify < install, "checksum must be verified before install" + + def test_checksum_mismatch_skips_install(self) -> None: + assert "checksum mismatch, skipping install" in INSTALL_SCRIPT + + def test_installs_onto_path(self) -> None: + assert ( + "install -m 0755 /tmp/caido-mcp-server /usr/local/bin/caido-mcp-server" + in INSTALL_SCRIPT + ) + + def test_idempotent_guard(self) -> None: + # Skips the whole block if the binary is already present. + assert "if ! command -v caido-mcp-server &>/dev/null; then" in INSTALL_SCRIPT + + def test_cleans_up_temp_download(self) -> None: + assert "rm -f /tmp/caido-mcp-server" in INSTALL_SCRIPT diff --git a/capabilities/web-security/tests/test_interrupted_tool_result_hook.py b/capabilities/web-security/tests/test_interrupted_tool_result_hook.py index ffc1151..9876f07 100644 --- a/capabilities/web-security/tests/test_interrupted_tool_result_hook.py +++ b/capabilities/web-security/tests/test_interrupted_tool_result_hook.py @@ -128,7 +128,6 @@ async def test_manifest_wires_hook_file() -> None: manifest_path = Path(__file__).resolve().parents[1] / "capability.yaml" manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) - assert manifest["version"] == "1.3.0" assert manifest["hooks"] == ["hooks/interrupted_tool_result.py"]