Build with the API / Exploited this week

Exploited this week

CVEs from the last 7 days that are in KEV or have a public exploit.

What you get

Combines two views of the last 7 days of CVEs, the ones CISA added to the Known Exploited Vulnerabilities catalog and the ones with a known public exploit, into one table sorted by CVSS, with a link to each CVE's ThreatCluster page.

Endpoints
/vulnerabilities
Credits per run
2
Key
Works on the free key

The code

exploited-this-week.py, 94 lines. Python 3, needs requests.

#!/usr/bin/env python3
"""exploited-this-week: CVEs from the last 7 days that are in KEV or have a public exploit.

What it does
  Combines two views of the last 7 days of CVEs, the ones CISA added to the Known
  Exploited Vulnerabilities catalog and the ones with a known public exploit, into one
  table sorted by CVSS, with a link to each CVE's ThreatCluster page.

Endpoints
  GET /vulnerabilities?days=7&kev_only=true     (1 credit)
  GET /vulnerabilities?days=7&has_exploit=true  (1 credit)

Cost: 2 credits per run. Works on the free tier (25 rows per list).

Usage
  python3 exploited-this-week.py
"""
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))


def main():
    kev = api_get("/vulnerabilities", days=7, kev_only="true", limit=25).json()
    time.sleep(PACE_SECONDS)
    poc = api_get("/vulnerabilities", days=7, has_exploit="true", limit=25).json()

    merged = {}
    for v in kev.get("cves", []) + poc.get("cves", []):
        merged.setdefault(v["cve_id"], v)
    rows = sorted(merged.values(), key=lambda v: (-(v.get("cvss_v3_score") or 0), v["cve_id"]))

    print("Last 7 days: %s CVE(s) in KEV, %s with a public exploit (showing up to 25 of each)\n"
          % (kev.get("total", "?"), poc.get("total", "?")))
    print("%-16s %5s %-9s %-4s %-4s %-10s  %s" % ("CVE", "CVSS", "severity", "KEV", "PoC", "published", "ThreatCluster page"))
    for v in rows:
        print("%-16s %5s %-9s %-4s %-4s %-10s  %s/entities/cve/%s" % (
            v["cve_id"], v.get("cvss_v3_score") if v.get("cvss_v3_score") is not None else "-",
            v.get("cvss_v3_severity") or "-", "yes" if v.get("in_kev") else "no",
            "yes" if v.get("has_exploit") else "no", (v.get("published_date") or "")[:10], SITE, v["cve_id"]))
    print("\n%d unique CVE(s)." % len(rows))
    report_credits()


if __name__ == "__main__":
    main()

The shell one-liner, exploited-this-week.sh:

#!/usr/bin/env bash
# exploited-this-week.sh: last-7-day CVEs that are in KEV or have a public exploit, sorted by CVSS. 2 credits. Free tier OK.
# usage: ./exploited-this-week.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 /vulnerabilities --data-urlencode days=7 --data-urlencode kev_only=true --data-urlencode limit=25; sleep 2.1
  tc_get /vulnerabilities --data-urlencode days=7 --data-urlencode has_exploit=true --data-urlencode limit=25; } \
 | jq -r --arg site "$SITE" '.cves[] | "\(.cvss_v3_score // 0)\t\(.cve_id)\t\(.cvss_v3_severity // "-")\tKEV=\(.in_kev)\tPoC=\(.has_exploit)\t\($site)/entities/cve/\(.cve_id)"' \
 | sort -t$'\t' -k2,2 -u | sort -t$'\t' -k1,1nr

Output

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

[threatcluster] credits used this run: 2, remaining today: 43
Last 7 days: 5 CVE(s) in KEV, 8 with a public exploit (showing up to 25 of each)

CVE               CVSS severity  KEV  PoC  published   ThreatCluster page
CVE-2026-83548    10.0 CRITICAL  yes  no   2026-09-01  https://threatcluster.io/entities/cve/CVE-2026-83548
CVE-2026-26897     9.8 CRITICAL  no   yes  2026-08-27  https://threatcluster.io/entities/cve/CVE-2026-26897
CVE-2026-37071     9.8 CRITICAL  no   yes  2026-08-27  https://threatcluster.io/entities/cve/CVE-2026-37071
CVE-2026-37072     9.8 CRITICAL  no   yes  2026-08-27  https://threatcluster.io/entities/cve/CVE-2026-37072
CVE-2026-81578     9.8 CRITICAL  yes  yes  2026-08-28  https://threatcluster.io/entities/cve/CVE-2026-81578
CVE-2026-82329     9.8 CRITICAL  yes  no   2026-08-28  https://threatcluster.io/entities/cve/CVE-2026-82329
CVE-2026-37065     9.1 CRITICAL  no   yes  2026-08-27  https://threatcluster.io/entities/cve/CVE-2026-37065
CVE-2026-55511     9.1 CRITICAL  no   yes  2026-08-28  https://threatcluster.io/entities/cve/CVE-2026-55511
CVE-2026-82078     9.1 CRITICAL  yes  no   2026-08-28  https://threatcluster.io/entities/cve/CVE-2026-82078
CVE-2026-82539     9.1 CRITICAL  no   yes  2026-08-30  https://threatcluster.io/entities/cve/CVE-2026-82539
CVE-2026-75604     9.0 CRITICAL  no   yes  2026-09-01  https://threatcluster.io/entities/cve/CVE-2026-75604
CVE-2026-83549     7.8 HIGH      yes  no   2026-09-01  https://threatcluster.io/entities/cve/CVE-2026-83549

12 unique CVE(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 exploited-this-week.py

The file on GitHub: exploited-this-week.py and exploited-this-week.sh.

Goes well with

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