Build with the API / Vendor in the news

Vendor in the news

Which of my vendors/products are in incident reporting this week?

What you get

For each vendor or product name you give it, pulls the highest-scoring threat clusters of the last 7 days whose title, summary or keywords mention that name, keeps those at or above a threat-score threshold, and prints one line per hit with a link to the ThreatCluster cluster page.

Endpoints
/threats
Credits per run
5
Key
Works on the free key

The code

vendor-in-the-news.py, 101 lines. Python 3, needs requests.

#!/usr/bin/env python3
"""vendor-in-the-news: which of my vendors/products are in incident reporting this week?

What it does
  For each vendor or product name you give it, pulls the highest-scoring threat clusters
  of the last 7 days whose title, summary or keywords mention that name, keeps those at
  or above a threat-score threshold, and prints one line per hit with a link to the
  ThreatCluster cluster page.

Endpoints
  GET /threats?keyword=<name>&time_filter=7d&sort_by=threat_score   (1 credit per vendor)

Cost: 1 credit per vendor name. Works on the free tier (7-day window, 25 rows per query).

Usage
  python3 vendor-in-the-news.py Microsoft Cisco Fortinet Ivanti Citrix
  python3 vendor-in-the-news.py --min-score 70 "Palo Alto" SonicWall
"""
import argparse
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_VENDORS = ["Microsoft", "Cisco", "Fortinet", "Ivanti", "Citrix"]


def main():
    ap = argparse.ArgumentParser(description="Incident clusters mentioning your vendors this week")
    ap.add_argument("vendors", nargs="*", default=DEFAULT_VENDORS, help="vendor/product names")
    ap.add_argument("--min-score", type=float, default=60.0, help="threat-score threshold (0-100)")
    args = ap.parse_args()

    total_hits = 0
    seen = set()  # the same cluster can match two vendors; print it once, under the first
    for i, vendor in enumerate(args.vendors):
        if i:
            time.sleep(PACE_SECONDS)
        data = api_get("/threats", keyword=vendor, time_filter="7d", sort_by="threat_score", limit=25).json()
        hits = [t for t in data.get("threats", [])
                if (t.get("threat_score") or 0) >= args.min_score and t.get("cluster_id") not in seen]
        print("%s: %d cluster(s) >= %g in the last 7 days" % (vendor, len(hits), args.min_score))
        for t in hits:
            seen.add(t.get("cluster_id"))
            total_hits += 1
            title = t.get("ai_title") or t.get("title") or "(untitled)"
            print("  %5.1f  %-7s  %s\n         %s" % (t.get("threat_score") or 0, t.get("urgency_level") or "-",
                                                   title, cluster_url(t)))
    print("\n%d cluster(s) across %d vendor name(s)." % (total_hits, len(args.vendors)))
    report_credits()


if __name__ == "__main__":
    main()

The shell one-liner, vendor-in-the-news.sh:

#!/usr/bin/env bash
# vendor-in-the-news.sh: clusters >= 60 mentioning each vendor named on the command line, last 7 days. 1 credit per vendor. Free tier OK.
# usage: ./vendor-in-the-news.sh Microsoft Cisco Fortinet
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'*}"; }
MIN="${MIN_SCORE:-60}"
for vendor in "$@"; do
  tc_get /threats --data-urlencode "keyword=$vendor" --data-urlencode time_filter=7d --data-urlencode sort_by=threat_score --data-urlencode limit=25 \
   | jq -r --arg v "$vendor" --arg site "$SITE" --argjson min "$MIN" '.threats[] | select(.threat_score >= $min) | "\(.threat_score)\t\($v)\t\(.ai_title // .title)\t\($site)/cluster/\(.slug)"'
  sleep 2.1  # 30 requests/min on the free tier
done

Output

From a run on 3 September 2026. Yours will differ as the corpus moves.

[threatcluster] credits used this run: 5, remaining today: 90
Microsoft: 13 cluster(s) >= 60 in the last 7 days
   75.0  medium   Malware Campaign Disables Windows Update in Corporate China
         https://threatcluster.io/cluster/malware-campaign-disables-windows-update-in-corporate-china-ce79d3b5
   72.5  medium   BlueDelta Espionage Campaign Using HOOKEDGE Targets European Governments
         https://threatcluster.io/cluster/bluedelta-espionage-campaign-using-hookedge-targets-european-172e0e04
   72.0  medium   Critical CVE-2026-62911 Exposes 22,000 Exchange Servers to Remote Code Execution
         https://threatcluster.io/cluster/critical-rce-vulnerability-in-microsoft-exchange-server-disc-34b82887
   69.5  medium   Multiple CVEs Disclosed for Windows Vulnerabilities
         https://threatcluster.io/cluster/multiple-cves-disclosed-for-microsoft-products-in-august-202-9eaa8d51
   69.0  medium   Cyber Campaign Targets Cambodia with Spark RAT via BYOVD Technique
         https://threatcluster.io/cluster/cyber-campaign-targets-cambodia-with-spark-rat-via-byovd-tec-53284768
   68.2  medium   Knight Office Phishing Kit Targets Microsoft 365 Accounts via Session Hijacking
         https://threatcluster.io/cluster/knight-office-phishing-kit-targets-microsoft-365-accounts-vi-713a0a04
   68.0  medium   Trojanized Exodus Wallet Installer Deploys Remote Access Trojan
         https://threatcluster.io/cluster/tampered-exodus-wallet-installer-distributes-remote-access-t-af67cbc6
   67.5  medium   RevStealer Malware Distributed via Fake Claude Opus 5 App
         https://threatcluster.io/cluster/revstealer-malware-targets-users-via-fake-ai-application-6960a5d2
   67.5  medium   Spring Ring: Coordinated Vishing Campaign Exploits Microsoft Teams
         https://threatcluster.io/cluster/spring-ring-voice-phishing-campaigns-target-microsoft-teams--bffd328e
   64.5  medium   AI-Powered Malware Targets Crypto Workers via Compromised Chat Links
         https://threatcluster.io/cluster/ai-powered-malware-targets-crypto-workers-via-compromised-ch-c94becaf
   63.5  medium   Phishing and Authentication Abuse Surge in Cybersecurity Incidents
         https://threatcluster.io/cluster/phishing-and-authentication-abuse-surge-in-cybersecurity-inc-e9a17724
   61.6  medium   NCSA and Microsoft Promote Zero Trust Amid Rising Cyber Threats in Thailand
         https://threatcluster.io/cluster/ncsa-and-microsoft-promote-zero-trust-amid-rising-cyber-thre-48335c23
   60.9  medium   Texas Launches Project Watershed 250 to Enhance Water Cybersecurity
         https://threatcluster.io/cluster/texas-launches-project-watershed-250-to-enhance-water-cybers-188b126a
Cisco: 1 cluster(s) >= 60 in the last 7 days
   77.8  medium   Fire Ant Threat Actor Targets Trusted Infrastructure in 2026
         https://threatcluster.io/cluster/china-nexus-threat-actor-fire-ant-targets-critical-infrastru-98a55c29
Fortinet: 0 cluster(s) >= 60 in the last 7 days
Ivanti: 1 cluster(s) >= 60 in the last 7 days
   80.8  medium   China-Linked QTFY Group Targets Critical Infrastructure with Advanced Exploits
         https://threatcluster.io/cluster/china-linked-qtfy-group-targets-critical-infrastructure-with-545ca28d
Citrix: 0 cluster(s) >= 60 in the last 7 days

15 cluster(s) across 5 vendor name(s).

Run it

  1. Get a free key at /api. Every account has one; it covers the endpoints above.
  2. export THREATCLUSTER_API_KEY=… your key.
  3. python3 vendor-in-the-news.py

The file on GitHub: vendor-in-the-news.py and vendor-in-the-news.sh.

Goes well with

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