os-normalizer 0.4.2__tar.gz → 0.5.0__tar.gz
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-0.4.2 → os_normalizer-0.5.0}/CHANGELOG.md +22 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/PKG-INFO +2 -1
- os_normalizer-0.5.0/RELEASING.md +20 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/constants.py +139 -2
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/cpe.py +57 -12
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/helpers.py +26 -15
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/models.py +19 -9
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/os_normalizer.py +76 -49
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/__init__.py +7 -3
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/bsd.py +2 -1
- os_normalizer-0.5.0/os_normalizer/parsers/esxi.py +83 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/linux.py +6 -5
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/macos.py +12 -14
- os_normalizer-0.5.0/os_normalizer/parsers/mobile.py +55 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/network/__init__.py +5 -1
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/network/cisco.py +13 -7
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/network/fortinet.py +15 -5
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/network/huawei.py +9 -3
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/network/juniper.py +10 -4
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/network/netgear.py +10 -3
- os_normalizer-0.5.0/os_normalizer/parsers/solaris.py +100 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/parsers/windows.py +39 -23
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/pyproject.toml +11 -1
- os_normalizer-0.4.2/RELEASING.md +0 -71
- os_normalizer-0.4.2/os_normalizer/parsers/mobile.py +0 -37
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.gitignore +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.python-version +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.uv-cache/.gitignore +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.uv-cache/.lock +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.uv-cache/CACHEDIR.TAG +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.uv-cache/interpreter-v4/7e11d242fb84b9e8/939db8dea853eb17.msgpack +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.uv-cache/sdists-v9/.git +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/.uv-cache/sdists-v9/.gitignore +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/LICENSE +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/README.md +0 -0
- {os_normalizer-0.4.2 → os_normalizer-0.5.0}/os_normalizer/__init__.py +0 -0
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
This file adheres to Keep a Changelog and Semantic Versioning.
|
|
5
5
|
|
|
6
|
+
## `v0.5.0` — [2025-10-30]
|
|
7
|
+
|
|
8
|
+
- Update release docs, add 3.14 classifier, sort imports [b7e07de]
|
|
9
|
+
- Add a ton more architectures and extract into constants [bd481f8]
|
|
10
|
+
- Add esxi and solaris support [8c14505]
|
|
11
|
+
- Set the unreleased tag for uv-ship [791361c]
|
|
12
|
+
|
|
13
|
+
## `v0.4.4` — [2025-10-25]
|
|
14
|
+
|
|
15
|
+
- Add uv-ship config [afd3bed]
|
|
16
|
+
- Merge pull request #2 from johnscillieri/codex/refactor-family-strings-into-constants [cf84c0f]
|
|
17
|
+
- Polish enum usage and update changelog [c3fb7f4]
|
|
18
|
+
- Merge pull request #1 from johnscillieri/codex/add-support-for-huawei-harmonyos [45fe0f9]
|
|
19
|
+
- Document HarmonyOS support addition [584ed2f]
|
|
20
|
+
- Added: HarmonyOS detection for Huawei devices, including dedicated parsing and metadata.
|
|
21
|
+
- Added: HarmonyOS normalization captures build identifiers (e.g., `5.0.0.107`) and propagates them into generated CPE data.
|
|
22
|
+
- Changed: Replaced string literals for OS families and precision tiers with shared enums and ordering constants across the normalization pipeline.
|
|
23
|
+
|
|
24
|
+
## [0.4.3] - 2025-10-20
|
|
25
|
+
|
|
26
|
+
- Fixed: Windows build parsing and inconsistent strings
|
|
27
|
+
|
|
6
28
|
## [0.4.2] - 2025-10-20
|
|
7
29
|
|
|
8
30
|
- Fixed: Windows product fingerprinting by build number
|
|
@@ -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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Releasing to PyPI
|
|
2
|
+
|
|
3
|
+
This project uses a modern PEP 621 `pyproject.toml` with the Hatchling build backend. Below are the steps to build and publish to PyPI using uv-ship.
|
|
4
|
+
|
|
5
|
+
## Prereqs
|
|
6
|
+
|
|
7
|
+
- Python 3.11+
|
|
8
|
+
- `uv` installed (https://github.com/astral-sh/uv)
|
|
9
|
+
- `uv-ship` installed (via `uv tool install uv-ship`)
|
|
10
|
+
- PyPI accounts and API tokens for TestPyPI and/or PyPI
|
|
11
|
+
|
|
12
|
+
## Run tests and lint
|
|
13
|
+
|
|
14
|
+
- `uv run pytest`
|
|
15
|
+
- Optionally: `uv run nox`
|
|
16
|
+
|
|
17
|
+
## Releasing
|
|
18
|
+
|
|
19
|
+
- `uv-ship next <type>` where type is major|minor|patch
|
|
20
|
+
- Follow the prompts and release the new version.
|
|
@@ -1,19 +1,156 @@
|
|
|
1
1
|
"""Constants and static lookup tables for the OS fingerprinting package."""
|
|
2
2
|
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OSFamily(StrEnum):
|
|
7
|
+
"""Canonical OS family identifiers used throughout the project."""
|
|
8
|
+
|
|
9
|
+
ANDROID = "android"
|
|
10
|
+
BSD = "bsd"
|
|
11
|
+
ESXI = "esxi"
|
|
12
|
+
HARMONYOS = "harmonyos"
|
|
13
|
+
IOS = "ios"
|
|
14
|
+
LINUX = "linux"
|
|
15
|
+
MACOS = "macos"
|
|
16
|
+
NETWORK = "network-os"
|
|
17
|
+
SOLARIS = "solaris"
|
|
18
|
+
WINDOWS = "windows"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PrecisionLevel(StrEnum):
|
|
22
|
+
"""Granularity levels for parsed OS version information."""
|
|
23
|
+
|
|
24
|
+
UNKNOWN = "unknown"
|
|
25
|
+
FAMILY = "family"
|
|
26
|
+
PRODUCT = "product"
|
|
27
|
+
MAJOR = "major"
|
|
28
|
+
MINOR = "minor"
|
|
29
|
+
PATCH = "patch"
|
|
30
|
+
BUILD = "build"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
PRECISION_ORDER = {
|
|
34
|
+
PrecisionLevel.BUILD: 6,
|
|
35
|
+
PrecisionLevel.PATCH: 5,
|
|
36
|
+
PrecisionLevel.MINOR: 4,
|
|
37
|
+
PrecisionLevel.MAJOR: 3,
|
|
38
|
+
PrecisionLevel.PRODUCT: 2,
|
|
39
|
+
PrecisionLevel.FAMILY: 1,
|
|
40
|
+
PrecisionLevel.UNKNOWN: 0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Architecture tokens recognised across parsers.
|
|
45
|
+
ARCHITECTURE_TOKENS = (
|
|
46
|
+
"x86_64",
|
|
47
|
+
"amd64",
|
|
48
|
+
"x64",
|
|
49
|
+
"x86",
|
|
50
|
+
"ia32",
|
|
51
|
+
"i386",
|
|
52
|
+
"i486",
|
|
53
|
+
"i586",
|
|
54
|
+
"i686",
|
|
55
|
+
"arm64",
|
|
56
|
+
"aarch64",
|
|
57
|
+
"arm64e",
|
|
58
|
+
"armv8",
|
|
59
|
+
"armv8l",
|
|
60
|
+
"arm",
|
|
61
|
+
"armv7",
|
|
62
|
+
"armv7l",
|
|
63
|
+
"armv7hf",
|
|
64
|
+
"armv6",
|
|
65
|
+
"armhf",
|
|
66
|
+
"armel",
|
|
67
|
+
"ppc64le",
|
|
68
|
+
"powerpc64le",
|
|
69
|
+
"ppc64",
|
|
70
|
+
"powerpc64",
|
|
71
|
+
"ppc",
|
|
72
|
+
"ppc32",
|
|
73
|
+
"ppcle",
|
|
74
|
+
"powerpc",
|
|
75
|
+
"sparc",
|
|
76
|
+
"sparc64",
|
|
77
|
+
"sparcv9",
|
|
78
|
+
"sun4u",
|
|
79
|
+
"sun4v",
|
|
80
|
+
"mips",
|
|
81
|
+
"mips32",
|
|
82
|
+
"mipsel",
|
|
83
|
+
"mips64",
|
|
84
|
+
"mips64el",
|
|
85
|
+
"mips64le",
|
|
86
|
+
"s390x",
|
|
87
|
+
"s390",
|
|
88
|
+
"riscv64",
|
|
89
|
+
"riscv",
|
|
90
|
+
"loongarch64",
|
|
91
|
+
"tilegx",
|
|
92
|
+
"alpha",
|
|
93
|
+
"ia64",
|
|
94
|
+
"itanium",
|
|
95
|
+
"hppa",
|
|
96
|
+
"parisc",
|
|
97
|
+
"m68k",
|
|
98
|
+
)
|
|
99
|
+
|
|
3
100
|
# Architecture synonyms
|
|
4
101
|
ARCH_SYNONYMS = {
|
|
5
|
-
"x64": "x86_64",
|
|
6
102
|
"x86_64": "x86_64",
|
|
7
103
|
"amd64": "x86_64",
|
|
104
|
+
"x64": "x86_64",
|
|
8
105
|
"x86": "x86",
|
|
106
|
+
"ia32": "x86",
|
|
9
107
|
"i386": "x86",
|
|
108
|
+
"i486": "x86",
|
|
109
|
+
"i586": "x86",
|
|
10
110
|
"i686": "x86",
|
|
11
|
-
"aarch64": "arm64",
|
|
12
111
|
"arm64": "arm64",
|
|
112
|
+
"aarch64": "arm64",
|
|
113
|
+
"arm64e": "arm64",
|
|
13
114
|
"armv8": "arm64",
|
|
115
|
+
"armv8l": "arm64",
|
|
116
|
+
"arm": "arm",
|
|
14
117
|
"armv7": "arm",
|
|
15
118
|
"armv7l": "arm",
|
|
119
|
+
"armv7hf": "arm",
|
|
120
|
+
"armv6": "arm",
|
|
121
|
+
"armhf": "arm",
|
|
122
|
+
"armel": "arm",
|
|
16
123
|
"ppc64le": "ppc64le",
|
|
124
|
+
"powerpc64le": "ppc64le",
|
|
125
|
+
"ppc64": "ppc64",
|
|
126
|
+
"powerpc64": "ppc64",
|
|
127
|
+
"ppc": "ppc",
|
|
128
|
+
"ppc32": "ppc",
|
|
129
|
+
"ppcle": "ppc",
|
|
130
|
+
"powerpc": "ppc",
|
|
131
|
+
"sparc": "sparc",
|
|
132
|
+
"sparc64": "sparc",
|
|
133
|
+
"sparcv9": "sparc",
|
|
134
|
+
"sun4u": "sparc",
|
|
135
|
+
"sun4v": "sparc",
|
|
136
|
+
"mips": "mips",
|
|
137
|
+
"mips32": "mips",
|
|
138
|
+
"mipsel": "mips",
|
|
139
|
+
"mips64": "mips64",
|
|
140
|
+
"mips64el": "mips64",
|
|
141
|
+
"mips64le": "mips64",
|
|
142
|
+
"s390x": "s390x",
|
|
143
|
+
"s390": "s390x",
|
|
144
|
+
"riscv64": "riscv64",
|
|
145
|
+
"riscv": "riscv64",
|
|
146
|
+
"loongarch64": "loongarch64",
|
|
147
|
+
"tilegx": "tilegx",
|
|
148
|
+
"alpha": "alpha",
|
|
149
|
+
"ia64": "ia64",
|
|
150
|
+
"itanium": "ia64",
|
|
151
|
+
"hppa": "parisc",
|
|
152
|
+
"parisc": "parisc",
|
|
153
|
+
"m68k": "m68k",
|
|
17
154
|
}
|
|
18
155
|
|
|
19
156
|
# Windows build map (build number range -> product name, marketing channel)
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
|
-
from os_normalizer.constants import WINDOWS_BUILD_MAP
|
|
12
|
+
from os_normalizer.constants import OSFamily, WINDOWS_BUILD_MAP
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from .models import OSData
|
|
@@ -39,12 +39,19 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
|
|
|
39
39
|
|
|
40
40
|
strategy controls version/update selection rules.
|
|
41
41
|
"""
|
|
42
|
-
|
|
42
|
+
raw_family = p.family
|
|
43
|
+
family = (
|
|
44
|
+
raw_family
|
|
45
|
+
if isinstance(raw_family, OSFamily)
|
|
46
|
+
else OSFamily._value2member_map_.get(str(raw_family).lower())
|
|
47
|
+
if raw_family
|
|
48
|
+
else None
|
|
49
|
+
)
|
|
43
50
|
vendor = (p.vendor or "").lower() if p.vendor else None
|
|
44
51
|
product = (p.product or "").lower() if p.product else ""
|
|
45
52
|
|
|
46
53
|
# Windows
|
|
47
|
-
if
|
|
54
|
+
if family == OSFamily.WINDOWS:
|
|
48
55
|
vtok = "microsoft"
|
|
49
56
|
prod_map = {
|
|
50
57
|
"windows 7": "windows_7",
|
|
@@ -68,11 +75,11 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
|
|
|
68
75
|
return vtok, base, "windows"
|
|
69
76
|
|
|
70
77
|
# macOS
|
|
71
|
-
if
|
|
78
|
+
if family == OSFamily.MACOS:
|
|
72
79
|
return "apple", "macos", "macos"
|
|
73
80
|
|
|
74
81
|
# Linux distros (use distro when present)
|
|
75
|
-
if
|
|
82
|
+
if family == OSFamily.LINUX:
|
|
76
83
|
d = (p.distro or "").lower()
|
|
77
84
|
if d == "ubuntu":
|
|
78
85
|
return "canonical", "ubuntu_linux", "ubuntu"
|
|
@@ -93,7 +100,7 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
|
|
|
93
100
|
return vendor or "linux", product or "linux", "linux"
|
|
94
101
|
|
|
95
102
|
# BSDs
|
|
96
|
-
if
|
|
103
|
+
if family == OSFamily.BSD:
|
|
97
104
|
if product and "freebsd" in product:
|
|
98
105
|
return "freebsd", "freebsd", "freebsd"
|
|
99
106
|
if product and "openbsd" in product:
|
|
@@ -102,8 +109,14 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
|
|
|
102
109
|
return "netbsd", "netbsd", "netbsd"
|
|
103
110
|
return vendor or "bsd", product or "bsd", "bsd"
|
|
104
111
|
|
|
112
|
+
if family == OSFamily.SOLARIS:
|
|
113
|
+
return "oracle", "solaris", "solaris"
|
|
114
|
+
|
|
115
|
+
if family == OSFamily.ESXI:
|
|
116
|
+
return "vmware", "esxi", "esxi"
|
|
117
|
+
|
|
105
118
|
# Network OS
|
|
106
|
-
if
|
|
119
|
+
if family == OSFamily.NETWORK:
|
|
107
120
|
if vendor == "cisco":
|
|
108
121
|
if product and ("ios xe" in product or "ios-xe" in product):
|
|
109
122
|
return "cisco", "ios_xe", "ios_xe"
|
|
@@ -120,13 +133,20 @@ def _map_vendor_product(p: OSData) -> tuple[str, str, str]:
|
|
|
120
133
|
return vendor or "network", (product or "firmware").replace(" ", "_"), "firmware"
|
|
121
134
|
|
|
122
135
|
# Mobile
|
|
123
|
-
if
|
|
124
|
-
return "google", "android",
|
|
125
|
-
if
|
|
126
|
-
return "apple", "iphone_os",
|
|
136
|
+
if family == OSFamily.ANDROID:
|
|
137
|
+
return "google", "android", OSFamily.ANDROID.value
|
|
138
|
+
if family == OSFamily.IOS:
|
|
139
|
+
return "apple", "iphone_os", OSFamily.IOS.value
|
|
140
|
+
if family == OSFamily.HARMONYOS:
|
|
141
|
+
return "huawei", "harmonyos", OSFamily.HARMONYOS.value
|
|
127
142
|
|
|
128
143
|
# Fallback
|
|
129
|
-
|
|
144
|
+
family_value = family.value if family else (str(raw_family).lower() if raw_family else "unknown")
|
|
145
|
+
return (
|
|
146
|
+
vendor or family_value or "unknown",
|
|
147
|
+
(product or family_value).replace(" ", "_"),
|
|
148
|
+
family_value,
|
|
149
|
+
)
|
|
130
150
|
|
|
131
151
|
|
|
132
152
|
def _fmt_version(p: OSData, strategy: str) -> tuple[str, str, str]:
|
|
@@ -192,6 +212,18 @@ def _fmt_version(p: OSData, strategy: str) -> tuple[str, str, str]:
|
|
|
192
212
|
ver = "*"
|
|
193
213
|
return ver, "*", (edition or "*")
|
|
194
214
|
|
|
215
|
+
if strategy in ("solaris", "esxi"):
|
|
216
|
+
if maj is not None:
|
|
217
|
+
ver = str(maj)
|
|
218
|
+
if minr is not None:
|
|
219
|
+
ver = f"{ver}.{minr}"
|
|
220
|
+
if pat is not None:
|
|
221
|
+
ver = f"{ver}.{pat}"
|
|
222
|
+
else:
|
|
223
|
+
ver = "*"
|
|
224
|
+
update = build or "*"
|
|
225
|
+
return ver, update, "*"
|
|
226
|
+
|
|
195
227
|
if strategy == "fortios":
|
|
196
228
|
if maj is not None and minr is not None and pat is not None:
|
|
197
229
|
ver = f"{maj}.{minr}.{pat}"
|
|
@@ -216,6 +248,19 @@ def _fmt_version(p: OSData, strategy: str) -> tuple[str, str, str]:
|
|
|
216
248
|
ver = "*"
|
|
217
249
|
return ver, "*", "*"
|
|
218
250
|
|
|
251
|
+
if strategy == "harmonyos":
|
|
252
|
+
if maj is not None:
|
|
253
|
+
if minr is not None and pat is not None:
|
|
254
|
+
ver = f"{maj}.{minr}.{pat}"
|
|
255
|
+
elif minr is not None:
|
|
256
|
+
ver = f"{maj}.{minr}"
|
|
257
|
+
else:
|
|
258
|
+
ver = f"{maj}"
|
|
259
|
+
else:
|
|
260
|
+
ver = "*"
|
|
261
|
+
update = build or "*"
|
|
262
|
+
return ver, update, "*"
|
|
263
|
+
|
|
219
264
|
# Generic fallback
|
|
220
265
|
if build:
|
|
221
266
|
ver = build
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import re
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from .constants import ARCH_SYNONYMS
|
|
6
|
+
from .constants import ARCH_SYNONYMS, ARCHITECTURE_TOKENS, PrecisionLevel
|
|
7
7
|
from .models import OSData
|
|
8
8
|
|
|
9
9
|
|
|
@@ -34,17 +34,17 @@ def precision_from_parts(
|
|
|
34
34
|
minor: int | None,
|
|
35
35
|
patch: int | None,
|
|
36
36
|
build: str | None,
|
|
37
|
-
) ->
|
|
37
|
+
) -> PrecisionLevel:
|
|
38
38
|
"""Derive a precision label from version components."""
|
|
39
39
|
if build:
|
|
40
|
-
return
|
|
40
|
+
return PrecisionLevel.BUILD
|
|
41
41
|
if patch is not None:
|
|
42
|
-
return
|
|
42
|
+
return PrecisionLevel.PATCH
|
|
43
43
|
if minor is not None:
|
|
44
|
-
return
|
|
44
|
+
return PrecisionLevel.MINOR
|
|
45
45
|
if major is not None:
|
|
46
|
-
return
|
|
47
|
-
return
|
|
46
|
+
return PrecisionLevel.MAJOR
|
|
47
|
+
return PrecisionLevel.PRODUCT
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def canonical_key(p: OSData) -> str:
|
|
@@ -61,7 +61,10 @@ def canonical_key(p: OSData) -> str:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
# Regex for extracting an architecture token from free-form text
|
|
64
|
-
|
|
64
|
+
_ARCH_PATTERN = "|".join(
|
|
65
|
+
sorted((re.escape(token) for token in ARCHITECTURE_TOKENS), key=len, reverse=True)
|
|
66
|
+
)
|
|
67
|
+
ARCH_TEXT_RE = re.compile(rf"\b({_ARCH_PATTERN})\b", re.IGNORECASE)
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
def extract_arch_from_text(text: str) -> str | None:
|
|
@@ -92,16 +95,24 @@ def parse_os_release(blob_text: str) -> dict[str, Any]:
|
|
|
92
95
|
return out
|
|
93
96
|
|
|
94
97
|
|
|
95
|
-
def update_confidence(p: OSData, precision: str) -> None:
|
|
98
|
+
def update_confidence(p: OSData, precision: PrecisionLevel | str) -> None:
|
|
96
99
|
"""Boost confidence based on the determined precision level.
|
|
97
100
|
|
|
98
101
|
The mapping mirrors the original ad-hoc values used throughout the monolithic file.
|
|
99
102
|
"""
|
|
103
|
+
if isinstance(precision, PrecisionLevel):
|
|
104
|
+
level = precision
|
|
105
|
+
else:
|
|
106
|
+
try:
|
|
107
|
+
level = PrecisionLevel(str(precision))
|
|
108
|
+
except ValueError:
|
|
109
|
+
level = PrecisionLevel.UNKNOWN
|
|
110
|
+
|
|
100
111
|
boost_map = {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
PrecisionLevel.BUILD: 0.85,
|
|
113
|
+
PrecisionLevel.PATCH: 0.80,
|
|
114
|
+
PrecisionLevel.MINOR: 0.75,
|
|
115
|
+
PrecisionLevel.MAJOR: 0.70,
|
|
116
|
+
PrecisionLevel.PRODUCT: 0.60,
|
|
106
117
|
}
|
|
107
|
-
p.confidence = max(p.confidence, boost_map.get(
|
|
118
|
+
p.confidence = max(p.confidence, boost_map.get(level, 0.5))
|
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from dataclasses import dataclass, field
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from dataclasses import fields as dataclass_fields
|
|
7
|
+
from enum import Enum
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
10
|
+
from .constants import OSFamily, PrecisionLevel
|
|
11
|
+
|
|
8
12
|
|
|
9
13
|
@dataclass
|
|
10
14
|
class OSData:
|
|
11
15
|
"""Structured representation of a parsed operating system."""
|
|
12
16
|
|
|
13
17
|
# Core identity
|
|
14
|
-
family:
|
|
18
|
+
family: OSFamily | None = None # windows, linux, macos, ios, android, bsd, solaris, esxi, network-os
|
|
15
19
|
vendor: str | None = None # Microsoft, Apple, Canonical, Cisco, Juniper, Fortinet, Huawei, Netgear…
|
|
16
20
|
product: str | None = None # Windows 11, Ubuntu, macOS, IOS XE, Junos, FortiOS, VRP, Firmware
|
|
17
21
|
edition: str | None = None # Pro/Enterprise/LTSC; universalk9/ipbase; etc.
|
|
@@ -37,7 +41,7 @@ class OSData:
|
|
|
37
41
|
build_id: str | None = None
|
|
38
42
|
|
|
39
43
|
# Meta information
|
|
40
|
-
precision:
|
|
44
|
+
precision: PrecisionLevel = PrecisionLevel.UNKNOWN # family|product|major|minor|patch|build
|
|
41
45
|
confidence: float = 0.0
|
|
42
46
|
evidence: dict[str, Any] = field(default_factory=dict)
|
|
43
47
|
|
|
@@ -47,7 +51,7 @@ class OSData:
|
|
|
47
51
|
def __str__(self) -> str: # pragma: no cover - formatting helper
|
|
48
52
|
parts: list[str] = []
|
|
49
53
|
|
|
50
|
-
if self.family ==
|
|
54
|
+
if self.family == OSFamily.WINDOWS:
|
|
51
55
|
return _format_windows(self)
|
|
52
56
|
|
|
53
57
|
# Prefer vendor + product; fallback to pretty_name; then family
|
|
@@ -57,7 +61,10 @@ class OSData:
|
|
|
57
61
|
elif self.pretty_name:
|
|
58
62
|
parts.append(self.pretty_name)
|
|
59
63
|
else:
|
|
60
|
-
|
|
64
|
+
if isinstance(self.family, OSFamily):
|
|
65
|
+
parts.append(self.family.value)
|
|
66
|
+
else:
|
|
67
|
+
parts.append(self.family or "Unknown OS")
|
|
61
68
|
|
|
62
69
|
# Version string (major[.minor[.patch]]) and optional build
|
|
63
70
|
ver_chunks: list[str] = []
|
|
@@ -100,8 +107,9 @@ class OSData:
|
|
|
100
107
|
parts.append(f"[build: {self.build_id}]")
|
|
101
108
|
|
|
102
109
|
# Precision/confidence summary
|
|
103
|
-
if self.precision and self.precision !=
|
|
104
|
-
|
|
110
|
+
if self.precision and self.precision != PrecisionLevel.UNKNOWN:
|
|
111
|
+
label = self.precision.value if isinstance(self.precision, PrecisionLevel) else str(self.precision)
|
|
112
|
+
parts.append(f"{{{label}:{self.confidence:.2f}}}")
|
|
105
113
|
elif self.confidence:
|
|
106
114
|
parts.append(f"{{{self.confidence:.2f}}}")
|
|
107
115
|
|
|
@@ -129,6 +137,8 @@ class OSData:
|
|
|
129
137
|
sval = none_str
|
|
130
138
|
elif name == "confidence" and isinstance(val, (int, float)):
|
|
131
139
|
sval = f"{float(val):.2f}"
|
|
140
|
+
elif isinstance(val, Enum):
|
|
141
|
+
sval = val.value
|
|
132
142
|
elif isinstance(val, list):
|
|
133
143
|
sval = ", ".join(str(x) for x in val)
|
|
134
144
|
elif isinstance(val, dict):
|
|
@@ -159,7 +169,7 @@ def _format_windows(p: OSData) -> str:
|
|
|
159
169
|
|
|
160
170
|
if __name__ == "__main__":
|
|
161
171
|
x = OSData(
|
|
162
|
-
family=
|
|
172
|
+
family=OSFamily.LINUX,
|
|
163
173
|
vendor="Fedora Project",
|
|
164
174
|
product="Fedora Linux",
|
|
165
175
|
version_major=33,
|
|
@@ -168,7 +178,7 @@ if __name__ == "__main__":
|
|
|
168
178
|
distro="fedora",
|
|
169
179
|
like_distros=[],
|
|
170
180
|
pretty_name="Fedora Linux",
|
|
171
|
-
precision=
|
|
181
|
+
precision=PrecisionLevel.MAJOR,
|
|
172
182
|
confidence=0.7,
|
|
173
183
|
evidence={"hit": "linux"},
|
|
174
184
|
)
|