Skip to content

Known exploited vulnerabilities digest

Who this is for: security teams who track CISA’s Known Exploited Vulnerabilities (KEV) catalog and want to know, without reading it by hand every day, when a new entry actually affects something you run.

Before you start: the connections cisa-kev and software-inventory, and the company variable security_email.

Every weekday morning, the workflow fetches CISA’s KEV catalog. If its catalogVersion hasn’t changed since the last run, it stops there: no inventory call, no email. If it has changed, it filters the catalog to entries added since the last one it processed, matches them against your software inventory by vendor and product, and emails security a CSV of the matches. It remembers the catalog version and the latest entry date it processed, so the next run only looks at what’s new. expect: alerts you if a morning’s run never succeeds, so a quiet inbox means nothing changed, not that the check silently stopped.

  • cisa-kev: a connection with no sign-in, base https://www.cisa.gov. It only reads CISA’s public KEV feed; there is nothing to authenticate.
  • software-inventory: a generic REST connection, read-only for this workflow. It only calls GET /api/v1/software.
  • It emails only security_email, one CSV of the matching entries and nothing else from the catalog.
  • No secret value is ever in the workflow.
  1. Create the cisa-kev connection with no sign-in and base address https://www.cisa.gov (see Create a connection).
  2. Create the software-inventory connection to your own inventory API. It must return { "items": [{ "vendor": "...", "product": "...", "owner": "...", "host": "..." }, ...] } from GET /api/v1/software; adapt the script’s field names to your API’s actual shape.
  3. Add the company variable security_email.
  4. Take it into use from the Liitora library (see Take a library workflow into use), then read the test cases on your copy, run them and approve it.
  • Your inventory’s real shape. Real inventory APIs rarely return exactly vendor/product/owner/host. Change the inventory mapping in the match step’s script to your API’s field names, and consider paging if your inventory is large (the CISA feed itself has a few thousand entries).
  • Alerting on dueDate instead of only new entries. Add a second scheduled check that reads the same since cursor and flags entries whose dueDate (CISA’s remediation deadline) is approaching, not only new ones.
  • A different matching rule. The example matches by exact vendor and product text (case-insensitive). If your inventory uses different vendor names than CISA does, add an alias table in the script before matching.
  • Fewer or more emails. If your inventory is large, matches on a busy day could be long; consider splitting the email into a summary with the CSV attached (as here) rather than the full list in the body.

Every weekday at 07:00 Helsinki time or when someone starts it by hand: fetch the CISA KEV catalog, then read the catalog version seen last time, then read the latest dateAdded processed last time and check whether the catalog changed since the last run. If the check “Check whether the catalog changed since the last run” passes, fetch the software inventory and remember the catalog version processed this run, then match new entries against the software inventory, then remember the latest dateAdded processed this run. If its condition is met, build a CSV of the matching vulnerabilities. If its condition is met, email security a CSV of the matching vulnerabilities.

What Details
Publisher Liitora
Version 1.0.0
Licence Apache-2.0
Content hash sha256:4899a9098dc357f08572dc4df797fbc491a8ecae29461c33922fd41c00952a0b
Tests 4
Connections cisa-kev, software-inventory
Company variables security_email
Secrets None
Personal data The name of each matching system’s owner, as your inventory records it.
Saved state Yes
Runs on your own runner No
Actions platform/csv.build@v1, platform/email.send@v1, platform/evaluate@v1, platform/http.request@v1, platform/state.get@v1, platform/state.set@v1, platform/transform@v1

Liitora signs each version of this workflow. When your company takes it into use, Liitora checks the content hash and the signature again before it copies the workflow into your company. See How the library is kept safe.

# A Liitora library workflow for a bank CISO (see the guide, Library > Known exploited
# vulnerabilities digest). Connections used: cisa-kev (no sign-in, base
# https://www.cisa.gov), software-inventory (a generic REST inventory API).
# Company variable used: security_email.
name: Known exploited vulnerabilities digest
description: Every weekday morning, fetch CISA's Known Exploited Vulnerabilities catalog, skip when it hasn't changed, match new entries against the software inventory by vendor and product, and email security a CSV of matches.
on:
schedule:
- cron: "0 7 * * 1-5"
timezone: Europe/Helsinki
manual: true
expect:
succeeds_by: "08:00"
on: weekdays
timezone: Europe/Helsinki
concurrency:
group: kev-digest
cancel_in_progress: false
steps:
- id: fetch_kev
name: Fetch the CISA KEV catalog
uses: platform/http.request@v1
connections:
api: cisa-kev
with:
method: GET
url: /sites/default/files/feeds/known_exploited_vulnerabilities.json
retry:
attempts: 3
backoff: exponential
on: [429, 5xx, timeout, network]
- id: last_version
name: Read the catalog version seen last time
uses: platform/state.get@v1
with:
key: kev_catalog_version
default: ""
- id: last_seen
name: Read the latest dateAdded processed last time
uses: platform/state.get@v1
with:
key: kev_last_date_added
default: "1970-01-01"
- id: changed
name: Check whether the catalog changed since the last run
needs: [fetch_kev, last_version]
uses: platform/evaluate@v1
with:
expression: ${{ steps.fetch_kev.outputs.json.catalogVersion != steps.last_version.outputs.value }}
- id: fetch_inventory
name: Fetch the software inventory
needs: [changed]
if: ${{ steps.changed.outputs.result }}
uses: platform/http.request@v1
connections:
api: software-inventory
with:
method: GET
url: /api/v1/software
retry:
attempts: 3
backoff: exponential
on: [429, 5xx, timeout, network]
- id: match
name: Match new entries against the software inventory
needs: [fetch_inventory, last_seen, changed]
if: ${{ steps.changed.outputs.result }}
uses: platform/transform@v1
with:
input:
entries: ${{ steps.fetch_kev.outputs.json.vulnerabilities }}
since: ${{ steps.last_seen.outputs.value }}
inventory: ${{ steps.fetch_inventory.outputs.json.items }}
script: |
const since = input.since;
const entries = input.entries || [];
const newEntries = entries.filter((e) => e.dateAdded > since);
const inventory = input.inventory || [];
const norm = (s) => (s || "").toLowerCase();
const matches = [];
for (const e of newEntries) {
for (const i of inventory) {
if (norm(i.vendor) === norm(e.vendorProject) && norm(i.product) === norm(e.product)) {
matches.push({
cveID: e.cveID,
vendorProject: e.vendorProject,
product: e.product,
vulnerabilityName: e.vulnerabilityName,
dateAdded: e.dateAdded,
dueDate: e.dueDate,
owner: i.owner,
host: i.host,
});
}
}
}
const maxDate = newEntries.reduce((m, e) => (e.dateAdded > m ? e.dateAdded : m), since);
return { matches, newCount: newEntries.length, maxDate };
- id: remember_version
name: Remember the catalog version processed this run
needs: [fetch_kev, changed]
if: ${{ steps.changed.outputs.result }}
uses: platform/state.set@v1
with:
key: kev_catalog_version
value: ${{ steps.fetch_kev.outputs.json.catalogVersion }}
- id: remember_seen
name: Remember the latest dateAdded processed this run
needs: [match, changed]
if: ${{ steps.changed.outputs.result }}
uses: platform/state.set@v1
with:
key: kev_last_date_added
value: ${{ steps.match.outputs.result.maxDate }}
- id: build_csv
name: Build a CSV of the matching vulnerabilities
needs: [match, changed]
if: ${{ steps.changed.outputs.result && steps.match.outputs.result.matches.length > 0 }}
uses: platform/csv.build@v1
with:
rows: ${{ steps.match.outputs.result.matches }}
columns: [cveID, vendorProject, product, vulnerabilityName, dateAdded, dueDate, owner, host]
filename: kev-matches.csv
- id: email_security
name: Email security a CSV of the matching vulnerabilities
needs: [build_csv, changed]
if: ${{ steps.changed.outputs.result && steps.match.outputs.result.matches.length > 0 }}
uses: platform/email.send@v1
with:
to: ["${{ vars.security_email }}"]
subject: "Known exploited vulnerabilities in your inventory: ${{ steps.match.outputs.result.newCount }} new, ${{ steps.match.outputs.result.matches.length }} matched"
text: "New entries in CISA's Known Exploited Vulnerabilities catalog match software in your inventory. The matches are attached as a CSV."
attachments: ["${{ artifacts.kev-matches.csv }}"]
  • 1.0.0 (2026-10-04): First version in the library.