Get Patch Tuesday KB Articles and CVEs from Microsoft's MSRC API with Python
Automate fetching the month’s Patch Tuesday KB articles and CVEs to kick off a server image build or an AI review; October 2026 alone had 2,575 CVEs.

On this page
TL;DR
To list this month’s Microsoft Patch Tuesday KB articles and CVEs from a script,
clone ms-patch-tuesday-fetcher (opens in a new tab),
install requests and beautifulsoup4, and run:
$ git clone https://github.com/EmmanuelTsouris/ms-patch-tuesday-fetcher.git
$ cd ms-patch-tuesday-fetcher
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install requests beautifulsoup4
$ python ms_patch_tuesday_fetcher/ms_patch_tuesday_fetcher.py --days 35It reads Microsoft’s MSRC API, which needs no API key, and prints the KB articles
and notable CVEs released in the last 35 days. Add --full-cves for every CVE in
the month, --product to narrow it to one product, and --json for output
another tool can read.
I wrote this as a challenge: could I build something fully automated that tells me when the Patch Tuesday updates are out? It’s one part of a bigger workflow, a proof of concept to answer that question. I did a little research before I began and didn’t find anything that fit what I had in mind. I wanted something I could run in the cloud, serverless, as the trigger for a larger workflow. That workflow could kick off a build of Windows Server images with the latest updates, or hand the month’s CVEs to an AI such as Claude, Codex, Gemini or Grok for analysis. The repository includes a Lambda example (opens in a new tab) for running the fetcher on a schedule in AWS Lambda.
I’ve worked with Windows my entire career, and Patch Tuesday has been part of it for most of that time. Microsoft moved its security fixes to one planned release a month (opens in a new tab) in late 2003. On the second Tuesday of each month, Microsoft publishes the month’s security updates, and the Security Update Guide shows them as a web page you read by hand. I wanted the same KB articles and CVEs from a script. I wrote the fetcher in September 2024; that first version listed KB articles only. CVE extraction and JSON output were added in a July 2026 update; the code quoted here, with some comments trimmed, is from the commit current on 21 October 2026, which also includes that day’s fix to the KB extractor.
The fetcher makes at most two kinds of request, and neither needs a key:
- The release notes at
https://api.msrc.microsoft.com/sug/v2.0/en-US/releaseNotelist every release with its KB table. - The monthly CVRF document at
https://api.msrc.microsoft.com/cvrf/v3.0/cvrf/<YYYY-Mon>carries the full per-CVE detail. CVRF is the Common Vulnerability Reporting Framework, the format the Microsoft Security Response Center (MSRC) uses for that document.
Run the fetcher for the last 35 days
In a terminal on your machine, clone the repository, create a virtual environment
inside it, and install the two dependencies. The python commands on this page assume
source .venv/bin/activate has run:
$ git clone https://github.com/EmmanuelTsouris/ms-patch-tuesday-fetcher.git
$ cd ms-patch-tuesday-fetcher
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install requests beautifulsoup4The repository states Python 3.10 or newer. Run the fetcher from the repository root:
$ python ms_patch_tuesday_fetcher/ms_patch_tuesday_fetcher.py --days 35Output
Fetching updates from the last 35 days...
Found 1 updates from the last 35 days.
- Title: October 2026 Security Updates, Released on: 2026-10-13T07:00:00-07:00
KB Articles:
- KB5126646 (Applies to: Windows Server 2025)
- KB5126651 (Applies to: Windows 10, version 1809, Windows Server 2019)
- KB5126655 (Applies to: Windows Server 2022)
...
CVEs (2):
- CVE-2026-96951 - Windows Shell Security Feature Bypass Vulnerability (Exploitation Detected)
- CVE-2026-108398 - .NET Framework Security Feature Bypass Vulnerability (Publicly disclosed)The ellipsis stands for the remaining KB rows; the full list was 19 rows, covering Windows, SQL Server, and Exchange. The run took 2.6 seconds.
--days is how far back from now a release can be and still be listed. The
default is 7. October’s release was published on 13 October, and a run on 21
October with the default printed Found 0 updates from the last 7 days. Use
--days 35 to cover the previous release whatever day of the month you run it;
two Patch Tuesdays can be up to 35 days apart.
Fetch the release notes
The first endpoint is one GET that returns every release note Microsoft has
published. It needs no API key. The only header the code sends is
Accept: application/json. At the time of the run the response held 192 items,
and each item has an HTML description.
def get_all_updates(show_raw=False, timeout=DEFAULT_TIMEOUT):
try:
response = requests.get(BASE_URL, headers=HEADERS, timeout=timeout)
response.raise_for_status()
updates = response.json()
if show_raw:
# Diagnostics go to stderr so they never corrupt stdout when a
# caller is consuming a machine-readable payload (e.g. CLI --json).
print("Raw Updates Response:", file=sys.stderr)
print(json.dumps(updates, indent=4), file=sys.stderr)
return updates.get('value', [])
except requests.exceptions.RequestException as e:
print(f"Error fetching updates from Microsoft API: {e}", file=sys.stderr)
return []Three choices in this function matter if you change it:
DEFAULT_TIMEOUTis 30 seconds, so a server that stops responding can’t block the script.- Any
RequestExceptionprints a message to stderr and returns an empty list. The script then reports no updates rather than a traceback, so read stderr when a run finds nothing. --rawdumps the response to stderr, which keeps stdout parseable when you also pass--json.
The endpoint has no server-side date filter, so the code downloads all 192 items
and filters in Python. filter_updates_by_date keeps the items whose
releaseDate is no older than --days days, compared against the current time in
UTC. Each item also has an id, a title, and a releaseNumber such as
2026-Oct. The dates in today’s payload end in offsets such as -07:00 and
-08:00, or in Z, so parse_release_date converts a trailing Z to +00:00,
calls datetime.fromisoformat, and treats a timestamp without an offset as UTC.
An item whose date it can’t parse is skipped, not fatal.
Extract the KB articles from the release-note HTML
The description is an HTML page, not data: a product-family summary table, a
“Notable CVEs” table, a republished table of CVEs from other numbering
authorities, and the KB table. The fetcher walks every table row and keeps the
ones where the first cell links to a bare KB number.
def extract_kb_from_description(description):
soup = BeautifulSoup(description, "html.parser")
kb_table_rows = soup.find_all('tr')
kb_list = []
for row in kb_table_rows:
columns = row.find_all('td')
# Header rows (only <th>) have no <td> and are skipped.
if len(columns) >= 2:
kb_link = columns[0].find('a')
# Other tables (e.g. "Notable CVEs") also link their first cell, so
# only a link whose text is a bare KB number counts.
if kb_link and kb_link.get_text(strip=True).isdigit():
kb_number = kb_link.get_text(strip=True)
product = columns[1].text
kb_list.append((kb_number, product))
return kb_listThe isdigit() test is the guard; see “KBCVE-2026-... listed as KB articles”
under Common failures for what happens without it.
Read the notable vulnerabilities from the same release note
By default the CVE list comes from the release note, so it costs no extra
request. Only one table in the description has CVE ID as its
first header, and it lists the CVEs Microsoft highlights: the publicly disclosed
and the actively exploited. extract_notable_cves_from_description reads that
table, takes the CVE-\d{4}-\d+ match from the first column, the title from the
second, and the third column, “Notable Item”, as the label.
The October run printed two CVEs because the release note lists only the notable
ones; --full-cves adds the rest.
Fetch the full CVE list from the CVRF document with --full-cves
The second endpoint is the monthly CVRF document, at cvrf/v3.0/cvrf/<YYYY-Mon>.
--full-cves adds one request per month in the window, cached for the run, so
two releases in the window still fetch each month once. For October 2026 the
document was 27,442,504 bytes, and it held 2,575 vulnerability entries and 351
products.
release_to_cvrf_id builds the 2026-Oct id from the release date. It uses a
hard-coded list of English month abbreviations, because strftime('%b') follows
the system locale and would produce a wrong id under a non-English locale.
$ python ms_patch_tuesday_fetcher/ms_patch_tuesday_fetcher.py --days 35 --full-cvesThe run took 7.0 seconds and printed CVEs (2575). The exploited one reads:
CVE-2026-96951 [Important] CVSS 8.8 - Windows Shell Security Feature Bypass Vulnerability (EXPLOITED) [KB5126646, KB5126651, KB5126653, KB5126655, KB5126657, KB5126836, KB5126868, KB5127646, KB5127648]Each vulnerability entry in the document has a CVE, a Title, a list of
Threats, CVSSScoreSets, Remediations, and ProductStatuses.
extract_cves_from_cvrf turns an entry into one record:
- The product names come from the document’s
ProductTree. - The severity is the most severe rating across the products the CVE affects, ordered Critical, Important, Moderate, Low.
- The CVSS score is the highest base score.
- The KB numbers come from the
Remediationswhose description is a KB number. A product with no KB, such as an app that updates itself, getskb: null.
The threat types are the code’s own reading of the document. Each threat carries
a numeric Type, and the code treats type 3 as the severity, type 0 as the
impact, and type 1 as the exploit status, a string like
Publicly Disclosed:No;Exploited:Yes. This mapping was not checked against
Microsoft’s CVRF documentation, so verify it there before you rely on it for
anything other than reading.
The notable wording from the release note is merged onto the matching entries, and a notable CVE that the document lacks is kept. If the CVRF fetch fails, the fetcher falls back to the notable list instead of failing the run.
Narrow the results to one product
To see what the month fixed on the machines you patched, add --product. It
is a case-insensitive substring match against each KB line and each CVE’s
product names (or its title, when a CVE has no product data); a release with
nothing left is dropped.
$ python ms_patch_tuesday_fetcher/ms_patch_tuesday_fetcher.py --days 35 --full-cves --product "Windows Server 2025"The result was one KB row, KB5126646 (Applies to: Windows Server 2025), and
CVEs (1068): 37 Critical, 1,030 Important, and one with no severity, an
advisory (ADV260001). Exactly one of them was exploited. A Critical line looks
like this:
CVE-2026-100202 [Critical] CVSS 9.8 - Windows Message Queuing Queue Manager Remote Code Execution Vulnerability [KB5126646]Note
Without --full-cves, the notable CVEs carry no product data, so --product
matches them on their title instead: a notable CVE stays only if its title
contains the product string. The fetcher prints this to stderr when you do that:
Note: --product filters CVEs by affected product only with --full-cves; without it, only KB articles are filtered.
Emit JSON for another tool
--json prints the same report as structured data, for a script or an assistant
to read. With the product filter above it printed one report holding 1,068 CVE
objects. The exploited one:
{
"id": "CVE-2026-96951",
"title": "Windows Shell Security Feature Bypass Vulnerability",
"severity": "Important",
"impact": "Security Feature Bypass",
"cvss": 8.8,
"exploited": true,
"publicly_disclosed": false,
"exploitation": "Exploitation Detected",
"notable": true,
"products": [
{ "product": "Windows Server 2025", "kb": "5126646" },
{ "product": "Windows Server 2025 (Server Core installation)", "kb": "5126646" }
]
}Run the fetcher with a coding agent
If you use a local coding agent, you can hand it the same job. Paste the prompt
below and replace <products> with the products you patch, for example
Windows Server 2025. The prompt keeps the agent to the clone and the virtual
environment and limits it to a few API runs, because the API can return 429.
Get this month's Microsoft Patch Tuesday exploited and Critical CVEs for these
products: <products>.
Rules:
- Change nothing on this system except a clone of the repository and a virtual
environment inside it. Do not install anything globally and do not edit files.
Apart from python3 -m venv .venv, which creates it, run pip and python only
from the clone's .venv, and never use sudo or --break-system-packages.
- Run at most 3 times in total, one product per run, one after another. If I
name more than 3 products, run the first 3 and tell me which you skipped. If
any run reports
"429 Client Error: Too Many Requests", stop and tell me; do not retry.
Steps:
1. Clone https://github.com/EmmanuelTsouris/ms-patch-tuesday-fetcher into a new
empty directory outside any existing repository (for example under the
system temp directory).
2. Create a virtual environment inside that clone with python3 -m venv .venv and
install requests and beautifulsoup4 into it with
.venv/bin/pip install --no-cache-dir requests beautifulsoup4, so pip leaves
no download cache outside the clone.
3. From the repository root, run this once per product, read-only:
.venv/bin/python ms_patch_tuesday_fetcher/ms_patch_tuesday_fetcher.py --days 35 --full-cves --product "<product>" --json
Read stderr as well as the JSON. If stderr has any "Error fetching" line,
the data is incomplete: an empty list [] can mean the request failed, and a
failed CVRF request leaves only the notable CVEs, with no severity. Stop and
tell me what failed instead of reporting.
If the JSON is an empty list [] and stderr is clean, either no release in the
window matched or the product string matched nothing in it. Say which product
you ran and stop; do not change the product string or widen --days without
telling me.
4. If the JSON holds more than one report, keep only the one with the newest
release_date; a 35-day window can include last month's release too.
5. From that report, give for each product: the exploited CVEs and the Critical
CVEs, each with its id, title, CVSS score, and KB numbers, plus the KB
articles the release lists for that product.
6. State which release (title and date) the data came from. Note that exploited
and severity come from the fetcher's own reading of the CVRF threat types,
which is unverified; say so in the report.Common failures
Found 0 updates
Error
Found 0 updates from the last 7 days.The window is shorter than the days since the release. The default is 7 days, and Patch Tuesday is once a month. Widen it:
$ python ms_patch_tuesday_fetcher/ms_patch_tuesday_fetcher.py --days 35429 Client Error: Too Many Requests
Error
429 Client Error: Too Many RequestsThe fetcher prints this to stderr after Error fetching updates from Microsoft API: and then reports no updates; the CLI prints No updates found.. With
--full-cves, a 429 on the second request prints Error fetching CVRF document <YYYY-Mon>: instead, and the run falls back to the notable CVEs only, so a short
CVE list with that stderr line means the same thing. The API refused the request because of the
request rate. This page doesn’t state a limit, because none was read from
Microsoft. Stop, wait, and run it again later; each run makes one request, plus
one per month with --full-cves.
KBCVE-2026-... listed as KB articles
The output lists entries such as KBCVE-2026-96951 among the KB articles. An
older revision of the KB extractor accepted any row with two or more cells, which
also matched the three-column “Notable CVEs” table, whose first cell is a link.
Update the clone to a revision that requires all-digit link text, as
extract_kb_from_description does earlier in this page:
$ git pullValueError: time data ... does not match format
Error
ValueError: time data '2026-10-13T07:00:00-07:00' does not match format '%Y-%m-%dT%H:%M:%SZ'A copy of the 2024 script parsed dates with a literal Z, and today’s payload
carries offsets such as -07:00 and -08:00. That version also compared the
release date with the host’s local time, so the day window shifted with the
host’s time zone. The current parse_release_date accepts offsets and compares in
UTC; update the clone with git pull.
Tested with
- OS
- macOS 27.0.1, Apple silicon
- Python
- 3.15.0
- requests
- 2.34.2
- beautifulsoup4
- 4.15.0
- Date
- October 2026