ThreatCull serves two kinds of HTTP endpoints:
- Feed URLs (
/o/...) hand an Output to a firewall, DNS server or SIEM. A Feed Token authorises them. - The REST API (
/api/v1/...) lets scripts read the state of an instance and change a few settings. An API token authorises it.
The examples use http://<server-ip>:6969 and an API token in $TOKEN.
Create a token inside the container. ThreatCull prints it once:
docker exec threatcull threatcull api-token create monitoring --user adminSend it in the Authorization header:
export TOKEN=tc_...
curl -H "Authorization: Bearer $TOKEN" http://<server-ip>:6969/api/v1/meA token acts as the user it was created for. ThreatCull stores only a hash, so a lost token can't be shown again: revoke it and create a new one.
docker exec threatcull threatcull api-token list
docker exec threatcull threatcull api-token revoke monitoringWithout a token, or with an invalid one, the API answers 401 {"detail": "authentication required"}.
Requests that use a token don't need the web UI's CSRF token.
The user the token belongs to: {"username": "admin"}.
Every Source, enabled or not. Each entry has id, name, family, url, format, kind
(ip or domain), role (blocklist or allowlist), category, refresh_minutes, custom,
enabled, disabled_reason, last_success_at, last_attempt_at, last_error, last_changed_at
(when the list last gained or lost an entry) and frozen (true for a Frozen Source).
curl -H "Authorization: Bearer $TOKEN" http://<server-ip>:6969/api/v1/sourcesEnables or disables a Source. Body: {"enabled": true} or {"enabled": false}. ThreatCull
returns the Source as GET /api/v1/sources shows it, or 404 for an unknown id.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"enabled": false}' http://<server-ip>:6969/api/v1/sources/hagezi-tif-domainsEvery Output: name, kind, categories, min_tier, max_entries (the cap, or null),
format, last_count and last_published_at. last_published_at is null until a Compile has
written the Output. feed_path is the feed URL path without the token, for example
/o/ip-high. Append /<feed-token> or ?token=<feed-token> to it.
curl -H "Authorization: Bearer $TOKEN" http://<server-ip>:6969/api/v1/outputsReplaces the Output's Feed Token and returns the new one once: {"name": "ip-high", "token": "..."}.
The old feed URL stops working at once. The response carries Cache-Control: no-store.
curl -X POST -H "Authorization: Bearer $TOKEN" \
http://<server-ip>:6969/api/v1/outputs/ip-high/rotateYour allowlist entries followed by the built-in ones. Each entry has value, kind, note,
origin and mine (your own network).
Adds an entry. Body: {"value": "203.0.113.7", "note": "office", "mine": true}. note and mine
are optional. An IP, CIDR or domain ThreatCull can't parse returns 400.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "example.org", "note": "our website"}' \
http://<server-ip>:6969/api/v1/allowlistRemoves an entry: {"value": "...", "removed": true}, or 404 when it isn't there.
Suggests your own public addresses and adds nothing: {"candidates": [{"value": ..., "reason": ...}]}.
Add ?public_ip=true to also ask api.ipify.org for the address your traffic leaves from.
The newest Fetches and Compiles first. limit is 1 to 200 (default 50). Each run has id, type
(fetch or compile), source_id, started_at, finished_at, duration_seconds (null
while running), timings (seconds per stage, such as {"download": 0.4, "parse": 12.1, "apply": 8.3}; {} for runs recorded before 2.4), status (running, ok, not_modified,
failed or blocked), counts, error and home_hits.
curl -H "Authorization: Bearer $TOKEN" "http://<server-ip>:6969/api/v1/runs?limit=10"The Compile settings: active_window_days, retention_days, stale_after_hours, tier_high,
tier_medium, max_shrink, max_stale_ratio and frozen_after_days. The API can't change them; use
Settings in the web UI.
Explains one IP, CIDR or domain: score, tier, every sightings entry (which Source lists it
and since when), allowlisted_by and the eligible_outputs it would go into. A value that isn't a
public IP, CIDR or domain returns 400.
curl -H "Authorization: Bearer $TOKEN" "http://<server-ip>:6969/api/v1/lookup?q=example.org"What ThreatCull is doing now and what comes next, as the status line in the web UI shows it.
running:truewhile a Fetch or Compile runs.trigger: what started it:run_now,force_compile,scheduled_fetchorscheduled_compile.step:fetchorcompile, withstep_started_at,elapsed_secondsandprevious_seconds(how long the same step took last time, ornullwhen unknown).source_idandsource_name: the Source being fetched. During Run now,positionandtotalgive "3 of 10".next_compile_at,next_fetch(source_id,source_name,at) andretries_waiting(Fetches that found a run in progress and will retry).scheduler_onisfalsewhen ThreatCull runs without its scheduler.unpublished_outputs: Outputs no Compile has written yet. Their feed URLs answer404.database_bytesandoutputs_bytes: disk used by the database (with its WAL files) and by the published Outputs.
curl -H "Authorization: Bearer $TOKEN" http://<server-ip>:6969/api/v1/statusThe API has no endpoint that starts a Fetch or Compile yet. Use Run now in the web UI, or wait for the schedule.
A device fetches an Output from either form:
http://<server-ip>:6969/o/<name>/<feed-token>
http://<server-ip>:6969/o/<name>?token=<feed-token>
Get the token by clicking Rotate token on Outputs (or with the rotate endpoint above). It shows once.
GETreturns the file.HEADreturns only its headers.- Responses carry
ETagandLast-Modified. A request with a matchingIf-None-MatchorIf-Modified-Sincegets304 Not Modifiedand no body, so a device that checks often costs little. - A wrong token, an unknown Output and an Output no Compile has written yet all return the same
404 not found. Checklast_published_atinGET /api/v1/outputsto tell them apart.
/outputs/<name>/download is the Download button in the web UI and needs a logged-in session.
A device given that URL gets redirected to the login page. AdGuard Home, for example, then reports
"data is HTML, not plain text". Give devices the feed URL.
GET /healthz needs no token and returns {"status": "ok", "version": "2.5.1"}. The container's
health check uses it.