os-normalizer 0.3.2__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of os-normalizer might be problematic. Click here for more details.

@@ -17,8 +17,27 @@ ARCH_SYNONYMS = {
17
17
  }
18
18
 
19
19
  # Windows build map (build number range -> product name, marketing channel)
20
+ # Notes:
21
+ # - This focuses on common client builds; server detection is handled separately
22
+ # and this map is not applied if a server product was already detected.
23
+ # - Marketing/channel labels use common public naming where applicable
24
+ # (e.g., 21H2/22H2 for Windows 10/11, RTM/SPx for older releases).
20
25
  WINDOWS_BUILD_MAP = [
21
- # Windows 10
26
+ # NT era (pre-Windows 10)
27
+ (1381, 1381, "Windows NT 4.0", "RTM"),
28
+ (2195, 2195, "Windows 2000", "RTM"),
29
+ (2600, 2600, "Windows XP", "RTM"),
30
+ # NT 5.2 builds are ambiguous (XP x64 vs Server 2003); keep consistent label
31
+ (3790, 3790, "Windows XP x64/Server 2003", "RTM"),
32
+ # Vista/7/8/8.1
33
+ (6000, 6000, "Windows Vista", "RTM"),
34
+ (6001, 6001, "Windows Vista", "SP1"),
35
+ (6002, 6002, "Windows Vista", "SP2"),
36
+ (7600, 7600, "Windows 7", "RTM"),
37
+ (7601, 7601, "Windows 7", "SP1"),
38
+ (9200, 9200, "Windows 8", "RTM"),
39
+ (9600, 9600, "Windows 8.1", "RTM"),
40
+ # Windows 10 (builds and marketing versions)
22
41
  (10240, 10240, "Windows 10", "1507"),
23
42
  (10586, 10586, "Windows 10", "1511"),
24
43
  (14393, 14393, "Windows 10", "1607"),
@@ -30,7 +49,7 @@ WINDOWS_BUILD_MAP = [
30
49
  (19041, 19045, "Windows 10", "2004/20H2/21H1/21H2/22H2"),
31
50
  # Windows 11
32
51
  (22000, 22000, "Windows 11", "21H2"),
33
- (22621, 22630, "Windows 11", "22H2"),
52
+ (22621, 22621, "Windows 11", "22H2"),
34
53
  (22631, 25999, "Windows 11", "23H2"),
35
54
  (26100, 26199, "Windows 11", "24H2"),
36
55
  ]
@@ -80,8 +99,29 @@ MACOS_DARWIN_MAP = {
80
99
  22: ("macOS", "13", "Ventura"),
81
100
  23: ("macOS", "14", "Sonoma"),
82
101
  24: ("macOS", "15", "Sequoia"),
102
+ 25: ("macOS", "26", "Tahoe"),
83
103
  }
84
104
 
105
+ # Windows Server build map (build number range -> product name, marketing channel)
106
+ # This is consulted only when the input looks server-like or when an explicit
107
+ # Windows Server product is already detected. Client mapping will not override
108
+ # explicit server detections.
109
+ WINDOWS_SERVER_BUILD_MAP = [
110
+ # Legacy server releases aligned with Vista/7/8/8.1
111
+ (3790, 3790, "Windows Server 2003", "RTM"),
112
+ (6001, 6001, "Windows Server 2008", "RTM"), # 6001 corresponds to 2008 RTM
113
+ (6002, 6002, "Windows Server 2008", "SP2"),
114
+ (7600, 7600, "Windows Server 2008 R2", "RTM"),
115
+ (7601, 7601, "Windows Server 2008 R2", "SP1"),
116
+ (9200, 9200, "Windows Server 2012", "RTM"),
117
+ (9600, 9600, "Windows Server 2012 R2", "RTM"),
118
+ # NT 10.0 based server releases
119
+ (14393, 14393, "Windows Server 2016", "1607"),
120
+ (17763, 17763, "Windows Server 2019", "1809"),
121
+ (20348, 20348, "Windows Server 2022", "21H2"),
122
+ # Windows Server 2025 (vNext) uses the 26100 train alongside client 24H2
123
+ (26100, 26199, "Windows Server 2025", "24H2"),
124
+ ]
125
+
85
126
  # Cisco train names (used for codename detection)
86
127
  CISCO_TRAIN_NAMES = {"Everest", "Fuji", "Gibraltar", "Amsterdam", "Denali"}
87
-
@@ -6,11 +6,10 @@ from .bsd import parse_bsd
6
6
  from .network import parse_network
7
7
 
8
8
  __all__ = [
9
- "parse_windows",
10
- "parse_macos",
9
+ "parse_bsd",
11
10
  "parse_linux",
11
+ "parse_macos",
12
12
  "parse_mobile",
13
- "parse_bsd",
14
13
  "parse_network",
14
+ "parse_windows",
15
15
  ]
16
-
@@ -18,7 +18,7 @@ LINUX_VER_FALLBACK_RE = re.compile(
18
18
 
19
19
 
20
20
  def parse_linux(text: str, data: dict[str, Any], p: OSData) -> OSData:
21
- """Populate an OSData instance with Linuxspecific details."""
21
+ """Populate an OSData instance with Linux-specific details."""
22
22
  p.kernel_name = "linux"
23
23
 
24
24
  osrel = _coerce_os_release(data.get("os_release")) if isinstance(data, dict) else None
@@ -37,7 +37,7 @@ def parse_linux(text: str, data: dict[str, Any], p: OSData) -> OSData:
37
37
  return p
38
38
 
39
39
 
40
- def _coerce_os_release(obj: Any) -> Optional[dict[str, Any]]:
40
+ def _coerce_os_release(obj: Any) -> dict[str, Any] | None:
41
41
  if isinstance(obj, str):
42
42
  return parse_os_release(obj)
43
43
  if isinstance(obj, dict):
@@ -45,7 +45,7 @@ def _coerce_os_release(obj: Any) -> Optional[dict[str, Any]]:
45
45
  return None
46
46
 
47
47
 
48
- def _extract_kernel_version(text: str) -> Optional[str]:
48
+ def _extract_kernel_version(text: str) -> str | None:
49
49
  m = KERNEL_RE.search(text)
50
50
  if m:
51
51
  return m.group(2)
@@ -66,6 +66,14 @@ def _apply_os_release(osrel: dict[str, Any], p: OSData) -> None:
66
66
 
67
67
  p.pretty_name = osrel.get("PRETTY_NAME") or osrel.get("NAME")
68
68
 
69
+ if not p.codename and p.pretty_name:
70
+ # Fallback: try to extract codename from parenthetical in pretty name (e.g. "Debian ... (buster)")
71
+ m = re.search(r"\(([^)]+)\)", str(p.pretty_name))
72
+ if m:
73
+ candidate = m.group(1).strip()
74
+ if candidate:
75
+ p.codename = candidate.title()
76
+
69
77
  _apply_version_id(osrel.get("VERSION_ID"), p)
70
78
 
71
79
  vcode = osrel.get("VERSION_CODENAME")
@@ -78,7 +86,9 @@ def _apply_os_release(osrel: dict[str, Any], p: OSData) -> None:
78
86
  p.vendor = _vendor_for_distro(p.distro) if p.distro else p.vendor
79
87
 
80
88
  name = osrel.get("NAME")
81
- p.product = (name if name else (p.distro or "Linux")).replace('"', "") if isinstance(name, str) else (p.distro or "Linux")
89
+ p.product = (
90
+ (name if name else (p.distro or "Linux")).replace('"', "") if isinstance(name, str) else (p.distro or "Linux")
91
+ )
82
92
 
83
93
  # Precision from version parts
84
94
  if p.version_patch is not None:
@@ -103,7 +113,7 @@ def _apply_version_id(vid: Any, p: OSData) -> None:
103
113
  p.version_patch = int(parts[2])
104
114
 
105
115
 
106
- def _vendor_for_distro(distro: Optional[str]) -> Optional[str]:
116
+ def _vendor_for_distro(distro: str | None) -> str | None:
107
117
  vendor_by_distro = {
108
118
  "ubuntu": "Canonical",
109
119
  "debian": "Debian",
@@ -30,9 +30,7 @@ def parse_fortinet(text: str, p: OSData) -> OSData:
30
30
  if len(nums) >= 3:
31
31
  p.version_patch = int(nums[2])
32
32
  p.version_build = v
33
- p.precision = (
34
- "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
35
- )
33
+ p.precision = "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
36
34
 
37
35
  bld = FORTI_BUILD_RE.search(text)
38
36
  if bld:
@@ -6,9 +6,7 @@ from os_normalizer.helpers import update_confidence
6
6
  from os_normalizer.models import OSData
7
7
 
8
8
  NETGEAR_RE = re.compile(r"\bnetgear\b|\bfirmware\b", re.IGNORECASE)
9
- NETGEAR_VER_RE = re.compile(
10
- r"\bV(\d+\.\d+\.\d+(?:\.\d+)?(?:_\d+\.\d+\.\d+)?)\b", re.IGNORECASE
11
- )
9
+ NETGEAR_VER_RE = re.compile(r"\bV(\d+\.\d+\.\d+(?:\.\d+)?(?:_\d+\.\d+\.\d+)?)\b", re.IGNORECASE)
12
10
  NETGEAR_MODEL_RE = re.compile(r"\b([RN][0-9]{3,4}[A-Z]?)\b", re.IGNORECASE)
13
11
 
14
12
 
@@ -29,9 +27,7 @@ def parse_netgear(text: str, p: OSData) -> OSData:
29
27
  if len(nums) >= 3:
30
28
  p.version_patch = int(nums[2])
31
29
  p.version_build = v
32
- p.precision = (
33
- "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
34
- )
30
+ p.precision = "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
35
31
 
36
32
  mdl = NETGEAR_MODEL_RE.search(text)
37
33
  if mdl:
@@ -10,6 +10,7 @@ from typing import Any, Optional
10
10
 
11
11
  from os_normalizer.constants import (
12
12
  WINDOWS_BUILD_MAP,
13
+ WINDOWS_SERVER_BUILD_MAP,
13
14
  WINDOWS_NT_CLIENT_MAP,
14
15
  WINDOWS_NT_SERVER_MAP,
15
16
  )
@@ -18,14 +19,17 @@ from os_normalizer.models import OSData
18
19
 
19
20
  # Regex patterns used only by the Windows parser
20
21
  WIN_EDITION_RE = re.compile(
21
- r"\b(professional|enterprise|home|education|ltsc|datacenter)\b",
22
+ r"\b(professional|pro|enterprise|home|education|ltsc|datacenter|standard)\b",
22
23
  re.IGNORECASE,
23
24
  )
24
25
  WIN_SP_RE = re.compile(r"\bSP\s?([0-9]+)\b", re.IGNORECASE)
25
26
  WIN_BUILD_RE = re.compile(r"\bbuild\s?(\d{4,6})\b", re.IGNORECASE)
26
27
  WIN_NT_RE = re.compile(r"\bnt\s?(\d+)\.(\d+)\b", re.IGNORECASE)
27
28
  WIN_FULL_NT_BUILD_RE = re.compile(r"\b(10)\.(0)\.(\d+)(?:\.(\d+))?\b")
28
- WIN_CHANNEL_RE = re.compile(r"\b(20H2|21H2|22H2|23H2|24H2|19H2|18H2|17H2|2004|1909|1809|1709|1511|1507)\b", re.IGNORECASE)
29
+ WIN_CHANNEL_RE = re.compile(
30
+ r"\b(24H2|23H2|22H2|21H2|21H1|20H2|2004|1909|1903|1809|1803|1709|1703|1607|1511|1507)\b",
31
+ re.IGNORECASE,
32
+ )
29
33
 
30
34
 
31
35
  def parse_windows(text: str, data: dict[str, Any], p: OSData) -> OSData:
@@ -47,7 +51,7 @@ def parse_windows(text: str, data: dict[str, Any], p: OSData) -> OSData:
47
51
  _apply_full_kernel_and_channel(text, p)
48
52
 
49
53
  # 5) Build number + marketing channel (fallback when only 'build 22631' is present)
50
- _apply_build_mapping(text, p)
54
+ _apply_build_mapping(text, p, server_like)
51
55
 
52
56
  # 6) Precision and version_major if applicable
53
57
  _finalize_precision_and_version(p)
@@ -75,6 +79,8 @@ def _detect_product_from_text(t: str) -> str:
75
79
  return "Windows 98"
76
80
 
77
81
  # Server explicit names
82
+ if "windows server 2012 r2" in t:
83
+ return "Windows Server 2012 R2"
78
84
  if "windows server 2022" in t or "win2k22" in t or "win2022" in t:
79
85
  return "Windows Server 2022"
80
86
  if "windows server 2019" in t or "win2k19" in t or "win2019" in t:
@@ -83,6 +89,8 @@ def _detect_product_from_text(t: str) -> str:
83
89
  return "Windows Server 2016"
84
90
  if "windows server 2012" in t or "win2k12" in t or "win2012" in t:
85
91
  return "Windows Server 2012"
92
+ if "windows server 2008 r2" in t:
93
+ return "Windows Server 2008 R2"
86
94
  if "windows server 2008" in t or "win2k8" in t or "win2008" in t:
87
95
  return "Windows Server 2008"
88
96
  if "windows server 2003" in t or "win2k3" in t or "win2003" in t:
@@ -97,7 +105,20 @@ def _detect_product_from_text(t: str) -> str:
97
105
 
98
106
  def _detect_edition(text: str) -> str | None:
99
107
  m = WIN_EDITION_RE.search(text)
100
- return m.group(1).title() if m else None
108
+ if not m:
109
+ return None
110
+ token = m.group(1).lower()
111
+ norm = {
112
+ "pro": "Professional",
113
+ "professional": "Professional",
114
+ "enterprise": "Enterprise",
115
+ "home": "Home",
116
+ "education": "Education",
117
+ "ltsc": "LTSC",
118
+ "datacenter": "Datacenter",
119
+ "standard": "Standard",
120
+ }
121
+ return norm.get(token, token.title())
101
122
 
102
123
 
103
124
  def _parse_service_pack(text: str, p: OSData) -> None:
@@ -138,26 +159,42 @@ def _apply_nt_mapping(text: str, p: OSData, server_like: bool) -> None:
138
159
  p.product = product
139
160
 
140
161
 
141
- def _apply_build_mapping(text: str, p: OSData) -> None:
162
+ def _apply_build_mapping(text: str, p: OSData, server_like: bool) -> None:
142
163
  m = WIN_BUILD_RE.search(text)
143
164
  if not m:
144
165
  return
145
166
  build_num = int(m.group(1))
146
167
  p.version_build = str(build_num)
147
168
 
148
- # Kernel version for recent Windows 10/11
149
- if (p.product == "Windows 10/11") or ("10.0" in text):
150
- p.kernel_version = f"{10}.{0}.{build_num}"
151
- else:
152
- p.kernel_version = None
169
+ # Kernel version string
170
+ if not p.kernel_version:
171
+ if (p.product == "Windows 10/11") or ("10.0" in text):
172
+ p.kernel_version = f"10.0.{build_num}"
173
+ else:
174
+ nt_mm = WIN_NT_RE.search(text)
175
+ if nt_mm:
176
+ maj, minr = int(nt_mm.group(1)), int(nt_mm.group(2))
177
+ p.kernel_version = f"{maj}.{minr}.{build_num}"
153
178
 
154
- # Only apply client build mapping if current product isn't an explicit Server
155
179
  is_server_product = isinstance(p.product, str) and "server" in p.product.lower()
156
- if not is_server_product:
180
+ if is_server_product or server_like:
181
+ # Apply server build mapping; do not override explicit server product names
182
+ for lo, hi, product_name, marketing in WINDOWS_SERVER_BUILD_MAP:
183
+ if lo <= build_num <= hi:
184
+ if not p.product or p.product in ("Windows", "Windows 10/11"):
185
+ p.product = product_name
186
+ # Only set channel for modern Server (2016+)
187
+ if build_num >= 14393:
188
+ p.channel = p.channel or marketing
189
+ break
190
+ else:
191
+ # Apply client build mapping
157
192
  for lo, hi, product_name, marketing in WINDOWS_BUILD_MAP:
158
193
  if lo <= build_num <= hi:
159
- p.product = product_name
160
- p.channel = marketing
194
+ # Only use build map to set product for Windows 10/11 trains
195
+ if build_num >= 10240:
196
+ p.product = product_name
197
+ p.channel = p.channel or marketing
161
198
  break
162
199
 
163
200
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: os-normalizer
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Normalize raw OS strings/metadata into structured data (family, product, version, arch).
5
5
  Project-URL: Homepage, https://github.com/johnscillieri/os-normalizer
6
6
  Project-URL: Repository, https://github.com/johnscillieri/os-normalizer
@@ -1,22 +1,22 @@
1
1
  os_normalizer/__init__.py,sha256=ZA2JnO3juuAn8xQBqUoUXxca10B-rE1MIuwtzDofpKs,212
2
- os_normalizer/constants.py,sha256=aIm6TiT95Mr7adABYwW6f57Hef0-_thopnNoHssiHFA,2676
2
+ os_normalizer/constants.py,sha256=XwwPXmb9poJBHCEyHyvUIE2OaTjXLF6xVI5-dnRr9x0,4686
3
3
  os_normalizer/cpe.py,sha256=G7hFiuVMlXM2d70ra7NA8v3a7kV7gYBt24bFunCAzk8,8742
4
4
  os_normalizer/helpers.py,sha256=IymYS2NAJ6J8ngI_HL5PH2_6zN6zzIpWMx1-05D2O3c,3347
5
5
  os_normalizer/models.py,sha256=_FAisDt9Pi5GDHIvzzfilEE8svvjyx6EcG2ZvMSRRJ0,5793
6
6
  os_normalizer/os_normalizer.py,sha256=IsWxE_Yes7FQxxNhA8twNrzmeRcl8QXTxjTfn0zCTTI,10143
7
- os_normalizer/parsers/__init__.py,sha256=-z-iu6os5LWuMUznvmfv_5LIw6Fbb3M1Auq2Wm7Rvfc,325
7
+ os_normalizer/parsers/__init__.py,sha256=l9kx_P7FUHy--AxygZ8Q8nlyxyoSRRTREmp40M73Gxg,324
8
8
  os_normalizer/parsers/bsd.py,sha256=Umm7RSXjQfv6pfziJ8BYlmOc80VlLRueTrYw1iR-UjY,2018
9
- os_normalizer/parsers/linux.py,sha256=dQA0D57agO6g7EqFCRZ2cv6sLoR3jpEJ9onWZFZi35c,3528
9
+ os_normalizer/parsers/linux.py,sha256=kSIYZ_hRJxJieV21u8TRcWfAjYOIXlCLBSwyINkuyA4,3867
10
10
  os_normalizer/parsers/macos.py,sha256=fU1YyiijzBdDAxUBSL2EQLhUu3JbOSx-N_KYvOg0XsI,3627
11
11
  os_normalizer/parsers/mobile.py,sha256=Ca864JhrO9zW5fs0zbs2VcMypKIkFHqZtxpJDbi12q0,1033
12
- os_normalizer/parsers/windows.py,sha256=DQtGwOhXqqwvdD_aShAZOiIYZ9hltq-6DwdL5AQjiYw,6589
12
+ os_normalizer/parsers/windows.py,sha256=RQ_DJ3OjG70g_vwIaX9UrH2ADKzTEzF-R4ID2YSlAS0,7972
13
13
  os_normalizer/parsers/network/__init__.py,sha256=TvRz08lNDZbr6yG8O3M7cLNu0hWAbtE1y7_9SwhP4g4,1596
14
14
  os_normalizer/parsers/network/cisco.py,sha256=ivhw85IHnHVT4sW-65F-ZGCR7yvu0mMMBfXRT4EzUfc,3203
15
- os_normalizer/parsers/network/fortinet.py,sha256=C-f5DgDS6mvda69vJ5bRM-blEE92E2rAEzYvbHXl2WM,1807
15
+ os_normalizer/parsers/network/fortinet.py,sha256=i6PVRxzO_onaCvo3_eI4csXFwGp4ZpQY0d1y2-Wodn0,1783
16
16
  os_normalizer/parsers/network/huawei.py,sha256=Su3eCRlmOCmpOPA_TGeH8gHY3-ZdXKFW4O3_W0SOPmk,1158
17
17
  os_normalizer/parsers/network/juniper.py,sha256=gskbaY4-LYWauM9yrvGMuCxPrNBKmUfIRPd1zsUUY7w,1275
18
- os_normalizer/parsers/network/netgear.py,sha256=AhgjVlhQiKvzuCHWAGMZCTucdOOSDt7cl9xONpb2aSA,1220
19
- os_normalizer-0.3.2.dist-info/METADATA,sha256=DugZSc8kZQ9tkKEEHKgIE7OTWJ_8QqlRYB4lk7E5_tg,6421
20
- os_normalizer-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- os_normalizer-0.3.2.dist-info/licenses/LICENSE,sha256=DN0enoiHxVkJ-hxmIchPaCQWrDsZwva5LY8XvG3UK8w,1083
22
- os_normalizer-0.3.2.dist-info/RECORD,,
18
+ os_normalizer/parsers/network/netgear.py,sha256=idVD7VTxb07LdhFPwb-sT3586ARoBrd1OmWpADAZUVc,1190
19
+ os_normalizer-0.3.3.dist-info/METADATA,sha256=aUqJk1GKj_dHxjHRbR4C79UTNi5zc_HjnRqfzK-fVkc,6421
20
+ os_normalizer-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ os_normalizer-0.3.3.dist-info/licenses/LICENSE,sha256=DN0enoiHxVkJ-hxmIchPaCQWrDsZwva5LY8XvG3UK8w,1083
22
+ os_normalizer-0.3.3.dist-info/RECORD,,