Baobaobaolin.com
date
entry
052
topic
integration
rev
—

Filtering CISA KEV by dateAdded for a weekly watch list misses changes to old entries: a field-by-field diff of 35 versions

I have a script for a weekly watch list that picks out entries from CISA's KEV (Known Exploited Vulnerabilities) JSON whose dateAdded falls in the week. That only shows vulnerabilities that are newly added. I diffed 35 versions from August 31 to October 16 field by field: old entries were changed several times in that window, 9 of them to "known to be used in ransomware," and the way the script is written, none of those 9 would appear in any week's list during the period.

CISA's KEV catalog has a public JSON feed, and every entry has a dateAdded. For a weekly vulnerability watch list, the direct approach is to filter entries whose dateAdded falls in the week. My own script does exactly that: args.since <= v['dateAdded'] <= args.until.

But old KEV entries get changed, and none of the changes observed here touched dateAdded. This note is about what filtering by added date misses. In order: where the data comes from, what actually changed across 35 versions, which changes matter for a watch list, why the version string can't be a key, and the diff I plan to switch to.

Data: the kev-data git history

The feed URL on cisa.gov serves the current content, and the file itself carries no revision history. CISA also maintains cisagov/kev-data on GitHub; its README says users should expect it and cisa.gov to be in sync within minutes, and states what the repo is for: the KEV has no revision history that's easy to get after the fact, and git's commit history can make tracking changes more transparent.

I used the GitHub API to list the 34 commits to known_exploited_vulnerabilities.json since 2026-09-01, plus one from August 31 as a baseline, 35 versions in all, and saved a copy of each. The latest is identical to the feed I fetched from cisa.gov the same day (catalogVersion 2026.10.16, count 1748). The comparison keys on cveID and compares each adjacent pair: which keys appear, which disappear, and which field values differ for the same key. Only versions kept on GitHub can be compared; any short-lived state on cisa.gov that was never committed isn't visible here.

What changed across 35 versions

entries added            61
entries removed           0
field changes (excluding the new field)
  knownRansomwareCampaignUse   9   all Unknown → Known
  forensicTriage               5
  shortDescription             4
  dueDate                      2
  notes                        1
new field forensicTriage  1,687   added to every entry on September 1

None of the 61 added entries had a dateAdded earlier than the commit date on which it appeared, so this sample has no case of backfilling an earlier added date; as long as the report runs every week with contiguous date ranges, filtering new entries by added date doesn't miss any to backfilling in this window. The trouble is everything else in that table: changes to entries that were already in the catalog. Filtering by added date only tells you whether an entry falls in this week's range, not whether it changed; entries whose added date is already outside the range never show up again.

There is an exception that lands inside a range: CVE-2023-22894 was added on October 8, and that day's second release changed its forensicTriage from No to Yes. Fetched after the second release, that week's list would select it because its added date falls in the range; but my script doesn't output forensicTriage, and nothing shows it changed from No to Yes that day.

Which changes belong on a watch list

The most notable are the 9 ransomware flags. The schema describes knownRansomwareCampaignUse as Known if the vulnerability is known to have been used in a ransomware campaign, Unknown if CISA lacks confirmation. These 9 have dateAdded values from 2021-11-03 to 2026-08-18; CVE-2016-7255 and CVE-2019-0859, for example, were both added in November 2021 and only flipped to Known on September 9 (UTC). None of their added dates falls in any week of this observation period, so for a weekly report filtered by dateAdded, the change to Known might as well not have happened.

My reading is that if an organization still runs the affected product and hasn't dealt with it, "an old vulnerability is confirmed as used in ransomware" deserves a nudge more than "one more new vulnerability," because it has been public for a long time and attackers have had time to tool it up. That's an inference; CISA's data doesn't say which matters more.

In the schema, dueDate is the due date for the requiredAction. The two changes went in opposite directions: CVE-2026-86950 moved from 2026-10-02 to 2026-10-13; CVE-2022-0995 moved from 2026-09-09 earlier to 2026-08-29, in a change that appeared in the October 8 (UTC) version, when the new deadline had already passed. The JSON doesn't say why. If you recorded these two deadlines before the October 8 change and afterwards only handle additions without refreshing old records, you're left holding the old values.

forensicTriage is a field added on September 1; the schema describes it as whether forensic triage is required per BOD 26-04, and only specifies a string, with Yes and No as the only values in this sample. All 1,687 entries gained it that day, so that comparison counts every entry as "changed." Five changed between Yes and No afterwards. In the October 16 version, 87 are Yes. If your code uses a fixed-field model that rejects unknown fields and hasn't added this one, the next read of any version from September 1 on fails to parse; if it reads a few fixed fields and doesn't diff, it never notices the new one. My script reads with json.load, so it's the latter.

catalogVersion can't be a version key

I first wanted to label each version by catalogVersion, and found that in this sample it's only a date. September 9 had two releases, both catalogVersion 2026.09.09, with dateReleased 12:56:45Z and 19:00:50Z; the first September 9 release changed 3 ransomware flags, the second added 4 entries. September 16 had three releases, all 2026.09.16.

So "I've already seen this catalogVersion" doesn't mean the content is the same. A script that has processed the day's first release and decides whether to reprocess by catalogVersion alone skips the later ones. The 35 versions all have different dateReleased values, but the schema doesn't promise it's unique; to tell content apart I'd hash the whole file, record the commit SHA for versions backfilled from GitHub, and keep dateReleased as the release time.

Switching to a field-by-field diff

If I were rewriting this weekly report, I'd have it save each JSON it fetches and compare against the previous one on the next run, instead of filtering by date. The core is roughly (with a sentinel, so "field missing" and "value is null" stay distinct):

MISSING = object()
prev = {v["cveID"]: v for v in json.load(open(old))["vulnerabilities"]}
curr = {v["cveID"]: v for v in json.load(open(new))["vulnerabilities"]}
added   = [k for k in curr if k not in prev]
removed = [k for k in prev if k not in curr]
changed = {k: {f: (prev[k].get(f, MISSING), curr[k].get(f, MISSING))
               for f in prev[k].keys() | curr[k].keys()
               if prev[k].get(f, MISSING) != curr[k].get(f, MISSING)}
           for k in curr if k in prev and prev[k] != curr[k]}

Additions are listed as before; changes are sorted by field: ransomware flags and deadline changes go on the list; forensicTriage flips go on too, since they change the forensic-triage requirement the catalog marks under BOD 26-04; this round's spelling and wording edits are only logged, and description changes that touch the required action get a separate look. Removals were 0 in this window, but neither the schema nor the README says entries are never removed, so keep that bucket. The first run has to establish a baseline; after a few missed weeks, comparing against the last copy you have shows the net change, and to recover the changes in between, backfill the versions kept in the kev-data git history for that period, which is where this note's data came from.

Running weekly misses things too. If an entry changes and changes back between two runs, comparing the two ends shows nothing; that didn't happen in these 35 versions, but a low-frequency diff can't see intermediate states by design.

If you remember one thing

dateAdded tells you when an entry came in, not what was changed afterwards; between late August and mid-October, 9 old vulnerabilities were changed to "known ransomware use," and a report filtered by added date couldn't see it. Save every version you fetch, diff field by field keyed on cveID, and tell versions apart by a file hash, not catalogVersion.

Revision history

  1. First published