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
21 changes: 20 additions & 1 deletion 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: "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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -215,3 +233,4 @@ keywords:
- interactsh
- oob-callbacks
- securitycontext
- caido
30 changes: 30 additions & 0 deletions capabilities/web-security/scripts/install_tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
138 changes: 138 additions & 0 deletions capabilities/web-security/tests/test_caido_go_mcp.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"]


Expand Down
Loading