Threat Radar: a live CTI dashboard at the edge
A static site can't fetch cross-origin threat feeds. An edge worker does it server-side and caches the result, so the upstreams get hit twice an hour no matter the traffic.
- Cloudflare Pages Functions
- Next.js (static export)
- TypeScript
- CISA KEV
- ransomware.live
- RSS
The problem
This site is a static export on Cloudflare Pages, with no server and no API
routes. A browser on desireddy.com can't fetch most threat feeds directly:
CISA's KEV JSON, ransomware.live and the security-news RSS feeds don't send
Access-Control-Allow-Origin, so the request is blocked before it starts.
And the feeds that do allow it would be hit once per visitor.
The approach
A Cloudflare Pages Function at /api/threat-feed handles it: a small edge
worker that lives alongside the static site with no framework adapter. It
fetches all the sources server-side (no CORS), parses the RSS itself (the
Workers runtime has no DOM), normalises everything to one shape, and returns
JSON with a 30-minute s-maxage. Cloudflare caches that at the edge, so the
upstreams are hit at most twice an hour per location no matter how much traffic
the page gets. There are no API keys; every source is free and keyless. Nothing
is stored, and there's no tracking.
What got built
Three source types behind one endpoint:
- Actively exploited: the CISA Known Exploited Vulnerabilities catalogue, filtered to recent additions, with the ransomware-campaign flag surfaced.
- Ransomware & extortion claims: ransomware.live recent victims, with implausible future-dated rows filtered out.
- Security news: six newsrooms (The Hacker News, Krebs, Dark Reading, The Record, SecurityWeek, SANS ISC), deduped by normalised title, with event promos and daily-podcast items filtered.
The front end is a client component with graceful loading and error states, a soft refresh every twenty minutes and on tab refocus. Full dashboard at Threat Radar; a compact version sits in the site header.
What it taught
Pages Functions coexist cleanly with output: export. The functions/
directory is picked up from the repo root and takes precedence over static
assets for its route, so you get one edge endpoint without giving up the static
build. The edge cache is the whole design: with s-maxage, traffic volume
stops mattering to the upstreams.