Imported from razor-ai/openhunt (
openhunt/caido/custom-workflows/SKILL.md). Install upstream withnpx skills add razor-ai/openhunt --skill custom-workflows. Copyright stays with the author.
OpenHunt Caido Custom Workflows
Overview
This directory contains 97 passive Caido workflows that form a detection net for the OpenHunt security testing pipeline. All workflows run as passive intercept handlers — they observe proxied traffic and create findings (signals, not confirmed vulnerabilities) via the Caido SDK.
Every workflow writes findings with reporter: "openhunt-detect" and uses deduplication keys to avoid flooding the findings tab with repeats.
Directory Structure
caido/custom-workflows/
├── SKILL.md # This file
├── README.md # Workflow summary table
│
├── hunt-admin-interface-detector/ # Access Control
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
├── hunt-business-logic-surface/ # Access Control
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
├── hunt-idor-surface-mapper/ # Access Control
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
├── hunt-mass-assignment-indicator/ # Access Control
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
├── hunt-race-condition-surface/ # Access Control
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
├── hunt-rate-limit-absence/ # Access Control
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
│
├── hunt-auto-auth-renewal/ # Session Management (request-trigger)
│ ├── definition.json
│ ├── javascript.ts
│ ├── manifest.json
│ └── README.md
│
├── hunt-account-takeover-surface/ # Authentication
├── hunt-cryptographic-exploitation-surface/ # Authentication
├── hunt-jwt-weakness-analyzer/ # Authentication
├── hunt-mfa-otp-surface-mapper/ # Authentication
├── hunt-oauth-oidc-surface-mapper/ # Authentication
├── hunt-saml-response-analyzer/ # Authentication
├── hunt-session-cookie-analyzer/ # Authentication / Session Management
├── hunt-type-juggling-surface/ # Authentication
├── hunt-auth-token-in-url/ # Authentication / Information Disclosure
│
├── hunt-cache-poisoning-surface/ # Caching & Proxies
├── hunt-reverse-proxy-misconfig/ # Caching & Proxies
├── hunt-web-cache-deception-surface/ # Caching & Proxies
│
├── hunt-cloud-resource-exposure/ # Cloud & Infrastructure
├── hunt-google-api-attack-surface/ # Cloud & Infrastructure
├── hunt-google-discovery-surface/ # Cloud & Infrastructure
├── hunt-graphql-attack-surface/ # Cloud & Infrastructure
├── hunt-subdomain-takeover/ # Cloud & Infrastructure
├── hunt-websocket-attack-surface/ # Cloud & Infrastructure
│
├── hunt-client-side-path-traversal/ # File Handling
├── hunt-file-upload-detector/ # File Handling
├── hunt-lfi-response-patterns/ # File Handling
├── hunt-path-traversal-indicators/ # File Handling
├── hunt-zip-slip-surface/ # File Handling
│
├── hunt-cookie-scope-creep/ # Headers & Security Configuration
├── hunt-hsts-bypass-detector/ # Headers & Security Configuration
├── hunt-security-headers/ # Headers & Security Configuration
│
├── hunt-http2-connection-surface/ # Infrastructure (Derived)
│
├── hunt-api-key-detector/ # Information Disclosure
├── hunt-error-debug-leak/ # Information Disclosure
├── hunt-google-protojson-error-oracle/ # Information Disclosure
├── hunt-internal-leak-detector/ # Information Disclosure
├── hunt-password-credential-response/ # Information Disclosure
├── hunt-scm-disclosure/ # Information Disclosure
├── hunt-sensitive-data-sniffer/ # Information Disclosure
├── hunt-stack-trace-detector/ # Information Disclosure
├── hunt-tech-fingerprint/ # Information Disclosure
│
├── hunt-command-injection-indicators/ # Injection
├── hunt-crlf-injection-surface/ # Injection
├── hunt-deserialization-surface/ # Injection
├── hunt-email-injection-surface/ # Injection
├── hunt-graphql-introspection-detector/ # Injection
├── hunt-ldap-injection-indicators/ # Injection
├── hunt-nosql-injection-surface/ # Injection
├── hunt-sql-error-detector/ # Injection
├── hunt-ssi-esi-indicators/ # Injection
├── hunt-ssti-indicator/ # Injection
├── hunt-xxe-xml-processing-surface/ # Injection
├── hunt-csv-formula-injection/ # Injection (Derived)
├── hunt-latex-injection-surface/ # Injection (Derived)
├── hunt-lucene-solr-injection/ # Injection (Derived)
├── hunt-xpath-injection-indicators/ # Injection (Derived)
├── hunt-xslt-processing-surface/ # Injection (Derived)
│
├── hunt-debug-mode-indicator/ # Miscellaneous High-Value
├── hunt-insecure-randomness/ # Miscellaneous High-Value
├── hunt-response-code-patterns/ # Miscellaneous High-Value
├── hunt-timing-attack-surface/ # Miscellaneous High-Value
├── hunt-webhook-integration/ # Miscellaneous High-Value
│
├── hunt-cors-misconfiguration/ # SSRF & Request Manipulation
├── hunt-csrf-surface-mapper/ # SSRF & Request Manipulation
├── hunt-hidden-parameter-surface/ # Request Manipulation
├── hunt-host-header-poisoning/ # SSRF & Request Manipulation
├── hunt-hpp-surface/ # SSRF & Request Manipulation
├── hunt-open-redirect-collector/ # SSRF & Request Manipulation
├── hunt-request-smuggling-indicators/ # SSRF & Request Manipulation
├── hunt-ssrf-parameter-mapper/ # SSRF & Request Manipulation
├── hunt-sspp-surface/ # Request Manipulation (Derived)
├── hunt-verb-tampering-surface/ # Request Manipulation (Derived)
│
├── hunt-clickjacking-surface/ # XSS & Client-Side
├── hunt-client-cve-surface/ # XSS & Client-Side
├── hunt-cross-site-leak-surface/ # XSS & Client-Side
├── hunt-csp-analyzer/ # XSS & Client-Side
├── hunt-css-injection-surface/ # XSS & Client-Side
├── hunt-dom-clobbering-surface/ # XSS & Client-Side
├── hunt-dom-sink-collector/ # XSS & Client-Side
├── hunt-electron-exploitation-surface/ # XSS & Client-Side
├── hunt-postmessage-handler/ # XSS & Client-Side
├── hunt-prototype-pollution/ # XSS & Client-Side
├── hunt-reflected-params/ # XSS & Client-Side
├── hunt-spa-state-manipulation/ # XSS & Client-Side
├── hunt-tabnabbing-surface/ # XSS & Client-Side
│
├── hunt-dns-rebinding-surface/ # Server-Side Request
└── hunt-headless-browser-surface/ # Server-Side Request
Each workflow folder contains exactly 4 files:
| File | Purpose |
|---|---|
definition.json |
Caido workflow graph: trigger node → in-scope filter → HTTPQL pre-filter → JavaScript node → end. Contains the JS code inline in the code input. |
javascript.ts |
Standalone copy of the detection logic (same code as embedded in definition.json). This is the source of truth for editing. |
manifest.json |
Workflow metadata: id (slug), name, version, author, description, url. |
README.md |
Human-readable docs: what it detects, detection criteria, signal output format, false positive rate, next steps, community overlap. |
Workflow Architecture
Trigger Types
- 88 workflows use
on-intercept-response— they analyze responses after they arrive from the server. - 1 workflow (
hunt-auto-auth-renewal) useson-intercept-request— it modifies outgoing requests to inject fresh auth tokens.
Standard Graph Structure (definition.json)
All passive workflows share a 5-node graph with an HTTPQL pre-filter:
Node 2: Trigger (on-intercept-response or on-intercept-request)
│
▼
Node 3: In Scope filter
│
├─ false → Node 1: Passive End (skip)
│
└─ true → Node 5: Matches HTTPQL (pre-filter)
│
├─ false → Node 1: Passive End (skip)
│
└─ true → Node 4: JavaScript (detection logic)
│
└─→ Node 1: Passive End
The exception is hunt-auto-auth-renewal, which is a utility workflow with no HTTPQL node and uses the older 4-node structure.
The definition.json encodes this as:
kind: always"passive"edition: always2id: a UUID (unique per workflow)graph.nodes[2]: trigger —definition_id: "caido/on-intercept-response"(or"caido/on-intercept-request")graph.nodes[3]: scope check —definition_id: "caido/in-scope", input refs the trigger's requestgraph.nodes[5]: HTTPQL filter —definition_id: "caido/matches-httpql", pre-filters traffic before JS executiongraph.nodes[4]: JS node —definition_id: "caido/http-code-js", inputs ref trigger's request and response
manifest.json Format
{
"author": { "name": "OpenHunt" },
"url": "https://github.com/larsencyber/openhunt",
"description": "<one-line description>",
"id": "hunt-<workflow-slug>",
"name": "<Human Readable Name>",
"version": "1.0.0"
}
Caido Workflow SDK API
All workflows use the same function signature:
export async function run({ request, response }, sdk) { ... }
Request Object
| Method | Returns | Notes |
|---|---|---|
request.getHost() |
string |
Hostname without port |
request.getPort() |
number |
|
request.getPath() |
string |
URL path without query string |
request.getMethod() |
string |
HTTP method |
request.getQuery() |
string | undefined |
Query string without leading ? |
request.getHeaders() |
Record<string, string[]> |
All headers, keys lowercased |
request.getHeader(name) |
string[] |
Single header values |
request.getBody() |
{ toText(): string, length: number } | undefined |
Request body |
request.getTls() |
boolean |
Whether HTTPS |
request.getUrl() |
string |
Full URL |
request.getId() |
string |
Request ID |
request.setHeader(name, value) |
void |
Mutate outgoing request (request-trigger only) |
request.removeHeader(name) |
void |
Remove header (request-trigger only) |
Response Object
| Method | Returns | Notes |
|---|---|---|
response.getCode() |
number |
HTTP status code |
response.getHeaders() |
Record<string, string[]> |
All headers, keys lowercased |
response.getHeader(name) |
string[] |
Single header values |
response.getBody() |
{ toText(): string, length: number } | undefined |
Response body |
response.getRoundtripTime() |
number |
Round-trip time in ms |
SDK Findings API
// Check if a finding with this dedup key already exists
const exists = await sdk.findings.exists({ dedupeKey: string });
// Create a new finding
await sdk.findings.create({
title: string, // Short descriptive title
description: string, // Detailed finding description
reporter: string, // Always "openhunt-detect" for these workflows
request: RequestObject, // The request object from run()
dedupeKey: string, // Unique key for deduplication
});
SDK Console API
sdk.console.log(msg); // Log to Caido console
sdk.console.debug(msg);
sdk.console.warn(msg);
sdk.console.error(msg);
SDK Environment API
sdk.env.getVar(name); // Get environment variable
sdk.env.getVars(); // Get all variables as [{name, value}]
await sdk.env.setVar({ name, value }); // Set variable
Common Patterns in Detection Code
1. Standard Guards (use in every response-trigger workflow)
export async function run({ request, response }, sdk) {
if (!response) return;
// Skip oversized responses (>500KB)
if ((response.getBody()?.length ?? 0) > 500000) return;
// Skip binary content types
const respHeaders = response.getHeaders();
const contentType = (respHeaders['content-type']?.[0] ?? '').toLowerCase();
const binaryTypes = ['image/', 'font/', 'audio/', 'video/',
'application/octet-stream', 'application/zip', 'application/pdf'];
if (binaryTypes.some(t => contentType.includes(t))) return;
const host = request.getHost();
const path = request.getPath();
const respBody = response.getBody()?.toText() ?? '';
// ... detection logic ...
}
2. Findings with Deduplication
Every workflow builds a deduplication key from a prefix + contextual data to avoid repeat findings:
// Per host + path dedup (most common — 60+ workflows)
const dedupeKey = `detect:hunt-<id>:${host}:${path}`;
// Per host only (e.g., CORS, HSTS, tech fingerprint)
const dedupeKey = `detect:hunt-<id>:${host}`;
// Per host + path + slug (for multi-detection workflows)
const slug = f.title.replace(/[^a-zA-Z0-9]/g, '').substring(0, 40);
const dedupeKey = `detect:hunt-<id>:${host}:${path}:${slug}`;
// Per host + param name (e.g., SSRF mapper)
const dedupeKey = `detect:hunt-<id>:${host}:${paramName}`;
// Per host + method + path (e.g., CSRF, mass assignment)
const dedupeKey = `detect:hunt-<id>:${host}:${method}:${path}`;
// Per host + cookie name (e.g., session cookie analyzer)
const dedupeKey = `detect:hunt-<id>:${host}:${cookieName}`;
// Always check before creating:
const exists = await sdk.findings.exists({ dedupeKey });
if (exists) continue;
3. Finding Description Format
All findings follow a consistent structure:
await sdk.findings.create({
title: `DETECT: <What Was Found>`,
description: [
`Host: ${host}`,
`Path: ${path}`,
`Confidence: High|Medium|Low`,
`<Detailed explanation of what was detected>`,
`Next step: <Actionable guidance for the tester>`,
`Skill: ~/.claude/skills/<path-to-relevant-skill>/`,
].join('\n'),
reporter: 'openhunt-detect',
request,
dedupeKey,
});
4. Multi-Finding Pattern
Workflows that detect multiple things in one pass collect findings in an array, then create them:
const findings = [];
// Detection 1
if (condition1) {
findings.push({ title: '...', detail: '...', confidence: 'High' });
}
// Detection 2
if (condition2) {
findings.push({ title: '...', detail: '...', confidence: 'Medium' });
}
// Create all findings with dedup
for (const f of findings) {
const slug = f.title.replace(/[^a-zA-Z0-9]/g, '').substring(0, 40);
const dedupeKey = `detect:hunt-<id>:${host}:${path}:${slug}`;
const exists = await sdk.findings.exists({ dedupeKey });
if (exists) continue;
await sdk.findings.create({ title: f.title, description: '...', reporter: 'openhunt-detect', request, dedupeKey });
}
5. Scanning Request Parameters (query + body)
Used by SSRF mapper, reflected params, HPP, and others:
// Query string
const query = request.getQuery() ?? '';
const params = new URLSearchParams(query);
for (const [name, value] of params.entries()) {
checkParam(name, value, 'query');
}
// POST body (form-urlencoded)
if (contentType.includes('application/x-www-form-urlencoded')) {
const bodyText = request.getBody()?.toText() ?? '';
const bodyParams = new URLSearchParams(bodyText);
for (const [name, value] of bodyParams) {
checkParam(name, value, 'body.form');
}
}
// POST body (JSON) — recursive scan
if (contentType.includes('application/json')) {
const json = JSON.parse(request.getBody()?.toText() ?? '{}');
const scanObj = (obj, prefix) => {
for (const [key, val] of Object.entries(obj)) {
if (typeof val === 'string') checkParam(key, val, `body.json${prefix ? '.' + prefix : ''}`);
else if (typeof val === 'object' && val !== null && !Array.isArray(val))
scanObj(val, prefix ? prefix + '.' + key : key);
}
};
scanObj(json, '');
}
6. HTML Title Extraction
const titleMatch = respBody.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
if (titleMatch) {
const title = titleMatch[1].trim().toLowerCase();
// ... check against keyword lists
}
7. Header Comment Block
Every javascript.ts starts with a standard metadata comment:
/**
* OpenHunt Detection: <Human Name>
* ID: hunt-<slug>
* Category: <Category>
* Colour: <hex>
* Dedup: <strategy>
* Skill refs: skills/<path>/
* Community overlap: <description>
*/
Workflow Categories
| Category | Count | Colour | Description |
|---|---|---|---|
| Access Control | 6 | #f97316 / #3b82f6 / #f59e0b |
Admin interfaces, business logic, IDOR, mass assignment, race conditions, rate limiting |
| Authentication | 9 | #a855f7 |
Account takeover, cryptographic exploitation, JWT, MFA/OTP, OAuth/OIDC, SAML, session cookies, type juggling, auth tokens in URLs |
| Session Management | 1 | #22c55e |
Auto auth token renewal |
| Caching & Proxies | 3 | #f59e0b |
Cache poisoning, reverse proxy misconfig, web cache deception |
| Cloud & Infrastructure | 6 | #3b82f6 / #ef4444 |
Cloud resources, GraphQL, Google API attack surface + discovery, subdomain takeover, WebSocket |
| File Handling | 5 | #ef4444 / #3b82f6 |
Client-side path traversal, file upload, LFI, path traversal, zip slip |
| Headers & Security | 3 | #a855f7 / #6b7280 |
Cookie scope, HSTS bypass, security headers |
| Infrastructure | 1 | #3b82f6 |
HTTP/2 connection surface |
| Information Disclosure | 9 | #ef4444 / #f97316 / #22c55e |
API keys, error leaks, Google ProtoJSON schema leakage, internal IPs, passwords, SCM, sensitive data, stack traces, tech fingerprinting |
| Injection | 16 | Mixed | SQL, NoSQL, command, email, LDAP, SSTI, SSI/ESI, XXE, XPath, XSLT, CSV formula, LaTeX, Lucene/Solr, deserialization, CRLF, GraphQL introspection |
| Miscellaneous | 5 | Mixed | Debug mode, insecure randomness, response codes, timing attacks, webhooks |
| SSRF & Request Manipulation | 10 | #f59e0b / #f97316 / #6b7280 |
CORS, CSRF, hidden parameters, host header poisoning, HPP, open redirect, request smuggling, SSRF, SSPP, verb tampering |
| XSS & Client-Side | 13 | Mixed | Clickjacking, client CVEs, cross-site leaks, CSP, CSS injection, DOM clobbering, DOM sinks, Electron exploitation, postMessage, prototype pollution, reflected params, SPA state manipulation, tabnabbing |
| Server-Side Request | 2 | Mixed | DNS rebinding, headless browser exploitation |
Colour Semantics
| Hex | Meaning | Usage |
|---|---|---|
#ef4444 |
Red — Critical | High-confidence detections (API keys, SQL errors, path traversal, XSS) |
#f97316 |
Orange — Warning | Medium-confidence or surface mapping (CRLF, SSTI, command injection) |
#f59e0b |
Amber — Surface | Attack surface identification (SSRF params, CSRF endpoints, cache poison) |
#3b82f6 |
Blue — Informational | Structural observations (file upload, GraphQL, IDOR patterns) |
#a855f7 |
Purple — Auth/Session | Authentication and session management related |
#22c55e |
Green — Utility | Helper workflows (tech fingerprint, auto auth renewal) |
#6b7280 |
Grey — Low severity | Missing headers, HPP, clickjacking |
Dedup Strategy Reference
| Strategy | Key Format | Used By |
|---|---|---|
| per-host-path | detect:hunt-<id>:<host>:<path> |
Most workflows (default) |
| per-host | detect:hunt-<id>:<host> |
CORS, HSTS, subdomain takeover, tech fingerprint, HTTP/2, request smuggling |
| per-host-path-slug | detect:hunt-<id>:<host>:<path>:<slug> |
Multi-detection workflows (admin interface, JWT, security headers) |
| per-host-method-path | detect:hunt-<id>:<host>:<method>:<path> |
CSRF, mass assignment |
| per-host + param name | detect:hunt-<id>:<host>:<paramName> |
SSRF parameter mapper |
| per-host-cookieName | detect:hunt-<id>:<host>:<cookieName> |
Session cookie analyzer |
Creating a New Workflow
Step 1: Create the folder
mkdir caido/custom-workflows/hunt-<your-slug>/
Step 2: Write javascript.ts
Start from this template:
/**
* OpenHunt Detection: <Your Detection Name>
* ID: hunt-<your-slug>
* Category: <Category>
* Colour: <hex>
* Dedup: <strategy>
* Skill refs: skills/<relevant-skill-path>/
* Community overlap: <description or "none">
*/
export async function run({ request, response }, sdk) {
if (!response) return;
if ((response.getBody()?.length ?? 0) > 500000) return;
const respHeaders = response.getHeaders();
const contentType = (respHeaders['content-type']?.[0] ?? '').toLowerCase();
const binaryTypes = ['image/', 'font/', 'audio/', 'video/', 'application/octet-stream', 'application/zip', 'application/pdf'];
if (binaryTypes.some(t => contentType.includes(t))) return;
const host = request.getHost();
const path = request.getPath();
const code = response.getCode();
const respBody = response.getBody()?.toText() ?? '';
const findings = [];
// --- Your detection logic here ---
// --- Create findings ---
for (const f of findings) {
const dedupeKey = `detect:hunt-<your-slug>:${host}:${path}`;
const exists = await sdk.findings.exists({ dedupeKey });
if (exists) continue;
await sdk.findings.create({
title: f.title,
description: `Host: ${host}\nPath: ${path}\nConfidence: ${f.confidence}\n${f.detail}\nNext step: <guidance>\nSkill: ~/.claude/skills/<path>/`,
reporter: 'openhunt-detect',
request,
dedupeKey,
});
}
}
Step 3: Create definition.json
Use this template — replace <NAME>, <DESCRIPTION>, <UUID>, and paste the full JS code into the data field of the code input:
{
"description": "<DESCRIPTION>",
"edition": 2,
"graph": {
"edges": [
{ "source": { "exec_alias": "exec", "node_id": 2 }, "target": { "exec_alias": "exec", "node_id": 3 } },
{ "source": { "exec_alias": "false", "node_id": 3 }, "target": { "exec_alias": "exec", "node_id": 1 } },
{ "source": { "exec_alias": "true", "node_id": 3 }, "target": { "exec_alias": "exec", "node_id": 5 } },
{ "source": { "exec_alias": "false", "node_id": 5 }, "target": { "exec_alias": "exec", "node_id": 1 } },
{ "source": { "exec_alias": "true", "node_id": 5 }, "target": { "exec_alias": "exec", "node_id": 4 } },
{ "source": { "exec_alias": "exec", "node_id": 4 }, "target": { "exec_alias": "exec", "node_id": 1 } }
],
"nodes": [
{ "alias": "passive_end", "definition_id": "caido/passive-end", "display": { "x": 0, "y": 370 }, "id": 1, "inputs": [], "name": "Passive End", "version": "0.1.0" },
{ "alias": "on_intercept_response", "definition_id": "caido/on-intercept-response", "display": { "x": 0, "y": 10 }, "id": 2, "inputs": [], "name": "On Intercept Response", "version": "0.1.0" },
{ "alias": "in_scope", "definition_id": "caido/in-scope", "display": { "x": 0, "y": 85 }, "id": 3, "inputs": [ { "alias": "request", "value": { "data": "$on_intercept_response.request", "kind": "ref" } } ], "name": "In Scope", "version": "0.1.0" },
{ "alias": "javascript", "definition_id": "caido/http-code-js", "display": { "x": 0, "y": 245 }, "id": 4, "inputs": [ { "alias": "request", "value": { "data": "$on_intercept_response.request", "kind": "ref" } }, { "alias": "response", "value": { "data": "$on_intercept_response.response", "kind": "ref" } }, { "alias": "code", "value": { "data": "<PASTE FULL JS CODE HERE — escaped for JSON>", "kind": "string" } } ], "name": "<NAME>", "version": "0.1.0" },
{ "alias": "matches_httpql", "definition_id": "caido/matches-httpql", "display": { "x": 0, "y": 165 }, "id": 5, "inputs": [ { "alias": "request", "value": { "data": "$on_intercept_response.request", "kind": "ref" } }, { "alias": "response", "value": { "data": "$on_intercept_response.response", "kind": "ref" } }, { "alias": "query", "value": { "data": "<HTTPQL FILTER QUERY>", "kind": "string" } } ], "name": "Matches HTTPQL", "version": "0.1.0" }
]
},
"id": "<UUID>",
"kind": "passive",
"name": "<NAME>"
}
For request-trigger workflows (like auto-auth-renewal), change:
- Node 2:
definition_id: "caido/on-intercept-request", alias:"on_intercept_request" - Node 3 input ref:
"$on_intercept_request.request" - Node 4 request input ref:
"$on_intercept_request.request" - Node 4 response input:
{ "data": "", "kind": "string" }(empty — no response available) - Omit Node 5 (HTTPQL filter) — utility workflows use the 4-node structure
Step 4: Create manifest.json
{
"author": { "name": "OpenHunt" },
"url": "https://github.com/larsencyber/openhunt",
"description": "<One-line description>",
"id": "hunt-<your-slug>",
"name": "<Human Readable Name>",
"version": "1.0.0"
}
Step 5: Create README.md
# <Human Readable Name>
**ID:** `hunt-<your-slug>`
**Category:** <Category>
**Kind:** Passive
**Colour:** `<hex>`
## What it detects
<Description of what the workflow identifies>
## Detection criteria
- <Bullet list of specific conditions>
## Signal output
- **Title:** `DETECT: <pattern>`
- **Reporter:** `openhunt-detect`
- **Dedup:** `detect:hunt-<slug>:<host>:<path>`
## False positive rate
**Low|Medium|High** — <explanation>
## Next steps
<Actionable guidance>
## Community workflow overlap
**<name>** — <description> OR **none**
Testing Workflows
The test harness lives at caido/tests/ and uses the mock SDK:
# Run tests (from repo root)
node caido/tests/mock-harness.mjs caido/custom-workflows/hunt-<slug>/javascript.ts
# Static validation of all workflows
node caido/tests/static-validator.mjs
The mock SDK (caido/tests/mock-sdk.mjs) provides:
MockRequest— builder pattern:new MockRequest().host('example.com').path('/admin').build()MockResponse— builder pattern:new MockResponse().status(200).header('content-type', 'text/html').body('<html>...').build()MockSDK— tracksfindings.created[]andfindings.existsKeysfor assertions
Naming Conventions
- Folder name:
hunt-<descriptive-slug>(all lowercase, hyphens) - Manifest ID: matches folder name exactly
- Finding title prefix: always
DETECT:followed by the detection name - Dedup key prefix: always
detect:hunt-<short-id>:followed by scope components - Reporter: always
openhunt-detect
Integration with Caido Plugins
Plugins that consume findings from these workflows should:
- Filter by reporter: Query findings where
reporter === 'openhunt-detect' - Parse finding titles: All titles start with
DETECT:— the rest identifies the detection type - Use dedup keys: The
dedupeKeyfield uniquely identifies each finding instance - Extract structured data from descriptions: Findings follow the format
Host: ...\nPath: ...\nConfidence: ...\n<details>\nNext step: ...\nSkill: ... - Reference the category table in the README.md to understand workflow groupings and colour semantics
Relationship to Other Skills
skills/caido-mode/— CLI tool for interacting with Caido's API (search history, replay, findings, scopes)skills/— The security testing skills referenced in finding descriptions viaSkill:lines