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.
What it does
Section titled “What it does”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.
What it can access
Section titled “What it can access”cisa-kev: a connection with no sign-in, basehttps://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 callsGET /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.
What you need to set up
Section titled “What you need to set up”- Create the
cisa-kevconnection with no sign-in and base addresshttps://www.cisa.gov(see Create a connection). - Create the
software-inventoryconnection to your own inventory API. It must return{ "items": [{ "vendor": "...", "product": "...", "owner": "...", "host": "..." }, ...] }fromGET /api/v1/software; adapt the script’s field names to your API’s actual shape. - Add the company variable
security_email. - 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.
How to adapt it
Section titled “How to adapt it”- Your inventory’s real shape. Real inventory APIs rarely return exactly
vendor/product/owner/host. Change theinventorymapping in thematchstep’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
dueDateinstead of only new entries. Add a second scheduled check that reads the samesincecursor and flags entries whosedueDate(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.
What Liitora shows on the approval page
Section titled “What Liitora shows on the approval page”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.
Trust details
Section titled “Trust details”| 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.
The full YAML
Section titled “The full YAML”# 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 digestdescription: 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 }}"]Changelog
Section titled “Changelog”- 1.0.0 (2026-10-04): First version in the library.