os-normalizer 0.3.2__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.

@@ -0,0 +1,61 @@
1
+ """Vendor-specific network OS parsers.
2
+
3
+ Each module exposes a vendor detection regex (or set) and a `parse_*`
4
+ function that mutates and returns an OSData instance.
5
+ """
6
+
7
+ from .cisco import (
8
+ CISCO_IOS_RE,
9
+ CISCO_IOS_XE_RE,
10
+ CISCO_NXOS_RE,
11
+ parse_cisco,
12
+ )
13
+ from .juniper import JUNOS_RE, parse_juniper
14
+ from .fortinet import FORTI_RE, parse_fortinet
15
+ from .huawei import HUAWEI_RE, parse_huawei
16
+ from .netgear import NETGEAR_RE, parse_netgear
17
+
18
+ from os_normalizer.models import OSData
19
+
20
+ __all__ = [
21
+ # Cisco
22
+ "CISCO_IOS_RE",
23
+ "CISCO_IOS_XE_RE",
24
+ "CISCO_NXOS_RE",
25
+ "parse_cisco",
26
+ # Juniper
27
+ "JUNOS_RE",
28
+ "parse_juniper",
29
+ # Fortinet
30
+ "FORTI_RE",
31
+ "parse_fortinet",
32
+ # Huawei
33
+ "HUAWEI_RE",
34
+ "parse_huawei",
35
+ # Netgear
36
+ "NETGEAR_RE",
37
+ "parse_netgear",
38
+ # Orchestrator
39
+ "parse_network",
40
+ ]
41
+
42
+
43
+ def parse_network(text: str, data: dict | None, p: OSData) -> OSData:
44
+ """Detect vendor and delegate to the correct parser."""
45
+ tl = text.lower()
46
+ if "cisco" in tl or CISCO_IOS_XE_RE.search(text) or CISCO_IOS_RE.search(text) or CISCO_NXOS_RE.search(text):
47
+ return parse_cisco(text, p)
48
+ if JUNOS_RE.search(text):
49
+ return parse_juniper(text, p)
50
+ if FORTI_RE.search(text):
51
+ return parse_fortinet(text, p)
52
+ if HUAWEI_RE.search(text):
53
+ return parse_huawei(text, p)
54
+ if NETGEAR_RE.search(text):
55
+ return parse_netgear(text, p)
56
+
57
+ # Unknown network vendor; keep coarse
58
+ p.vendor = p.vendor or "Unknown-Network"
59
+ p.product = p.product or "Network OS"
60
+ p.precision = "family"
61
+ return p
@@ -0,0 +1,96 @@
1
+ """Cisco network OS parsing (IOS, IOS XE, NX-OS)."""
2
+
3
+ import re
4
+
5
+ from os_normalizer.helpers import update_confidence
6
+ from os_normalizer.models import OSData
7
+
8
+ # Detection and parsing regex
9
+ CISCO_IOS_XE_RE = re.compile(r"(ios[\s-]?xe)", re.IGNORECASE)
10
+ CISCO_IOS_RE = re.compile(r"\bios(?!\s?xe)\b", re.IGNORECASE)
11
+ CISCO_NXOS_RE = re.compile(r"\bnx-?os\b|\bNexus Operating System\b", re.IGNORECASE)
12
+ CISCO_VERSION_RE = re.compile(
13
+ r"\bVersion\s+([0-9]+\.[0-9.()a-zA-Z]+)\b|\bnxos\.(\d+\.\d+(?:\.\d+|\(\d+\)))",
14
+ re.IGNORECASE,
15
+ )
16
+ CISCO_IMAGE_RE = re.compile(r"\b([a-z0-9][a-z0-9_.-]+\.bin)\b", re.IGNORECASE)
17
+ CISCO_MODEL_RE = re.compile(
18
+ r"\b(N9K-[A-Z0-9-]+|C\d{3,4}[\w-]+|ASR\d{3,4}[\w-]*|ISR\d{3,4}[\w/-]*|Catalyst\s?\d{3,4}[\w-]*)\b",
19
+ re.IGNORECASE,
20
+ )
21
+ CISCO_EDITION_RE = re.compile(
22
+ r"\b(universalk9|ipbase|adv(ip)?services|metroipaccess|securityk9|datak9)\b",
23
+ re.IGNORECASE,
24
+ )
25
+
26
+
27
+ def parse_cisco(text: str, p: OSData) -> OSData:
28
+ p.vendor = "Cisco"
29
+ p.family = p.family or "network-os"
30
+
31
+ # Detect product line
32
+ if CISCO_IOS_XE_RE.search(text):
33
+ p.product, p.kernel_name = "IOS XE", "ios-xe"
34
+ elif CISCO_NXOS_RE.search(text):
35
+ p.product, p.kernel_name = "NX-OS", "nx-os"
36
+ elif CISCO_IOS_RE.search(text):
37
+ p.product, p.kernel_name = "IOS", "ios"
38
+ else:
39
+ p.product = p.product or "Cisco OS"
40
+
41
+ # Version (Version X or nxos.X from text)
42
+ vm = CISCO_VERSION_RE.search(text)
43
+ if vm:
44
+ ver = vm.group(1) or vm.group(2)
45
+ if ver:
46
+ p.evidence["version_raw"] = ver
47
+ num = re.findall(r"\d+", ver)
48
+ if len(num) >= 1:
49
+ p.version_major = int(num[0])
50
+ if len(num) >= 2:
51
+ p.version_minor = int(num[1])
52
+ if len(num) >= 3:
53
+ p.version_patch = int(num[2])
54
+ p.version_build = ver
55
+ p.precision = (
56
+ "patch" if p.version_patch is not None else ("minor" if p.version_minor is not None else "major")
57
+ )
58
+
59
+ # Image filename
60
+ img = CISCO_IMAGE_RE.search(text)
61
+ if img:
62
+ p.build_id = img.group(1)
63
+ p.precision = "build"
64
+
65
+ # If NX-OS and only got version via filename, parse nxos.A.B.C.bin
66
+ if not p.version_major and p.build_id:
67
+ m = re.search(r"nxos\.(\d+)\.(\d+)\.(\d+)", p.build_id, re.IGNORECASE)
68
+ if m:
69
+ p.version_major = int(m.group(1))
70
+ p.version_minor = int(m.group(2))
71
+ p.version_patch = int(m.group(3))
72
+ p.version_build = f"{p.version_major}.{p.version_minor}.{p.version_patch}"
73
+ p.precision = "patch"
74
+
75
+ # Model
76
+ mm = CISCO_MODEL_RE.search(text)
77
+ if mm:
78
+ p.hw_model = mm.group(1)
79
+
80
+ # Edition (universalk9/ipbase)
81
+ fl = CISCO_EDITION_RE.search(text)
82
+ if fl:
83
+ p.edition = fl.group(1).lower()
84
+
85
+ # Train codename
86
+ from os_normalizer.constants import CISCO_TRAIN_NAMES
87
+
88
+ tl = text.lower()
89
+ for train in CISCO_TRAIN_NAMES:
90
+ if train.lower() in tl:
91
+ p.codename = train
92
+ break
93
+
94
+ # Boost confidence based on precision
95
+ update_confidence(p, p.precision if p.precision in ("build", "patch") else "minor")
96
+ return p
@@ -0,0 +1,56 @@
1
+ """Fortinet FortiOS parsing."""
2
+
3
+ import re
4
+
5
+ from os_normalizer.helpers import update_confidence
6
+ from os_normalizer.models import OSData
7
+
8
+ FORTI_RE = re.compile(r"\bforti(os|gate)\b", re.IGNORECASE)
9
+ FORTI_VER_RE = re.compile(r"\bv?(\d+\.\d+(?:\.\d+)?)\b", re.IGNORECASE)
10
+ FORTI_BUILD_RE = re.compile(r"\bbuild\s?(\d{3,5})\b", re.IGNORECASE)
11
+ FORTI_IMG_RE = re.compile(r"\b(FGT_[0-9.]+-build\d{3,5})\b", re.IGNORECASE)
12
+ FORTI_MODEL_RE = re.compile(r"\b(FortiGate-?\d+[A-Z]?|FG-\d+[A-Z]?)\b", re.IGNORECASE)
13
+ FORTI_CHANNEL_RE = re.compile(r"\((GA|Patch|Beta)\)", re.IGNORECASE)
14
+
15
+
16
+ def parse_fortinet(text: str, p: OSData) -> OSData:
17
+ p.vendor = "Fortinet"
18
+ p.product = "FortiOS"
19
+ p.family = p.family or "network-os"
20
+ p.kernel_name = "fortios"
21
+
22
+ ver = FORTI_VER_RE.search(text)
23
+ if ver:
24
+ v = ver.group(1)
25
+ nums = re.findall(r"\d+", v)
26
+ if nums:
27
+ p.version_major = int(nums[0])
28
+ if len(nums) >= 2:
29
+ p.version_minor = int(nums[1])
30
+ if len(nums) >= 3:
31
+ p.version_patch = int(nums[2])
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
+ )
36
+
37
+ bld = FORTI_BUILD_RE.search(text)
38
+ if bld:
39
+ p.version_build = (p.version_build or "") + f"+build.{bld.group(1)}"
40
+ p.precision = "build"
41
+
42
+ img = FORTI_IMG_RE.search(text)
43
+ if img:
44
+ p.build_id = img.group(1)
45
+ p.precision = "build"
46
+
47
+ mdl = FORTI_MODEL_RE.search(text)
48
+ if mdl:
49
+ p.hw_model = mdl.group(1).replace("FortiGate-", "FG-")
50
+
51
+ ch = FORTI_CHANNEL_RE.search(text)
52
+ if ch:
53
+ p.channel = ch.group(1).upper()
54
+
55
+ update_confidence(p, p.precision if p.precision in ("build", "patch") else "minor")
56
+ return p
@@ -0,0 +1,38 @@
1
+ """Huawei VRP parsing."""
2
+
3
+ import re
4
+
5
+ from os_normalizer.helpers import update_confidence
6
+ from os_normalizer.models import OSData
7
+
8
+ HUAWEI_RE = re.compile(r"\bhuawei\b|\bvrp\b", re.IGNORECASE)
9
+ HUAWEI_VER_RE = re.compile(r"\bV(\d{3})R(\d{3})C(\d+)(SPC\d+)?\b", re.IGNORECASE)
10
+ HUAWEI_RAWVER_RE = re.compile(r"\bV\d{3}R\d{3}C\d+(?:SPC\d+)?\b", re.IGNORECASE)
11
+ HUAWEI_MODEL_RE = re.compile(r"\b(S\d{4}-\d{2}[A-Z-]+|CE\d{4}[A-Z-]*|AR\d{3,4}[A-Z-]*)\b", re.IGNORECASE)
12
+
13
+
14
+ def parse_huawei(text: str, p: OSData) -> OSData:
15
+ p.vendor = "Huawei"
16
+ p.product = "VRP"
17
+ p.family = p.family or "network-os"
18
+ p.kernel_name = "vrp"
19
+
20
+ raw = HUAWEI_RAWVER_RE.search(text)
21
+ if raw:
22
+ p.version_build = raw.group(0)
23
+
24
+ vm = HUAWEI_VER_RE.search(text)
25
+ if vm:
26
+ maj, r, _c = vm.group(1), vm.group(2), vm.group(3)
27
+ p.version_major = int(maj)
28
+ p.version_minor = int(r)
29
+ p.precision = "minor"
30
+
31
+ mdl = HUAWEI_MODEL_RE.search(text)
32
+ if mdl:
33
+ p.hw_model = mdl.group(1)
34
+
35
+ p.build_id = p.version_build or p.build_id
36
+
37
+ update_confidence(p, p.precision if p.precision in ("minor", "build") else "major")
38
+ return p
@@ -0,0 +1,42 @@
1
+ """Juniper Junos parsing."""
2
+
3
+ import re
4
+
5
+ from os_normalizer.helpers import update_confidence
6
+ from os_normalizer.models import OSData
7
+
8
+ JUNOS_RE = re.compile(r"\bjunos\b", re.IGNORECASE)
9
+ JUNOS_VER_RE = re.compile(r"\b(\d{1,2}\.\d{1,2}R\d+(?:-\w+\d+)?)\b", re.IGNORECASE)
10
+ JUNOS_PKG_RE = re.compile(r"\b(jinstall-[a-z0-9_.-]+\.tgz)\b", re.IGNORECASE)
11
+ JUNOS_MODEL_RE = re.compile(r"\b(EX\d{3,4}-\d{2}[A-Z]?|QFX\d{3,4}\w*|SRX\d{3,4}\w*|MX\d{2,3}\w*)\b", re.IGNORECASE)
12
+
13
+
14
+ def parse_juniper(text: str, p: OSData) -> OSData:
15
+ p.vendor = "Juniper"
16
+ p.product = "Junos"
17
+ p.family = p.family or "network-os"
18
+ p.kernel_name = "junos"
19
+
20
+ vm = JUNOS_VER_RE.search(text)
21
+ if vm:
22
+ ver = vm.group(1)
23
+ p.evidence["version_raw"] = ver
24
+ nums = re.findall(r"\d+", ver)
25
+ if nums:
26
+ p.version_major = int(nums[0])
27
+ if len(nums) >= 2:
28
+ p.version_minor = int(nums[1])
29
+ p.version_build = ver
30
+ p.precision = "minor"
31
+
32
+ pkg = JUNOS_PKG_RE.search(text)
33
+ if pkg:
34
+ p.build_id = pkg.group(1)
35
+ p.precision = "build"
36
+
37
+ mdl = JUNOS_MODEL_RE.search(text)
38
+ if mdl:
39
+ p.hw_model = mdl.group(1)
40
+
41
+ update_confidence(p, p.precision if p.precision in ("build", "minor") else "major")
42
+ return p
@@ -0,0 +1,41 @@
1
+ """Netgear firmware parsing."""
2
+
3
+ import re
4
+
5
+ from os_normalizer.helpers import update_confidence
6
+ from os_normalizer.models import OSData
7
+
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
+ )
12
+ NETGEAR_MODEL_RE = re.compile(r"\b([RN][0-9]{3,4}[A-Z]?)\b", re.IGNORECASE)
13
+
14
+
15
+ def parse_netgear(text: str, p: OSData) -> OSData:
16
+ p.vendor = "Netgear"
17
+ p.product = "Firmware"
18
+ p.family = p.family or "network-os"
19
+ p.kernel_name = "firmware"
20
+
21
+ vm = NETGEAR_VER_RE.search(text)
22
+ if vm:
23
+ v = vm.group(1)
24
+ nums = re.findall(r"\d+", v)
25
+ if nums:
26
+ p.version_major = int(nums[0])
27
+ if len(nums) >= 2:
28
+ p.version_minor = int(nums[1])
29
+ if len(nums) >= 3:
30
+ p.version_patch = int(nums[2])
31
+ 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
+ )
35
+
36
+ mdl = NETGEAR_MODEL_RE.search(text)
37
+ if mdl:
38
+ p.hw_model = mdl.group(1)
39
+
40
+ update_confidence(p, "minor" if p.precision == "major" else p.precision)
41
+ return p
@@ -0,0 +1,197 @@
1
+ """Windows specific parsing logic.
2
+
3
+ Refactored for clarity: vendor/edition detection, NT mapping, and build
4
+ mapping are handled by focused helpers. Behavior is preserved while
5
+ avoiding ambiguous NT mappings when server signals are present.
6
+ """
7
+
8
+ import re
9
+ from typing import Any, Optional
10
+
11
+ from os_normalizer.constants import (
12
+ WINDOWS_BUILD_MAP,
13
+ WINDOWS_NT_CLIENT_MAP,
14
+ WINDOWS_NT_SERVER_MAP,
15
+ )
16
+ from os_normalizer.helpers import update_confidence
17
+ from os_normalizer.models import OSData
18
+
19
+ # Regex patterns used only by the Windows parser
20
+ WIN_EDITION_RE = re.compile(
21
+ r"\b(professional|enterprise|home|education|ltsc|datacenter)\b",
22
+ re.IGNORECASE,
23
+ )
24
+ WIN_SP_RE = re.compile(r"\bSP\s?([0-9]+)\b", re.IGNORECASE)
25
+ WIN_BUILD_RE = re.compile(r"\bbuild\s?(\d{4,6})\b", re.IGNORECASE)
26
+ WIN_NT_RE = re.compile(r"\bnt\s?(\d+)\.(\d+)\b", re.IGNORECASE)
27
+ 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
+
30
+
31
+ def parse_windows(text: str, data: dict[str, Any], p: OSData) -> OSData:
32
+ """Populate an OSData instance with Windows-specific details."""
33
+ p.kernel_name = "nt"
34
+
35
+ # 1) Product and edition from free text
36
+ p.product = p.product or _detect_product_from_text(text.lower())
37
+ p.edition = p.edition or _detect_edition(text)
38
+
39
+ # 2) Service Pack
40
+ _parse_service_pack(text, p)
41
+
42
+ # 3) NT version mapping (client vs server)
43
+ server_like = _is_server_like(text.lower())
44
+ _apply_nt_mapping(text, p, server_like)
45
+
46
+ # 4) Full kernel version (e.g., 10.0.22621.2715) + channel token if present
47
+ _apply_full_kernel_and_channel(text, p)
48
+
49
+ # 5) Build number + marketing channel (fallback when only 'build 22631' is present)
50
+ _apply_build_mapping(text, p)
51
+
52
+ # 6) Precision and version_major if applicable
53
+ _finalize_precision_and_version(p)
54
+
55
+ # 7) Vendor + confidence
56
+ p.vendor = "Microsoft"
57
+ update_confidence(p, p.precision)
58
+ return p
59
+
60
+
61
+ def _detect_product_from_text(t: str) -> str:
62
+ if "windows 11" in t or "win11" in t:
63
+ return "Windows 11"
64
+ if "windows 10" in t or "win10" in t:
65
+ return "Windows 10"
66
+ if "windows 8.1" in t or "win81" in t:
67
+ return "Windows 8.1"
68
+ if "windows 8" in t or "win8" in t:
69
+ return "Windows 8"
70
+ if "windows 7" in t or "win7" in t:
71
+ return "Windows 7"
72
+ if "windows me" in t or "windows millenium" in t:
73
+ return "Windows ME"
74
+ if "windows 98" in t or "win98" in t:
75
+ return "Windows 98"
76
+
77
+ # Server explicit names
78
+ if "windows server 2022" in t or "win2k22" in t or "win2022" in t:
79
+ return "Windows Server 2022"
80
+ if "windows server 2019" in t or "win2k19" in t or "win2019" in t:
81
+ return "Windows Server 2019"
82
+ if "windows server 2016" in t or "win2k16" in t or "win2016" in t:
83
+ return "Windows Server 2016"
84
+ if "windows server 2012" in t or "win2k12" in t or "win2012" in t:
85
+ return "Windows Server 2012"
86
+ if "windows server 2008" in t or "win2k8" in t or "win2008" in t:
87
+ return "Windows Server 2008"
88
+ if "windows server 2003" in t or "win2k3" in t or "win2003" in t:
89
+ return "Windows Server 2003"
90
+ if "windows server 2000" in t or "win2k" in t or "win2000" in t:
91
+ return "Windows Server 2000"
92
+
93
+ if "windows" in t:
94
+ return "Windows"
95
+ return "Unknown"
96
+
97
+
98
+ def _detect_edition(text: str) -> str | None:
99
+ m = WIN_EDITION_RE.search(text)
100
+ return m.group(1).title() if m else None
101
+
102
+
103
+ def _parse_service_pack(text: str, p: OSData) -> None:
104
+ sp = WIN_SP_RE.search(text)
105
+ if sp:
106
+ p.version_patch = int(sp.group(1))
107
+ p.evidence["service_pack"] = sp.group(0)
108
+
109
+
110
+ def _is_server_like(t: str) -> bool:
111
+ return any(
112
+ kw in t
113
+ for kw in (
114
+ "server",
115
+ "datacenter",
116
+ "standard",
117
+ "essentials",
118
+ "foundation",
119
+ "core", # server core often appears
120
+ "hyper-v",
121
+ )
122
+ )
123
+
124
+
125
+ def _apply_nt_mapping(text: str, p: OSData, server_like: bool) -> None:
126
+ nt = WIN_NT_RE.search(text)
127
+ if not nt:
128
+ return
129
+ major, minor = int(nt.group(1)), int(nt.group(2))
130
+ p.evidence["nt_version"] = f"{major}.{minor}"
131
+
132
+ # If product already explicitly set (e.g., "Windows Server 2019"), keep it
133
+ if p.product and p.product not in ("Windows", "Windows 10/11"):
134
+ return
135
+
136
+ product = WINDOWS_NT_SERVER_MAP.get((major, minor)) if server_like else WINDOWS_NT_CLIENT_MAP.get((major, minor))
137
+ if product:
138
+ p.product = product
139
+
140
+
141
+ def _apply_build_mapping(text: str, p: OSData) -> None:
142
+ m = WIN_BUILD_RE.search(text)
143
+ if not m:
144
+ return
145
+ build_num = int(m.group(1))
146
+ p.version_build = str(build_num)
147
+
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
153
+
154
+ # Only apply client build mapping if current product isn't an explicit Server
155
+ is_server_product = isinstance(p.product, str) and "server" in p.product.lower()
156
+ if not is_server_product:
157
+ for lo, hi, product_name, marketing in WINDOWS_BUILD_MAP:
158
+ if lo <= build_num <= hi:
159
+ p.product = product_name
160
+ p.channel = marketing
161
+ break
162
+
163
+
164
+ def _apply_full_kernel_and_channel(text: str, p: OSData) -> None:
165
+ # Full NT kernel version, e.g., 10.0.22621.2715
166
+ m = WIN_FULL_NT_BUILD_RE.search(text)
167
+ if m:
168
+ build = m.group(3)
169
+ suffix = m.group(4)
170
+ p.version_build = p.version_build or build
171
+ p.kernel_version = f"10.0.{build}{('.' + suffix) if suffix else ''}"
172
+ # Record evidence for NT 10.0 if not set via NT mapping
173
+ p.evidence.setdefault("nt_version", "10.0")
174
+
175
+ # Marketing channel token in free text, e.g., 22H2 (case-insensitive)
176
+ ch = WIN_CHANNEL_RE.search(text)
177
+ if ch and not p.channel:
178
+ p.channel = ch.group(1).upper()
179
+
180
+
181
+ def _finalize_precision_and_version(p: OSData) -> None:
182
+ if p.version_build:
183
+ p.precision = "build"
184
+ return
185
+ if p.version_patch is not None:
186
+ p.precision = "patch"
187
+ return
188
+ if p.product and any(x in p.product for x in ("7", "8", "10", "11")):
189
+ digits = re.findall(r"\d+", p.product)
190
+ if digits:
191
+ p.version_major = int(digits[0])
192
+ p.precision = "major"
193
+ return
194
+ if p.product:
195
+ p.precision = "product"
196
+ else:
197
+ p.precision = "family"