ProxyWatch exposes three distinct HTTP APIs for introspection and control:
- Debug API — live classifier state (candidates, roles, signals, FP-report traces). Enabled with
-debug-api ADDR. - Agent Debug API — per-agent introspection when running in connect (agent) mode. Enabled with
-agent-debug-api ADDR. - Contour API — tunnel status + protocol verification when running headless Contour. Enabled with
-contour-api ADDR.
All APIs speak JSON. None are authenticated — bind them only to trusted interfaces (e.g. 127.0.0.1:PORT or a management VLAN). Responses always set Content-Type: application/json.
Exposes the detection state of the host ProxyWatch is running on (local mode) or all connected agents (server mode). This is the primary API for operator tooling, regression testing, and CI diffing.
# Local monitoring with debug API
sudo ./proxywatch-linux-amd64 -debug-api 127.0.0.1:7890
# Server (ingest) mode with debug API
sudo ./proxywatch-linux-amd64 -listen 0.0.0.0:50051 -debug-api 127.0.0.1:7890Where it lives: internal/detection/output/debug_api.go.
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Health / cycle counter. |
GET |
/candidates |
All classified candidates with filter support. |
GET |
/candidate/<pid> |
Single candidate by PID. |
GET |
/metrics |
Role + state histograms, classifier counters. |
GET |
/agents |
Connected agents (server mode only). |
GET |
/agent/<host>/... |
Per-agent sub-views (server mode only). |
GET |
/diff/<baseline> |
Diff current state against a stored baseline. |
GET |
/fp-report |
FP-verdict trace per candidate (signals, reasons, online evidence, tier-2 preserves). |
GET |
/fp-report/<filter> |
Filtered FP report. |
GET |
/online/status |
Online-verification (Authenticode OCSP) runtime status. |
GET |
/online/verdict/<sha256> |
Online-verification verdict for a specific executable hash. |
GET |
/operator/labels |
List all operator training labels. |
POST |
/operator/label |
Create or update an operator label. |
GET |
/operator/label/<sha256> |
Fetch a single operator label by hash. |
DELETE |
/operator/label/<sha256> |
Clear an operator label. |
Liveness + a classifier tick counter. Useful for CI to confirm the binary is up and classifying before running tests.
{
"ok": true,
"cycle": 4143,
"host": "DEMO",
"updated": "2026-04-15T15:12:41Z",
"candidates": 24,
"server": false
}Returns the current classified-candidate slice with optional filters.
Query parameters:
| Param | Meaning | Example |
|---|---|---|
name |
Substring match on process name (case-insensitive) | ?name=sshd |
role |
Exact match on role or role family | ?role=control-pivot |
state |
Substring match on state | ?state=tunneling |
pid |
Exact PID | ?pid=9556 |
Combine filters with &. Example:
curl -s 'http://127.0.0.1:7890/candidates?role=control-pivot&state=tunneling' | jqResponse schema (per item):
Key fields on each candidate. See CandidateSnapshot for the full list.
{
"cycle": 4143,
"host": "DEMO",
"updated": "2026-04-15T15:12:41Z",
"count": 1,
"items": [
{
"pid": 9556,
"name": "sshd.exe",
"exe_path": "C:\\Windows\\System32\\OpenSSH\\sshd.exe",
"cmd": "C:\\Windows\\System32\\OpenSSH\\sshd.exe -z",
"user": "DEMO\\ops",
"parent_pid": 7008,
"role": "control-pivot",
"role_family": "control-pivot",
"state": "tunneling",
"score": 72,
"confidence": 88,
"active_proxying": true,
"strong_evidence": true,
"traffic_verified": false,
"signals": ["pivot-non-loopback-internal", "outbound-system-path"],
"reasons": ["Pivot active (58s left): internal forwarding in relay context — TCP relay → 172.16.1.2:22"],
"inbound_total": 0,
"out_total": 1,
"out_external": 0,
"out_internal": 1,
"out_loopback": 0,
"seen_seconds": 173,
"listener_count": 0,
"conn_count": 1,
"io_read_bps": 0,
"io_write_bps": 1024,
"ml_role": "outbound",
"ml_confidence": 0.83,
"suggested_role": "outbound"
}
]
}Single candidate by PID. Returns 404 if not found.
curl -s http://127.0.0.1:7890/candidate/9556 | jqRole histogram, state histogram, and top-level classifier counters. Suitable for scraping into a dashboard.
{
"cycle": 4143,
"role_counts": { "control-channel": 4, "control-pivot": 2, "outbound": 18, "listener": 1 },
"state_counts": { "watch": 22, "tunneling": 2, "exited": 1 },
"candidates": 25
}Lists connected agents and exposes their per-agent views. Returns {"error":"not server mode"} with HTTP 404 when running locally without -listen.
Sub-paths under /agent/<host>/:
/agent/<host>/candidates/agent/<host>/candidate/<pid>/agent/<host>/fp-report
Diff current state against a stored baseline. Used for regression checks during rule development — after making a classifier change, compare output against the previous baseline to see which candidates shifted role or score.
Per-candidate FP-verdict trace. Each entry carries the full rationale for the role/score assignment: signals, reasons, trust evidence, tier-2 preserves, FP-shape blockers, would-suppress flags. This is the authoritative endpoint for "why was this process classified as X?".
Key fields (see FPReportEntry):
{
"pid": 1672,
"name": "cheerful_glove.exe",
"role": "control-channel",
"score": 100,
"signals": ["pivot-multiplex-relay", "pivot-reverse-tunnel-shape", "outbound-known-vendor"],
"reasons": ["Persistent reverse control channel detected"],
"sha256": "…",
"known_vendor_path": false,
"signed": false,
"authenticode_trust": "untrusted",
"authenticode_publisher": "",
"authenticode_ocsp_checked": false,
"online_known_benign": false,
"online_known_malicious": false,
"publisher_dns_aligned": false,
"benign_control_client": false,
"traffic_verified": true,
"strong_evidence": true,
"active_proxying": true,
"fp_shape_score": 0,
"fp_shape_blockers": ["state:strong-evidence"],
"would_suppress": false,
"tier2_preserved": false,
"has_internal_conn": true,
"has_non_loopback_listener": false,
"conn_internal_remotes": 4,
"conn_external_remotes": 1,
"tunneling_state": true
}Online-verification runtime state — current posture (live / cache-only / off), how many entries are in the reputation cache, last OCSP fetch time, OCSP error counter.
Live verification is the default posture on all platforms. On Windows, it runs full Authenticode + OCSP through the WinTrust API. On Linux/macOS, it falls back to path + ownership trust hints. Operators can override via PROXYWATCH_ONLINE_VERIFY:
- unset,
live,on,1,true— default, run live verification. cache-only,offline— read the persisted reputation cache but make no outbound calls. Use in air-gapped environments.off,0,false,disable— fully disabled.
Returns the online-verification verdict for an executable hash. Returns 404 if the hash isn't in the cache.
Lists all operator training labels currently persisted.
{
"count": 3,
"labels": [
{ "sha256": "abc…", "verdict": "malicious", "reason": "sliver beacon", "created_at": "2026-04-10T10:00:00Z" }
]
}Create or update an operator label by SHA-256 hash.
Body:
{
"sha256": "a1b2c3…",
"verdict": "benign",
"reason": "internal ops tool"
}verdict must be one of benign, malicious, session, beacon, tunnel, pivot. Response:
{ "ok": true, "label": { "sha256": "…", "verdict": "benign", "reason": "…" } }Labels flow into the ML training pipeline on the next retrain cycle. Useful for suppressing an internal red-team binary that keeps tripping as control-channel.
Fetch a single label by hash. 404 if not found.
Clear an operator label. The next retrain will drop its contribution from the training data.
curl -XDELETE http://127.0.0.1:7890/operator/label/a1b2c3…When ProxyWatch runs in connect (agent) mode — -connect <server>:50051 — this API exposes the agent's local classification state on the agent host itself. The server's main /debug-api already aggregates all agents, so this is mainly for debugging what the agent sees before it streams to the server.
./proxywatch-windows-amd64.exe -connect 10.0.0.5:50051 -agent-debug-api 127.0.0.1:7891Where it lives: internal/agent/debug.go.
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Health + agent identity. |
GET |
/candidates |
Agent's current classified candidates (same shape as debug-api). |
GET |
/candidate/<pid> |
Single candidate by PID. |
GET |
/diff |
Diff agent-local vs last-streamed-to-server. |
GET |
/fp-report |
Agent's local FP-report. |
Schemas mirror the main debug API. Use this when you suspect the agent→server stream is lossy or transforming data.
Exposes Contour's tunnel state and protocol verification when running the tunnel headless — i.e. without the TUI. Relevant only if you're driving Contour programmatically.
# Headless contour server with API
./proxywatch-linux-amd64 -contour-server -contour-api 127.0.0.1:7892 -contour-ports 8080
# Headless contour client with API
./proxywatch-windows-amd64.exe -contour-client 10.0.0.5 -contour-api 127.0.0.1:7892 -contour-ports 8080Where it lives: internal/contour/api/api.go.
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Health. |
GET |
/protocols |
List supported tunnel protocols. |
GET |
/status |
Active tunnel status (running, role, protocol, ports, direction, target, recent log lines). |
POST |
/tunnel/start |
Start a tunnel. |
POST |
/tunnel/stop |
Stop the active tunnel. |
GET |
/verify/<protocol> |
Verify tunnel protocol behavior (wire signature checks). GET /verify/all runs the full matrix. |
{
"protocols": ["http", "https", "ws", "dns", "ssh", "smtp", "ftp", "redis", "postgres", "socks5"]
}{
"running": true,
"role": "server",
"proto": "http",
"direction": "Forward",
"ports": [8080],
"target": "",
"log_tail": ["client connected 10.0.0.10", "frame 1 relayed 1.2 KB"]
}Body:
{
"role": "server",
"proto": "http",
"direction": "Forward",
"ports": [8080, 8443],
"target": "10.0.0.5"
}Returns 409 if a tunnel is already running.
Cancels the active tunnel context. Returns {"ok": true} or 404 if nothing is running.
Runs the wire-signature validation for a single protocol. /verify/all runs every protocol in /protocols and returns a matrix of pass/fail with per-check detail. Verification can take up to ~120s for the full matrix — the API uses a 120s write timeout accordingly.
# Start a scan through a SOCKS tunnel in the background, poll for tunneling state
( for i in $(seq 1 20); do
curl -s 'http://127.0.0.1:7890/candidates?name=sshd' | jq '.items[] | {pid, role, state, ap:.active_proxying}'
sleep 1
done ) &
proxychains4 -f /etc/proxychains4.conf nmap -sT -p 1-500 172.16.1.130
waitcurl -s http://127.0.0.1:7890/fp-report > baseline-before.json
# … apply classifier changes …
curl -s http://127.0.0.1:7890/fp-report > baseline-after.json
diff <(jq -S . baseline-before.json) <(jq -S . baseline-after.json) | lessHASH=$(sha256sum /path/to/internal-tool | awk '{print $1}')
curl -X POST http://127.0.0.1:7890/operator/label \
-H 'Content-Type: application/json' \
-d "{\"sha256\":\"$HASH\",\"verdict\":\"benign\",\"reason\":\"internal ops tool\"}"- No built-in auth. Bind the debug APIs only to
127.0.0.1or a management interface. - Reverse-proxy for remote access. If you need remote access (e.g. SOC tooling), put the API behind an authenticated reverse proxy (nginx + mTLS, Tailscale ACL, etc.).
- Operator-label mutation is write-access to the ML training set. Treat
POST /operator/labelandDELETE /operator/label/<hash>endpoints as administrative — an attacker that can write labels can teach the classifier to suppress their own tooling. - Don't expose the contour API publicly.
POST /tunnel/startlets callers open tunnels from the host; that's appropriate for operator driving but not for untrusted reachability.
| Version | Change |
|---|---|
| v1.0.6 | Added /online/status, /online/verdict/<hash>, /operator/labels, /operator/label, /operator/label/<hash>. tunneling_state in /fp-report now reflects strict real-time flow gate, not topology. |
| v1.0.5 | Added /diff/<baseline>, services reachability fields in contour API. |
| v1.0.4 | /candidates filter query parameters added. |
⚠️ Significant portions of this documentation were authored with AI assistance and may not cover every response field. Authoritative source is the code — see the linked files in each section.