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.
- os_normalizer/__init__.py +10 -0
- os_normalizer/constants.py +87 -0
- os_normalizer/cpe.py +265 -0
- os_normalizer/helpers.py +107 -0
- os_normalizer/models.py +159 -0
- os_normalizer/os_normalizer.py +313 -0
- os_normalizer/parsers/__init__.py +16 -0
- os_normalizer/parsers/bsd.py +69 -0
- os_normalizer/parsers/linux.py +121 -0
- os_normalizer/parsers/macos.py +111 -0
- os_normalizer/parsers/mobile.py +37 -0
- os_normalizer/parsers/network/__init__.py +61 -0
- os_normalizer/parsers/network/cisco.py +96 -0
- os_normalizer/parsers/network/fortinet.py +56 -0
- os_normalizer/parsers/network/huawei.py +38 -0
- os_normalizer/parsers/network/juniper.py +42 -0
- os_normalizer/parsers/network/netgear.py +41 -0
- os_normalizer/parsers/windows.py +197 -0
- os_normalizer-0.3.2.dist-info/METADATA +172 -0
- os_normalizer-0.3.2.dist-info/RECORD +22 -0
- os_normalizer-0.3.2.dist-info/WHEEL +4 -0
- os_normalizer-0.3.2.dist-info/licenses/LICENSE +21 -0
|
@@ -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"
|