os-normalizer 0.4.3__py3-none-any.whl → 0.5.0__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/constants.py +139 -2
- os_normalizer/cpe.py +57 -12
- os_normalizer/helpers.py +26 -15
- os_normalizer/models.py +19 -9
- os_normalizer/os_normalizer.py +76 -49
- os_normalizer/parsers/__init__.py +7 -3
- os_normalizer/parsers/bsd.py +2 -1
- os_normalizer/parsers/esxi.py +83 -0
- os_normalizer/parsers/linux.py +6 -5
- os_normalizer/parsers/macos.py +12 -14
- os_normalizer/parsers/mobile.py +23 -5
- os_normalizer/parsers/network/__init__.py +5 -1
- os_normalizer/parsers/network/cisco.py +13 -7
- os_normalizer/parsers/network/fortinet.py +15 -5
- os_normalizer/parsers/network/huawei.py +9 -3
- os_normalizer/parsers/network/juniper.py +10 -4
- os_normalizer/parsers/network/netgear.py +10 -3
- os_normalizer/parsers/solaris.py +100 -0
- os_normalizer/parsers/windows.py +7 -6
- {os_normalizer-0.4.3.dist-info → os_normalizer-0.5.0.dist-info}/METADATA +2 -1
- os_normalizer-0.5.0.dist-info/RECORD +24 -0
- os_normalizer-0.4.3.dist-info/RECORD +0 -22
- {os_normalizer-0.4.3.dist-info → os_normalizer-0.5.0.dist-info}/WHEEL +0 -0
- {os_normalizer-0.4.3.dist-info → os_normalizer-0.5.0.dist-info}/licenses/LICENSE +0 -0
os_normalizer/parsers/bsd.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from os_normalizer.constants import PrecisionLevel
|
|
6
7
|
from os_normalizer.helpers import (
|
|
7
8
|
parse_semver_like,
|
|
8
9
|
precision_from_parts,
|
|
@@ -48,7 +49,7 @@ def parse_bsd(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
|
48
49
|
# Prefer variant-anchored version pattern; fall back to generic semver
|
|
49
50
|
x, y, z = _extract_version(text)
|
|
50
51
|
p.version_major, p.version_minor, p.version_patch = x, y, z
|
|
51
|
-
p.precision = precision_from_parts(x, y, z, None) if x else
|
|
52
|
+
p.precision = precision_from_parts(x, y, z, None) if x else PrecisionLevel.PRODUCT
|
|
52
53
|
|
|
53
54
|
# Channel from explicit markers/suffixes
|
|
54
55
|
ch = BSD_CHANNEL_RE.search(text)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""VMware ESXi specific parsing logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from os_normalizer.constants import PrecisionLevel
|
|
9
|
+
from os_normalizer.helpers import parse_semver_like, precision_from_parts, update_confidence
|
|
10
|
+
from os_normalizer.models import OSData
|
|
11
|
+
|
|
12
|
+
ESXI_PRODUCT_RE = re.compile(
|
|
13
|
+
r"VMware\s+ESXi\s+(\d+(?:\.\d+){1,3})(?:\s+(?:build|Build)\s*[-#]?(\d+))?",
|
|
14
|
+
re.IGNORECASE,
|
|
15
|
+
)
|
|
16
|
+
VMKERNEL_RE = re.compile(
|
|
17
|
+
r"VMkernel\s+\S+\s+(\d+(?:\.\d+){1,3})(?:\s+#(\d+))?",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
ESXCLI_VERSION_RE = re.compile(r"^Version:\s*(\d+(?:\.\d+){1,3})\s*$", re.IGNORECASE | re.MULTILINE)
|
|
21
|
+
ESXCLI_BUILD_RE = re.compile(r"^Build:\s*(\d+)\s*$", re.IGNORECASE | re.MULTILINE)
|
|
22
|
+
ESXCLI_UPDATE_RE = re.compile(r"^Update:\s*(.+?)\s*$", re.IGNORECASE | re.MULTILINE)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_esxi(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
26
|
+
"""Populate an OSData instance with ESXi-specific details."""
|
|
27
|
+
p.vendor = p.vendor or "VMware"
|
|
28
|
+
p.product = p.product or "VMware ESXi"
|
|
29
|
+
p.kernel_name = "vmkernel"
|
|
30
|
+
|
|
31
|
+
version: str | None = None
|
|
32
|
+
build: str | None = None
|
|
33
|
+
channel: str | None = None
|
|
34
|
+
|
|
35
|
+
prod_match = ESXI_PRODUCT_RE.search(text)
|
|
36
|
+
if prod_match:
|
|
37
|
+
version = prod_match.group(1)
|
|
38
|
+
build = prod_match.group(2) or build
|
|
39
|
+
|
|
40
|
+
kernel_match = VMKERNEL_RE.search(text)
|
|
41
|
+
if kernel_match:
|
|
42
|
+
version = version or kernel_match.group(1)
|
|
43
|
+
build = build or kernel_match.group(2)
|
|
44
|
+
|
|
45
|
+
version_line = ESXCLI_VERSION_RE.search(text)
|
|
46
|
+
if version_line and not version:
|
|
47
|
+
version = version_line.group(1)
|
|
48
|
+
|
|
49
|
+
build_line = ESXCLI_BUILD_RE.search(text)
|
|
50
|
+
if build_line and not build:
|
|
51
|
+
build = build_line.group(1)
|
|
52
|
+
|
|
53
|
+
update_line = ESXCLI_UPDATE_RE.search(text)
|
|
54
|
+
if update_line:
|
|
55
|
+
channel = update_line.group(1).strip()
|
|
56
|
+
|
|
57
|
+
if build:
|
|
58
|
+
p.version_build = build
|
|
59
|
+
|
|
60
|
+
if channel:
|
|
61
|
+
# Normalise "3" -> "Update 3", but keep existing text if already descriptive
|
|
62
|
+
if channel.isdigit():
|
|
63
|
+
p.channel = f"Update {channel}"
|
|
64
|
+
elif channel.lower().startswith("update"):
|
|
65
|
+
p.channel = channel
|
|
66
|
+
else:
|
|
67
|
+
p.channel = channel
|
|
68
|
+
|
|
69
|
+
if version:
|
|
70
|
+
p.kernel_version = version
|
|
71
|
+
major, minor, patch = parse_semver_like(version)
|
|
72
|
+
if major is not None:
|
|
73
|
+
p.version_major = major
|
|
74
|
+
if minor is not None:
|
|
75
|
+
p.version_minor = minor
|
|
76
|
+
if patch is not None:
|
|
77
|
+
p.version_patch = patch
|
|
78
|
+
p.precision = precision_from_parts(p.version_major, p.version_minor, p.version_patch, p.version_build)
|
|
79
|
+
else:
|
|
80
|
+
p.precision = PrecisionLevel.PRODUCT
|
|
81
|
+
|
|
82
|
+
update_confidence(p, p.precision)
|
|
83
|
+
return p
|
os_normalizer/parsers/linux.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
6
|
+
from os_normalizer.constants import PrecisionLevel
|
|
6
7
|
from os_normalizer.helpers import parse_os_release, update_confidence
|
|
7
8
|
from os_normalizer.models import OSData
|
|
8
9
|
|
|
@@ -31,7 +32,7 @@ def parse_linux(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
|
31
32
|
_apply_os_release(osrel, p)
|
|
32
33
|
else:
|
|
33
34
|
p.product = p.product or "Linux"
|
|
34
|
-
p.precision =
|
|
35
|
+
p.precision = PrecisionLevel.FAMILY
|
|
35
36
|
|
|
36
37
|
update_confidence(p, p.precision)
|
|
37
38
|
return p
|
|
@@ -92,13 +93,13 @@ def _apply_os_release(osrel: dict[str, Any], p: OSData) -> None:
|
|
|
92
93
|
|
|
93
94
|
# Precision from version parts
|
|
94
95
|
if p.version_patch is not None:
|
|
95
|
-
p.precision =
|
|
96
|
+
p.precision = PrecisionLevel.PATCH
|
|
96
97
|
elif p.version_minor is not None:
|
|
97
|
-
p.precision =
|
|
98
|
+
p.precision = PrecisionLevel.MINOR
|
|
98
99
|
elif p.version_major is not None:
|
|
99
|
-
p.precision =
|
|
100
|
+
p.precision = PrecisionLevel.MAJOR
|
|
100
101
|
else:
|
|
101
|
-
p.precision =
|
|
102
|
+
p.precision = PrecisionLevel.FAMILY
|
|
102
103
|
|
|
103
104
|
|
|
104
105
|
def _apply_version_id(vid: Any, p: OSData) -> None:
|
os_normalizer/parsers/macos.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from os_normalizer.constants import MACOS_ALIASES, MACOS_DARWIN_MAP
|
|
6
|
+
from os_normalizer.constants import MACOS_ALIASES, MACOS_DARWIN_MAP, PRECISION_ORDER, PrecisionLevel
|
|
7
7
|
from os_normalizer.helpers import update_confidence
|
|
8
8
|
from os_normalizer.models import OSData
|
|
9
9
|
|
|
@@ -14,10 +14,6 @@ DARWIN_RE = re.compile(
|
|
|
14
14
|
)
|
|
15
15
|
MACOS_VER_FALLBACK_RE = re.compile(r"\bmacos\s?(\d+)(?:\.(\d+))?", re.IGNORECASE)
|
|
16
16
|
|
|
17
|
-
# Local precision order for simple comparisons
|
|
18
|
-
_PRECISION_ORDER = {"family": 0, "product": 1, "major": 2, "minor": 3, "patch": 4, "build": 5}
|
|
19
|
-
|
|
20
|
-
|
|
21
17
|
def parse_macos(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
22
18
|
"""Populate an OSData instance with macOS-specific details."""
|
|
23
19
|
t = text
|
|
@@ -50,7 +46,7 @@ def _apply_alias_hint(tl: str, p: OSData) -> None:
|
|
|
50
46
|
parts = normalized.split()
|
|
51
47
|
if len(parts) == 2 and parts[1].isdigit():
|
|
52
48
|
p.version_major = int(parts[1])
|
|
53
|
-
p.precision = _max_precision(p.precision,
|
|
49
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
|
|
54
50
|
|
|
55
51
|
|
|
56
52
|
def _apply_darwin_mapping(t: str, p: OSData) -> None:
|
|
@@ -66,12 +62,12 @@ def _apply_darwin_mapping(t: str, p: OSData) -> None:
|
|
|
66
62
|
p.product = prod
|
|
67
63
|
if ver.isdigit():
|
|
68
64
|
p.version_major = int(ver)
|
|
69
|
-
p.precision = _max_precision(p.precision,
|
|
65
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
|
|
70
66
|
else:
|
|
71
67
|
x, y, *_ = ver.split(".")
|
|
72
68
|
p.version_major = int(x)
|
|
73
69
|
p.version_minor = int(y)
|
|
74
|
-
p.precision = _max_precision(p.precision,
|
|
70
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MINOR)
|
|
75
71
|
p.codename = code
|
|
76
72
|
|
|
77
73
|
|
|
@@ -84,9 +80,9 @@ def _apply_version_fallback(t: str, p: OSData) -> None:
|
|
|
84
80
|
p.version_major = int(mm.group(1))
|
|
85
81
|
if mm.group(2):
|
|
86
82
|
p.version_minor = int(mm.group(2))
|
|
87
|
-
p.precision = _max_precision(p.precision,
|
|
83
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MINOR)
|
|
88
84
|
else:
|
|
89
|
-
p.precision = _max_precision(p.precision,
|
|
85
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
|
|
90
86
|
|
|
91
87
|
|
|
92
88
|
def _apply_codename_fallback(tl: str, p: OSData) -> None:
|
|
@@ -98,14 +94,16 @@ def _apply_codename_fallback(tl: str, p: OSData) -> None:
|
|
|
98
94
|
# Provide at least major version from the map
|
|
99
95
|
if isinstance(ver, str) and ver.isdigit():
|
|
100
96
|
p.version_major = int(ver)
|
|
101
|
-
p.precision = _max_precision(p.precision,
|
|
97
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
|
|
102
98
|
elif isinstance(ver, str) and "." in ver:
|
|
103
99
|
x, *_ = ver.split(".")
|
|
104
100
|
if x.isdigit():
|
|
105
101
|
p.version_major = int(x)
|
|
106
|
-
p.precision = _max_precision(p.precision,
|
|
102
|
+
p.precision = _max_precision(p.precision, PrecisionLevel.MAJOR)
|
|
107
103
|
break
|
|
108
104
|
|
|
109
105
|
|
|
110
|
-
def _max_precision(current:
|
|
111
|
-
|
|
106
|
+
def _max_precision(current: PrecisionLevel, new_label: PrecisionLevel) -> PrecisionLevel:
|
|
107
|
+
if not isinstance(current, PrecisionLevel):
|
|
108
|
+
current = PrecisionLevel(current)
|
|
109
|
+
return new_label if PRECISION_ORDER.get(new_label, 0) > PRECISION_ORDER.get(current, 0) else current
|
os_normalizer/parsers/mobile.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Mobile device specific parsing logic."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
6
|
+
from os_normalizer.constants import OSFamily, PrecisionLevel
|
|
5
7
|
from os_normalizer.helpers import (
|
|
6
8
|
parse_semver_like,
|
|
7
9
|
precision_from_parts,
|
|
@@ -14,11 +16,21 @@ def parse_mobile(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
|
14
16
|
"""Populate an OSData instance with mobile device-specific details."""
|
|
15
17
|
t = text.lower()
|
|
16
18
|
|
|
19
|
+
# Detect if it's HarmonyOS before other mobile platforms to avoid vendor overlaps
|
|
20
|
+
if OSFamily.HARMONYOS.value in t:
|
|
21
|
+
p.product = "HarmonyOS"
|
|
22
|
+
p.vendor = "Huawei"
|
|
23
|
+
m = re.search(r"\b(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?\b", text)
|
|
24
|
+
if m:
|
|
25
|
+
p.version_major = int(m.group(1))
|
|
26
|
+
p.version_minor = int(m.group(2)) if m.group(2) else None
|
|
27
|
+
p.version_patch = int(m.group(3)) if m.group(3) else None
|
|
28
|
+
p.version_build = m.group(4) if m.group(4) else None
|
|
17
29
|
# Detect if it's iOS or Android
|
|
18
|
-
|
|
30
|
+
elif OSFamily.IOS.value in t or "ipados" in t:
|
|
19
31
|
p.product = "iOS/iPadOS"
|
|
20
32
|
p.vendor = "Apple"
|
|
21
|
-
elif
|
|
33
|
+
elif OSFamily.ANDROID.value in t:
|
|
22
34
|
p.product = "Android"
|
|
23
35
|
p.vendor = "Google"
|
|
24
36
|
else:
|
|
@@ -27,9 +39,15 @@ def parse_mobile(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
|
27
39
|
p.vendor = None
|
|
28
40
|
|
|
29
41
|
# Extract version info using semver-like parsing
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
if p.version_major is None:
|
|
43
|
+
x, y, z = parse_semver_like(t)
|
|
44
|
+
p.version_major, p.version_minor, p.version_patch = x, y, z
|
|
45
|
+
|
|
46
|
+
p.precision = (
|
|
47
|
+
precision_from_parts(p.version_major, p.version_minor, p.version_patch, p.version_build)
|
|
48
|
+
if p.version_major is not None
|
|
49
|
+
else PrecisionLevel.PRODUCT
|
|
50
|
+
)
|
|
33
51
|
|
|
34
52
|
# Boost confidence based on precision
|
|
35
53
|
update_confidence(p, p.precision)
|
|
@@ -15,6 +15,7 @@ from .fortinet import FORTI_RE, parse_fortinet
|
|
|
15
15
|
from .huawei import HUAWEI_RE, parse_huawei
|
|
16
16
|
from .netgear import NETGEAR_RE, parse_netgear
|
|
17
17
|
|
|
18
|
+
from os_normalizer.constants import OSFamily, PrecisionLevel
|
|
18
19
|
from os_normalizer.models import OSData
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
@@ -57,5 +58,8 @@ def parse_network(text: str, data: dict | None, p: OSData) -> OSData:
|
|
|
57
58
|
# Unknown network vendor; keep coarse
|
|
58
59
|
p.vendor = p.vendor or "Unknown-Network"
|
|
59
60
|
p.product = p.product or "Network OS"
|
|
60
|
-
p.
|
|
61
|
+
if not isinstance(p.family, OSFamily):
|
|
62
|
+
p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
|
|
63
|
+
p.family = p.family or OSFamily.NETWORK
|
|
64
|
+
p.precision = PrecisionLevel.FAMILY
|
|
61
65
|
return p
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from os_normalizer.constants import CISCO_TRAIN_NAMES, OSFamily, PrecisionLevel
|
|
5
6
|
from os_normalizer.helpers import update_confidence
|
|
6
7
|
from os_normalizer.models import OSData
|
|
7
8
|
|
|
@@ -26,7 +27,9 @@ CISCO_EDITION_RE = re.compile(
|
|
|
26
27
|
|
|
27
28
|
def parse_cisco(text: str, p: OSData) -> OSData:
|
|
28
29
|
p.vendor = "Cisco"
|
|
29
|
-
|
|
30
|
+
if not isinstance(p.family, OSFamily):
|
|
31
|
+
p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
|
|
32
|
+
p.family = p.family or OSFamily.NETWORK
|
|
30
33
|
|
|
31
34
|
# Detect product line
|
|
32
35
|
if CISCO_IOS_XE_RE.search(text):
|
|
@@ -53,14 +56,16 @@ def parse_cisco(text: str, p: OSData) -> OSData:
|
|
|
53
56
|
p.version_patch = int(num[2])
|
|
54
57
|
p.version_build = ver
|
|
55
58
|
p.precision = (
|
|
56
|
-
|
|
59
|
+
PrecisionLevel.PATCH
|
|
60
|
+
if p.version_patch is not None
|
|
61
|
+
else (PrecisionLevel.MINOR if p.version_minor is not None else PrecisionLevel.MAJOR)
|
|
57
62
|
)
|
|
58
63
|
|
|
59
64
|
# Image filename
|
|
60
65
|
img = CISCO_IMAGE_RE.search(text)
|
|
61
66
|
if img:
|
|
62
67
|
p.build_id = img.group(1)
|
|
63
|
-
p.precision =
|
|
68
|
+
p.precision = PrecisionLevel.BUILD
|
|
64
69
|
|
|
65
70
|
# If NX-OS and only got version via filename, parse nxos.A.B.C.bin
|
|
66
71
|
if not p.version_major and p.build_id:
|
|
@@ -70,7 +75,7 @@ def parse_cisco(text: str, p: OSData) -> OSData:
|
|
|
70
75
|
p.version_minor = int(m.group(2))
|
|
71
76
|
p.version_patch = int(m.group(3))
|
|
72
77
|
p.version_build = f"{p.version_major}.{p.version_minor}.{p.version_patch}"
|
|
73
|
-
p.precision =
|
|
78
|
+
p.precision = PrecisionLevel.PATCH
|
|
74
79
|
|
|
75
80
|
# Model
|
|
76
81
|
mm = CISCO_MODEL_RE.search(text)
|
|
@@ -83,8 +88,6 @@ def parse_cisco(text: str, p: OSData) -> OSData:
|
|
|
83
88
|
p.edition = fl.group(1).lower()
|
|
84
89
|
|
|
85
90
|
# Train codename
|
|
86
|
-
from os_normalizer.constants import CISCO_TRAIN_NAMES
|
|
87
|
-
|
|
88
91
|
tl = text.lower()
|
|
89
92
|
for train in CISCO_TRAIN_NAMES:
|
|
90
93
|
if train.lower() in tl:
|
|
@@ -92,5 +95,8 @@ def parse_cisco(text: str, p: OSData) -> OSData:
|
|
|
92
95
|
break
|
|
93
96
|
|
|
94
97
|
# Boost confidence based on precision
|
|
95
|
-
update_confidence(
|
|
98
|
+
update_confidence(
|
|
99
|
+
p,
|
|
100
|
+
p.precision if p.precision in (PrecisionLevel.BUILD, PrecisionLevel.PATCH) else PrecisionLevel.MINOR,
|
|
101
|
+
)
|
|
96
102
|
return p
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from os_normalizer.constants import OSFamily, PrecisionLevel
|
|
5
6
|
from os_normalizer.helpers import update_confidence
|
|
6
7
|
from os_normalizer.models import OSData
|
|
7
8
|
|
|
@@ -16,7 +17,9 @@ FORTI_CHANNEL_RE = re.compile(r"\((GA|Patch|Beta)\)", re.IGNORECASE)
|
|
|
16
17
|
def parse_fortinet(text: str, p: OSData) -> OSData:
|
|
17
18
|
p.vendor = "Fortinet"
|
|
18
19
|
p.product = "FortiOS"
|
|
19
|
-
|
|
20
|
+
if not isinstance(p.family, OSFamily):
|
|
21
|
+
p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
|
|
22
|
+
p.family = p.family or OSFamily.NETWORK
|
|
20
23
|
p.kernel_name = "fortios"
|
|
21
24
|
|
|
22
25
|
ver = FORTI_VER_RE.search(text)
|
|
@@ -30,17 +33,21 @@ def parse_fortinet(text: str, p: OSData) -> OSData:
|
|
|
30
33
|
if len(nums) >= 3:
|
|
31
34
|
p.version_patch = int(nums[2])
|
|
32
35
|
p.version_build = v
|
|
33
|
-
p.precision =
|
|
36
|
+
p.precision = (
|
|
37
|
+
PrecisionLevel.PATCH
|
|
38
|
+
if p.version_patch is not None
|
|
39
|
+
else (PrecisionLevel.MINOR if p.version_minor is not None else PrecisionLevel.MAJOR)
|
|
40
|
+
)
|
|
34
41
|
|
|
35
42
|
bld = FORTI_BUILD_RE.search(text)
|
|
36
43
|
if bld:
|
|
37
44
|
p.version_build = (p.version_build or "") + f"+build.{bld.group(1)}"
|
|
38
|
-
p.precision =
|
|
45
|
+
p.precision = PrecisionLevel.BUILD
|
|
39
46
|
|
|
40
47
|
img = FORTI_IMG_RE.search(text)
|
|
41
48
|
if img:
|
|
42
49
|
p.build_id = img.group(1)
|
|
43
|
-
p.precision =
|
|
50
|
+
p.precision = PrecisionLevel.BUILD
|
|
44
51
|
|
|
45
52
|
mdl = FORTI_MODEL_RE.search(text)
|
|
46
53
|
if mdl:
|
|
@@ -50,5 +57,8 @@ def parse_fortinet(text: str, p: OSData) -> OSData:
|
|
|
50
57
|
if ch:
|
|
51
58
|
p.channel = ch.group(1).upper()
|
|
52
59
|
|
|
53
|
-
update_confidence(
|
|
60
|
+
update_confidence(
|
|
61
|
+
p,
|
|
62
|
+
p.precision if p.precision in (PrecisionLevel.BUILD, PrecisionLevel.PATCH) else PrecisionLevel.MINOR,
|
|
63
|
+
)
|
|
54
64
|
return p
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from os_normalizer.constants import OSFamily, PrecisionLevel
|
|
5
6
|
from os_normalizer.helpers import update_confidence
|
|
6
7
|
from os_normalizer.models import OSData
|
|
7
8
|
|
|
@@ -14,7 +15,9 @@ HUAWEI_MODEL_RE = re.compile(r"\b(S\d{4}-\d{2}[A-Z-]+|CE\d{4}[A-Z-]*|AR\d{3,4}[A
|
|
|
14
15
|
def parse_huawei(text: str, p: OSData) -> OSData:
|
|
15
16
|
p.vendor = "Huawei"
|
|
16
17
|
p.product = "VRP"
|
|
17
|
-
|
|
18
|
+
if not isinstance(p.family, OSFamily):
|
|
19
|
+
p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
|
|
20
|
+
p.family = p.family or OSFamily.NETWORK
|
|
18
21
|
p.kernel_name = "vrp"
|
|
19
22
|
|
|
20
23
|
raw = HUAWEI_RAWVER_RE.search(text)
|
|
@@ -26,7 +29,7 @@ def parse_huawei(text: str, p: OSData) -> OSData:
|
|
|
26
29
|
maj, r, _c = vm.group(1), vm.group(2), vm.group(3)
|
|
27
30
|
p.version_major = int(maj)
|
|
28
31
|
p.version_minor = int(r)
|
|
29
|
-
p.precision =
|
|
32
|
+
p.precision = PrecisionLevel.MINOR
|
|
30
33
|
|
|
31
34
|
mdl = HUAWEI_MODEL_RE.search(text)
|
|
32
35
|
if mdl:
|
|
@@ -34,5 +37,8 @@ def parse_huawei(text: str, p: OSData) -> OSData:
|
|
|
34
37
|
|
|
35
38
|
p.build_id = p.version_build or p.build_id
|
|
36
39
|
|
|
37
|
-
update_confidence(
|
|
40
|
+
update_confidence(
|
|
41
|
+
p,
|
|
42
|
+
p.precision if p.precision in (PrecisionLevel.MINOR, PrecisionLevel.BUILD) else PrecisionLevel.MAJOR,
|
|
43
|
+
)
|
|
38
44
|
return p
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from os_normalizer.constants import OSFamily, PrecisionLevel
|
|
5
6
|
from os_normalizer.helpers import update_confidence
|
|
6
7
|
from os_normalizer.models import OSData
|
|
7
8
|
|
|
@@ -14,7 +15,9 @@ JUNOS_MODEL_RE = re.compile(r"\b(EX\d{3,4}-\d{2}[A-Z]?|QFX\d{3,4}\w*|SRX\d{3,4}\
|
|
|
14
15
|
def parse_juniper(text: str, p: OSData) -> OSData:
|
|
15
16
|
p.vendor = "Juniper"
|
|
16
17
|
p.product = "Junos"
|
|
17
|
-
|
|
18
|
+
if not isinstance(p.family, OSFamily):
|
|
19
|
+
p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
|
|
20
|
+
p.family = p.family or OSFamily.NETWORK
|
|
18
21
|
p.kernel_name = "junos"
|
|
19
22
|
|
|
20
23
|
vm = JUNOS_VER_RE.search(text)
|
|
@@ -27,16 +30,19 @@ def parse_juniper(text: str, p: OSData) -> OSData:
|
|
|
27
30
|
if len(nums) >= 2:
|
|
28
31
|
p.version_minor = int(nums[1])
|
|
29
32
|
p.version_build = ver
|
|
30
|
-
p.precision =
|
|
33
|
+
p.precision = PrecisionLevel.MINOR
|
|
31
34
|
|
|
32
35
|
pkg = JUNOS_PKG_RE.search(text)
|
|
33
36
|
if pkg:
|
|
34
37
|
p.build_id = pkg.group(1)
|
|
35
|
-
p.precision =
|
|
38
|
+
p.precision = PrecisionLevel.BUILD
|
|
36
39
|
|
|
37
40
|
mdl = JUNOS_MODEL_RE.search(text)
|
|
38
41
|
if mdl:
|
|
39
42
|
p.hw_model = mdl.group(1)
|
|
40
43
|
|
|
41
|
-
update_confidence(
|
|
44
|
+
update_confidence(
|
|
45
|
+
p,
|
|
46
|
+
p.precision if p.precision in (PrecisionLevel.BUILD, PrecisionLevel.MINOR) else PrecisionLevel.MAJOR,
|
|
47
|
+
)
|
|
42
48
|
return p
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from os_normalizer.constants import OSFamily, PrecisionLevel
|
|
5
6
|
from os_normalizer.helpers import update_confidence
|
|
6
7
|
from os_normalizer.models import OSData
|
|
7
8
|
|
|
@@ -13,7 +14,9 @@ NETGEAR_MODEL_RE = re.compile(r"\b([RN][0-9]{3,4}[A-Z]?)\b", re.IGNORECASE)
|
|
|
13
14
|
def parse_netgear(text: str, p: OSData) -> OSData:
|
|
14
15
|
p.vendor = "Netgear"
|
|
15
16
|
p.product = "Firmware"
|
|
16
|
-
|
|
17
|
+
if not isinstance(p.family, OSFamily):
|
|
18
|
+
p.family = OSFamily(p.family) if p.family in OSFamily._value2member_map_ else None
|
|
19
|
+
p.family = p.family or OSFamily.NETWORK
|
|
17
20
|
p.kernel_name = "firmware"
|
|
18
21
|
|
|
19
22
|
vm = NETGEAR_VER_RE.search(text)
|
|
@@ -27,11 +30,15 @@ def parse_netgear(text: str, p: OSData) -> OSData:
|
|
|
27
30
|
if len(nums) >= 3:
|
|
28
31
|
p.version_patch = int(nums[2])
|
|
29
32
|
p.version_build = v
|
|
30
|
-
p.precision =
|
|
33
|
+
p.precision = (
|
|
34
|
+
PrecisionLevel.PATCH
|
|
35
|
+
if p.version_patch is not None
|
|
36
|
+
else (PrecisionLevel.MINOR if p.version_minor is not None else PrecisionLevel.MAJOR)
|
|
37
|
+
)
|
|
31
38
|
|
|
32
39
|
mdl = NETGEAR_MODEL_RE.search(text)
|
|
33
40
|
if mdl:
|
|
34
41
|
p.hw_model = mdl.group(1)
|
|
35
42
|
|
|
36
|
-
update_confidence(p,
|
|
43
|
+
update_confidence(p, PrecisionLevel.MINOR if p.precision == PrecisionLevel.MAJOR else p.precision)
|
|
37
44
|
return p
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Solaris specific parsing logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from os_normalizer.constants import PrecisionLevel
|
|
9
|
+
from os_normalizer.helpers import precision_from_parts, update_confidence
|
|
10
|
+
from os_normalizer.models import OSData
|
|
11
|
+
|
|
12
|
+
SUNOS_UNAME_RE = re.compile(
|
|
13
|
+
r"SunOS\s+\S+\s+(\d+(?:\.\d+)+)(?:\s+(\d+(?:\.\d+){1,4}))?",
|
|
14
|
+
re.IGNORECASE,
|
|
15
|
+
)
|
|
16
|
+
SOLARIS_RELEASE_RE = re.compile(
|
|
17
|
+
r"(?:Oracle\s+)?Solaris\s+(\d+(?:\.\d+){0,4})",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
GENERIC_BUILD_RE = re.compile(r"\bGeneric_(\S+)", re.IGNORECASE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_solaris(text: str, data: dict[str, Any], p: OSData) -> OSData:
|
|
24
|
+
"""Populate an OSData instance with Solaris-specific details."""
|
|
25
|
+
# Baseline identity
|
|
26
|
+
p.vendor = p.vendor or "Oracle"
|
|
27
|
+
p.product = p.product or "Oracle Solaris"
|
|
28
|
+
p.kernel_name = "sunos"
|
|
29
|
+
|
|
30
|
+
kernel_version: str | None = None
|
|
31
|
+
release_version: str | None = None
|
|
32
|
+
|
|
33
|
+
# Extract version information from uname-style lines
|
|
34
|
+
uname_match = SUNOS_UNAME_RE.search(text)
|
|
35
|
+
if uname_match:
|
|
36
|
+
kernel_version = uname_match.group(1)
|
|
37
|
+
release_version = uname_match.group(2) or release_version
|
|
38
|
+
|
|
39
|
+
# /etc/release style information
|
|
40
|
+
release_match = SOLARIS_RELEASE_RE.search(text)
|
|
41
|
+
if release_match:
|
|
42
|
+
release_version = release_match.group(1)
|
|
43
|
+
|
|
44
|
+
# Normalise kernel version token if present
|
|
45
|
+
if kernel_version:
|
|
46
|
+
p.kernel_version = kernel_version
|
|
47
|
+
|
|
48
|
+
# Use the release token if available; fall back to kernel version
|
|
49
|
+
version_source = release_version or kernel_version
|
|
50
|
+
|
|
51
|
+
version_build: str | None = None
|
|
52
|
+
major: int | None = None
|
|
53
|
+
minor: int | None = None
|
|
54
|
+
patch: int | None = None
|
|
55
|
+
|
|
56
|
+
if version_source:
|
|
57
|
+
major, minor, patch, version_build = _split_solaris_version(version_source)
|
|
58
|
+
|
|
59
|
+
# Collect Generic_ build tags and prefer non-empty value
|
|
60
|
+
build_match = GENERIC_BUILD_RE.search(text)
|
|
61
|
+
if build_match:
|
|
62
|
+
version_build = version_build or build_match.group(1)
|
|
63
|
+
|
|
64
|
+
if major is not None:
|
|
65
|
+
p.version_major = major
|
|
66
|
+
if minor is not None:
|
|
67
|
+
p.version_minor = minor
|
|
68
|
+
if patch is not None:
|
|
69
|
+
p.version_patch = patch
|
|
70
|
+
if version_build:
|
|
71
|
+
p.version_build = version_build
|
|
72
|
+
|
|
73
|
+
if version_source:
|
|
74
|
+
p.precision = precision_from_parts(p.version_major, p.version_minor, p.version_patch, p.version_build)
|
|
75
|
+
else:
|
|
76
|
+
p.precision = PrecisionLevel.PRODUCT
|
|
77
|
+
|
|
78
|
+
update_confidence(p, p.precision)
|
|
79
|
+
return p
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _split_solaris_version(version: str) -> tuple[int | None, int | None, int | None, str | None]:
|
|
83
|
+
"""Convert Solaris version tokens into (major, minor, patch, build)."""
|
|
84
|
+
parts = [int(token) for token in re.findall(r"\d+", version)]
|
|
85
|
+
if not parts:
|
|
86
|
+
return None, None, None, None
|
|
87
|
+
|
|
88
|
+
if parts[0] == 5 and len(parts) >= 2:
|
|
89
|
+
# SunOS 5.x maps to Solaris x
|
|
90
|
+
major = parts[1]
|
|
91
|
+
remainder = parts[2:]
|
|
92
|
+
else:
|
|
93
|
+
major = parts[0]
|
|
94
|
+
remainder = parts[1:]
|
|
95
|
+
|
|
96
|
+
minor = remainder[0] if len(remainder) >= 1 else None
|
|
97
|
+
patch = remainder[1] if len(remainder) >= 2 else None
|
|
98
|
+
extra = remainder[2:] if len(remainder) >= 3 else []
|
|
99
|
+
build = ".".join(str(x) for x in extra) if extra else None
|
|
100
|
+
return major, minor, patch, build
|
os_normalizer/parsers/windows.py
CHANGED
|
@@ -12,6 +12,7 @@ from os_normalizer.constants import (
|
|
|
12
12
|
WINDOWS_NT_SERVER_MAP,
|
|
13
13
|
WINDOWS_PRODUCT_PATTERNS,
|
|
14
14
|
WINDOWS_SERVER_BUILD_MAP,
|
|
15
|
+
PrecisionLevel,
|
|
15
16
|
)
|
|
16
17
|
from os_normalizer.helpers import extract_arch_from_text, update_confidence
|
|
17
18
|
|
|
@@ -289,13 +290,13 @@ def _derive_precision(
|
|
|
289
290
|
minor: int | None,
|
|
290
291
|
patch: int | None,
|
|
291
292
|
build: str | None,
|
|
292
|
-
) ->
|
|
293
|
+
) -> PrecisionLevel:
|
|
293
294
|
if build:
|
|
294
|
-
return
|
|
295
|
+
return PrecisionLevel.BUILD
|
|
295
296
|
if patch is not None and patch != 0:
|
|
296
|
-
return
|
|
297
|
+
return PrecisionLevel.PATCH
|
|
297
298
|
if minor is not None:
|
|
298
|
-
return
|
|
299
|
+
return PrecisionLevel.MINOR
|
|
299
300
|
if major is not None:
|
|
300
|
-
return
|
|
301
|
-
return
|
|
301
|
+
return PrecisionLevel.MAJOR
|
|
302
|
+
return PrecisionLevel.PRODUCT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: os-normalizer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -38,6 +38,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.11
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
40
40
|
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
41
42
|
Classifier: Topic :: Software Development :: Libraries
|
|
42
43
|
Classifier: Topic :: System :: Operating System
|
|
43
44
|
Requires-Python: >=3.11
|