Skip to content

Commit ed69b09

Browse files
authored
Merge pull request #19 from mawussid/fix/wua-applicability-subfeatures-ui
Fix/WUA applicability subfeatures UI
2 parents 1b86a86 + 7899092 commit ed69b09

14 files changed

Lines changed: 1243 additions & 139 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,4 @@ dmypy.json
136136

137137
# Cython debug symbols
138138
cython_debug/
139+
data/known_clients.json

check_build.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
#!/usr/bin/env python3
2+
"""check_build.py — Update data/win_builds.json from Microsoft release health pages.
3+
4+
Scrapes the official Microsoft documentation to discover new Windows build
5+
numbers and adds them to the local data/win_builds.json dictionary, organized
6+
by OS family (Windows 10, Windows 11, Windows Server XXXX, etc.).
7+
8+
Sources:
9+
- Windows 10: https://learn.microsoft.com/en-us/windows/release-health/release-information
10+
- Windows 11: https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
11+
- Windows Server: https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info
12+
13+
Usage:
14+
python check_build.py # update data/win_builds.json
15+
python check_build.py --dry-run # show what would change, don't write
16+
python check_build.py --wipe-clients # delete data/known_clients.json (reset IP cache)
17+
"""
18+
19+
import json
20+
import os
21+
import re
22+
import datetime
23+
import argparse
24+
from urllib.request import urlopen, Request
25+
from urllib.error import URLError
26+
27+
# ---------------------------------------------------------------------------
28+
# Configuration
29+
# ---------------------------------------------------------------------------
30+
31+
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
32+
_DATA_DIR = os.path.join(_SCRIPT_DIR, "data")
33+
_BUILDS_JSON = os.path.join(_DATA_DIR, "win_builds.json")
34+
_CLIENTS_JSON = os.path.join(_DATA_DIR, "known_clients.json")
35+
36+
# Client sources: scrape "Version XXX (OS build NNNNN)" patterns
37+
CLIENT_SOURCES = [
38+
{
39+
"url": "https://learn.microsoft.com/en-us/windows/release-health/release-information",
40+
"section": "Windows 10",
41+
},
42+
{
43+
"url": "https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information",
44+
"section": "Windows 11",
45+
},
46+
]
47+
48+
# Server source: scrape "Windows Server XXXX (OS build NNNNN)" headers
49+
SERVER_SOURCE = {
50+
"url": "https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info",
51+
}
52+
53+
HEADERS = {
54+
"User-Agent": "check_build/1.0 (pywsus; build dictionary updater)",
55+
"Accept": "text/html",
56+
}
57+
58+
# ---------------------------------------------------------------------------
59+
# Scraping
60+
# ---------------------------------------------------------------------------
61+
62+
def fetch_page(url):
63+
"""Fetch a page and return its HTML as a string."""
64+
req = Request(url, headers=HEADERS)
65+
try:
66+
with urlopen(req, timeout=15) as resp:
67+
return resp.read().decode("utf-8", errors="replace")
68+
except URLError as e:
69+
print(f" [!] Failed to fetch {url}: {e}")
70+
return ""
71+
72+
73+
def extract_client_builds(html_content):
74+
"""Extract { base_build: version_label } from a Win10/Win11 release health page.
75+
76+
Looks for patterns like 'Version 22H2 (OS build 19045)'.
77+
"""
78+
results = {}
79+
80+
# Pattern 1: "Version 22H2 (OS build 19045)" style headers
81+
for m in re.finditer(
82+
r'Version\s+([\w.]+)\s*\(OS\s+build\s+(\d{5,})\)',
83+
html_content, re.IGNORECASE
84+
):
85+
results[m.group(2)] = m.group(1)
86+
87+
# Pattern 2: table rows with version + build pairs
88+
version_pat = re.compile(
89+
r'<td[^>]*>\s*(?:Version\s+)?(1[5-9]\d{2}|2[0-9]H[12]|2[0-9]{3})\s*</td>',
90+
re.IGNORECASE
91+
)
92+
build_pat = re.compile(r'<td[^>]*>\s*(\d{5,})(?:\.\d+)?\s*</td>')
93+
94+
for row in re.finditer(r'<tr[^>]*>(.*?)</tr>', html_content, re.DOTALL):
95+
row_html = row.group(1)
96+
ver_m = version_pat.search(row_html)
97+
bld_m = build_pat.search(row_html)
98+
if ver_m and bld_m:
99+
build = bld_m.group(1)
100+
if build not in results:
101+
results[build] = ver_m.group(1)
102+
103+
return results
104+
105+
106+
def extract_server_builds(html_content):
107+
"""Extract { section_name: { base_build: version } } from the Server page.
108+
109+
Looks for detail headers like 'Windows Server 2025 (OS build 26100)'
110+
and main table rows like 'Windows Server 2019 (version 1809)'.
111+
"""
112+
results = {} # { "Windows Server 2025": { "26100": "24H2" }, ... }
113+
114+
# Step 1: extract section → base_build from detail headers
115+
# Pattern: "Windows Server XXXX (OS build NNNNN)"
116+
section_builds = {}
117+
for m in re.finditer(
118+
r'Windows\s+Server\s+(\d{4})\s*\(OS\s+build\s+(\d{5,})\)',
119+
html_content, re.IGNORECASE
120+
):
121+
name = f"Windows Server {m.group(1)}"
122+
base_build = m.group(2)
123+
section_builds[name] = base_build
124+
125+
# Step 2: extract version from main table
126+
# Pattern: "Windows Server XXXX (version YYYY)" or just "Windows Server XXXX"
127+
version_map = {}
128+
for m in re.finditer(
129+
r'Windows\s+Server\s+(\d{4})\s*\(version\s+([\w.]+)\)',
130+
html_content, re.IGNORECASE
131+
):
132+
version_map[f"Windows Server {m.group(1)}"] = m.group(2)
133+
134+
# Step 3: for servers without explicit version, try to derive it
135+
# Server 2025 = build 26100 = "24H2", Server 2022 = build 20348 = "21H2"
136+
_KNOWN_SERVER_VERSIONS = {
137+
"Windows Server 2025": "24H2",
138+
"Windows Server 2022": "21H2",
139+
}
140+
141+
# Combine
142+
for name, base_build in section_builds.items():
143+
version = version_map.get(name, _KNOWN_SERVER_VERSIONS.get(name, ""))
144+
if name not in results:
145+
results[name] = {}
146+
if version:
147+
results[name][base_build] = version
148+
149+
return results
150+
151+
152+
# ---------------------------------------------------------------------------
153+
# JSON management
154+
# ---------------------------------------------------------------------------
155+
156+
def load_json():
157+
"""Load the existing data/win_builds.json or create a skeleton."""
158+
if os.path.exists(_BUILDS_JSON):
159+
with open(_BUILDS_JSON, "r", encoding="utf-8") as f:
160+
return json.load(f)
161+
return {
162+
"_comment": "OS family → { build_number: version }. Updated by check_build.py",
163+
"_updated": "",
164+
"_sources": [s["url"] for s in CLIENT_SOURCES] + [SERVER_SOURCE["url"]],
165+
}
166+
167+
168+
def save_json(data):
169+
"""Write the updated JSON file with sorted builds per section."""
170+
data["_updated"] = datetime.date.today().isoformat()
171+
for key, val in data.items():
172+
if key.startswith("_") or not isinstance(val, dict):
173+
continue
174+
data[key] = dict(sorted(val.items(), key=lambda x: int(x[0])))
175+
os.makedirs(_DATA_DIR, exist_ok=True)
176+
with open(_BUILDS_JSON, "w", encoding="utf-8") as f:
177+
json.dump(data, f, indent=4, ensure_ascii=False)
178+
f.write("\n")
179+
180+
181+
# ---------------------------------------------------------------------------
182+
# Main
183+
# ---------------------------------------------------------------------------
184+
185+
def main():
186+
parser = argparse.ArgumentParser(
187+
description="Update data/win_builds.json from Microsoft release health pages.",
188+
epilog=(
189+
"Examples:\n"
190+
" python check_build.py # fetch latest builds\n"
191+
" python check_build.py --dry-run # preview without saving\n"
192+
" python check_build.py --wipe-clients # reset known client IPs\n"
193+
),
194+
formatter_class=argparse.RawDescriptionHelpFormatter,
195+
)
196+
parser.add_argument("--dry-run", action="store_true",
197+
help="Show changes without writing to disk")
198+
parser.add_argument("--wipe-clients", action="store_true",
199+
help="Reset data/known_clients.json (clear all known client IPs)")
200+
args = parser.parse_args()
201+
202+
# --- Wipe clients if requested ---
203+
if args.wipe_clients:
204+
os.makedirs(_DATA_DIR, exist_ok=True)
205+
empty = {"_comment": "Known WSUS clients — { ip: {build, arch, os_desc} }. Auto-populated by pywsus."}
206+
with open(_CLIENTS_JSON, "w", encoding="utf-8") as f:
207+
json.dump(empty, f, indent=2, ensure_ascii=False)
208+
f.write("\n")
209+
print(f"[*] Cleared {_CLIENTS_JSON}")
210+
return
211+
212+
data = load_json()
213+
added = {}
214+
215+
# Count existing
216+
existing_count = sum(len(v) for k, v in data.items()
217+
if not k.startswith("_") and isinstance(v, dict))
218+
print(f"[*] Current dictionary: {existing_count} builds in {_BUILDS_JSON}")
219+
print()
220+
221+
# --- Client builds (Windows 10, Windows 11) ---
222+
for source in CLIENT_SOURCES:
223+
url = source["url"]
224+
section = source["section"]
225+
226+
print(f"[*] Fetching {section} release info...")
227+
html_content = fetch_page(url)
228+
if not html_content:
229+
continue
230+
231+
discovered = extract_client_builds(html_content)
232+
print(f" Found {len(discovered)} build(s) on page")
233+
234+
existing = data.setdefault(section, {})
235+
for build, version in discovered.items():
236+
if build not in existing:
237+
added[f"{section}/{build}"] = version
238+
existing[build] = version
239+
print(f" [+] NEW: {section} build {build} → version {version}")
240+
elif existing[build] != version:
241+
print(f" [~] UPDATE: {section} build {build}: "
242+
f"{existing[build]}{version}")
243+
existing[build] = version
244+
added[f"{section}/{build}"] = version
245+
246+
# --- Server builds ---
247+
print(f"[*] Fetching Windows Server release info...")
248+
html_content = fetch_page(SERVER_SOURCE["url"])
249+
if html_content:
250+
server_results = extract_server_builds(html_content)
251+
total_server = sum(len(v) for v in server_results.values())
252+
print(f" Found {total_server} build(s) across {len(server_results)} server edition(s)")
253+
254+
for section, builds in server_results.items():
255+
existing = data.setdefault(section, {})
256+
for build, version in builds.items():
257+
if build not in existing:
258+
added[f"{section}/{build}"] = version
259+
existing[build] = version
260+
print(f" [+] NEW: {section} build {build} → version {version}")
261+
elif existing[build] != version:
262+
print(f" [~] UPDATE: {section} build {build}: "
263+
f"{existing[build]}{version}")
264+
existing[build] = version
265+
added[f"{section}/{build}"] = version
266+
267+
print()
268+
269+
if not added:
270+
print("[*] Dictionary is already up to date, no new builds found.")
271+
else:
272+
print(f"[+] {len(added)} new/updated build(s):")
273+
for key, version in sorted(added.items()):
274+
print(f" {key}{version}")
275+
276+
if added and not args.dry_run:
277+
save_json(data)
278+
print(f"\n[*] Saved to {_BUILDS_JSON}")
279+
elif added and args.dry_run:
280+
print(f"\n[*] Dry run — changes NOT saved")
281+
282+
total = sum(len(v) for k, v in data.items()
283+
if not k.startswith("_") and isinstance(v, dict))
284+
families = sum(1 for k in data if not k.startswith("_") and isinstance(data[k], dict))
285+
print(f"\n[*] Total: {total} builds / {families} OS families")
286+
287+
288+
if __name__ == "__main__":
289+
main()

data/win_builds.json

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"_comment": "OS family → { build_number: version }. Updated by check_build.py",
3+
"_updated": "2026-04-01",
4+
"_sources": [
5+
"https://learn.microsoft.com/en-us/windows/release-health/release-information",
6+
"https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information",
7+
"https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info"
8+
],
9+
"Windows 7": {
10+
"7600": "RTM",
11+
"7601": "SP1"
12+
},
13+
"Windows 8": {
14+
"9200": "RTM"
15+
},
16+
"Windows 8.1": {
17+
"9600": "Update 1"
18+
},
19+
"Windows 10": {
20+
"10240": "1507",
21+
"10586": "1511",
22+
"14393": "1607",
23+
"15063": "1703",
24+
"16299": "1709",
25+
"17134": "1803",
26+
"17763": "1809",
27+
"18362": "1903",
28+
"18363": "1909",
29+
"19041": "2004",
30+
"19042": "20H2",
31+
"19043": "21H1",
32+
"19044": "21H2",
33+
"19045": "22H2"
34+
},
35+
"Windows 11": {
36+
"22000": "21H2",
37+
"22621": "22H2",
38+
"22631": "23H2",
39+
"26100": "24H2",
40+
"26200": "25H2",
41+
"28000": "26H1"
42+
},
43+
"Windows Server 2008": {
44+
"6003": "SP2"
45+
},
46+
"Windows Server 2008 R2": {
47+
"7601": "SP1"
48+
},
49+
"Windows Server 2012": {
50+
"9200": "RTM"
51+
},
52+
"Windows Server 2012 R2": {
53+
"9600": "RTM"
54+
},
55+
"Windows Server 2016": {
56+
"14393": "1607"
57+
},
58+
"Windows Server 2019": {
59+
"17763": "1809"
60+
},
61+
"Windows Server 2022": {
62+
"20348": "21H2"
63+
},
64+
"Windows Server 2025": {
65+
"26100": "24H2"
66+
}
67+
}

0 commit comments

Comments
 (0)