Skip to content

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

Step by step

  1. 1. Get a key

    Sign in, open Settings, then API & Feeds, and select Generate API key. The key is shown once, so copy it. Every account has one and the free key covers this recipe.

    The ThreatCluster API page with the Get a free key button and the quick start, which begins with creating a key
    The API page. The key itself is created in Settings, under API & Feeds.
  2. 2. Download the script and set the key

    The script is one file with one dependency. It reads the key from the environment, so the key never goes into the file.

    curl -sO https://raw.githubusercontent.com/Jam0k/Cyber-Threat-Intelligence-API/main/examples/recipes/trending-actors.py
    pip install requests
    export THREATCLUSTER_API_KEY=tc_live_your_key_here
  3. 3. Run it

    No arguments. The script prints the most-mentioned threat actors, ransomware groups and malware of the last 7 days, with the change on the week before.

    python3 trending-actors.py
    A terminal running the script and printing a table of threat actors with mention counts, percentage change and links, led by ShinyHunters
    Run on 29 September 2026. ShinyHunters led with 21 mentions.
  4. 4. Open a profile

    Each name links to its profile: an overview, objectives, the techniques it uses and recent coverage.

    The ThreatCluster profile for ShinyHunters with an overview, 160 occurrences, first and last seen dates and its objectives
    The profile for the top row of the run above.
  5. 5. Pick the types you track

    Pass the entity types you want. The run costs one credit.

    python3 trending-actors.py --types apt_group malware cve

The code

trending-actors.py, 101 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).
    # Keys without a daily cap send no remaining-credits header.
    left = ", remaining today: %s" % _credits["remaining"] if _credits["remaining"] is not None else ""
    sys.stderr.write("[threatcluster] credits used this run: %d%s\n" % (_credits["used"], left))

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 29 September 2026. Yours will differ as the corpus moves.

== apt_group (last 7d) ==
   entity                       mentions   change  page
   ShinyHunters                       21   +2000%  https://threatcluster.io/entities/apt-group/ShinyHunters
   Lazarus Group                      10    +100%  https://threatcluster.io/entities/apt-group/Lazarus%20Group
   Salt Typhoon                        6     +20%  https://threatcluster.io/entities/apt-group/Salt%20Typhoon
   Apt28                               2    +100%  https://threatcluster.io/entities/apt-group/Apt28
   Unc3886                             6      NEW  https://threatcluster.io/entities/apt-group/Unc3886
   Wizard Spider                       5      NEW  https://threatcluster.io/entities/apt-group/Wizard%20Spider
   Water Hydra                         3      NEW  https://threatcluster.io/entities/apt-group/Water%20Hydra
   SideCopy                            3      NEW  https://threatcluster.io/entities/apt-group/SideCopy
   Unc6240                             2      NEW  https://threatcluster.io/entities/apt-group/Unc6240
   Storm-2992                          2      NEW  https://threatcluster.io/entities/apt-group/Storm-2992

== ransomware_group (last 7d) ==
   entity                       mentions   change  page
   Clop                               28    +300%  https://threatcluster.io/entities/ransomware-group/Clop
   Cl0p                               10    +233%  https://threatcluster.io/entities/ransomware-group/Cl0p
   Ryuk                               10    +150%  https://threatcluster.io/entities/ransomware-group/Ryuk
   SilentRansomGroup                   5    +400%  https://threatcluster.io/entities/ransomware-group/SilentRansomGroup
   Pear                                5    +400%  https://threatcluster.io/entities/ransomware-group/Pear
   Krybit                              3     +50%  https://threatcluster.io/entities/ransomware-group/Krybit
   Medusa                              2    +100%  https://threatcluster.io/entities/ransomware-group/Medusa
   Booba Project                       6      NEW  https://threatcluster.io/entities/ransomware-group/Booba%20Project
   Orova                               3      NEW  https://threatcluster.io/entities/ransomware-group/Orova
   MedusaLocker                        2      NEW  https://threatcluster.io/entities/ransomware-group/MedusaLocker

== malware (last 7d) ==
   entity                       mentions   change  page
   Closedquorum                       10    +150%  https://threatcluster.io/entities/malware/Closedquorum
   Pegasus                             9    +125%  https://threatcluster.io/entities/malware/Pegasus
   TraderTraitor                       6    +200%  https://threatcluster.io/entities/malware/TraderTraitor
   SecTopRAT                           5    +400%  https://threatcluster.io/entities/malware/SecTopRAT
   BigDiskBuster                       4    +300%  https://threatcluster.io/entities/malware/BigDiskBuster
   Predator                            5     +67%  https://threatcluster.io/entities/malware/Predator
   Emotet                              4    +100%  https://threatcluster.io/entities/malware/Emotet
   Trickbot                            4     +33%  https://threatcluster.io/entities/malware/Trickbot
   Mini Shai-Hulud                     2    +100%  https://threatcluster.io/entities/malware/Mini%20Shai-Hulud
   Stuxnet                             2    +100%  https://threatcluster.io/entities/malware/Stuxnet

[threatcluster] credits used this run: 1

The file on GitHub: trending-actors.pyand trending-actors.sh.

Goes well with

All recipes: /build. Endpoint reference: /api/public/v1/docs.