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 are hit twice an hour regardless of traffic.
The problem
This site is a static export on Cloudflare Pages — no server, 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 — 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. 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, 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.