Build with the API / Trending threat actors
Trending threat actors
Which threat actors, ransomware crews and malware families are rising this week?
What you get
Prints the trending APT groups, ransomware groups and malware families of the last 7 days: how many clusters mention them, the change versus the previous window, whether they are new to the corpus, and the ThreatCluster entity page for each.
- Endpoints
/entities/trending- Credits per run
- 1
- Key
- Works on the free key
The code
trending-actors.py, 100 lines. Python 3, needs requests.
#!/usr/bin/env python3
"""trending-actors: which threat actors, ransomware crews and malware families are rising this week?
What it does
Prints the trending APT groups, ransomware groups and malware families of the last
7 days: how many clusters mention them, the change versus the previous window, whether
they are new to the corpus, and the ThreatCluster entity page for each.
Endpoints
GET /entities/trending?time_filter=7d&limit=10 (1 credit)
Cost: 1 credit per run. Works on the free tier.
Usage
python3 trending-actors.py
python3 trending-actors.py --types apt_group malware cve
"""
import argparse
from urllib.parse import quote
import json
import os
import sys
import time
import requests
API_BASE = os.environ.get("THREATCLUSTER_API_BASE", "https://threatcluster.io/api/public/v1").rstrip("/")
API_KEY = os.environ.get("THREATCLUSTER_API_KEY", "")
SITE = os.environ.get("THREATCLUSTER_SITE", "https://threatcluster.io") # where the linked pages live
PACE_SECONDS = 2.1 # free keys get 30 requests/min; ~28/min keeps loops clear of 429s
_credits = {"used": 0, "remaining": None}
def _auth_headers():
# A tc_... key is sent as X-API-Key; anything else is a short-lived bearer (e.g. from `tc login`).
if not API_KEY:
sys.exit("THREATCLUSTER_API_KEY is not set. Free keys: https://threatcluster.io/about/api")
if API_KEY.startswith("tc_"):
return {"X-API-Key": API_KEY}
return {"Authorization": "Bearer " + API_KEY}
def api_get(path, _allow=(), **params):
"""GET one endpoint. Exits non-zero with the API's own error body, unless the
status is listed in _allow (per-item lookups where a 404 is itself an answer)."""
r = requests.get(API_BASE + path, headers=_auth_headers(), params=params, timeout=60)
if "X-Request-Cost" in r.headers:
_credits["used"] += int(r.headers["X-Request-Cost"])
if "X-RateLimit-Remaining" in r.headers:
_credits["remaining"] = r.headers["X-RateLimit-Remaining"]
if r.status_code >= 400 and r.status_code not in _allow:
try:
body = json.dumps(r.json())
except ValueError:
body = r.text[:500]
sys.exit("HTTP %d from %s: %s" % (r.status_code, path, body))
return r
def cluster_url(cluster):
# The free payload carries `slug` (canonical page) and `cluster_id`; the last 8 hex
# chars of the id also resolve, via a 301 to the slug, when slug is missing.
ident = cluster.get("slug") or cluster.get("short_id") or cluster.get("cluster_id", "").replace("-", "")[-8:]
return "%s/cluster/%s" % (SITE, ident)
def report_credits():
# Stderr, so it never pollutes piped output (brief.md, blocklists, JSON).
remaining = _credits["remaining"] if _credits["remaining"] is not None else "n/a"
sys.stderr.write("[threatcluster] credits used this run: %d, remaining today: %s\n" % (_credits["used"], remaining))
DEFAULT_TYPES = ["apt_group", "ransomware_group", "malware"]
def main():
ap = argparse.ArgumentParser(description="Trending actors/malware this week")
ap.add_argument("--types", nargs="+", default=DEFAULT_TYPES, help="entity types to show")
ap.add_argument("--limit", type=int, default=10, help="rows per type (free tier caps at 10)")
args = ap.parse_args()
data = api_get("/entities/trending", time_filter="7d", limit=args.limit).json()
trending = data.get("trending", {})
for etype in args.types:
rows = trending.get(etype) or []
print("== %s (last %s) ==" % (etype, data.get("time_filter", "7d")))
if not rows:
print(" nothing trending\n")
continue
print(" %-28s %8s %8s %s" % ("entity", "mentions", "change", "page"))
for r in rows:
change = "NEW" if r.get("is_new") else ("%+.0f%%" % r["change"] if r.get("change") is not None else "-")
# quote(safe='') because entity values can contain '/', '#' or spaces.
print(" %-28s %8s %8s %s/entities/%s/%s" % ((r.get("value") or "")[:28], r.get("frequency", "?"), change,
SITE, etype.replace("_", "-"), quote(str(r.get("value") or ""), safe="")))
print()
report_credits()
if __name__ == "__main__":
main()
The shell one-liner, trending-actors.sh:
#!/usr/bin/env bash
# trending-actors.sh: trending APT groups, ransomware groups and malware of the last 7 days. 1 credit. Free tier OK.
# usage: ./trending-actors.sh
set -euo pipefail
: "${THREATCLUSTER_API_KEY:?set THREATCLUSTER_API_KEY (free keys: https://threatcluster.io/about/api)}"
BASE="${THREATCLUSTER_API_BASE:-https://threatcluster.io/api/public/v1}"
SITE="${THREATCLUSTER_SITE:-https://threatcluster.io}"
# A tc_... key goes in X-API-Key; anything else is a bearer (e.g. from `tc login`).
if [[ "$THREATCLUSTER_API_KEY" == tc_* ]]; then AUTH="X-API-Key: $THREATCLUSTER_API_KEY"; else AUTH="Authorization: Bearer $THREATCLUSTER_API_KEY"; fi
# tc_get <path> [curl -G args...]: prints the body; on HTTP >= 400 prints the API error to stderr and fails.
tc_get() { local path=$1; shift; local out code
out=$(curl -sS -G -H "$AUTH" -w $'\n%{http_code}' "$BASE$path" "$@"); code=${out##*$'\n'}
if [ "$code" -ge 400 ]; then echo "HTTP $code from $path: ${out%$'\n'*}" >&2; return 1; fi; printf '%s' "${out%$'\n'*}"; }
tc_get /entities/trending --data-urlencode time_filter=7d --data-urlencode limit=10 \
| jq -r '.trending | to_entries[] | select(.key == "apt_group" or .key == "ransomware_group" or .key == "malware")
| "== \(.key) ==", (.value[] | " \(.value)\t\(.frequency) mentions\t\(if .is_new then "NEW" else "\(.change)%" end)")'
Output
From a run on 3 September 2026. Yours will differ as the corpus moves.
[threatcluster] credits used this run: 1, remaining today: 47 == apt_group (last 7d) == entity mentions change page TeamPCP 4 +300% https://threatcluster.io/entities/apt-group/TeamPCP ShinyHunters 5 +67% https://threatcluster.io/entities/apt-group/ShinyHunters Apt28 4 +100% https://threatcluster.io/entities/apt-group/Apt28 Apt29 5 +25% https://threatcluster.io/entities/apt-group/Apt29 MuddyWater 2 +100% https://threatcluster.io/entities/apt-group/MuddyWater Smoke Sandstorm 2 +100% https://threatcluster.io/entities/apt-group/Smoke%20Sandstorm Silver Fox 5 NEW https://threatcluster.io/entities/apt-group/Silver%20Fox Lazarus Group 4 NEW https://threatcluster.io/entities/apt-group/Lazarus%20Group Sandworm 3 NEW https://threatcluster.io/entities/apt-group/Sandworm Stardust Chollima 2 NEW https://threatcluster.io/entities/apt-group/Stardust%20Chollima == ransomware_group (last 7d) == entity mentions change page Ryuk 4 +300% https://threatcluster.io/entities/ransomware-group/Ryuk 8Base 2 +100% https://threatcluster.io/entities/ransomware-group/8Base Rhysida 10 NEW https://threatcluster.io/entities/ransomware-group/Rhysida Aur0ra 4 NEW https://threatcluster.io/entities/ransomware-group/Aur0ra Interlock 4 NEW https://threatcluster.io/entities/ransomware-group/Interlock WannaCry 4 NEW https://threatcluster.io/entities/ransomware-group/WannaCry The Gentlemen 3 NEW https://threatcluster.io/entities/ransomware-group/The%20Gentlemen Krybit 3 NEW https://threatcluster.io/entities/ransomware-group/Krybit Jadepuffer 3 NEW https://threatcluster.io/entities/ransomware-group/Jadepuffer INC 2 NEW https://threatcluster.io/entities/ransomware-group/INC == malware (last 7d) == entity mentions change page Vidar 24 +500% https://threatcluster.io/entities/malware/Vidar LummaC2 19 +850% https://threatcluster.io/entities/malware/LummaC2 Pegasus 7 +600% https://threatcluster.io/entities/malware/Pegasus AMOS 8 +167% https://threatcluster.io/entities/malware/AMOS GoCaracal 3 +200% https://threatcluster.io/entities/malware/GoCaracal Shai-hulud 3 +200% https://threatcluster.io/entities/malware/Shai-hulud Bandook 3 +200% https://threatcluster.io/entities/malware/Bandook Predator 4 +33% https://threatcluster.io/entities/malware/Predator SecTopRAT 2 +100% https://threatcluster.io/entities/malware/SecTopRAT Hookedge 2 +100% https://threatcluster.io/entities/malware/Hookedge
Run it
- Get a free key at /api. Every account has one; it covers the endpoints above.
export THREATCLUSTER_API_KEY=… your key.python3 trending-actors.py
The file on GitHub: trending-actors.py and trending-actors.sh.
Goes well with
- Weekly executive brief: The week's top 10 threat clusters as a Markdown brief.
- A lookup tool for LLM agents: A tool-calling function `threatcluster_lookup(query)` for LLM agents.
- Brand and domain dark-web monitor: Are my brands or domains showing up on the dark web?
- Bash integration: curl and jq in shell scripts and cron
All recipes: /build. Endpoint reference: /api/public/v1/docs.