<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adam Lewandowski</title>
    <description>The latest articles on DEV Community by Adam Lewandowski (@adam_lewandowski_59674796).</description>
    <link>https://dev.to/adam_lewandowski_59674796</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4050293%2F750b6f7a-b1ce-4960-96cc-941de7fc52bb.jpg</url>
      <title>DEV Community: Adam Lewandowski</title>
      <link>https://dev.to/adam_lewandowski_59674796</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adam_lewandowski_59674796"/>
    <language>en</language>
    <item>
      <title>How to Read a Cisco Security Advisory: Fixed Releases, the Gold Star, and the FMC/FTD Compatibility Trap</title>
      <dc:creator>Adam Lewandowski</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:26:37 +0000</pubDate>
      <link>https://dev.to/adam_lewandowski_59674796/how-to-read-a-cisco-security-advisory-fixed-releases-the-gold-star-and-the-fmcftd-compatibility-1jbc</link>
      <guid>https://dev.to/adam_lewandowski_59674796/how-to-read-a-cisco-security-advisory-fixed-releases-the-gold-star-and-the-fmcftd-compatibility-1jbc</guid>
      <description>&lt;p&gt;A Cisco security advisory lands in your inbox. Somewhere between "we should look at this" and an approved change request, a surprising number of teams go wrong: they patch to the wrong version, they schedule the firewall upgrade before the manager upgrade, or they burn a maintenance window on a Medium while an actively exploited Critical waits in the queue.&lt;/p&gt;

&lt;p&gt;None of that comes from laziness. A Cisco PSIRT advisory is a dense document with its own vocabulary, and the pieces you need for a change request are spread across three places: the advisory itself, the Software Download portal, and the compatibility guide for your product line. This article walks through how to read all three, using Cisco Secure Firewall (FTD/FMC), ASA, and ISE as the working examples, and ends with a reusable checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a Cisco PSIRT advisory
&lt;/h2&gt;

&lt;p&gt;Every advisory published by Cisco's Product Security Incident Response Team (PSIRT) follows the same structure. The sections that matter for a change request:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Impact Rating (SIR) vs CVSS.&lt;/strong&gt; Cisco publishes both a CVSS base score and its own four-level Security Impact Rating: Critical, High, Medium, Low. The SIR usually tracks the CVSS qualitative bands, but not always. Per Cisco's own terminology documentation, PSIRT will apply a SIR that is higher or lower than the CVSS score would indicate when ease of exploitation or how widely the technology is deployed warrants it. Practical takeaway: read both, but treat the SIR as Cisco's editorial judgment on top of the raw score. Note also that Low-SIR bugs usually skip standalone advisories entirely and live in release note enclosures in the Bug Search Tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Affected and not-affected products.&lt;/strong&gt; Cisco is unusually explicit here, and the "not affected" list saves real work. Advisories name specific products and, critically, specific release trains. A bug in the FMC web interface may not touch FTD or ASA at all. Confirm your exact product before you open a ticket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed releases.&lt;/strong&gt; This is the table most people jump to, and the one most people misread. More on it in the next section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workarounds and mitigations.&lt;/strong&gt; Cisco draws a hard line: a workaround only counts as a workaround if it can be applied locally on the vulnerable device and eliminates the vulnerability. Anything less is a mitigation. Either way, Cisco's stated position is that software updates are the preferred resolution, and a workaround that gets accidentally removed during later changes silently reopens the attack surface. If the advisory says "there are no workarounds that address this vulnerability" (a common sentence on the serious ones), your only real option is the upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exploitation status.&lt;/strong&gt; Advisories get updated after publication. The line "Cisco PSIRT is aware of attempted or successful exploitation" can appear two weeks after initial disclosure. Check the current version of the advisory, not the copy someone pasted into a ticket on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  First fixed release is not the release you should install
&lt;/h2&gt;

&lt;p&gt;The fixed software section of an advisory answers one narrow question: for each affected release train, what is the earliest build that contains the fix? That is the "first fixed release."&lt;/p&gt;

&lt;p&gt;It does not answer the question you are actually asking, which is "what should I upgrade to?" Those are different questions for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First fixed is per-train.&lt;/strong&gt; A vulnerability affecting 7.0.x, 7.2.x, 7.4.x, and 7.6.x will have a separate first fixed release for each train. The advisory will not tell you whether staying on your old train is wise; it only tells you the minimum build on that train that closes this one hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First fixed closes one advisory.&lt;/strong&gt; The first fixed release for today's advisory may still be exposed to last month's advisory, or next month's. This is why Cisco pushes the Software Checker tool (on sec.cloudapps.cisco.com), which lets you enter a product and release and get back every advisory that applies, with the release that clears all of them. Run the Checker, not just the single advisory table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommended releases are chosen for stability, not just security.&lt;/strong&gt; Cisco separately maintains suggested or recommended releases per product, selected for software quality, stability, and longevity across the whole install base. The first fixed release for an advisory might be a build that is three days old; the suggested release is one Cisco has watched behave in production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The sane pattern for a change request: use the advisory and Software Checker to establish the floor (any build below this is unacceptable), then pick the actual target from the suggested release, as long as it is at or above that floor. When a fresh Critical drops, the suggested release usually catches up within days because Cisco knows everyone is about to upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gold star actually means, and what it does not
&lt;/h2&gt;

&lt;p&gt;On the Cisco Software Download portal, the suggested release for a product is marked with a gold star next to the version number. For Secure Firewall, the star also surfaces in the compatibility guide, which names a current suggested release for FTD and for FMC (at the time of writing, 7.6.4 for threat defense and 7.6.5 for the management center).&lt;/p&gt;

&lt;p&gt;The star is genuinely useful. It is Cisco's public answer to "which build would you run?" and it reflects field experience, not just recency. But it has three limits worth knowing before you build process around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is login-gated.&lt;/strong&gt; Browsing downloads for some product lines requires a Cisco.com account with entitlement (ASA images are a classic example). Tooling that scrapes public pages may never see the star at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It moves without notice.&lt;/strong&gt; There is no RSS feed or changelog for "the star moved from 7.6.3 to 7.6.4." If your baseline document says "we standardize on the suggested release," someone has to actually check the portal on a cadence, or you are standardizing on a memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is one star per product, not per constraint.&lt;/strong&gt; The starred release does not know about your hardware, your managed-device versions, or a feature you depend on that changed behavior. It is an input to your decision, not the decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat the gold star as a strong default and a tiebreaker, and record the date you observed it in the change request, because six months later nobody will be able to reconstruct why 7.6.4 was chosen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FTD/FMC compatibility trap
&lt;/h2&gt;

&lt;p&gt;Here is the rule that turns a one-device change request into a two-device project, straight from Cisco's upgrade documentation: a customer-deployed management center must run the same or a newer version as its managed devices. You cannot upgrade a device past its FMC, and this applies even to maintenance (third-digit) releases. The FMC gets upgraded first, always.&lt;/p&gt;

&lt;p&gt;The same rule bites during onboarding: try to register an FTD running a newer version than the FMC and registration fails outright. Cisco has a dedicated troubleshooting document just for that error.&lt;/p&gt;

&lt;p&gt;Why this matters for advisory response:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An FTD advisory is implicitly an FMC change too.&lt;/strong&gt; If the first fixed FTD release is newer than your FMC version, the real change request is "upgrade FMC, then upgrade FTD," with two maintenance windows, two rollback plans, and a compatibility check between the new pair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An FMC advisory can often ship alone.&lt;/strong&gt; Because FMC is allowed to run ahead of its devices, an FMC-only fix (patch the manager, leave the sensors) is usually the fastest Cisco firewall change you can make. This is worth internalizing, because some of the worst recent Cisco vulnerabilities have been FMC-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compatibility guide is a first-class source.&lt;/strong&gt; Before any version lands in a change request, check the Secure Firewall management center compatibility guide for the exact FMC/FTD pairing. Not every FMC version can manage every FTD version, and hardware support gets dropped at major releases. "The advisory says 7.6.x is fixed" is not the same as "my FMC 7.2 estate can manage 7.6.x devices."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ISE is different: patch trains, not point releases
&lt;/h2&gt;

&lt;p&gt;ISE does not follow the FTD model. An ISE version (3.3, 3.4) receives numbered cumulative patches, and Cisco's ISE documentation is explicit that patches are cumulative: installing patch 5 gives you everything in patches 1 through 4. So an ISE advisory's fixed release column typically reads like "3.3 Patch 6" or "3.4 Patch 3," and the change request is a patch install, not a version upgrade, which is a much smaller change.&lt;/p&gt;

&lt;p&gt;Two ISE-specific wrinkles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hot patches exist.&lt;/strong&gt; For urgent issues, Cisco has shipped targeted hot patches ahead of the next cumulative patch. The 2025 ISE unauthenticated RCE cluster (CVE-2025-20281, CVE-2025-20282, CVE-2025-20337) is a real example: Cisco published hot patch files tied to a specific bug ID for 3.3 and 3.4 while the cumulative patches caught up. A hot patch is a bridge, not a destination; plan the cumulative patch behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch level is part of your version.&lt;/strong&gt; "We run ISE 3.4" is not enough information to evaluate an advisory. Track the patch level as inventory data, or every ISE advisory triage starts with logging into the admin portal to find out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A worked example: the March 2026 FMC pair
&lt;/h2&gt;

&lt;p&gt;Here is how this played out on a real advisory set. On March 4, 2026, Cisco disclosed two Critical vulnerabilities in Secure Firewall Management Center: CVE-2026-20079, an authentication bypass rooted in a boot-time process misconfiguration that let crafted HTTP requests execute scripts as root, and CVE-2026-20131, an unauthenticated remote code execution flaw from insecure Java deserialization in the web management interface. Both scored CVSS 10.0. Both carried the sentence you never want to read: no workarounds.&lt;/p&gt;

&lt;p&gt;Reading them with the framework above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Affected products:&lt;/strong&gt; on-premises FMC only. FTD, ASA, and cloud-delivered firewall management were not affected, and Cisco patched its own cloud environments with no customer action needed. Anyone who burned time assessing their ASA fleet skipped the "not affected" list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixed releases:&lt;/strong&gt; each FMC train got its own first fixed release, and the two CVEs' fixed lists were not identical. That is exactly the situation the Software Checker exists for: check both CVEs at once and get one release that clears both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility:&lt;/strong&gt; because FMC may run ahead of managed FTDs, this was patchable as a manager-only change. Teams that knew the compatibility rule shipped it fast; teams that assumed "firewall upgrade" scoped it like a fleet project and lost weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploitation:&lt;/strong&gt; on March 18, Cisco updated the advisory to note active exploitation, and on March 19, CISA added CVE-2026-20131 to the Known Exploited Vulnerabilities catalog with a federal remediation deadline attached. An advisory that was "urgent" on March 4 became "this weekend, emergency change" two weeks later. Only teams re-checking the advisory and KEV caught the escalation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist: advisory to change request
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Check CISA KEV first. If the CVE is listed, exploitation is confirmed and your timeline is now measured in days. Note the KEV due date in the ticket.&lt;/li&gt;
&lt;li&gt;Read the advisory's affected products section against your actual inventory, including the not-affected list. Stop here if you are not affected, and write down why.&lt;/li&gt;
&lt;li&gt;Record the SIR, the CVSS score, and the exploitation status line, with the date you checked, since all three can change after publication.&lt;/li&gt;
&lt;li&gt;Check for workarounds. If one exists, note it as a bridge option only; if the advisory says none exist, say so explicitly in the change request to justify urgency.&lt;/li&gt;
&lt;li&gt;Run the Cisco Software Checker for your product and current release to get the release that clears all open advisories, not just this one. That is your floor.&lt;/li&gt;
&lt;li&gt;Look up the suggested (gold star) release on the download portal or compatibility guide. If it meets the floor, it is your target. Record the star position and date.&lt;/li&gt;
&lt;li&gt;Firewall estates: verify the FMC/FTD pairing in the compatibility guide. If a device fix requires a newer FMC, the FMC upgrade is a prerequisite task in the same change request. FMC upgrades first, always.&lt;/li&gt;
&lt;li&gt;ISE estates: confirm the required patch level, remember patches are cumulative, and treat any hot patch as temporary with a follow-up task for the cumulative patch.&lt;/li&gt;
&lt;li&gt;Write the change request with the floor, the target, the source links, and the observed dates, so the decision is reconstructible at audit time.&lt;/li&gt;
&lt;li&gt;Set a re-check reminder for seven days. Advisories get updated, KEV grows, and stars move.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Cisco gives you everything you need: a structured advisory, a checker tool, a curated release recommendation, and detailed compatibility documentation. The failure mode is treating the advisory as the whole story. It is one of four documents, and the change request writes itself once you read all four in the right order: KEV, advisory, Software Checker, compatibility guide. Teams that internalize that order patch faster, and they never again schedule an FTD upgrade their FMC cannot manage.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;About the author:&lt;/strong&gt; Adam Lewandowski is a network and security engineer (CompTIA Security+, CCNA, VMware VCP-DCV) working with Cisco FTD/FMC/ISE, Windows Server/MECM, and VMware environments. Connect on &lt;a href="https://www.linkedin.com/in/adam-lewandowski-157342234/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I write The Patch Window, a free 5-minute weekly brief on which enterprise patches can't wait - Cisco, Windows Server, VMware. Subscribe: &lt;a href="https://the-patch-window.beehiiv.com" rel="noopener noreferrer"&gt;https://the-patch-window.beehiiv.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cisco</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>CISA KEV catalog: a working sysadmin's guide to actually using it</title>
      <dc:creator>Adam Lewandowski</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:24:51 +0000</pubDate>
      <link>https://dev.to/adam_lewandowski_59674796/cisa-kev-catalog-a-working-sysadmins-guide-to-actually-using-it-2o15</link>
      <guid>https://dev.to/adam_lewandowski_59674796/cisa-kev-catalog-a-working-sysadmins-guide-to-actually-using-it-2o15</guid>
      <description>&lt;p&gt;Most enterprise teams know the CISA Known Exploited Vulnerabilities catalog the same way they know the weather: a headline scrolls past ("CISA adds three vulnerabilities to KEV catalog"), someone forwards it, and everyone nods. That is a waste of the single most operationally useful list in vulnerability management. The KEV is small, machine-readable, updated near-daily, and every entry on it has one property your scanner output cannot give you: a real attacker has already used it against a real network.&lt;/p&gt;

&lt;p&gt;This is a guide to the catalog itself: what it promises, what it doesn't, how the feeds are structured, how to map entries to your own estate without fooling yourself, and how to combine it with EPSS and vendor advisories into a defensible patch-ordering rule. Everything here is verified against the live feed and CISA's own pages as of late July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the KEV is, and what it is not
&lt;/h2&gt;

&lt;p&gt;CISA describes the KEV as &lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities" rel="noopener noreferrer"&gt;the authoritative source of vulnerabilities that have been exploited in the wild&lt;/a&gt;. Entry is gated by &lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities" rel="noopener noreferrer"&gt;three criteria&lt;/a&gt;, all of which must hold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The vulnerability has an assigned CVE ID.&lt;/li&gt;
&lt;li&gt;There is reliable evidence of active exploitation in the wild.&lt;/li&gt;
&lt;li&gt;There is a clear remediation action, such as a vendor-provided update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read those criteria as exclusions and the catalog's real shape appears. No CVE assigned yet? Not in the KEV, even if exploitation is rampant. Exploitation reported but CISA's evidence bar not met? Not in the KEV. Actively exploited but no fix or mitigation exists? Not in the KEV. The catalog is a curated floor, not a census. As of the 2026.07.29 release the feed contains 1,656 entries, against an ecosystem publishing tens of thousands of CVEs per year. Absence from the KEV is not evidence of safety; presence is close to proof of danger. That asymmetry is the whole point, and it is why the correct reading of the list is "everything on here is urgent" rather than "everything urgent is on here."&lt;/p&gt;

&lt;p&gt;The distribution is also worth knowing before you build anything on top of it. Microsoft dominates with 382 entries, followed by Cisco (95), Apple (93), Adobe (80), Google (72), then the appliance vendors that have defined the last few years of mass exploitation: Ivanti (35), Fortinet (29). 332 entries, about one in five, carry a known-ransomware-campaign flag. If you run a typical enterprise stack, a large slice of this catalog is aimed directly at you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The due dates: BOD 22-01 is gone, and its replacement raised the stakes
&lt;/h2&gt;

&lt;p&gt;The KEV was created by Binding Operational Directive 22-01 in November 2021, which required US federal civilian agencies to remediate each listed CVE by a per-entry due date. If your mental model of KEV deadlines still comes from that era ("two or three weeks for new entries"), it is out of date. On June 10, 2026, CISA issued &lt;a href="https://www.cisa.gov/news-events/directives/bod-26-04-prioritizing-security-updates-based-risk" rel="noopener noreferrer"&gt;BOD 26-04: Prioritizing Security Updates Based on Risk&lt;/a&gt;, which supersedes and revokes BOD 22-01 outright. The KEV catalog itself continues, with the same inclusion criteria, but due dates are now set by a four-variable risk model: public exposure of the asset, KEV status, exploit automatability, and technical impact (partial versus total control). Remediation timelines run from three calendar days, with mandatory forensic triage for the worst combinations, down to fix-on-next-upgrade for vulnerabilities that trip none of the variables. CISA's &lt;a href="https://github.com/cisagov/vulnrichment" rel="noopener noreferrer"&gt;Vulnrichment program&lt;/a&gt; publishes answers for three of the four variables for every CVE; only asset exposure is yours to determine.&lt;/p&gt;

&lt;p&gt;You can see the regime change directly in the feed. Of the 39 entries added since June 10, 34 carry a three-day deadline and the rest fourteen days. Under the old directive, three weeks was routine. A concrete example from the day I pulled the data: CVE-2026-20316, a hard-coded password vulnerability in Cisco Secure Firewall Management Center, was added on 2026-07-29 with a due date of 2026-08-01. Three days, for an FMC bug, over a weekend.&lt;/p&gt;

&lt;p&gt;Why should you care if you are not a federal agency? Two reasons. First, the due dates are free triage: they encode CISA's judgment about exploitation velocity and impact, computed by people with incident data you will never see. When a directive built on that data says "three days," treating your own internet-facing FMC as a 30-day ticket is a choice you should at least make consciously. Second, the KEV increasingly shows up in places with teeth: cyber-insurance questionnaires, audit frameworks, and customer security reviews commonly ask how you track and remediate KEV-listed vulnerabilities. "We monitor the catalog and apply the federal timelines to exposed assets" is a clean, defensible answer that costs very little to make true.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feeds: skip the webpage, consume the data
&lt;/h2&gt;

&lt;p&gt;The browsable catalog page is fine for humans, but the operational interfaces are the feeds, all free and unauthenticated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON: &lt;code&gt;https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CSV: &lt;code&gt;https://www.cisa.gov/sites/default/files/csv/known_exploited_vulnerabilities.csv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;JSON schema: &lt;code&gt;https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities_schema.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JSON document has a small envelope (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;catalogVersion&lt;/code&gt;, &lt;code&gt;dateReleased&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;) and a &lt;code&gt;vulnerabilities&lt;/code&gt; array. Each entry carries: &lt;code&gt;cveID&lt;/code&gt;, &lt;code&gt;vendorProject&lt;/code&gt;, &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;vulnerabilityName&lt;/code&gt;, &lt;code&gt;dateAdded&lt;/code&gt;, &lt;code&gt;shortDescription&lt;/code&gt;, &lt;code&gt;requiredAction&lt;/code&gt;, &lt;code&gt;dueDate&lt;/code&gt;, &lt;code&gt;knownRansomwareCampaignUse&lt;/code&gt; (the string &lt;code&gt;Known&lt;/code&gt; or &lt;code&gt;Unknown&lt;/code&gt;), &lt;code&gt;notes&lt;/code&gt; (semicolon-separated advisory URLs), and &lt;code&gt;cwes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a minimal watcher in plain Node.js (18 or later, no dependencies) that pulls the feed and prints recent additions matching your vendors. I ran this exact script against the live feed while writing this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// kev-watch.mjs - Node 18+ (native fetch, top-level await)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cisco&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vmware&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;windows server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solarwinds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DAYS_BACK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FEED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`KEV feed returned HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;catalogVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vulnerabilities&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;DAYS_BACK&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vulnerabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;haystack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vendorProject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateAdded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;haystack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`KEV &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;catalogVersion&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; total entries)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; matches added in the last &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DAYS_BACK&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; days:\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;knownRansomwareCampaignUse&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Known&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;  &amp;lt;&amp;lt; RANSOMWARE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`due &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cveID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vendorProject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output from 2026-07-29:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KEV 2026.07.29 (1656 total entries)
2 matches added in the last 30 days:

due 2026-07-16  CVE-2008-4128  Cisco: IOS
due 2026-08-01  CVE-2026-20316  Cisco: Secure Firewall Management Center (FMC)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule that however you like and pipe it to email or chat. If you want the fuller version of this idea, a daily brief that also folds in Microsoft's MSRC feed and checks your firmware versions for compliance drift, I walked through that build in a separate article: &lt;a href="https://dev.to/adam_lewandowski_59674796/build-a-5-minute-morning-security-brief-cisa-kev-microsoft-msrc-to-a-compliance-drift-report-in-3ggd"&gt;Build a 5-minute morning security brief&lt;/a&gt;. This piece stays on the catalog itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping KEV to your estate: the false-negative trap
&lt;/h2&gt;

&lt;p&gt;Keyword matching on &lt;code&gt;vendorProject&lt;/code&gt; and &lt;code&gt;product&lt;/code&gt; is where every homegrown KEV consumer starts, and where most of them quietly rot. The fields are human-written strings, not a controlled vocabulary, and they will betray a naive filter in at least three ways.&lt;/p&gt;

&lt;p&gt;First, vendors change names. VMware entries added before the Broadcom acquisition sit under &lt;code&gt;vendorProject: "VMware"&lt;/code&gt;; newer ones, including vCenter Server and Aria Operations entries, sit under &lt;code&gt;"Broadcom"&lt;/code&gt;. A filter that matches only &lt;code&gt;vmware&lt;/code&gt; silently misses new vCenter KEVs. The same class of problem applies to renamed products: the Cisco FMC entry above helpfully notes "formerly known as Firepower Management Center" in its description, but nothing forces future entries to do that.&lt;/p&gt;

&lt;p&gt;Second, vague product strings. Entries like Zyxel's &lt;code&gt;product: "Multiple Products"&lt;/code&gt; match no sensible product keyword. If you filter on product names alone, these vanish.&lt;/p&gt;

&lt;p&gt;Third, your inventory lies. Matching the feed is the easy half; the hard half is knowing that the thing in the entry actually exists on your network, including the appliance someone racked in 2019 that never made it into the CMDB.&lt;/p&gt;

&lt;p&gt;Practical rules that keep the approach honest: match on the concatenation of vendor and product, not product alone; include acquirer names alongside legacy vendor names (VMware and Broadcom, SolarWinds and N-able, and so on); treat your keyword list as config that gets reviewed when the catalog surprises you; and once a week, eyeball the full list of new additions regardless of matches, which at current volume (172 additions so far in 2026) is a two-minute skim. The keyword filter is a tripwire, not a coverage guarantee. If you need a guarantee, that is what a scanner with CPE-based matching is for, and even those disagree with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  A ranking rule: KEV, then EPSS, then the vendor advisory
&lt;/h2&gt;

&lt;p&gt;The KEV answers exactly one question: is this being exploited? It says nothing about how likely exploitation of everything else is, and nothing about how bad exploitation would be for you. So combine three signals, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KEV membership is a gate, not a score.&lt;/strong&gt; Anything in the KEV that exists in your estate goes to the front of the queue, full stop. Within that set, order by &lt;code&gt;dueDate&lt;/code&gt; and put &lt;code&gt;knownRansomwareCampaignUse: "Known"&lt;/code&gt; entries first. Exposure decides the calendar: for internet-facing assets, treat CISA's due date as your due date; for internal-only assets, the next scheduled window is usually defensible. That is exactly the spirit of BOD 26-04's exposure variable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;EPSS ranks everything the KEV is silent on.&lt;/strong&gt; The &lt;a href="https://www.first.org/epss/" rel="noopener noreferrer"&gt;Exploit Prediction Scoring System&lt;/a&gt; from FIRST estimates the probability a CVE will be exploited in the next 30 days, and the &lt;a href="https://api.first.org/data/v1/epss" rel="noopener noreferrer"&gt;free API&lt;/a&gt; is one GET request per CVE. It complements the KEV precisely because it is predictive where the KEV is confirmatory. Know its blind spot at the seam, though: brand-new KEV entries often have no meaningful EPSS score yet. On the day CVE-2026-20316 hit the catalog, EPSS had no score for it at all, while the 2008-vintage Cisco IOS entry scored 0.33, higher than 98 percent of all CVEs. KEV first, EPSS second is not just a slogan; the ordering is load-bearing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The vendor advisory decides what you actually do.&lt;/strong&gt; Every KEV entry links its advisory in the &lt;code&gt;notes&lt;/code&gt; field. The advisory tells you the fixed version, whether a workaround exists, and whether your specific configuration is affected. In a Cisco shop, the KEV tells you FMC is under active attack; only the Cisco advisory tells you whether your release train has a fixed build and what the mitigation is until your window opens.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a one-line policy: KEV match on an exposed asset means patch now on CISA's clock; KEV match internal-only means next window, ransomware-flagged first; no KEV match means rank by EPSS and severity and handle in normal cadence, with the vendor advisory as the source of truth for the fix itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;p&gt;The KEV is the best free signal in vulnerability management, and it will still fail you in specific, predictable ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It lags by design.&lt;/strong&gt; Evidence of in-the-wild exploitation has to exist and reach CISA's bar before an entry appears. For a zero-day being exploited before the patch exists, criterion three blocks listing until a fix ships, and by the time the entry lands you may be days behind attackers. The KEV can never be your early-warning system for your crown-jewel products; vendor PSIRT feeds and emergency advisories own that window. It also lags in the other direction: that Cisco IOS entry added in July 2026 is CVE-2008-4128, an eighteen-year-old vulnerability. Exploitation evidence arrives when it arrives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It carries no severity or applicability context.&lt;/strong&gt; Entries are not scored, and the due date reflects CISA's risk model for federal networks, not your exposure. Two entries with identical due dates can differ wildly in what they mean for you. The catalog cannot know whether you have the product, whether it is reachable, or whether a compensating control already blocks the exploitation path. It is an input to prioritization, not a prioritization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some entries are dead weight, and rarely one is just dead.&lt;/strong&gt; A catalog that only grows accumulates entries for products that went end-of-life years ago; those still matter if the EOL gear is still racked, but they clutter naive dashboards. And the evidence bar, while high, is not infallible: CISA &lt;a href="https://www.cisa.gov/news-events/alerts/2023/12/01/cisa-removes-one-known-exploited-vulnerability-catalog" rel="noopener noreferrer"&gt;removed CVE-2022-28958 from the catalog&lt;/a&gt; in December 2023 after the D-Link "vulnerability" was found not to exist at all and the CVE was rejected. Removals are rare enough to be news, which is itself a good sign, but sync deletions, not just additions, if you mirror the feed into anything downstream.&lt;/p&gt;

&lt;p&gt;None of this argues against the catalog. It argues against treating it as a complete risk picture rather than what it is: a short, high-confidence list of proven attacker behavior, refreshed almost daily, in a format a sysadmin can consume with thirty lines of code. Most organizations have not done even that. Be the one that has.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adam Lewandowski is a network and security engineer (CompTIA Security+, CCNA, VMware VCP-DCV) working with Cisco FTD/FMC/ISE, Windows Server, MECM, and VMware environments. Connect on &lt;a href="https://www.linkedin.com/in/adam-lewandowski-157342234/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write The Patch Window, a free 5-minute weekly brief on which enterprise patches can't wait - Cisco, Windows Server, VMware. Subscribe: &lt;a href="https://the-patch-window.beehiiv.com" rel="noopener noreferrer"&gt;https://the-patch-window.beehiiv.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>sysadmin</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop Sorting the Patch Queue by CVSS: Exploitation Evidence Should Decide What You Patch First</title>
      <dc:creator>Adam Lewandowski</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:17:03 +0000</pubDate>
      <link>https://dev.to/adam_lewandowski_59674796/stop-sorting-the-patch-queue-by-cvss-exploitation-evidence-should-decide-what-you-patch-first-455n</link>
      <guid>https://dev.to/adam_lewandowski_59674796/stop-sorting-the-patch-queue-by-cvss-exploitation-evidence-should-decide-what-you-patch-first-455n</guid>
      <description>&lt;p&gt;Most enterprise patch programs still run on a rule written a decade ago: criticals and highs inside 30 days, mediums inside 90, lows when someone gets around to it. The sort key for all of it is a CVSS base score. I think that rule is now actively harmful, and I'll go further: any patch-prioritization process that starts with severity instead of exploitation evidence is optimizing the wrong variable, and 2026 is the year the data made that undeniable.&lt;/p&gt;

&lt;p&gt;CVSS answers one question: how bad would it be if this vulnerability were exploited? It says nothing about whether it will be. Those are different questions with wildly different answers, and the gap between them is where patch programs go to die. NVD's own documentation is blunt about this: &lt;a href="https://nvd.nist.gov/vuln-metrics/cvss" rel="noopener noreferrer"&gt;CVSS is not a measure of risk&lt;/a&gt;. We took a technical severity metric and quietly promoted it into a scheduling algorithm. Nobody who publishes the standard ever asked us to do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic stopped working
&lt;/h2&gt;

&lt;p&gt;Start with volume. The CVE ecosystem published over 48,000 new CVEs in 2025, and the pace is still accelerating: NIST reports that &lt;a href="https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth" rel="noopener noreferrer"&gt;submissions grew 263% between 2020 and 2025&lt;/a&gt;, with early-2026 volume running about a third higher again. Zafran's analysis of the 2025 numbers puts it at &lt;a href="https://www.zafran.io/resources/the-2025-spike-in-vulnerabilities-isnt-the-full-story" rel="noopener noreferrer"&gt;a record year even after you control for reclassification noise&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now the killer stat. Across the entire history of the CVE program, only about 5% of published vulnerabilities are ever observed being exploited in the wild. That's not vendor marketing; it's the finding of the &lt;a href="https://www.tenable.com/analyst-research/a-visual-exploration-of-exploitation-in-the-wild" rel="noopener noreferrer"&gt;Cyentia Institute and FIRST's joint study of exploitation data&lt;/a&gt;, the people who run EPSS. Meanwhile, more than half of all scored CVEs land at CVSS 7.0 or above.&lt;/p&gt;

&lt;p&gt;Put those together and the math is brutal. FIRST publishes &lt;a href="https://www.first.org/epss/model" rel="noopener noreferrer"&gt;a direct comparison of remediation strategies&lt;/a&gt;: a "patch everything CVSS 7+" policy forces you to remediate 57.4% of all scored CVEs to catch 82.2% of the ones that get exploited, and only about 4% of what you patched urgently ever sees exploitation activity. Ninety-six percent of your emergency effort is spent on vulnerabilities attackers never touch. An EPSS-driven threshold on the same data catches a comparable share of real exploitation for a fraction of the effort.&lt;/p&gt;

&lt;p&gt;In an enterprise, that wasted effort isn't abstract. Change windows are the scarcest resource in operations. Anyone who has scheduled an FTD upgrade through an HA pair, or coordinated an ISE patch across a deployment that authenticates every switchport in the building, knows a maintenance window is a negotiated, finite, politically expensive thing. Spending one on a CVSS 9.1 that has no exploit, no proof of concept, and no exposure, while a "7.2 with active exploitation" waits its turn in the 30-day queue, is malpractice by spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026 is the year the bill came due
&lt;/h2&gt;

&lt;p&gt;If severity-sorted patching worked, we'd see it in breach data. We see the opposite.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.verizon.com/business/resources/reports/dbir/" rel="noopener noreferrer"&gt;Verizon 2026 DBIR&lt;/a&gt; reported that exploitation of vulnerabilities is now the number one initial access vector in breaches, at 31% of initial access, up from 20% the year before. That's &lt;a href="https://watchtowr.com/resources/verizon-dbir-2026-vulnerability-exploitation/" rel="noopener noreferrer"&gt;the first time in the report's 19-year history&lt;/a&gt; that exploitation has beaten credential abuse and phishing for the top spot. The same report's remediation numbers are worse: organizations took a &lt;a href="https://www.helpnetsecurity.com/2026/05/20/verizon-2026-dbir-findings/" rel="noopener noreferrer"&gt;median of 43 days to remediate known-exploited vulnerabilities, up from 32, and only 26% of KEVs were fully remediated at all&lt;/a&gt;, down from 38%.&lt;/p&gt;

&lt;p&gt;Read that again. The industry is getting slower at patching the small set of vulnerabilities we know attackers are using, while drowning in a queue sorted by a number that doesn't predict attacker behavior.&lt;/p&gt;

&lt;p&gt;The attacker side of the ledger is moving the other direction. VulnCheck's &lt;a href="https://www.vulncheck.com/blog/state-of-exploitation-2026" rel="noopener noreferrer"&gt;2026 exploit intelligence reporting&lt;/a&gt; found that 28.96% of vulnerabilities known to be exploited in 2025 were exploited on or before the day their CVE was published, up from 23.6% in 2024. Google's Mandiant team measured &lt;a href="https://hadrian.io/blog/understanding-the-new-negative-time-to-exploit" rel="noopener noreferrer"&gt;average time-to-exploit going negative&lt;/a&gt; across vulnerabilities disclosed in 2024: on average, exploitation now begins before disclosure. Against that reality, a tiered SLA of "criticals in 30 days" isn't a control. It's a ritual.&lt;/p&gt;

&lt;p&gt;Even the institution at the center of the scoring pipeline has conceded the point. In April 2026, NIST announced that the NVD &lt;a href="https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth" rel="noopener noreferrer"&gt;will no longer attempt to enrich every CVE&lt;/a&gt;. It now prioritizes enrichment for CVEs in CISA's Known Exploited Vulnerabilities catalog (targeting one business day) and for critical federal software; everything else is marked lowest priority. The organization that assigns the scores triages its own workload by exploitation evidence. Your patch program should take the hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steelman: what CVSS defenders get right
&lt;/h2&gt;

&lt;p&gt;The strongest objection to evidence-based prioritization is that evidence is a lagging indicator. By the time a CVE lands in &lt;a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog" rel="noopener noreferrer"&gt;CISA's KEV catalog&lt;/a&gt;, someone has already been compromised with it, and if a third of exploitation starts at or before disclosure, waiting for confirmation means you're structurally late. That's true, and it's the best argument in the room.&lt;/p&gt;

&lt;p&gt;But it cuts against CVSS too, and harder. A base score computed on day zero can't distinguish the 9.8 that will be mass-exploited within 48 hours from the 9.8 that will never be weaponized; both sit identically in your queue. Exploitation evidence at least converges on the truth as signals arrive: vendor advisories confirming in-the-wild activity, public proof-of-concept code, EPSS movement. &lt;a href="https://research.empiricalsecurity.com/research/introducing-epss-version-4" rel="noopener noreferrer"&gt;EPSS v4&lt;/a&gt;, released in March 2025, exists precisely to close the lag problem, producing a daily-updated probability of exploitation for every published CVE before confirmation ever arrives. It's imperfect and probabilistic, and it still beats a static severity number at the only question that matters for scheduling: what happens next.&lt;/p&gt;

&lt;p&gt;Two more objections deserve honest answers. First, compliance: plenty of audit frameworks and customer contracts are written in CVSS SLAs, and you can't unilaterally ignore them. Fine. Report in CVSS, prioritize by evidence; the two are not in conflict, because the evidence-driven queue will comfortably beat a severity SLA on the vulnerabilities auditors actually care about. Second, EPSS and KEV know nothing about your environment. Also true. No global score does, including CVSS. Exposure and asset criticality are your inputs, and they were always your job.&lt;/p&gt;

&lt;p&gt;Where CVSS genuinely earns its keep is as an impact term. Once exploitation likelihood puts a vulnerability on the urgent list, severity helps decide whether the response is "next window" or "wake people up." That's a demotion from sort key to tie-breaker, and it's the right job for the metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to run instead
&lt;/h2&gt;

&lt;p&gt;Here's the structure I'd defend for a typical enterprise stack, the Cisco FTD/FMC/ISE, Windows Server, MECM, VMware world I work in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 0, out-of-band, days not weeks:&lt;/strong&gt; anything on your stack that appears in the KEV catalog, in a vendor advisory confirming active exploitation, or that is internet-facing with a high EPSS score. This list is small by construction. The entire KEV catalog held 1,655 entries as of late July 2026, accumulated over five years, across every vendor on earth. Filtered to one organization's products, a KEV match is a rare, unambiguous, drop-everything signal. Federal agencies are already required to work this way under &lt;a href="https://www.cisa.gov/news-events/directives/bod-22-01-reducing-significant-risk-known-exploited-vulnerabilities" rel="noopener noreferrer"&gt;BOD 22-01&lt;/a&gt;; the due dates ship free with the catalog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1, current cycle, pulled forward:&lt;/strong&gt; elevated EPSS, or public PoC plus reachable exposure. Watch the trajectory, not the snapshot; a score jumping an order of magnitude overnight is telling you something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2, the monthly wave:&lt;/strong&gt; everything else rides your normal cadence. This is where the long tail gets handled, and it's why evidence-based prioritization isn't an excuse to stop routine patching. MECM doesn't care about your opinions on scoring; a healthy monthly baseline quietly remediates most of the 95% that never gets exploited, without ever consuming an emergency window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special case: edge devices.&lt;/strong&gt; Firewalls, VPN concentrators, anything with a management plane near the internet. The last two DBIRs document attackers converging on exactly this class, and these are the boxes where "we'll patch next month" has repeatedly meant "we were exploited last week." If an edge product you run lands on KEV, that's a Tier 0 event with a same-week window, HA pair or not.&lt;/p&gt;

&lt;p&gt;The tooling cost of all this is close to zero, which removes the last excuse. The KEV catalog is a public JSON feed; EPSS scores are a free daily CSV. I run a small script each morning that diffs the KEV feed against the product list I'm responsible for and flags anything new, and building it took less time than a single well-attended CAB meeting. You do not need a risk-based vulnerability management platform to start, although the good ones are essentially selling this exact join at scale.&lt;/p&gt;

&lt;p&gt;Severity tells you how hard the punch would land. Evidence tells you a punch is coming. Only one of those should decide what your team does this weekend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write The Patch Window, a free 5-minute weekly brief on which enterprise patches can't wait - Cisco, Windows Server, VMware. Subscribe: &lt;a href="https://the-patch-window.beehiiv.com" rel="noopener noreferrer"&gt;https://the-patch-window.beehiiv.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adam Lewandowski is a network and security engineer (CompTIA Security+, CCNA, CCNP, VMware VCP-DCV) with several years of hands-on enterprise operations across Cisco FTD/FMC/ISE, Windows Server, MECM, and VMware. He builds automation that takes the repetitive judgment calls out of security operations — without taking the humans out of the loop. Find him on &lt;a href="https://www.linkedin.com/in/adam-lewandowski-157342234/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>infosec</category>
    </item>
    <item>
      <title>Build a 5-Minute Morning Security Brief: CISA KEV + Microsoft MSRC to a Compliance-Drift Report in Node.js</title>
      <dc:creator>Adam Lewandowski</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:15:59 +0000</pubDate>
      <link>https://dev.to/adam_lewandowski_59674796/build-a-5-minute-morning-security-brief-cisa-kev-microsoft-msrc-to-a-compliance-drift-report-in-3ggd</link>
      <guid>https://dev.to/adam_lewandowski_59674796/build-a-5-minute-morning-security-brief-cisa-kev-microsoft-msrc-to-a-compliance-drift-report-in-3ggd</guid>
      <description>&lt;p&gt;If you run a Cisco/Windows/VMware estate, your morning vulnerability check probably looks like mine used to: open the CISA KEV catalog, open the MSRC update guide, open Cisco's software download pages to see if the gold-star release moved, skim a couple of news feeds, and try to remember what version of FTD is actually deployed. Twenty minutes of tab-juggling, most of it re-reading things that didn't change overnight.&lt;/p&gt;

&lt;p&gt;This is a tutorial for automating that into a single dated HTML report that lands before you sit down. The tool is called &lt;strong&gt;sec-watch&lt;/strong&gt;. It's around 600 lines of plain Node.js, no npm dependencies, and it answers three questions every morning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is anything in my stack being actively exploited?&lt;/strong&gt; (CISA Known Exploited Vulnerabilities catalog)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What did the latest Patch Tuesday drop on my Windows Server and Configuration Manager estate?&lt;/strong&gt; (Microsoft MSRC CVRF feed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Am I drifting off Cisco's recommended releases?&lt;/strong&gt; (gold-star compliance for FTD, FMC, ISE, ASA, Secure Client)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two of those come from clean, free, no-auth JSON feeds. The third does not, and the workaround for that is half the reason this article exists. There's also a scheduling trick at the end that I haven't seen used much: the script signals "action needed" to the scheduler through its exit code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config.mjs (your estate)      targets.json (last-known Cisco targets)
        │                              │
        ▼                              ▼
index.mjs ──▶ kev.mjs ────┐
          ──▶ msrc.mjs ───┼──▶ compliance.mjs ──▶ report.mjs ──▶ reports/YYYY-MM-DD.html
          ──▶ intel.json ─┘                                      reports/latest.html
                                                        └──▶ exit code 0 or 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is a plain &lt;code&gt;.mjs&lt;/code&gt; module. &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;AbortSignal.timeout&lt;/code&gt; are built into Node 20+, so &lt;code&gt;package.json&lt;/code&gt; is just metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sec-watch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node index.mjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node index.mjs --open"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=20"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Describe your estate in one config file
&lt;/h2&gt;

&lt;p&gt;Everything downstream keys off a single &lt;code&gt;config.mjs&lt;/code&gt;. Each tracked product gets a &lt;code&gt;current&lt;/code&gt; version (what you actually run), plus keywords used to match it against the two feeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;lookbackDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// how far back "new" vulns count&lt;/span&gt;
  &lt;span class="na"&gt;kevTrackedDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// actionable KEV window for tracked products&lt;/span&gt;

  &lt;span class="na"&gt;tracked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ftd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cisco Secure Firewall Threat Defense (FTD)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cisco&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;goldStar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FILL ME e.g. 7.4.2.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;kevKeywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firepower threat defense&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secure firewall threat defense&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ftd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;winsrv2022&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Windows Server 2022&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Microsoft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;goldStar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FILL ME e.g. latest CU 2026-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;msrcKeywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Windows Server 2022&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esxi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VMware ESXi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;VMware&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;goldStar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FILL ME e.g. 8.0 U3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;kevKeywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esxi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...FMC, ISE, ASA, Secure Client, Server 2019/2025, MECM, SolarWinds, vCenter&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="c1"&gt;// Vendors you don't version-track but want KEV visibility on&lt;/span&gt;
  &lt;span class="na"&gt;watchKeywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ios xe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fortinet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pan-os&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;citrix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;netscaler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ivanti&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exchange server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;veeam&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vcenter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solarwinds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manageengine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...the full list in my config is ~60 entries&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two deliberate choices here. First, &lt;code&gt;kevKeywords&lt;/code&gt; and &lt;code&gt;msrcKeywords&lt;/code&gt; are separate, because the two feeds name products differently (KEV says "Firepower Threat Defense", MSRC product names are long strings like "Windows Server 2022 (Server Core installation)"). Second, &lt;code&gt;watchKeywords&lt;/code&gt; casts a wide net over adjacent infrastructure. You don't run FortiGate, but if a pre-auth RCE on a competing firewall goes into KEV, you want to know, because the same attacker attention tends to rotate through the whole edge-device category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Pull the KEV catalog
&lt;/h2&gt;

&lt;p&gt;CISA publishes the entire Known Exploited Vulnerabilities catalog as one JSON file. No API key, no rate-limit dance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEV_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchKev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KEV_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`KEV fetch &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vulnerabilities&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matching is a lowercase substring check across the fields KEV gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vendorProject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vulnerabilityName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortDescription&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is what happens after matching. Tracked products get split into a recent actionable window and an all-time count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recentCutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kevTrackedDays&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackedRecent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackedUniq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateAdded&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;recentCutoff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackedOlder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trackedUniq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;trackedRecent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother? Because ASA and ISE have KEV entries going back years. If you're current on patches, those are history, not work. Showing "2 new in the last 90 days, plus 11 older entries you should already have covered" keeps the report honest without drowning the signal. Watch-list vendors get an even tighter filter: only entries added within &lt;code&gt;lookbackDays&lt;/code&gt; (3 days) appear at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Parse Patch Tuesday from the MSRC CVRF feed
&lt;/h2&gt;

&lt;p&gt;Microsoft's CVRF API is also free and unauthenticated. You list the monthly documents, grab the newest, and walk its vulnerability array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UPDATES_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.msrc.microsoft.com/cvrf/v3.0/updates&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-obvious part is that severity and exploitation status live in a &lt;code&gt;Threats&lt;/code&gt; array, tagged by numeric type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Threat Type per MSRC CVRF: 0=Impact, 1=Exploit Status, 3=Severity&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;severityOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vuln&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Threats&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each CVE lists affected products by ID, so you build an ID-to-name map from the document's &lt;code&gt;ProductTree&lt;/code&gt;, then match names against your &lt;code&gt;msrcKeywords&lt;/code&gt;. A monthly document covers hundreds of CVEs across everything Microsoft ships; after filtering to Windows Server and Configuration Manager, a typical month leaves 40 to 80 relevant entries. Those get sorted so the report reads top-down in priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sort: critical/exploited first, then by CVSS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/critical/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/important/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cvss&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cvss&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That exploited-and-critical-first ordering repeats everywhere in sec-watch. The report renderer applies the same idea to research highlights (&lt;code&gt;(b.exploited - a.exploited) || (sevRank(b.severity) - sevRank(a.severity))&lt;/code&gt;), and rows with known ransomware campaign use get a red row tint. A morning brief you read in five minutes must put the item that ruins your week on line one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Gold-star compliance drift
&lt;/h2&gt;

&lt;p&gt;Cisco marks a "Suggested Release" with a gold star on its download pages. In a typical FTD/ISE shop that star is the de facto patch-level standard: an auditor asks whether you're on it, and change requests reference it. But there is no API for it. The star lives in web pages, and occasionally moves.&lt;/p&gt;

&lt;p&gt;sec-watch handles this with a two-layer design. &lt;code&gt;targets.json&lt;/code&gt; stores the last-known-good target per product, with the source URL and the date it was seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cisco_gold_star"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ftd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"suggested_release"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.6.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.cisco.com/.../threat-defense-compatibility.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"seen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-27"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each run, fresh research results merge into this file, but only when the lookup actually returned a version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mergeAndSaveTargets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webGoldStar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dateStr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cisco_gold_star&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cisco_gold_star&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;webGoldStar&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suggested_release&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suggested_release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cisco_gold_star&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;suggested_release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suggested_release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dateStr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...persist if changed&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a failed lookup degrades to "compare against the last version we verified" instead of losing compliance data. The report notes the fallback so you know the target might be stale.&lt;/p&gt;

&lt;p&gt;Version comparison itself is deliberately dumb. Cisco writes versions as "7.4.2.1", "Version 7.4.2.1", or "3.3 Patch 4" depending on the page, so both sides get normalized before an equality check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/version|release|patch/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;patch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;0-9a-z.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Equal means &lt;code&gt;COMPLIANT&lt;/code&gt;, different means &lt;code&gt;DRIFT&lt;/code&gt;, and an unfilled &lt;code&gt;current&lt;/code&gt; means &lt;code&gt;UNKNOWN_CURRENT&lt;/code&gt;. No greater-than or less-than logic, on purpose: gold-star compliance means being &lt;em&gt;on&lt;/em&gt; the suggested release, and being ahead of it is still a state worth flagging to a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The research step, and the intel.json escape hatch
&lt;/h2&gt;

&lt;p&gt;The gold-star lookup and a "notable new vulnerabilities" section both need live web research. My first version shelled out to an LLM CLI (Claude Code) with web-search tools enabled, prompted it for strict JSON, and parsed the result. It works, and the code is in the repo, but a nested CLI call that browses the web can take minutes and occasionally times out. That's a bad property for the one step of your morning pipeline that runs unattended.&lt;/p&gt;

&lt;p&gt;So the workflow I actually use is the &lt;code&gt;--intel&lt;/code&gt; flag. Any research process, human or AI, writes a plain &lt;code&gt;intel.json&lt;/code&gt; file, and the report renders from that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;USE_INTEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Manual daily workflow: read intel.json populated in-session&lt;/span&gt;
  &lt;span class="c1"&gt;// from live web research. No nested CLI -&amp;gt; no timeouts / contention.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./intel.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;web&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cisco_gold_star&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cisco_gold_star&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highlights&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;analyst_note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;analyst_note&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;web_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each highlight is a structured record: category, product, CVE, severity, an &lt;code&gt;exploited&lt;/code&gt; boolean, a summary, a recommended action, and a real source URL. In practice I do the research in an interactive AI session each morning (with a strict instruction: only items from the last 3 days, every item needs a source URL you actually visited), it writes &lt;code&gt;intel.json&lt;/code&gt;, and then &lt;code&gt;node index.mjs --intel --open&lt;/code&gt; renders everything in a few seconds. The file format is the contract; where the research comes from is swappable. If you want zero AI involvement, run &lt;code&gt;node index.mjs --no-web&lt;/code&gt; and you still get KEV, MSRC, and compliance against the stored targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Schedule it, and make the exit code mean something
&lt;/h2&gt;

&lt;p&gt;The last lines of &lt;code&gt;index.mjs&lt;/code&gt; are my favorite part of the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Exit non-zero if there's drift or an actively-exploited tracked product, so&lt;/span&gt;
&lt;span class="c1"&gt;// a scheduler can flag the run as "action needed".&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actionNeeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;compliance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tracked&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actionNeeded&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 0 means the estate is quiet: no drift, no fresh KEV entries against tracked products. Exit 2 means the report contains something that needs a human. Windows Task Scheduler records the last run result per task, so a glance at the console (or any monitoring that watches task results) distinguishes "ran fine, nothing urgent" from "ran fine, go read the report" without opening the HTML. The same convention plugs straight into cron plus an alerting wrapper, or a CI job that fails visibly on exit 2. Reserving 2 keeps it distinct from exit 1, which the fatal-error handler uses, so crashes and findings never look alike.&lt;/p&gt;

&lt;p&gt;Registration is a short PowerShell script using the native scheduled-task cmdlets. The settings matter more than the trigger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$Settings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-ScheduledTaskSettingsSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StartWhenAvailable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RunOnlyIfNetworkAvailable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionTimeLimit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-TimeSpan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Minutes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DontStopIfGoingOnBatteries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AllowStartIfOnBatteries&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-StartWhenAvailable&lt;/code&gt; is the one people forget: if the machine was asleep at 07:00, the task runs at wake instead of silently skipping the day. &lt;code&gt;-RunOnlyIfNetworkAvailable&lt;/code&gt; avoids a run full of fetch errors on a cold boot. The script also resolves the full path to &lt;code&gt;node.exe&lt;/code&gt; before registering, because Task Scheduler does not reliably inherit your interactive PATH.&lt;/p&gt;

&lt;p&gt;Every run writes both a dated file (&lt;code&gt;reports/2026-05-27.html&lt;/code&gt;) and a stable &lt;code&gt;reports/latest.html&lt;/code&gt;, so you can bookmark one URL and also keep an audit trail of what you knew on a given morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyword matching is crude.&lt;/strong&gt; Substring matching against KEV text can over-match (short keywords like "ftd" hitting unrelated descriptions) and under-match (a vendor renames a product line). Keep keywords specific and skim the raw catalog occasionally to check your filters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gold-star target is only as good as the research.&lt;/strong&gt; There is no Cisco API for the suggested release, and the star sometimes lags or differs between the compatibility matrix and the download portal. The report footer says it plainly: verify against Cisco.com before acting. This tool is a prompt, not an authority.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MSRC coverage is the latest monthly document only.&lt;/strong&gt; Out-of-band releases usually revise the current month's document, but a revision to an older month would be missed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version comparison is string equality.&lt;/strong&gt; "7.6.4" versus "7.6.4.1" is DRIFT even though you might be deliberately ahead. That's a feature for compliance reporting and an annoyance otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It watches feeds, not your boxes.&lt;/strong&gt; The &lt;code&gt;current&lt;/code&gt; versions in config are hand-maintained. If you upgrade FTD and forget to update the config, the report lies to you. Wiring it to pull versions from FMC's API or your CMDB is the obvious next step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KEV and Patch Tuesday are not the whole threat picture.&lt;/strong&gt; Vendor advisories (Cisco PSIRT, VMware VMSA) publish before things reach KEV. The research step partly covers this gap, but a feed-only run has a blind spot measured in days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that stops the tool from doing its actual job: replacing twenty minutes of tab-juggling with five minutes of reading, every morning, with a paper trail. Build the boring version first. The feeds are free, the whole thing is dependency-free Node, and once the report is landing daily you'll know exactly which of the limitations above is worth fixing for your estate.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write The Patch Window, a free 5-minute weekly brief on which enterprise patches can't wait - Cisco, Windows Server, VMware. Subscribe: &lt;a href="https://the-patch-window.beehiiv.com" rel="noopener noreferrer"&gt;https://the-patch-window.beehiiv.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adam Lewandowski is a network and security engineer (CompTIA Security+, CCNA, CCNP, VMware VCP-DCV) with several years of hands-on enterprise operations across Cisco FTD/FMC/ISE, Windows Server, MECM, and VMware. He builds automation that takes the repetitive judgment calls out of security operations — without taking the humans out of the loop. Find him on &lt;a href="https://www.linkedin.com/in/adam-lewandowski-157342234/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>node</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Piping Raw Alerts into an LLM: Building a SOC Triage Agent That Correlates First and Escalates Last</title>
      <dc:creator>Adam Lewandowski</dc:creator>
      <pubDate>Tue, 28 Jul 2026 01:01:36 +0000</pubDate>
      <link>https://dev.to/adam_lewandowski_59674796/stop-piping-raw-alerts-into-an-llm-building-a-soc-triage-agent-that-correlates-first-and-escalates-20pp</link>
      <guid>https://dev.to/adam_lewandowski_59674796/stop-piping-raw-alerts-into-an-llm-building-a-soc-triage-agent-that-correlates-first-and-escalates-20pp</guid>
      <description>&lt;p&gt;Every few weeks another demo lands on my feed: someone pipes a security alert into a large language model, gets back a paragraph that sounds like a SOC analyst, and declares Tier-1 automated. Having spent years on the receiving end of enterprise alert queues — Cisco FTD intrusion events, Defender detections, sign-in risk alerts — I can tell you why those demos fall apart in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alerts in isolation are noise.&lt;/strong&gt; The signal is the &lt;em&gt;story across&lt;/em&gt; alerts: the same actor showing up in a risky Entra ID sign-in, then AWS recon, then a persistence action. An LLM triaging one alert at a time can't see the campaign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost scales with alert volume, and alert volume is enormous.&lt;/strong&gt; If every event costs an API call, your triage bill grows linearly with your noisiest sensor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free-text verdicts aren't actionable.&lt;/strong&gt; "This looks suspicious" doesn't page anyone. You need structured output a downstream system can route on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you haven't worked a queue like this, the ratios are hard to believe. A single FTD intrusion policy tuned one notch too aggressive will emit thousands of IPS events a day, most of them the same few signatures firing on scanner traffic that never reached anything vulnerable. A MECM patch wave lights up endpoint AV with detections on installer behavior that looks exactly like the droppers the heuristics were trained on. In most shops the alert-to-incident ratio is measured in orders of magnitude, not percentages. So the interesting engineering problem was never "can a model write a plausible paragraph about one alert." It's what you do with everything else.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Aegis&lt;/strong&gt;, a working AI SOC analyst, to demonstrate what I think the right shape is: &lt;strong&gt;normalize → correlate → &lt;em&gt;then&lt;/em&gt; decide whether an LLM is even needed → structured verdict → gated containment.&lt;/strong&gt; It ingests synthetic telemetry from four sources (AWS GuardDuty, CloudTrail, Entra ID sign-in logs, Microsoft Defender), stitches 17 raw events into incidents, auto-suppresses the false positives without spending a token, and hands only the genuinely ambiguous or dangerous cases to a Claude tool-use agent that must investigate before ruling.&lt;/p&gt;

&lt;p&gt;The whole thing runs offline with zero dependencies (a deterministic rule engine stands in for the LLM), and upgrades to the live agent the moment an &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; is present. In the demo run, &lt;strong&gt;3 of 4 incidents cost $0.00&lt;/strong&gt;. Only the real attack touches the API.&lt;/p&gt;

&lt;p&gt;This article walks through the architecture and the design decisions, with real code from the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline at a glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ingest  ─▶  correlate  ─▶  AI triage  ─▶  response playbook  ─▶  report
 (4 sources)  (mini-SIEM)   (Claude agent)  (Graph / PS / AWS)   (HTML + console)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-soc-analyst/
├─ run_demo.py                 # one-command entry point
├─ data/                       # synthetic telemetry (one file per source)
├─ src/aegis/
│   ├─ schema.py               # normalized SecurityEvent / Incident model
│   ├─ ingest.py               # source-specific normalizers → common schema
│   ├─ correlate.py            # union-find correlation engine (the mini-SIEM)
│   ├─ enrich.py               # threat-intel + identity enrichment (pluggable)
│   ├─ agent.py                # Claude tool-use agent + heuristic fallback
│   ├─ playbooks.py            # incident → containment actions
│   └─ report.py               # console + self-contained HTML report
└─ powershell/
    ├─ Invoke-EntraContainment.ps1   # Graph API identity containment
    └─ Get-SecureScoreDelta.ps1      # Secure Score posture job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Normalize everything into one schema
&lt;/h2&gt;

&lt;p&gt;GuardDuty findings, CloudTrail records, Entra sign-in logs, and Defender alerts all have wildly different JSON shapes, and worse, different severity vocabularies. GuardDuty gives you a 0–10 float; Microsoft gives you strings like "Informational." If you don't collapse those early, every downstream component grows source-specific branches.&lt;/p&gt;

&lt;p&gt;Aegis flattens everything into one &lt;code&gt;SecurityEvent&lt;/code&gt; dataclass, and severity coercion happens in exactly one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntEnum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;INFO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;LOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;MEDIUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;HIGH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;CRITICAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;from_any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# GuardDuty numeric severity: 0-3.9 low, 4-6.9 medium, 7-8.9 high, 9+ critical.
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CRITICAL&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CRITICAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HIGH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moderate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;informational&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deliberate choice: unknown severity strings fall back to &lt;code&gt;MEDIUM&lt;/code&gt;, not &lt;code&gt;INFO&lt;/code&gt;. If a source starts emitting a vocabulary you didn't anticipate, you want those events &lt;em&gt;above&lt;/em&gt; your triage floor, not silently dropped below it.&lt;/p&gt;

&lt;p&gt;Adding a source is one normalizer function. The CloudTrail normalizer is a good example of encoding operational knowledge at ingest time. It scores write and defense-evasion API calls higher than read-only recon, so the correlation layer naturally weights persistence and tamper actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;high_impact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreateAccessKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;StopLogging&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DeleteTrail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PutUserPolicy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AttachUserPolicy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CreateUser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UpdateAssumeRolePolicy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;recon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GetCallerIdentity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ListUsers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ListBuckets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ListRoles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DescribeInstances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Correlate by actor, not just alert
&lt;/h2&gt;

&lt;p&gt;This is the part most "LLM SOC" demos skip, and it's the part that actually matters. Aegis clusters events with a union-find over shared identity or shared source IP within a sliding time window, then unions overlapping clusters transitively — because attackers pivot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# ordered by time; nothing further is in-window
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_linked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtle bug most first attempts hit: the same human shows up under different identity strings in different control planes. A user's Entra UPN and their AWS IAM CLI user are, to the correlation engine, different principals — unless you teach it otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_same_identity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 'j.okafor@corp.example', 'j.okafor-cli', and 'j.okafor'
&lt;/span&gt;    &lt;span class="c1"&gt;# all refer to the same human — a real attacker pivots across these.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this heuristic naive? Absolutely. It encodes one org's naming convention, and in production you'd back it with an identity graph (Entra ID ↔ AWS SSO mappings). But the architectural point stands: cross-control-plane identity resolution is a first-class correlation feature, not an enrichment afterthought. In the demo, this is exactly what stitches the Tor sign-in → MFA fatigue → AWS recon → &lt;code&gt;CreateAccessKey&lt;/code&gt; persistence → &lt;code&gt;StopLogging&lt;/code&gt; tamper → endpoint loader → mailbox-rule exfil chain into &lt;strong&gt;one P1 incident&lt;/strong&gt; instead of seven mediums scattered across four consoles.&lt;/p&gt;

&lt;p&gt;And this isn't a cloud-only problem. In a typical Cisco-and-Microsoft enterprise, one administrator exists as &lt;code&gt;j.okafor@corp.com&lt;/code&gt; in Entra sign-in logs, &lt;code&gt;CORP\jokafor&lt;/code&gt; in Windows Security events, a bare &lt;code&gt;jokafor&lt;/code&gt; in the ISE live-session table (or only an endpoint MAC address, if the session authenticated via MAB), a local account name in FMC audit records, and &lt;code&gt;CORP.LOCAL\jokafor&lt;/code&gt; to vCenter SSO. Five control planes, five spellings, zero agreement on a canonical key. A correlation engine that treats those strings as five different people will hand you five medium alerts where there is one story. SIEM vendors sell identity resolution as an enrichment add-on; I'd argue it belongs in the correlation key itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Decide whether the LLM is even needed
&lt;/h2&gt;

&lt;p&gt;Here's the cost-control lever that separates a demo from something deployable. Aegis runs &lt;em&gt;tiered triage&lt;/em&gt;: a deterministic pre-filter auto-resolves the obvious cases for free, and only escalates incidents that carry a malicious indicator, a high-signal ATT&amp;amp;CK technique, or genuine ambiguity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_should_escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Incident&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reputations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;enrich&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src_ips&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;has_bad_ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reputation&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;malicious&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suspicious&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reputations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;high_signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mitre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;HIGH_SIGNAL_TECHNIQUES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_bad_ip&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;high_signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="c1"&gt;# No malicious indicators: auto-resolve service accounts and low-severity noise.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;enrich&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_service_account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_severity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="c1"&gt;# Ambiguous medium+ with no clear signal — worth a judgment call.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_severity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MEDIUM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HIGH_SIGNAL_TECHNIQUES&lt;/code&gt; is a short, opinionated set — persistence (&lt;code&gt;T1098.001&lt;/code&gt;), cloud-log tampering (&lt;code&gt;T1562.008&lt;/code&gt;), PowerShell execution (&lt;code&gt;T1059.001&lt;/code&gt;), mailbox-rule exfil (&lt;code&gt;T1114.003&lt;/code&gt;), MFA fatigue (&lt;code&gt;T1621&lt;/code&gt;), Tor proxying (&lt;code&gt;T1090.003&lt;/code&gt;). These are the techniques where a wrong auto-suppress is expensive, so they always get a judgment call.&lt;/p&gt;

&lt;p&gt;The suppressions every shop ends up writing are the same ones this pre-filter encodes: the internal vulnerability scanner tripping IPS signatures on schedule, the EICAR file the AV team drops every quarter, backup and CI service accounts authenticating from datacenter egress at 3 a.m. doing exactly what they're supposed to do. The failure mode is also the same everywhere. Suppression rules written broadly ("ignore this signature," "ignore this subnet") keep matching long after the environment changes, and one day the real thing rides in under an old exception. That's why &lt;code&gt;_should_escalate&lt;/code&gt; is structured the way it is: suppression requires the &lt;em&gt;absence&lt;/em&gt; of malicious indicators plus a positive reason to close (known service account, low severity), while the high-signal technique list is an always-escalate override no suppression can beat. Suppress by evidence, escalate by category — the inverse of how most alert-tuning debt accumulates.&lt;/p&gt;

&lt;p&gt;In the demo run, three incidents (the research scanner, the EICAR test, the CI/CD service account) are closed by the free engine, and the API is invoked only for the actual intrusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: A tool-use agent that must investigate before ruling
&lt;/h2&gt;

&lt;p&gt;When an incident &lt;em&gt;does&lt;/em&gt; escalate, it isn't a single prompt. The Claude agent gets a SOC-analyst system prompt and four tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Threat-intel reputation for a source IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_user_context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is this principal privileged? A service account?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_mitre_technique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolve an ATT&amp;amp;CK ID to name + tactic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;submit_verdict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Strict-schema&lt;/strong&gt; final ruling — the only way to finish&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trick that makes the output dependable is &lt;code&gt;submit_verdict&lt;/code&gt;. It's a tool with &lt;code&gt;"strict": True&lt;/code&gt; and a closed schema; the model can't finish the loop without committing to typed fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submit_verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Submit the final triage verdict. Call exactly once, after investigating.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_true_positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boolean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;High&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maximum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attack_narrative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_true_positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attack_narrative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additionalProperties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No parsing prose, no regexing severity out of a paragraph. The verdict routes straight into playbook selection and the report. The agent loop also records every tool call into a human-readable trace, so the analyst reviewing the verdict can see &lt;em&gt;how&lt;/em&gt; the machine investigated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI agent investigation (via claude-haiku-4-5):
    lookup_ip(185.220.101.47) -&amp;gt; malicious (tor-exit-node, anonymizer, c2-adjacent)
    get_user_context(j.okafor@...) -&amp;gt; standard user
    get_mitre_technique(T1562.008) -&amp;gt; T1562.008 — Impair Defenses: Disable Cloud Logs
    ✓ submit_verdict -&amp;gt; Critical/P1 (TRUE positive)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That trace is not a gimmick. If you're going to let a model influence incident priority, "show your work" is the minimum bar for analyst trust, and for the post-incident review when it gets one wrong.&lt;/p&gt;

&lt;p&gt;Two more production-shaped details in the agent path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail safe, always.&lt;/strong&gt; The public &lt;code&gt;triage()&lt;/code&gt; entry point wraps the live agent in a &lt;code&gt;try/except&lt;/code&gt;; any API error or step-limit overrun falls back to the deterministic engine and annotates the verdict with what happened. One API hiccup never stalls the alert queue mid-shift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is visible, not a mystery.&lt;/strong&gt; Every run tracks token usage per incident and prints an estimated dollar cost, with a cheap default model (Haiku-class) and an env-var override (&lt;code&gt;AEGIS_MODEL&lt;/code&gt;) for deployments that want deeper reasoning on escalated incidents only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Containment that names its control plane
&lt;/h2&gt;

&lt;p&gt;A triage verdict that ends in "investigate further" is a demo. Aegis maps verdicts to concrete containment actions, and every action names the real automation that would carry it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disable compromised account &amp;amp; revoke sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Microsoft Graph (Entra ID)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;automation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pwsh ./powershell/Invoke-EntraContainment.ps1 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-UserPrincipalName &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;upn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -DisableAccount -RevokeSessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Risky/failed MFA sign-in from a malicious IP indicates account &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;takeover; disabling the account and revoking refresh tokens cuts &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active attacker sessions immediately.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;destructive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;destructive=True&lt;/code&gt; flag is load-bearing: every account-disable, session-revoke, MFA-reset, and endpoint-isolation action is gated behind human approval, and the companion PowerShell (&lt;code&gt;Invoke-EntraContainment.ps1&lt;/code&gt;) runs in simulation unless &lt;code&gt;-Execute&lt;/code&gt; is explicitly passed.&lt;/p&gt;

&lt;p&gt;Auto-containment earns trust slowly, and it should. The classic failure is an action keyed on the wrong attribute. Quarantine-by-identity in a NAC deployment like ISE will happily kill every session the account holds, including the endpoints nobody remembered were authenticating with it. Auto-disabling a "compromised" account that turns out to be a service identity takes down every integration that depends on it, usually at the worst possible hour. Aegis encodes one small defense against this class of mistake directly in the playbook layer: when it recommends perimeter blocks, it filters to reputationally-bad IPs only, so the actor's legitimate corporate egress — which the identity linkage correctly pulled &lt;em&gt;into&lt;/em&gt; the incident — never lands on a deny-list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you before you build one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalize-first paid for itself immediately.&lt;/strong&gt; Correlation, enrichment, triage, and reporting share one schema; adding a fifth source is one function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered escalation is the difference between a toy and a bill you can defend.&lt;/strong&gt; The LLM is a scarce senior analyst, not a firehose destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict tool schemas beat prompt-engineering for structured output.&lt;/strong&gt; Forcing the final answer through a closed-schema tool call eliminated an entire class of parsing failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building the heuristic fallback as a real second implementation&lt;/strong&gt; (not a stub) meant the demo works offline &lt;em&gt;and&lt;/em&gt; gave the live agent a benchmark: both engines reason over identical enrichment, so their disagreements are informative.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Honest limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The telemetry is synthetic and small.&lt;/strong&gt; Seventeen hand-crafted events prove the architecture, not the recall. Real environments bring malformed timestamps, delayed log delivery, and volumes where the windowed pairwise correlation pass needs streaming or bucketing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity matching is a naming-convention heuristic.&lt;/strong&gt; Production needs a real identity graph (Entra ↔ AWS SSO ↔ HR system), and it will still be the hardest data-quality problem in the pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The threat-intel and identity enrichment are local lookups.&lt;/strong&gt; They're pluggable by design — swapping in GreyNoise/AbuseIPDB is the intended path — but reputation-driven verdicts are only as good as the feed behind them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM's verdicts haven't been evaluated at scale.&lt;/strong&gt; Before trusting it with suppression decisions, you'd want a labeled incident corpus and a measured false-negative rate. An "it sounded right on four incidents" evaluation is exactly the trap this article warns about.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My bottom line: I'd deploy this pattern today, but not with this blast radius. Start with the agent as an enrichment-and-draft layer — it investigates, writes the narrative, proposes severity and priority, and a human confirms every verdict. Suppression authority, the right to close an incident unseen, comes last and only per alert class, after the agent has run in shadow mode against human tier-1 decisions long enough to measure a false-negative rate you'd sign your name to. Agreement rate is the vanity metric; the number that matters is how often the machine says "benign" when the human said "incident." Until you can put a measured value on that, the correlate-first architecture is doing most of the heavy lifting anyway. And that part costs nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;em&gt;I write The Patch Window, a free 5-minute weekly brief on which enterprise patches can't wait - Cisco, Windows Server, VMware. Subscribe: &lt;a href="https://the-patch-window.beehiiv.com" rel="noopener noreferrer"&gt;https://the-patch-window.beehiiv.com&lt;/a&gt;&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Adam Lewandowski is a network and security engineer (CompTIA Security+, CCNA, CCNP, VMware VCP-DCV) with several years of hands-on enterprise operations across Cisco FTD/FMC/ISE, Windows Server, MECM, and VMware. He builds automation that takes the repetitive judgment calls out of security operations — without taking the humans out of the loop. Find him on &lt;a href="https://www.linkedin.com/in/adam-lewandowski-157342234/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
      <category>python</category>
    </item>
  </channel>
</rss>
