Skip to content

Security & Attacker

The security API covers four interconnected systems: findings (security vulnerabilities detected by Ghost’s passive interceptor and AI agent), security tools (external scanners like Nuclei, SQLMap, and Frida that extend Ghost’s capabilities), Frida (runtime instrumentation for bypassing SSL pinning, root detection, and custom hooking), and the request attacker (a fuzzer that tests endpoints by sending modified payloads and analyzing responses).

These systems work together — the AI agent uses the security tools to scan, the findings store collects results from both passive detection and active testing, and the attacker lets you manually fuzz specific parameters to validate suspected vulnerabilities.

Findings represent security issues detected in your traffic. They can come from three sources:

  • Passive detection — Ghost’s security interceptor automatically scans every response for common issues (exposed credentials, missing security headers, information disclosure)
  • AI agent analysis — the agent uses tools to identify more complex issues (broken authentication, IDOR vulnerabilities, injection points)
  • Active testing — scanners like Nuclei and SQLMap report findings back through the agent

There is no POST endpoint for creating findings — they are created exclusively by the security interceptor and agent tools internally. The API lets you list, inspect, update the status of, and delete findings.

GET /api/v1/findings?session_id=01HWXYZ...

Returns findings for a session with optional filtering.

Query parameters:

ParameterRequiredValuesDescription
session_idYesULID stringWhich session’s findings to list
typeNoauth, injection, exposure, config, crypto, access, sessionFilter by vulnerability category
severityNocritical, high, medium, low, infoFilter by severity level
statusNoopen, confirmed, false_positive, fixedFilter by review status
flow_idNoULID stringShow only findings related to a specific flow
limitNo1–500 (default: all)Maximum findings to return. Capped at 500 regardless of the value you send
offsetNo≥ 0Skip this many findings for pagination

Response:

[
{
"id": "01HWXYZ...",
"session_id": "01HWABC...",
"flow_id": "01HWDEF...",
"type": "exposure",
"severity": "high",
"confidence": 0.85,
"title": "API Key Exposed in Response Body",
"description": "The response body contains what appears to be an API key in plain text...",
"evidence": "Found pattern: sk_live_abc123... in response body",
"remediation": "Remove API keys from response bodies. Use server-side session tokens instead.",
"cwe_id": "CWE-200",
"owasp_id": "A01:2021",
"status": "open",
"source": "passive",
"metadata": {
"host": "api.example.com",
"path": "/api/config"
},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
]

Finding fields explained:

FieldTypeDescription
idstringUnique finding identifier (ULID)
session_idstringWhich session this finding belongs to
flow_idstringThe specific HTTP flow where this issue was detected (may be empty for session-level findings)
typestringVulnerability category — see type values below
severitystringHow serious this issue is — critical (actively exploitable), high (likely exploitable), medium (potential risk), low (minor concern), info (informational)
confidencenumberHow confident Ghost is in this finding (0.0 to 1.0). Passive detections use pattern matching and may have lower confidence; active testing with confirmed exploitation has higher confidence
titlestringShort summary of the issue
descriptionstringDetailed explanation of what was found and why it matters
evidencestringThe specific data that triggered this finding (matched patterns, suspicious values, etc.)
remediationstringSuggested fix for the issue
cwe_idstringCommon Weakness Enumeration identifier (e.g., “CWE-200” for information exposure)
owasp_idstringOWASP Top 10 mapping (e.g., “A01:2021” for broken access control)
statusstringReview status — starts as "open", can be updated to "confirmed", "false_positive", or "fixed"
sourcestringHow this finding was detected: "passive" (interceptor), "active" (scanner/tool), or "ai" (agent analysis)
metadataobjectAdditional context as key-value pairs (host, path, tool name, etc.). Always an object, never null
created_attimestampWhen the finding was first detected
updated_attimestampWhen the finding was last modified (status changes update this)

Finding types:

TypeDescription
authAuthentication issues (weak tokens, missing auth, broken session management)
injectionInjection vulnerabilities (SQL injection, XSS, command injection, etc.)
exposureInformation disclosure (API keys, credentials, internal paths, stack traces in responses)
configSecurity misconfiguration (missing headers, CORS issues, verbose errors)
cryptoCryptographic issues (weak algorithms, insecure transport, certificate problems)
accessAccess control issues (IDOR, privilege escalation, missing authorization)
sessionSession management issues (predictable tokens, missing expiration, insecure cookies)
GET /api/v1/findings/stats?session_id=01HWXYZ...

Returns aggregate counts for a session’s findings, broken down by severity, type, and status.

Response:

{
"total": 42,
"by_severity": {"critical": 2, "high": 8, "medium": 15, "low": 12, "info": 5},
"by_type": {"config": 10, "exposure": 8, "session": 6, "auth": 5, "injection": 5, "crypto": 4, "access": 4},
"by_status": {"open": 30, "confirmed": 8, "false_positive": 4}
}
GET /api/v1/findings/{id}

Returns a single finding by ID. Same fields as the list endpoint.

Returns 404 if not found.

PATCH /api/v1/findings/{id}/status

Updates the review status of a finding. This is how you mark findings as confirmed (real issue), false positive (not actually a problem), or fixed (resolved).

Request body (64 KB limit):

{
"status": "confirmed"
}

Valid status values: "open", "confirmed", "false_positive", "fixed"

If you send an invalid status, the error message explicitly lists all valid values.

After a successful update, the finding is re-fetched from the database to return the complete state with the updated updated_at timestamp.

Response: Updated FindingDTO

Broadcasts a finding.updated WebSocket event.

DELETE /api/v1/findings/{id}

Permanently removes a finding. No cascade — only the finding row is deleted (findings don’t have child records).

Response: {"ok": true}

Broadcasts a finding.deleted WebSocket event with {"id": "..."}.

Ghost integrates with external security scanners. These endpoints let you check which tools are installed, install new ones, and enable/disable them.

GET /api/v1/security/tools

Returns all known security tools with their installation status and configuration.

Response:

[
{
"name": "frida",
"installed": true,
"version": "16.5.9",
"description": "Runtime instrumentation — SSL pinning bypass, root detection bypass, crypto function tracing",
"enabled": true,
"pinned_version": "16.5.9",
"package_manager": "pip3",
"package_manager_ok": true,
"installable": true
},
{
"name": "nuclei",
"installed": true,
"version": "3.2.0",
"description": "Template-based vulnerability scanner — 9,000+ checks for XSS, SQLi, misconfigs, CVEs",
"enabled": true,
"package_manager": "brew",
"package_manager_ok": true,
"installable": true
}
]
FieldTypeDescription
namestringTool identifier
installedbooleanWhether the tool binary is found on the system
versionstringDetected version (empty if not installed)
descriptionstringWhat this tool does
enabledbooleanWhether Ghost is allowed to use this tool (controlled via toggle endpoint)
pinned_versionstringVersion Ghost expects (currently only Frida has this: “16.5.9”)
package_managerstringWhich package manager can install this tool (brew, choco, pip3)
package_manager_okbooleanWhether that package manager is available on this system
installablebooleanWhether Ghost can install this tool automatically

Known tools:

Frida is always listed first (handled specially), followed by these external scanners:

ToolBinaryPackage (macOS/Windows)Requires PythonDescription
Frida(special)pip3YesRuntime instrumentation — SSL pinning bypass, root detection bypass, crypto function tracing
Nucleinucleibrew / chocoNoTemplate-based vulnerability scanner — 9,000+ checks for XSS, SQLi, misconfigs, CVEs
Dalfoxdalfoxbrew / —NoXSS specialist — deep parameter analysis, DOM XSS, blind XSS detection
ffufffufbrew / chocoNoPath and parameter fuzzer — endpoint discovery, IDOR testing at scale
SQLMapsqlmapbrew / chocoYesSQL injection detection — tests captured flows for injection vulnerabilities
TruffleHogtrufflehogbrew / —NoSecret scanner — 800+ verified detector types for API keys, tokens, credentials
Katanakatanabrew / —NoJS endpoint discovery — finds untested API paths from app’s own JavaScript
Semgrepsemgrepbrew / —YesStatic analysis — DOM XSS sinks, eval, innerHTML, postMessage in captured JS
POST /api/v1/security/tools/install

Installs a security tool and streams the installation progress as Server-Sent Events.

Request body (1 KB limit):

{
"tool": "nuclei"
}

Response: SSE stream (not JSON)

SSE events:

EventDataDescription
progress{"message": "Installing nuclei via brew..."}Installation progress updates (newlines are escaped)
error{"error": "brew not found"}Installation failed
doneFull tool status JSONInstallation completed successfully

Installation timeout: 10 minutes (Frida installation via pip can be slow). The install runs with its own background context — disconnecting the HTTP request will cancel the install.

Pre-flight errors (before SSE starts):

  • 400 — unknown tool name
  • 409 — tool is already installed (includes current version)
  • 412 — package manager not found, or no installation package available for this operating system
PATCH /api/v1/security/tools/{name}/toggle

Enables or disables a security tool. Disabled tools won’t be offered to the AI agent and won’t appear as available for scanning.

Request body (1 KB limit):

{
"enabled": false
}

Response:

{
"name": "nuclei",
"enabled": false
}

The setting is persisted to Ghost’s config file.

Frida is a runtime instrumentation toolkit that lets you inject JavaScript into running applications. Ghost integrates Frida for SSL pinning bypass (so the app trusts Ghost’s CA), root/jailbreak detection bypass (so the app doesn’t refuse to run on a testing device), and custom hooking (inspect function arguments, modify return values, trace API calls).

GET /api/v1/frida/status

Checks whether Frida is installed and which version.

Response:

{
"installed": true,
"version": "16.5.9",
"pinned_version": "16.5.9",
"package_manager": "pip3",
"package_manager_ok": true
}
GET /api/v1/frida/devices

Lists devices visible to Frida (local, USB-connected, remote).

Response:

[
{
"id": "local",
"name": "Local System",
"type": "local"
},
{
"id": "abc123",
"name": "iPhone 16 Pro",
"type": "usb"
}
]

Device types: "local", "usb", "remote", "socket"

If Frida is not installed, returns an empty array [] with status 200 (not an error — this prevents console spam when the Frida panel is open but Frida isn’t set up).

GET /api/v1/frida/devices/{id}/apps

Lists installed applications on a Frida device.

Response:

[
{
"pid": 12345,
"name": "My App",
"identifier": "com.example.myapp"
}
]

The pid is 0 if the app is not currently running.

GET /api/v1/frida/devices/{id}/processes

Lists running processes on a Frida device.

Response:

[
{
"pid": 12345,
"name": "myapp"
}
]
POST /api/v1/frida/attach

Attaches Frida to a running application and injects a JavaScript script.

Request body (512 KB limit — the script code is embedded in the request):

{
"device_id": "local",
"app": "com.example.myapp",
"script": "Interceptor.attach(Module.findExportByName(null, 'SSL_read'), { ... });",
"session_type": "ssl_bypass",
"label": "SSL Bypass for MyApp"
}
FieldRequiredDefaultDescription
device_idYesFrida device ID
appYesApplication identifier or process name
scriptYesJavaScript code to inject
session_typeNo"custom"Type of instrumentation: "ssl_bypass", "root_bypass", "trace", or "custom"
labelNoHuman-readable label for the session

Response: The created session:

{
"key": "local:com.example.myapp:ssl_bypass",
"device_id": "local",
"app": "com.example.myapp",
"type": "ssl_bypass",
"label": "SSL Bypass for MyApp",
"source": "panel",
"started_at": "2024-01-15T10:30:00Z"
}

The key is a composite identifier (deviceID:app:type) used to reference this session in other endpoints. The source is always "panel" for API-initiated sessions (vs "agent" for AI-initiated ones).

Broadcasts frida.session.started WebSocket event.

POST /api/v1/frida/spawn

Spawns an application with Frida instrumentation from the start — unlike attach (which hooks into an already-running process), spawn starts the app fresh with the script injected from the very first instruction. This is necessary for SSL bypass on apps that verify certificates during startup.

Same request/response format as attach. Same 512 KB body limit.

POST /api/v1/frida/detach

Detaches Frida from a session, removing all injected hooks.

Request body (4 KB limit):

{
"key": "local:com.example.myapp:ssl_bypass"
}

Response: {"ok": true}

Returns 404 if the session doesn’t exist.

Broadcasts frida.session.stopped WebSocket event.

GET /api/v1/frida/sessions

Returns all active Frida sessions.

Response: Array of session objects (same format as the attach response). Always returns [] if no sessions, never null.

GET /api/v1/frida/sessions/{key}/output?cursor=0

Returns console output from a Frida session. The output is stored in a ring buffer that holds the last 1,000 lines per session — older lines are automatically dropped as new ones arrive.

Path parameter: {key} — session key (URL-decoded, since it contains colons)

Query parameter:

  • cursor (optional, default: 0) — pass 0 to get all available lines, or pass the cursor from a previous response to get only new lines since then

Response:

{
"lines": ["[*] SSL_read hooked at 0x7fff...", "[*] Intercepted 2048 bytes"],
"cursor": 42
}

Pass cursor: 42 on your next request to get only lines added after that point. This is how the frontend implements efficient polling without re-fetching the entire history.

DELETE /api/v1/frida/sessions/{key}

Stops a Frida session (same as detach, but using DELETE method with the key in the URL path).

Response: {"ok": true}

Ghost can save Frida scripts to the database for reuse.

List:

GET /api/v1/frida/scripts

Returns all saved scripts.

Response:

[
{
"id": "01HWXYZ...",
"name": "iOS SSL Bypass",
"description": "Hooks SecTrustEvaluate and related functions",
"code": "...",
"category": "ssl_bypass",
"platform": "ios",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
]
FieldDescription
category"ssl_bypass", "root_bypass", "trace", or "custom"
platform"ios", "android", or empty string for cross-platform scripts

Create:

POST /api/v1/frida/scripts

Request body (256 KB limit):

{
"name": "Custom Hook",
"description": "Hooks the login function",
"code": "Interceptor.attach(...);",
"category": "custom",
"platform": "android"
}

name and code are required. category defaults to "custom".

Response: 201 Created with the saved script object.

Update:

PUT /api/v1/frida/scripts/{id}

Partial update — only non-empty fields in the request body are applied. The handler fetches the existing script, merges your changes, and saves.

Request body (256 KB limit): Same format as create, but all fields are optional.

Response: Updated script object.

Delete:

DELETE /api/v1/frida/scripts/{id}

Response: {"ok": true}

The request attacker (fuzzer) takes a captured HTTP request, marks specific parameters for testing, and sends hundreds or thousands of modified versions with different payloads to find vulnerabilities. It’s like having a patient tester who tries every possible SQL injection string, XSS payload, or path traversal sequence against your endpoint.

POST /api/v1/attacker/run

Starts a fuzzing attack. Returns 202 Accepted immediately — the attack runs asynchronously in a background goroutine. Results stream via WebSocket events.

Request body (1 MB limit):

{
"flow_id": "01HWXYZ...",
"insertion_points": [
{
"location": "query_param",
"name": "search",
"marker": "FUZZ1",
"original": "hello"
}
],
"payloads": {
"FUZZ1": {
"type": "wordlist",
"name": "xss-basic"
}
},
"attack_mode": "sniper",
"match_rules": [
{
"type": "body_contains",
"value": "<script>",
"negative": false
}
],
"max_requests": 500,
"threads": 5,
"delay_ms": 50,
"follow_redirects": false
}

Insertion points — where to inject payloads:

FieldDescription
locationWhere in the request: "header", "query_param", "body_json", "body_form", "body_raw", "cookie", "path_segment", "method"
nameThe parameter name (e.g., header name, query parameter name)
markerMarker name to link this point to a payload set (e.g., "FUZZ1", "FUZZ2")
originalThe original value at this position (for baseline comparison)

Payload configuration — what to inject:

FieldDescription
typePayload source: "wordlist" (built-in list), "numbers" (range), "custom" (your values), "null" (empty/null values)
nameWordlist name (when type is "wordlist")
valuesArray of custom payload strings (when type is "custom")
from / to / stepNumber range (when type is "numbers")
encodingHow to encode payloads before injection: "none", "url", "double_url", "base64", "html", "hex"

Match rules — how to detect interesting responses:

Rule TypeDescription
status_diffResponse status code differs from the baseline
length_diffResponse body length differs from baseline
body_containsResponse body contains a specific string
body_regexResponse body matches a regular expression
time_gt_msResponse time exceeds a threshold (for time-based detection)
header_containsA response header contains a specific string

The negative flag inverts a rule — {"type": "body_contains", "value": "error", "negative": true} matches when the response does NOT contain “error”.

Validation:

  • flow_id is required
  • At least one insertion point is required
  • At least one payload set is required

Response: {"status": "started", "flow_id": "01HWXYZ..."}

Errors: 503 (attacker engine not available), 409 (an attack is already running), 400 (validation failures)

POST /api/v1/attacker/stop

Stops the running attack. No request body needed.

Response: {"ok": true}

Returns 409 if no attack is currently running.

GET /api/v1/attacker/status

Check whether an attack is currently running.

Response:

{
"running": true
}
GET /api/v1/attacker/wordlists

Returns all built-in payload wordlists with their names and payload counts. Wordlists are embedded in the Ghost binary via go:embed from a payloads/ directory.

Response:

[
{"name": "xss-basic", "count": 45},
{"name": "sqli-generic", "count": 62},
{"name": "path-traversal", "count": 30}
]
EventPayloadDescription
attacker:started{"flow_id": "...", "baseline": {"status": 200, "length": 1234, "time": 45}}Attack begun, baseline response captured
attacker:progress{"completed": 50, "total": 100, "matched": 3, "result": {...}}Per-request result with payload, status code, response length, timing, match details, and diff from baseline
attacker:completedFull summary (see below)Attack finished
attacker:error{"error": "message"}Attack failed

Attack summary (in attacker:completed):

FieldDescription
total_requestsTotal number of requests planned
completedHow many actually completed
matchedHow many matched your rules
interestingHow many showed notable differences from baseline
errorsHow many requests failed
baseline_status / baseline_length / baseline_time_msThe baseline response metrics for comparison
status_distributionMap of status code → count
top_interestingArray of the most interesting results
durationTotal attack duration

Per-result fields (in progress and top_interesting):

FieldDescription
indexRequest number
payloadThe payload string that was sent
positionWhere it was injected (e.g., "query_param:search")
status_codeResponse status code
response_lengthResponse body size in bytes
response_time_msResponse time in milliseconds
matchedWhether any match rules triggered
match_detailsWhich rule matched and why
interestingWhether this result is notably different from baseline
diff_from_baseDescription of differences (e.g., "length +266", "status 500 (was 200)")
EventTriggerPayload
finding.createdNew finding detected by interceptor or agentFinding DTO
finding.updatedFinding status changed via PATCHUpdated finding DTO
finding.deletedFinding removed via DELETE{"id": "..."}
frida.session.startedFrida attached/spawnedSession DTO
frida.session.stoppedFrida detachedSession key
frida.outputConsole output from injected scriptOutput line
attacker:startedAttack begunBaseline info
attacker:progressEach request completedResult + progress counts
attacker:completedAttack finishedFull summary
attacker:errorAttack failedError message
LimitValueContext
Finding body size64 KBStatus update request
Finding list cap500Maximum findings per request
Security tool install timeout10 minutesBackground install context
Tool install/toggle body1 KBSmall JSON payloads
Frida attach/spawn body512 KBScript code embedded in request
Frida script body256 KBSaved script create/update
Frida detach body4 KBSession key
Frida console buffer1,000 linesPer-session ring buffer
Attacker body1 MBAttack configuration