etherlyzer 0.5.1__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.
- etherlyzer/__init__.py +6 -0
- etherlyzer/exporter.py +31 -0
- etherlyzer/formatters.py +154 -0
- etherlyzer/ieee/__init__.py +0 -0
- etherlyzer/ieee/catalog.py +130 -0
- etherlyzer/ieee/csv_parser.py +80 -0
- etherlyzer/ieee/index.py +199 -0
- etherlyzer/ieee/registry.py +184 -0
- etherlyzer/terminal/__init__.py +0 -0
- etherlyzer/terminal/cli.py +58 -0
- etherlyzer/terminal/commands/__init__.py +8 -0
- etherlyzer/terminal/commands/bulk.py +56 -0
- etherlyzer/terminal/commands/format.py +42 -0
- etherlyzer/terminal/commands/identify.py +115 -0
- etherlyzer/terminal/commands/info.py +43 -0
- etherlyzer/terminal/commands/normalize.py +104 -0
- etherlyzer/terminal/commands/update.py +25 -0
- etherlyzer/terminal/utility.py +20 -0
- etherlyzer/util/__init__.py +0 -0
- etherlyzer/util/dirs.py +6 -0
- etherlyzer/util/settings.py +94 -0
- etherlyzer-0.5.1.dist-info/METADATA +387 -0
- etherlyzer-0.5.1.dist-info/RECORD +26 -0
- etherlyzer-0.5.1.dist-info/WHEEL +4 -0
- etherlyzer-0.5.1.dist-info/entry_points.txt +3 -0
- etherlyzer-0.5.1.dist-info/licenses/LICENSE +9 -0
etherlyzer/__init__.py
ADDED
etherlyzer/exporter.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from etherlyzer.util.settings import config
|
|
2
|
+
from etherlyzer.formatters import MACFormatter
|
|
3
|
+
|
|
4
|
+
from etherlyzer.ieee.registry import IEEEEntry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RegistryMatchExporter:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def export_csv_to_console(entries: list[IEEEEntry]):
|
|
10
|
+
for entry in entries:
|
|
11
|
+
try:
|
|
12
|
+
print(
|
|
13
|
+
f"{entry.registry}{config.field_separator}"
|
|
14
|
+
f"{MACFormatter.format_default(entry.assignment)}{config.field_separator}"
|
|
15
|
+
f"{entry.organization_name}{config.field_separator}"
|
|
16
|
+
f"{entry.organization_address}"
|
|
17
|
+
f""
|
|
18
|
+
)
|
|
19
|
+
except AttributeError():
|
|
20
|
+
# Lookup not found.
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def export_humanized_to_console(entries: list[IEEEEntry]):
|
|
25
|
+
for entry in entries:
|
|
26
|
+
mac = MACFormatter.format_default(entry.assignment)
|
|
27
|
+
try:
|
|
28
|
+
print(f"{entry.registry}: {mac} {entry.organization_name}")
|
|
29
|
+
except AttributeError():
|
|
30
|
+
# Lookup not found.
|
|
31
|
+
pass
|
etherlyzer/formatters.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from etherlyzer.util.settings import config
|
|
5
|
+
from string import punctuation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MacMaskLetter(StrEnum):
|
|
9
|
+
EXCESS_INVALID = "I"
|
|
10
|
+
INVALID = "i"
|
|
11
|
+
EXCESS_HEX = "V"
|
|
12
|
+
HEX = "v"
|
|
13
|
+
EXCESS_RELAXED_TYPO = "R"
|
|
14
|
+
RELAXED_TYPO = "r"
|
|
15
|
+
EXCESS_LENIENT_TYPO = "L"
|
|
16
|
+
LENIENT_TYPO = "l"
|
|
17
|
+
PUNCTUATION = "p"
|
|
18
|
+
SEPERATOR = "-"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MACFormatter:
|
|
22
|
+
HEX_DIGITS = frozenset("0123456789ABCDEFabcdef")
|
|
23
|
+
VALID_SEPERATORS = frozenset(" -.:")
|
|
24
|
+
STRICT_TYPOS: dict[str, str] = {" ": "", "\t": ""}
|
|
25
|
+
RELAXED_TYPOS: dict[str, str] = { # noqa: RUF012
|
|
26
|
+
**STRICT_TYPOS,
|
|
27
|
+
"o": "0",
|
|
28
|
+
"O": "0",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
LENIENT_TYPOS: dict[str, str] = { # noqa: RUF012
|
|
32
|
+
**RELAXED_TYPOS,
|
|
33
|
+
"i": "1",
|
|
34
|
+
"I": "1",
|
|
35
|
+
"l": "1",
|
|
36
|
+
"L": "1",
|
|
37
|
+
"s": "5",
|
|
38
|
+
"S": "5",
|
|
39
|
+
"ø": "0",
|
|
40
|
+
"Ø": "0",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def format_default(cls, mac: str) -> str:
|
|
45
|
+
mac = cls.normalize(mac)
|
|
46
|
+
mac = config.mac_separator.join(
|
|
47
|
+
mac[i: i + config.mac_block_size] for i in range(0, len(mac), config.mac_block_size)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return getattr(mac, config.mac_case.value)()
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def format_with_stuffed_hex(cls, mac: str, stuffed_hex: str) -> str:
|
|
54
|
+
mac = (cls.normalize(mac) + 12 * stuffed_hex)[:12]
|
|
55
|
+
return cls.format_default(mac)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def format_custom(
|
|
59
|
+
cls,
|
|
60
|
+
mac: str,
|
|
61
|
+
block_size: int,
|
|
62
|
+
separator: Literal[".", ":", "-"],
|
|
63
|
+
case: Literal["upper", "lower"],
|
|
64
|
+
fix_typos: bool,
|
|
65
|
+
) -> str:
|
|
66
|
+
if fix_typos:
|
|
67
|
+
mac = cls.fix_typos(mac)
|
|
68
|
+
mac = cls.normalize(mac)
|
|
69
|
+
if len(mac) > 12:
|
|
70
|
+
raise ValueError(f"Malformed MAC address is of length: {len(mac)}!")
|
|
71
|
+
mac = separator.join(mac[i: i + block_size] for i in range(0, len(mac), block_size))
|
|
72
|
+
if case == "upper":
|
|
73
|
+
return mac.upper()
|
|
74
|
+
return mac.lower()
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def validate(cls, in_addr: str) -> tuple[str, str]:
|
|
78
|
+
normalized_mac = ""
|
|
79
|
+
validation_mask = ""
|
|
80
|
+
valid_letter_count = 0
|
|
81
|
+
typo_count = 0
|
|
82
|
+
|
|
83
|
+
def valid_norm_mac_check():
|
|
84
|
+
return valid_letter_count + typo_count < 12
|
|
85
|
+
|
|
86
|
+
for idx, ch in enumerate(in_addr):
|
|
87
|
+
if ch in cls.HEX_DIGITS:
|
|
88
|
+
if valid_norm_mac_check():
|
|
89
|
+
normalized_mac += ch.lower()
|
|
90
|
+
validation_mask += MacMaskLetter.HEX
|
|
91
|
+
else:
|
|
92
|
+
normalized_mac += ch.lower()
|
|
93
|
+
validation_mask += MacMaskLetter.EXCESS_HEX
|
|
94
|
+
valid_letter_count += 1
|
|
95
|
+
elif ch in cls.VALID_SEPERATORS:
|
|
96
|
+
pass
|
|
97
|
+
elif ch in punctuation:
|
|
98
|
+
pass
|
|
99
|
+
elif ch in cls.RELAXED_TYPOS:
|
|
100
|
+
if valid_norm_mac_check():
|
|
101
|
+
normalized_mac += ch
|
|
102
|
+
validation_mask += MacMaskLetter.RELAXED_TYPO
|
|
103
|
+
else:
|
|
104
|
+
normalized_mac += ch
|
|
105
|
+
validation_mask += MacMaskLetter.EXCESS_RELAXED_TYPO
|
|
106
|
+
typo_count += 1
|
|
107
|
+
elif ch in cls.LENIENT_TYPOS:
|
|
108
|
+
if valid_norm_mac_check():
|
|
109
|
+
normalized_mac += ch
|
|
110
|
+
validation_mask += MacMaskLetter.LENIENT_TYPO
|
|
111
|
+
else:
|
|
112
|
+
normalized_mac += ch
|
|
113
|
+
validation_mask += MacMaskLetter.EXCESS_LENIENT_TYPO
|
|
114
|
+
typo_count += 1
|
|
115
|
+
else:
|
|
116
|
+
if valid_norm_mac_check():
|
|
117
|
+
normalized_mac += ch
|
|
118
|
+
validation_mask += MacMaskLetter.INVALID
|
|
119
|
+
else:
|
|
120
|
+
normalized_mac += ch
|
|
121
|
+
validation_mask += MacMaskLetter.EXCESS_INVALID
|
|
122
|
+
if len(validation_mask) < 12:
|
|
123
|
+
validation_mask = validation_mask.ljust(12, "M")
|
|
124
|
+
return normalized_mac, validation_mask
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def mask_is_valid(cls, mask: str) -> bool:
|
|
128
|
+
return len(mask) == 12 and all([ch == "v" for ch in mask])
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def normalize(cls, mac: str) -> str:
|
|
132
|
+
mac = "".join(c for c in mac.lower() if c in cls.HEX_DIGITS)
|
|
133
|
+
return mac
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def fix_typos(cls, mac: str) -> str:
|
|
137
|
+
for c in mac:
|
|
138
|
+
if c in cls.LENIENT_TYPOS:
|
|
139
|
+
mac = mac.replace(c, cls.LENIENT_TYPOS[c])
|
|
140
|
+
elif c not in cls.HEX_DIGITS:
|
|
141
|
+
mac = mac.replace(c, "#")
|
|
142
|
+
return mac
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
print(
|
|
147
|
+
MACFormatter().format_custom(
|
|
148
|
+
mac="ab:cd:ef:fg:de:dd",
|
|
149
|
+
separator=":",
|
|
150
|
+
block_size=2,
|
|
151
|
+
case="upper",
|
|
152
|
+
fix_typos=False,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from etherlyzer.ieee.registry import RegCategory, IEEEEntry, EtherTypeEntry, IEEERegistry
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Catalog:
|
|
5
|
+
IEEE_REGISTRIES = { # noqa
|
|
6
|
+
"mal": IEEERegistry(
|
|
7
|
+
enabled=True,
|
|
8
|
+
name="MA-L",
|
|
9
|
+
model=IEEEEntry,
|
|
10
|
+
full_name="MAC Address Block Large",
|
|
11
|
+
legacy_name="OUI",
|
|
12
|
+
category=RegCategory.MAC,
|
|
13
|
+
prefix_bits=24,
|
|
14
|
+
address_bits=24,
|
|
15
|
+
address_count=16_777_216,
|
|
16
|
+
legacy=False,
|
|
17
|
+
description=(
|
|
18
|
+
"Large IEEE MAC address allocation. Formerly known as the "
|
|
19
|
+
"Organizationally Unique Identifier (OUI). Used by vendors "
|
|
20
|
+
"requiring large address spaces."
|
|
21
|
+
),
|
|
22
|
+
url="https://standards-oui.ieee.org/oui/oui.csv",
|
|
23
|
+
),
|
|
24
|
+
"mam": IEEERegistry(
|
|
25
|
+
enabled=True,
|
|
26
|
+
name="MA-M",
|
|
27
|
+
model=IEEEEntry,
|
|
28
|
+
full_name="MAC Address Block Medium",
|
|
29
|
+
legacy_name="OUI-28",
|
|
30
|
+
category=RegCategory.MAC,
|
|
31
|
+
prefix_bits=28,
|
|
32
|
+
address_bits=20,
|
|
33
|
+
address_count=1_048_576,
|
|
34
|
+
legacy=False,
|
|
35
|
+
description=(
|
|
36
|
+
"Medium-sized IEEE MAC address allocation intended for "
|
|
37
|
+
"organizations requiring fewer addresses than MA-L."
|
|
38
|
+
),
|
|
39
|
+
url="https://standards-oui.ieee.org/oui28/mam.csv",
|
|
40
|
+
),
|
|
41
|
+
"mas": IEEERegistry(
|
|
42
|
+
enabled=True,
|
|
43
|
+
name="MA-S",
|
|
44
|
+
model=IEEEEntry,
|
|
45
|
+
full_name="MAC Address Block Small",
|
|
46
|
+
legacy_name="OUI-36",
|
|
47
|
+
category=RegCategory.MAC,
|
|
48
|
+
prefix_bits=36,
|
|
49
|
+
address_bits=12,
|
|
50
|
+
address_count=4_096,
|
|
51
|
+
legacy=False,
|
|
52
|
+
description=(
|
|
53
|
+
"Small IEEE MAC address allocation for embedded devices, "
|
|
54
|
+
"IoT, industrial equipment, and smaller manufacturers."
|
|
55
|
+
),
|
|
56
|
+
url="https://standards-oui.ieee.org/oui36/oui36.csv",
|
|
57
|
+
),
|
|
58
|
+
"cid": IEEERegistry(
|
|
59
|
+
enabled=True,
|
|
60
|
+
name="CID",
|
|
61
|
+
model=IEEEEntry,
|
|
62
|
+
full_name="Company Identifier",
|
|
63
|
+
legacy_name=None,
|
|
64
|
+
category=RegCategory.IDENTIFIER,
|
|
65
|
+
legacy=False,
|
|
66
|
+
description=(
|
|
67
|
+
"Unique company identifiers assigned by IEEE. Identifies "
|
|
68
|
+
"organizations independently of MAC address allocations."
|
|
69
|
+
),
|
|
70
|
+
url="https://standards-oui.ieee.org/cid/cid.csv",
|
|
71
|
+
),
|
|
72
|
+
"iab": IEEERegistry(
|
|
73
|
+
enabled=True,
|
|
74
|
+
name="IAB",
|
|
75
|
+
model=IEEEEntry,
|
|
76
|
+
full_name="Individual Address Block",
|
|
77
|
+
legacy_name=None,
|
|
78
|
+
category=RegCategory.MAC,
|
|
79
|
+
prefix_bits=36,
|
|
80
|
+
address_bits=12,
|
|
81
|
+
address_count=4_096,
|
|
82
|
+
legacy=True,
|
|
83
|
+
description=(
|
|
84
|
+
"Legacy IEEE MAC address allocation scheme superseded by "
|
|
85
|
+
"MA-S. Retained for compatibility with older hardware."
|
|
86
|
+
),
|
|
87
|
+
url="https://standards-oui.ieee.org/iab/iab.csv",
|
|
88
|
+
),
|
|
89
|
+
"ethertype": IEEERegistry(
|
|
90
|
+
enabled=True,
|
|
91
|
+
name="EtherType",
|
|
92
|
+
model=EtherTypeEntry,
|
|
93
|
+
full_name="EtherType Registry",
|
|
94
|
+
legacy_name=None,
|
|
95
|
+
category=RegCategory.PROTOCOL,
|
|
96
|
+
legacy=False,
|
|
97
|
+
description=(
|
|
98
|
+
"Registry mapping EtherType values to Ethernet protocols, "
|
|
99
|
+
"including IPv4, IPv6, ARP, VLAN tagging, LLDP, MPLS, "
|
|
100
|
+
"802.1X, and many vendor-specific protocols."
|
|
101
|
+
),
|
|
102
|
+
url="https://standards-oui.ieee.org/ethertype/eth.csv",
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def db_is_initialized(cls):
|
|
108
|
+
db_init_checks: list[bool] = []
|
|
109
|
+
for registry in cls.IEEE_REGISTRIES.values():
|
|
110
|
+
db_init_checks.append(registry.filepath.exists())
|
|
111
|
+
if not all([check for check in db_init_checks]):
|
|
112
|
+
return False
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def get_registry(registry_name: str) -> IEEERegistry | None:
|
|
117
|
+
return Catalog.IEEE_REGISTRIES.get(registry_name, None)
|
|
118
|
+
|
|
119
|
+
def get_all_registries(self, check_for_updates=True) -> list[IEEERegistry]:
|
|
120
|
+
for registry in self.IEEE_REGISTRIES.values():
|
|
121
|
+
# If not checking for updates, we still need a present dataset.
|
|
122
|
+
if (check_for_updates and registry.need_updates()) or not self.db_is_initialized():
|
|
123
|
+
|
|
124
|
+
status = registry.save()
|
|
125
|
+
if status is False:
|
|
126
|
+
break
|
|
127
|
+
return list(self.IEEE_REGISTRIES.values())
|
|
128
|
+
|
|
129
|
+
def __getitem__(self, key):
|
|
130
|
+
return self.IEEE_REGISTRIES[key]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from dataclasses import MISSING, fields
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IEEERegistryReader:
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def normalize(name: str) -> str:
|
|
15
|
+
return (
|
|
16
|
+
name.strip()
|
|
17
|
+
.lower()
|
|
18
|
+
.replace("-", "_")
|
|
19
|
+
.replace(" ", "_")
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def convert(value: str, typ: type) -> Any:
|
|
24
|
+
if value == "":
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
if typ is int:
|
|
28
|
+
return int(value)
|
|
29
|
+
|
|
30
|
+
if typ is float:
|
|
31
|
+
return float(value)
|
|
32
|
+
|
|
33
|
+
if typ is bool:
|
|
34
|
+
return value.lower() in {
|
|
35
|
+
"true",
|
|
36
|
+
"1",
|
|
37
|
+
"yes",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def load(cls, path: str | Path, model: type[T]) -> list[T]:
|
|
44
|
+
model_fields = {
|
|
45
|
+
cls.normalize(field.name): field
|
|
46
|
+
for field in fields(model)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
objects: list[T] = []
|
|
50
|
+
|
|
51
|
+
with open(path, newline="", encoding="utf-8") as f:
|
|
52
|
+
reader = csv.DictReader(f)
|
|
53
|
+
|
|
54
|
+
for row in reader:
|
|
55
|
+
kwargs = {}
|
|
56
|
+
for header, value in row.items():
|
|
57
|
+
field_name = cls.normalize(header)
|
|
58
|
+
field = model_fields.get(field_name)
|
|
59
|
+
|
|
60
|
+
if field is None:
|
|
61
|
+
continue
|
|
62
|
+
kwargs[field.name] = cls.convert(
|
|
63
|
+
value,
|
|
64
|
+
field.type,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
cls.validate(model_fields, kwargs)
|
|
68
|
+
objects.append(model(**kwargs))
|
|
69
|
+
|
|
70
|
+
return objects
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def validate(model_fields, kwargs):
|
|
74
|
+
missing = [
|
|
75
|
+
field.name
|
|
76
|
+
for field in model_fields.values()
|
|
77
|
+
if (field.name not in kwargs and field.default is MISSING and field.default_factory is MISSING)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
if missing: raise ValueError(f"Missing required fields: {', '.join(missing)}")
|
etherlyzer/ieee/index.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
from etherlyzer.ieee.registry import RegCategory, IEEEEntry, EtherTypeEntry, IEEERegistry
|
|
9
|
+
|
|
10
|
+
HEX_DIGITS = frozenset("0123456789ABCDEF")
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(slots=True)
|
|
16
|
+
class MACIndex:
|
|
17
|
+
prefixes_36: dict[str, IEEEEntry] = field(default_factory=dict)
|
|
18
|
+
prefixes_28: dict[str, IEEEEntry] = field(default_factory=dict)
|
|
19
|
+
prefixes_24: dict[str, IEEEEntry] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
def insert(self, registry: IEEERegistry, entries: Iterable[IEEEEntry]) -> None:
|
|
22
|
+
match registry.prefix_bits:
|
|
23
|
+
case 36:
|
|
24
|
+
target = self.prefixes_36
|
|
25
|
+
case 28:
|
|
26
|
+
target = self.prefixes_28
|
|
27
|
+
case 24:
|
|
28
|
+
target = self.prefixes_24
|
|
29
|
+
case _:
|
|
30
|
+
raise ValueError(f"Unsupported prefix length: {registry.prefix_bits}")
|
|
31
|
+
|
|
32
|
+
target.update({entry.assignment.upper(): entry for entry in entries})
|
|
33
|
+
|
|
34
|
+
def lookup(self, mac: str) -> IEEEEntry | None:
|
|
35
|
+
mac = "".join(c for c in mac.upper() if c in HEX_DIGITS)
|
|
36
|
+
|
|
37
|
+
# Order is important - longest prefix match
|
|
38
|
+
return (
|
|
39
|
+
self.prefixes_36.get(mac[:9])
|
|
40
|
+
or self.prefixes_28.get(mac[:7])
|
|
41
|
+
or self.prefixes_24.get(mac[:6])
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def walk(self):
|
|
45
|
+
yield from self.prefixes_36
|
|
46
|
+
yield from self.prefixes_28
|
|
47
|
+
yield from self.prefixes_24
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class VendorIndex:
|
|
52
|
+
vendors: dict[str, list[IEEEEntry]] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def normalize_vendor_name(name: str) -> str:
|
|
56
|
+
name = name.casefold()
|
|
57
|
+
name = re.sub(r"[.,/\\()\-_'\"&]+", " ", name)
|
|
58
|
+
name = re.sub(r"\s+", " ", name)
|
|
59
|
+
return name.strip()
|
|
60
|
+
|
|
61
|
+
def insert(self, entry: IEEEEntry) -> None:
|
|
62
|
+
key = self.normalize_vendor_name(entry.organization_name)
|
|
63
|
+
self.vendors.setdefault(key, []).append(entry)
|
|
64
|
+
|
|
65
|
+
def lookup(self, vendor: str) -> list[IEEEEntry] | None:
|
|
66
|
+
ieee_mac_entries = self.vendors.get(self.normalize_vendor_name(vendor))
|
|
67
|
+
ieee_mac_entries.sort(
|
|
68
|
+
key=lambda entry: int(
|
|
69
|
+
"".join(c for c in entry.assignment if c in "0123456789ABCDEFabcdef"),
|
|
70
|
+
16,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return ieee_mac_entries
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True)
|
|
77
|
+
class ProtocolIndex:
|
|
78
|
+
ethertype: dict[str, EtherTypeEntry] = field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
def insert(
|
|
81
|
+
self,
|
|
82
|
+
registry: IEEERegistry,
|
|
83
|
+
entries: Iterable[EtherTypeEntry],
|
|
84
|
+
) -> None:
|
|
85
|
+
self.ethertype.update({entry.assignment.upper(): entry for entry in entries})
|
|
86
|
+
|
|
87
|
+
def lookup(self, ethertype: str) -> EtherTypeEntry | None:
|
|
88
|
+
ethertype = "".join(c for c in ethertype.upper() if c in HEX_DIGITS)
|
|
89
|
+
return self.ethertype.get(ethertype)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(slots=True)
|
|
93
|
+
class IdentifierIndex:
|
|
94
|
+
cid: dict[str, IEEEEntry] = field(default_factory=dict)
|
|
95
|
+
opid: dict[str, IEEEEntry] = field(default_factory=dict)
|
|
96
|
+
manid: dict[str, IEEEEntry] = field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
def insert(self, registry: IEEERegistry, entries: Iterable[IEEEEntry]) -> None:
|
|
99
|
+
match registry.name:
|
|
100
|
+
case "CID":
|
|
101
|
+
target = self.cid
|
|
102
|
+
case "OPID":
|
|
103
|
+
target = self.opid
|
|
104
|
+
case "MANID":
|
|
105
|
+
target = self.manid
|
|
106
|
+
case _:
|
|
107
|
+
raise ValueError(f"Unsupported identifier registry: {registry.name}")
|
|
108
|
+
|
|
109
|
+
target.update({entry.assignment.upper(): entry for entry in entries})
|
|
110
|
+
|
|
111
|
+
def lookup(self, identifier: str) -> IEEEEntry | None:
|
|
112
|
+
identifier = identifier.upper()
|
|
113
|
+
|
|
114
|
+
return self.cid.get(identifier) or self.opid.get(identifier) or self.manid.get(identifier)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(slots=True)
|
|
118
|
+
class IEEEIndex:
|
|
119
|
+
mac_index: MACIndex = field(default_factory=MACIndex)
|
|
120
|
+
protocol_index: ProtocolIndex = field(default_factory=ProtocolIndex)
|
|
121
|
+
identifier_index: IdentifierIndex = field(default_factory=IdentifierIndex)
|
|
122
|
+
vendor_index: VendorIndex = field(default_factory=VendorIndex)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_registries(
|
|
126
|
+
cls,
|
|
127
|
+
registries: Iterable[IEEERegistry],
|
|
128
|
+
) -> IEEEIndex:
|
|
129
|
+
|
|
130
|
+
index = cls()
|
|
131
|
+
|
|
132
|
+
for registry in registries:
|
|
133
|
+
entries = registry.load()
|
|
134
|
+
|
|
135
|
+
match registry.category:
|
|
136
|
+
case RegCategory.MAC:
|
|
137
|
+
index.mac_index.insert(registry, entries)
|
|
138
|
+
|
|
139
|
+
case RegCategory.PROTOCOL:
|
|
140
|
+
index.protocol_index.insert(registry, entries)
|
|
141
|
+
|
|
142
|
+
case RegCategory.IDENTIFIER:
|
|
143
|
+
index.identifier_index.insert(registry, entries)
|
|
144
|
+
|
|
145
|
+
case _:
|
|
146
|
+
raise ValueError(f"Unsupported registry category: {registry.category}")
|
|
147
|
+
mac_walker = index.mac_index.walk()
|
|
148
|
+
v_index = VendorIndex()
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
try:
|
|
152
|
+
entry = index.get_from_mac_index(next(mac_walker))
|
|
153
|
+
if entry is not None:
|
|
154
|
+
v_index.insert(entry)
|
|
155
|
+
except StopIteration:
|
|
156
|
+
break
|
|
157
|
+
index.vendor_index = v_index
|
|
158
|
+
return index
|
|
159
|
+
|
|
160
|
+
def get_from_mac_index(self, mac: str) -> IEEEEntry | None:
|
|
161
|
+
return self.mac_index.lookup(mac)
|
|
162
|
+
|
|
163
|
+
def get_from_ethertype_index(self, ethertype: str) -> EtherTypeEntry | None:
|
|
164
|
+
return self.protocol_index.lookup(ethertype)
|
|
165
|
+
|
|
166
|
+
def get_identifier(self, identifier: str) -> IEEEEntry | None:
|
|
167
|
+
return self.identifier_index.lookup(identifier)
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def get_bulk(
|
|
171
|
+
ieee_index: MACIndex | IdentifierIndex | ProtocolIndex,
|
|
172
|
+
path_or_text: Path | list[str],
|
|
173
|
+
) -> list[IEEEEntry]:
|
|
174
|
+
# Pass a list[str] from interactive or pass a Path
|
|
175
|
+
if isinstance(path_or_text, Path):
|
|
176
|
+
with open(path_or_text) as f:
|
|
177
|
+
text_data = f.readlines()
|
|
178
|
+
else:
|
|
179
|
+
text_data = path_or_text
|
|
180
|
+
ieee_entries: set[IEEEEntry] = set()
|
|
181
|
+
for line in text_data:
|
|
182
|
+
ieee_entry = ieee_index.lookup(line)
|
|
183
|
+
if ieee_entry:
|
|
184
|
+
ieee_entries.add(ieee_entry)
|
|
185
|
+
|
|
186
|
+
ieee_entries: list[IEEEEntry] = list(ieee_entries)
|
|
187
|
+
# Sort by name, and then by registry-name reversed
|
|
188
|
+
ieee_entries = sorted(ieee_entries, key=lambda e: e.organization_name)
|
|
189
|
+
ieee_entries = sorted(ieee_entries, key=lambda e: e.registry, reverse=True)
|
|
190
|
+
|
|
191
|
+
return ieee_entries
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def get_single(indextype: VendorIndex | MACIndex | IdentifierIndex | ProtocolIndex, mac: str):
|
|
195
|
+
ieee_entry = indextype.lookup(mac)
|
|
196
|
+
if ieee_entry:
|
|
197
|
+
return ieee_entry
|
|
198
|
+
else:
|
|
199
|
+
return None
|