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: 9 additions & 6 deletions capabilities/web-security/capability.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
schema: 1
name: web-security
version: "1.10.0"
version: "1.11.0"
description: >
Web application penetration testing with 80+ attack technique playbooks
Web application penetration testing with 82 attack technique playbooks
covering HTTP desync/request smuggling, cache poisoning, SSRF, SSTI, DOM
vulnerabilities, authentication bypasses, parser differentials,
AEM/Sling exploitation, GraphQL, OAuth, and client-side attacks.
Includes HTTP client tooling with OOB callbacks via webhook.site
(API-key aware) and interactsh, Caido
integration via MCP, the Python caido-sdk-client, and the caido-mode
TypeScript SDK CLI (@caido/sdk-client / caido-ts) for curl-through-Caido
(API-key aware) and interactsh, four coexisting Caido surfaces
(caido-cli server, the Python caido-sdk-client, lightweight and
full-surface MCP servers, and the caido-mode TypeScript SDK CLI on
@caido/sdk-client / caido-ts) for curl-through-Caido
testing, match & replace rules, and replay handoffs; Burp proxy
integration via MCP, browser automation via
agent-browser, JS static analysis via jxscout, AST-based code pattern
Expand Down Expand Up @@ -135,7 +136,9 @@ dependencies:
python:
- "fastmcp>=2.0"
- "httpx>=0.28"
- "caido-sdk-client"
# >= 0.3.0: versioned GraphQL transport (transport/latest vs transport/v0_56).
# Older releases break against Caido >= 0.57 replay schema. See mcp/caido.py.
- "caido-sdk-client>=0.3.0"
- "cryptography>=41.0"
- "pydantic>=2.0"
scripts:
Expand Down
16 changes: 10 additions & 6 deletions capabilities/web-security/docker/Dockerfile.runtime
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@
#
# Tools with bundled SDK/MCP integration (require a running instance
# reachable by network — the client library and MCP server are included):
# - Caido — caido-sdk-client (Python) + caido-mcp-server + the
# caido-mode TypeScript SDK CLI (@caido/sdk-client / caido-ts,
# vendored under skills/caido-mode) are all wired in; set
# CAIDO_URL to a running Caido instance. The caido-mode skill's
# node_modules are installed at provision time by
# - Caido — four surfaces against one instance, all keyed off CAIDO_URL:
# (1) caido-cli headless server, pinned >= 0.57.x
# (2) caido MCP Python, caido-sdk-client >= 0.3.0
# (3) caido-go MCP upstream Go binary, pinned + checksummed
# (4) caido-mode skill TS CLI on @caido/sdk-client 0.4.0
# (vendored under skills/caido-mode)
# The 0.57 replay schema break is why (1) and (2) carry floors —
# see scripts/install_tools.sh and mcp/caido.py. The caido-mode
# skill's node_modules are installed at provision time by
# scripts/install_tools.sh (the skill dir is mounted at runtime,
# not baked into this image); tsx is pre-installed globally here
# as a cold-start aid.
Expand Down Expand Up @@ -131,7 +135,7 @@ RUN npm install -g tsx
RUN pip install --no-cache-dir \
"fastmcp>=2.0" \
"httpx>=0.28" \
"caido-sdk-client" \
"caido-sdk-client>=0.3.0" \
"pacu" \
"ast-grep-cli"

Expand Down
113 changes: 98 additions & 15 deletions capabilities/web-security/mcp/caido.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
# requires-python = ">=3.12"
# dependencies = [
# "fastmcp>=2.0",
# "caido-sdk-client",
# "caido-sdk-client>=0.3.0",
# ]
# ///
"""Caido proxy tools — wraps the caido-sdk-client for host interaction.

Requires caido-sdk-client >= 0.3.0. Earlier releases hardcode the pre-0.57
replay schema (they select `collection`/`activeEntry` on ReplaySession and omit
`ReplaySessionKind`), so `caido_replay_request` and `caido_replay_sessions`
fail against Caido >= 0.57 with "Unknown field collection on type
ReplaySession". 0.3.0 added the versioned transport split (transport/latest vs
transport/v0_56) and negotiates the right schema per instance.

Auth resolution order:
1. CAIDO_PAT env var → PATAuthOptions (no connect() needed)
2. ~/.caido-mcp/token.json → TokenAuthOptions + connect() for refresh
Expand All @@ -27,14 +34,27 @@

from caido_sdk_client import Client
from caido_sdk_client.types.finding import CreateFindingOptions
from caido_sdk_client.types.replay_session import ReplaySendOptions
from caido_sdk_client.types.network import ConnectionInfoInput
from caido_sdk_client.types.replay_session import (
CreateReplaySessionFromRaw,
CreateReplaySessionOptions,
ReplaySendOptions,
)
from caido_sdk_client.types.scope import CreateScopeOptions
from fastmcp import FastMCP

DEFAULT_CAIDO_URL = "http://localhost:8080"
DEFAULT_TOKEN_PATH = Path.home() / ".caido-mcp" / "token.json"
MAX_OUTPUT_CHARS = 50_000
CONNECT_TIMEOUT = 30

# Upper bound on `replay.send()`. On Caido >= 0.57 the SDK starts a replay task
# and then waits on a task-finished *subscription*. If the target responds
# before that subscription is established, the completion event is missed and
# the await never returns — reproducible against a fast (localhost) target.
# The send itself still succeeds server-side, so on timeout we fall back to
# reading the session's active entry instead of hanging the MCP call forever.
REPLAY_SEND_TIMEOUT = 30
_SAFE_GET_RETRIES = 1
_SAFE_GET_RETRY_DELAY = 2.0 # seconds

Expand Down Expand Up @@ -233,6 +253,46 @@ async def caido_get_request(
return "\n".join(lines)


async def _replay_result_from_session(client: Client, session_id: object) -> str:
"""Recover a replay result by reading the session's newest entry.

Used when `replay.send()` times out waiting on the task-finished
subscription. The send has still happened server-side, so the entry
carries the real request/response.
"""
lines = ["status: DONE (recovered — task subscription timed out)"]
try:
session = await client.replay.sessions.get(session_id)
if session is None:
return "status: UNKNOWN\nerror: replay session vanished after send"
conn = await session.entries().last(1).execute()
if not conn.edges:
return "status: UNKNOWN\nerror: replay session has no entries"
# Re-fetch by id: entries listed via the session carry no response body.
entry = await client.replay.entries.get(conn.edges[-1].node.id)
if entry is None:
return "status: UNKNOWN\nerror: replay entry not found"
except Exception as exc: # noqa: BLE001 - surface, never mask
return f"status: UNKNOWN\nerror: could not recover replay result: {exc}"

lines.append(f"entry_id: {entry.id}")
request = getattr(entry, "request", None)
if request is not None:
lines.append(f"request_id: {request.id}")
# `response` hangs off the entry, not off entry.request.
response = getattr(entry, "response", None)
if response is not None:
lines.append(f"response: {response.status_code} ({response.length} bytes)")
raw = getattr(response, "raw", None)
if raw:
text = raw.decode(errors="replace")
truncated = text[:MAX_OUTPUT_CHARS]
if len(text) > MAX_OUTPUT_CHARS:
truncated += f"\n\n... [TRUNCATED: {len(text)} chars total]"
lines.append(truncated)
return "\n".join(lines)


@mcp.tool
async def caido_replay_request(
raw_request: Annotated[str, "Raw HTTP request including request line"],
Expand All @@ -248,22 +308,45 @@ async def caido_replay_request(
return err

assert client is not None
session = await client.replay.sessions.create()
result = await client.replay.send(
session.id,
ReplaySendOptions(
raw=raw_request.replace("\\r\\n", "\r\n").encode(),
host=host,
port=port if port is not None else (443 if tls else 80),
tls=tls,
),
raw = raw_request.replace("\\r\\n", "\r\n").encode()
connection = ConnectionInfoInput(
host=host,
port=port if port is not None else (443 if tls else 80),
is_tls=tls,
)

status_str = (
result.task_status
if isinstance(result.task_status, str)
else str(result.task_status)
# The session must be SEEDED with the request. On Caido >= 0.57 `send()`
# updates the draft of an existing entry and then starts a replay task — a
# bare `sessions.create()` yields a session with no entries, so send()
# aborts with "Replay session has no entries". Creating from raw gives the
# session its first entry.
session = await client.replay.sessions.create(
CreateReplaySessionOptions(
request_source=CreateReplaySessionFromRaw(raw=raw, connection=connection)
)
)
# Connection details are nested under `connection` (ConnectionInfoInput) —
# they are not flat kwargs on ReplaySendOptions.
try:
result = await asyncio.wait_for(
client.replay.send(
session.id,
ReplaySendOptions(raw=raw, connection=connection),
),
timeout=REPLAY_SEND_TIMEOUT,
)
except TimeoutError:
# The task-finished subscription was missed (see REPLAY_SEND_TIMEOUT).
# The request itself has almost certainly been sent, so recover the
# result from the session rather than reporting a false failure.
return await _replay_result_from_session(client, session.id)

# ReplaySendResult exposes `status` ("DONE" | "CANCELLED" | "ERROR"). It may
# arrive as a TaskStatus enum, whose str() is "TaskStatus.DONE" — unwrap to
# the bare value so output is stable across SDK versions.
status_value = getattr(result, "status", None)
status_value = getattr(status_value, "value", status_value)
status_str = status_value if isinstance(status_value, str) else str(status_value)
lines = [f"status: {status_str}"]
if result.error:
lines.append(f"error: {result.error}")
Expand Down
11 changes: 9 additions & 2 deletions capabilities/web-security/scripts/install_tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,17 @@ if ! command -v kr &>/dev/null; then
fi

# -- Caido CLI -------------------------------------------------------------
# Downloads the latest Caido CLI binary. Auth is handled at runtime via
# Pinned Caido CLI (headless server) release. Auth is handled at runtime via
# CAIDO_URL + CAIDO_PAT env vars or the device flow login.
#
# Keep this pin >= 0.57.0. The vendored caido-mode skill runs on
# @caido/sdk-client 0.4.0, which targets the 0.57 replay schema (ReplaySession
# as an interface, `kind: ReplaySessionKind!` on createReplaySession, and
# task-based sending via startReplayTask). Pinning an older server here puts
# the client and server on opposite sides of that schema break.
# tests/test_caido_mode_skill.py enforces the floor.
if ! command -v caido-cli &>/dev/null; then
CAIDO_VERSION="0.45.0"
CAIDO_VERSION="0.57.1"
case "$ARCH" in
aarch64|arm64) CAIDO_ARCH="aarch64" ;;
*) CAIDO_ARCH="x86_64" ;;
Expand Down
80 changes: 69 additions & 11 deletions capabilities/web-security/skills/caido-mode/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
---
name: caido-mode
description: "Full Caido TypeScript SDK CLI (the official @caido/sdk-client / caido-ts library). Search HTTP history with HTTPQL, test with curl proxied through Caido (caching auth in reusable static curl config files), add match & replace (tamper) rules, manage findings/scopes/filters/environments, and organize handoffs into named replay sessions and collections. Use for rich write-side Caido automation (M&R rules, replay handoff, curl-through-Caido) that the lightweight caido-sdk (Python) and caido-proxy (MCP) skills do not cover. Requires Node.js + a reachable Caido instance."
tags: [worker]
compatibility: "Requires Node.js >= 18 and a reachable Caido instance >= 0.57 (CAIDO_URL). Vendored node_modules must be installed."
metadata:
upstream: "caido/skills@41697d8 (PR #22)"
upstream-skill: caido-mode
sdk: "@caido/sdk-client 0.4.0"
role: worker
---

<!--
VENDORED SKILL — provenance and local patches.

Source: https://github.com/caido/skills, skills/caido-mode, at commit 41697d8
("feat(caido-mode): revamp — curl workflow, M&R rules, multi-instance auth,
pagination fixes", PR #22). Everything except this SKILL.md is byte-identical
to upstream; re-sync by copying the upstream tree over the sibling files.

This SKILL.md is a DELIBERATE FORK. Preserve these local sections on re-sync:

1. Frontmatter — Dreadnode skill format: `description` written for the
skill router, plus `compatibility` / `metadata`.
Upstream's `tags:` key is NOT read by the Dreadnode
loader (dreadnode/agents/skills.py accepts only name,
description, allowed-tools, license, compatibility,
metadata) — it is carried in `metadata.role` instead.
2. "Where this lives" — upstream says ~/.claude/skills/caido-mode/; here the
skill is capability-relative at skills/caido-mode/.
3. "Which Caido surface" — routing across the four surfaces this capability
ships. Upstream ships caido-mode standalone.
4. Hosted-runtime auth note in "Authentication setup".

Version contract: this skill runs @caido/sdk-client 0.4.0, which targets the
Caido 0.57 replay schema. scripts/install_tools.sh pins caido-cli >= 0.57.1 to
match; tests/test_caido_mode_skill.py enforces both ends.
-->

# Caido Mode Skill

A CLI over Caido's API, built on the official **Caido TypeScript SDK** (`@caido/sdk-client`,
Expand All @@ -25,19 +57,45 @@ Every command is then `npx tsx caido-client.ts <command>` and outputs JSON unles
also run `npx --prefix skills/caido-mode tsx skills/caido-mode/caido-client.ts <command>` without
`cd`, but `cd skills/caido-mode` is simplest.)

## Relationship to the other Caido skills (no interference)
## Which Caido surface should I use?

The capability ships **four** Caido surfaces against one instance. They are independent and do
not collide — pick by the job, not by habit:

| Surface | Use it for | Skill |
|---|---|---|
| **`caido-mode`** (this skill) | curl-through-Caido testing, Match & Replace rules, replay-session/collection handoff | this file |
| **`caido-sdk`** (Python lib) | quick in-process read/replay when you're already scripting Python | `caido-sdk` |
| **`caido` MCP** (Python, 9 tools) | lightweight history search / replay / findings as tool calls | `caido-proxy` |
| **`caido-go` MCP** (Go, 66+ tools) | batch send, race windows, intercept, environments, WS streams, tamper rules | `caido-proxy` |

Decision shortcut:

- **Iterating on requests against a target** → this skill (curl + `-K` config).
- **One-off lookup mid-conversation** → an MCP tool call (`caido-proxy`).
- **Need a Caido feature no other surface exposes** (intercept queue, race window, WS) →
`caido-go` MCP.
- **Already inside a Python script** → `caido-sdk`.

### Auth isolation (why these never clobber each other)

All four resolve the same instance from `CAIDO_URL`, but they store credentials in **different
places**, deliberately:

The capability ships three independent Caido surfaces — pick one, they don't collide:
| Surface | Credential store | Env |
|---|---|---|
| `caido-mode` | `~/.claude/config/secrets.json` → `.caido.instances[<url>]` | `CAIDO_PAT`, `CAIDO_PROXY` |
| `caido-sdk`, `caido` MCP | `~/.caido-mcp/token.json` | `CAIDO_PAT` |
| `caido-go` MCP | `~/.caido-mcp/token.json` | `CAIDO_ACCESS_TOKEN` (`CAIDO_PAT` deprecated alias) |

- **`caido-mode` (this skill)** — TypeScript SDK CLI. Best for curl-through-Caido testing,
Match & Replace rules, and replay-session/collection handoffs.
- **`caido-sdk`** — direct Python `caido-sdk-client` calls for quick read/replay in-process.
- **`caido-proxy` / `caido-go` MCP** — MCP tool surface for history search, replay, findings.
Running `setup` here never touches `~/.caido-mcp/token.json`, and `caido-mcp-server login` never
touches `secrets.json`. Set up whichever surfaces you need, in any order.

All target the **same** Caido instance via `CAIDO_URL`/`CAIDO_PAT`. This skill caches its own auth
token in `~/.claude/config/secrets.json` (under `.caido`), which is **separate** from the
`~/.caido-mcp/token.json` used by the Python SDK skill and the MCP servers — so setting up
`caido-mode` never clobbers the other skills' auth, and vice-versa.
> **Token types are not interchangeable.** The Go MCP wants the **local instance access token**
> (from the Caido GUI: devtools console →
> `JSON.parse(localStorage.CAIDO_AUTHENTICATION).accessToken`). A Caido **Cloud PAT** (prefixed
> `caido_`) authenticates the cloud dashboard API, not your local instance. Feeding a Cloud PAT to
> `CAIDO_ACCESS_TOKEN` yields `Invalid token`.

## How to operate (read this first)

Expand Down
Loading
Loading