Dev.to Security 🔐 Cybersecurity 👁 0 📖 6 min read

Get a weekly CISA KEV digest with n8n (free workflow)

CISA's Known Exploited Vulnerabilities (KEV) catalog is the shortest useful patch list there is: every entry is a CVE that CISA has evidence of being exploited in the wild. CISA publishes it as a public JSON feed with no

CISA's Known Exploited Vulnerabilities (KEV) catalog is the shortest useful patch list there is: every entry is a CVE that CISA has evidence of being exploited in the wild. CISA publishes it as a public JSON feed with no API key, which makes it a good fit for a small n8n workflow.

This post walks through a workflow that runs every Monday, keeps the entries added in the previous week (Mon-Sun), stars the vendors you actually run, and emails your team an HTML digest. You can build it from the steps below, or get the finished one free: download page with setup notes or the raw workflow JSON.

The feed

https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json

It returns one object with catalogVersion, count and a vulnerabilities array (1,758 entries in catalog 2026.10.26). Each entry has the fields you need for a digest:

Field Example use
cveID the row key
vendorProject, product watchlist matching and display
dateAdded the "new last week" filter
dueDate the remediation deadline for US federal civilian agencies under BOD 22-01. Other organizations can use it as a target date.
knownRansomwareCampaignUse Known or Unknown
requiredAction CISA's one-line instruction

The data is CC0 1.0. Credit CISA and don't imply endorsement.

The workflow

Nine nodes, all core (plus sticky notes):

  1. Every Monday 08:00 (Schedule Trigger) → Flag: live run (Code: return [{json:{_dryRun:false}}])
  2. Test workflow (manual) (Manual Trigger) → Flag: dry run (Code: return [{json:{_dryRun:true}}])
  3. Both flags → Download CISA KEV feed (HTTP Request, GET, 30 s timeout)
  4. Filter & build digest (Code)
  5. Dry run? (IF on _dryRun) → true: Dry run: log only (No Op); false: Email digest (Send Email, SMTP, HTML)

Time zone: the 08:00 trigger uses the workflow's time zone (Settings > Timezone). The workflow JSON doesn't set one, so it falls back to the instance's GENERIC_TIMEZONE, which defaults to America/New_York (self-hosted; on n8n Cloud the default is the owner's detected time zone, or GMT). The week window doesn't use that time zone: the Code node compares CISA's date-only dateAdded with the run's UTC date. If you're more than 8 hours ahead of UTC (for example Tokyo, Sydney or Auckland), Monday 08:00 local is still Sunday in UTC, so each digest covers Sunday to Saturday and says "from to ". To get Mon-Sun there, set the trigger hour to at least your UTC offset, rounded up, using your summer offset if you have daylight saving.

The Code node

This is the core of it, trimmed for the post:

const WATCHLIST = ['microsoft','google','apple','fortinet','citrix','ivanti','atlassian','oracle','adobe','apache'];
const LOOKBACK_DAYS = 7; // keep 7 for the weekly Monday schedule

const feed = $input.first().json;
const vulns = Array.isArray(feed.vulnerabilities) ? feed.vulnerabilities : [];
if (!vulns.length) throw new Error('KEV feed returned no vulnerabilities; check the URL or network.');

// dateAdded is date-only (YYYY-MM-DD), so compare UTC date strings, not timestamps.
const day = d => d.toISOString().slice(0,10);
const end = day(new Date());                                   // exclusive: today's additions go into next week's digest
const start = day(new Date(Date.now() - LOOKBACK_DAYS*864e5)); // inclusive

const recent = vulns
  .filter(v => typeof v.dateAdded === 'string' && v.dateAdded >= start && v.dateAdded < end)
  .map(v => ({ ...v, watch: WATCHLIST.includes(String(v.vendorProject).toLowerCase()) }))
  .sort((a, b) => (b.watch - a.watch) || (a.dueDate > b.dueDate ? 1 : -1));
// ...then build an HTML table from `recent` (escape every field) and return
// { newCount, watchHits, html } for the email node.

Three details matter:

  • Compare date strings, not timestamps. dateAdded has no time, so new Date('2026-10-23') is midnight UTC. My first version compared it to Date.now() minus seven days, and anything CISA added on a Monday after the run had aged out by the next Monday, so it was never reported. With string comparison and today excluded, a Monday run covers the previous Monday through Sunday (UTC dates), exactly once.
  • Fail loudly on an empty feed. A network hiccup that returns nothing shouldn't produce a cheerful "no new entries" email.
  • Escape everything you put into the HTML. The feed is trusted, but escaping is cheap.

The email

Subject: =Weekly KEV patch digest: {{ $json.newCount }} new ({{ $json.watchHits }} on watchlist). Body: ={{ $json.html }}, in HTML format. A week with no additions still sends a short "No new KEV entries added in the previous week (Mon-Sun, to )." email, so silence always means something broke.

Tip: link each CVE in the table to an explainer page so readers don't have to search. KEV Explained has a plain page for every KEV entry at https://kev-explained.pages.dev/cve/<CVE-ID>/, with the required action, due date and links to NVD and the vendor advisory.

Three things that bit me

  1. isExecuted is not a reliable dry-run switch. My first version checked $('Test workflow (manual)').isExecuted before sending. That's fine when you click Test in the editor. But n8n execute on the CLI started from the schedule trigger and the email went out. Separate flag nodes per trigger plus an IF node are explicit and easy to see on the canvas.
  2. Expressions need the leading =. If you edit the workflow JSON by hand, a subject like Weekly digest: {{ $json.newCount }} is sent literally. It has to be =Weekly digest: {{ $json.newCount }}.
  3. Vendor names are CISA's, not yours. Matching is on lowercase vendorProject. Microsoft, Fortinet and Ivanti are what you'd guess, but PHP shows up as both PHP and PHP Group. Run a quick jq -r '.vulnerabilities[].vendorProject' known_exploited_vulnerabilities.json | sort -u on the feed before you fill in the watchlist.

Want it daily? (untested sketch)

Untested sketch: switch the Schedule Trigger to a Days interval; split the Code node so it returns one item per CVE; add a Remove Duplicates node (Operation: Remove Items Processed in Previous Executions → Keep Items Where: Value Is New → Value to Dedupe On: cveID); then build the HTML in a second Code node. I haven't tested this; run it alongside the weekly one first.

Testing it

On self-hosted n8n 2.44.7, against the live feed (catalog 2026.10.26), a test copy with the clock pinned to Monday Oct 26 found 8 entries added Oct 19-25 (6 dated Oct 20, 2 dated Oct 23) and sent its digest to a local test mail server. The same test with the clock pinned to Monday Nov 2, on self-hosted n8n against the same feed, picks up the 2 entries dated Oct 26, which the Oct 26 run correctly left out. I haven't tested it on n8n Cloud or n8n 1.x.

Before you activate it:

  • Set the workflow time zone.
  • Attach an SMTP credential and replace the placeholder recipient.
  • Click Test workflow and check the Code node output.
  • Run it once from the schedule trigger to a test mailbox, then activate it.

Ideas to extend it

  • Add EPSS scores with a second HTTP Request to FIRST's API, and sort by them within the watchlist.
  • Send to Teams or Slack instead of email.
  • Open a ticket for each watchlist hit, with the CISA due date as the deadline.

I built this workflow for Runbook Foundry; the full JSON is free at the link above. Drafted with AI assistance; test-run on self-hosted n8n 2.44.7.

Not affiliated with n8n GmbH or CISA. Not tested on n8n Cloud. n8n is a trademark of n8n GmbH. KEV data: CISA Known Exploited Vulnerabilities Catalog, CC0 1.0; use does not imply endorsement by CISA or DHS.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.