The Blog Watches the Watchers: Building a Threat-Intel Page With No Servers

7/26/2026

There's a Security Feeds page on this blog now. It shows which vulnerabilities are being actively exploited this week, which new CVEs actually matter, what the AI security world is publishing, and it updates itself twice a day. Here's the part I enjoy telling people: this site has no server. No database, no cron daemon, no dashboard product, no agent running anywhere I have to patch. It's static HTML on Cloudflare Pages.

So where does the "live" come from?

The trick: the repository is the database

The whole system is one scheduled GitHub Action and one JSON file. Twice a day, the workflow wakes up, runs a fetch script, and if anything changed, commits the result to the repo. Cloudflare sees the commit, rebuilds the site, and the page is current again. The data store is git. The scheduler is GitHub. The renderer is a build step. Every component is something I already had.

I keep coming back to how much this pattern generalises. We reach for infrastructure by reflex: surely a live threat-intel dashboard needs a service? But "live" for humans means "fresh when I look at it," and twice a day is fresher than most people check any dashboard. Once you accept that, an enormous class of monitoring pages collapses into: fetch, normalise, commit.

Choosing sources like a GRC person

I evaluated a long list of feeds and cut ruthlessly on three criteria: is there a real API or feed (no HTML scraping: scrapers are maintenance debt with a countdown timer), will the licence let me republish (US government data is public domain; GitHub's advisory database is CC-BY; a vendor blog is neither), and will it still work in two years without me touching it.

That filter produced a core I'd recommend to anyone: CISA KEV for ground truth on active exploitation, NVD for the CVE firehose, GitHub Security Advisories for the package supply chain, and EPSS scores to predict what gets exploited next. Around it sit SANS ISC diaries, arXiv's LLM-security research, the AI Incident Database, and abuse.ch IOCs.

The filter also killed some darlings. VirusTotal was on my original list: it's the tool everyone names first. But VT is an enrichment service, not a feed, and its terms prohibit republishing data. So instead of ingesting it, every CVE on the page gets a "Check on VirusTotal" deep-link. Readers get the capability; I stay inside the licence. Knowing what you're allowed to do with data is not the glamorous part of security, but it's the part that keeps your project alive.

The hardest design decision: trusting nothing

Here's the thing that took the most thought, and it's not the plumbing. A security-feeds page ingests text written by other people and renders it on my domain. That is (say it with me) untrusted input meets privileged context. The irony of getting XSS'd by your own threat-intelligence page would be unbearable.

So the pipeline treats every feed as hostile. Strings are stripped of control characters, length-capped, and rendered as text, never as HTML. URLs must be https or they're rejected. And the summariser (a small model that condenses verbose advisories into two sentences) receives feed content wrapped in data tags with explicit instructions that it is content to describe, never instructions to follow. If someone plants "ignore previous instructions" inside a CVE description, the worst case is a slightly confused summary, not a hijacked page. I wrote about this failure mode in the MCP threat model; it was strange and satisfying to defend against it in my own build a few weeks later.

One more rule fell out of that thinking: the risk ratings are never AI-assigned. The badge that says critical comes from a deterministic formula: KEV-listed means critical, full stop; otherwise CVSS crossed with EPSS. A language model writes prose on the page; it does not make judgements. If a reader challenges a rating, I can show them arithmetic, not vibes.

Reliability is a design property, not a hope

Feeds die. Sites put up WAFs, endpoints move, rate limits appear. I learned this before shipping a single line, because half the sources I tested were unreachable from the sandbox I developed in, which turned out to be the best resilience test I could have asked for.

So every source is independent: one failing keeps its last-good data and gets an amber "stale" badge on the page instead of breaking anything. The fetch script has zero npm dependencies: plain Node, no install step, no supply chain to poison inside a workflow that holds a write token. If nothing changed since the last run, nothing is committed, so the git history stays quiet on quiet days. And if a source stays dead for a week, the workflow files exactly one GitHub issue about it, then shuts up.

None of this was hard to build. All of it had to be decided. That's the real lesson I'd hand to anyone building their own: the fetch loop is an afternoon; the trust boundaries, licence choices, and failure behaviour are the actual engineering.

The page is live at /security-feeds. The next time someone tells you real-time security visibility requires a platform subscription, ask them what "real-time" means for a human who checks twice a day. And then show them a git log.