feat: resolve recon device manufacturers locally
This commit is contained in:
@@ -1158,6 +1158,12 @@ OUI_VENDORS = {
|
|||||||
'FC633E': 'Google', 'FCF080': 'Apple',
|
'FC633E': 'Google', 'FCF080': 'Apple',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OUI_DATA_PATHS = [
|
||||||
|
'/usr/share/nmap/nmap-mac-prefixes',
|
||||||
|
'/usr/share/macchanger/OUI.list',
|
||||||
|
]
|
||||||
|
_oui_identity_cache = None
|
||||||
|
|
||||||
|
|
||||||
def _oui_prefix(mac):
|
def _oui_prefix(mac):
|
||||||
"""'C8:9E:43:64:80:80' / 'C89E43648080' -> 'C89E43' (uppercase, no colons)."""
|
"""'C8:9E:43:64:80:80' / 'C89E43648080' -> 'C89E43' (uppercase, no colons)."""
|
||||||
@@ -1179,6 +1185,41 @@ def oui_vendor(mac):
|
|||||||
return OUI_VENDORS.get(prefix, 'Unknown')
|
return OUI_VENDORS.get(prefix, 'Unknown')
|
||||||
|
|
||||||
|
|
||||||
|
def oui_identity(mac):
|
||||||
|
"""Resolve a MAC to a JSON-safe manufacturer identity."""
|
||||||
|
global _oui_identity_cache
|
||||||
|
prefix = _oui_prefix(mac)
|
||||||
|
if prefix is None:
|
||||||
|
return {'manufacturer': 'Unknown', 'model': None, 'oui': None,
|
||||||
|
'source': 'unknown'}
|
||||||
|
if int(prefix[1], 16) & 2:
|
||||||
|
return {'manufacturer': 'Local/Randomized', 'model': None, 'oui': prefix,
|
||||||
|
'source': 'local'}
|
||||||
|
|
||||||
|
if _oui_identity_cache is None:
|
||||||
|
_oui_identity_cache = {}
|
||||||
|
for path, source in zip(OUI_DATA_PATHS, ('nmap', 'macchanger')):
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as data_file:
|
||||||
|
for line in data_file:
|
||||||
|
match = re.match(r'^\s*([0-9A-Fa-f:.-]+)\s+(.+)$', line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
line_prefix = _oui_prefix(match.group(1))
|
||||||
|
if line_prefix and line_prefix not in _oui_identity_cache:
|
||||||
|
name = match.group(2).strip()
|
||||||
|
if name:
|
||||||
|
_oui_identity_cache[line_prefix] = (name, source)
|
||||||
|
except (OSError, IOError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
manufacturer, source = _oui_identity_cache.get(
|
||||||
|
prefix, (OUI_VENDORS.get(prefix, 'Unknown'),
|
||||||
|
'builtin' if prefix in OUI_VENDORS else 'unknown'))
|
||||||
|
return {'manufacturer': manufacturer, 'model': None, 'oui': prefix,
|
||||||
|
'source': source}
|
||||||
|
|
||||||
|
|
||||||
def band_of(freq):
|
def band_of(freq):
|
||||||
"""Channel frequency (MHz) -> '2.4' | '5' | '6' | '--'."""
|
"""Channel frequency (MHz) -> '2.4' | '5' | '6' | '--'."""
|
||||||
if freq is None:
|
if freq is None:
|
||||||
|
|||||||
@@ -896,6 +896,58 @@ class OuiVendorTest(unittest.TestCase):
|
|||||||
self.assertEqual(server.oui_vendor(None), 'Unknown')
|
self.assertEqual(server.oui_vendor(None), 'Unknown')
|
||||||
self.assertEqual(server.oui_vendor('--'), 'Unknown')
|
self.assertEqual(server.oui_vendor('--'), 'Unknown')
|
||||||
|
|
||||||
|
def test_oui_identity_prefers_nmap_then_macchanger(self):
|
||||||
|
server._oui_identity_cache = None
|
||||||
|
files = {
|
||||||
|
'/nmap': 'C89E43 Apple Corporation\n',
|
||||||
|
'/mac': 'C89E43 fallback\n',
|
||||||
|
}
|
||||||
|
with mock.patch.object(server, 'OUI_DATA_PATHS', ['/nmap', '/mac']), \
|
||||||
|
mock.patch('builtins.open', side_effect=lambda p, *a, **k:
|
||||||
|
mock.mock_open(read_data=files[p]).return_value):
|
||||||
|
value = server.oui_identity('C89E43648080')
|
||||||
|
self.assertEqual(value['manufacturer'], 'Apple Corporation')
|
||||||
|
self.assertEqual(value['source'], 'nmap')
|
||||||
|
self.assertIsNone(value['model'])
|
||||||
|
|
||||||
|
def test_oui_identity_uses_macchanger_when_nmap_is_missing(self):
|
||||||
|
server._oui_identity_cache = None
|
||||||
|
files = {'/mac': 'C8-9E-43 fallback\n'}
|
||||||
|
|
||||||
|
def open_file(path, *args, **kwargs):
|
||||||
|
if path not in files:
|
||||||
|
raise OSError('missing')
|
||||||
|
return mock.mock_open(read_data=files[path]).return_value
|
||||||
|
|
||||||
|
with mock.patch.object(server, 'OUI_DATA_PATHS', ['/nmap', '/mac']), \
|
||||||
|
mock.patch('builtins.open', side_effect=open_file):
|
||||||
|
value = server.oui_identity('C89E43648080')
|
||||||
|
self.assertEqual(value['manufacturer'], 'fallback')
|
||||||
|
self.assertEqual(value['source'], 'macchanger')
|
||||||
|
|
||||||
|
def test_oui_identity_handles_missing_files_unknown_and_local(self):
|
||||||
|
server._oui_identity_cache = None
|
||||||
|
with mock.patch.object(server, 'OUI_DATA_PATHS', ['/missing']), \
|
||||||
|
mock.patch('builtins.open', side_effect=OSError('missing')):
|
||||||
|
unknown = server.oui_identity('AC:BB:CC:00:00:01')
|
||||||
|
self.assertEqual(unknown['manufacturer'], 'Unknown')
|
||||||
|
self.assertEqual(unknown['source'], 'unknown')
|
||||||
|
self.assertEqual(unknown['oui'], 'ACBBCC')
|
||||||
|
self.assertIsNone(unknown['model'])
|
||||||
|
|
||||||
|
local = server.oui_identity('02:11:22:33:44:55')
|
||||||
|
self.assertEqual(local['manufacturer'], 'Local/Randomized')
|
||||||
|
self.assertEqual(local['source'], 'local')
|
||||||
|
self.assertEqual(local['oui'], '021122')
|
||||||
|
|
||||||
|
def test_oui_identity_falls_back_to_builtin_vendors(self):
|
||||||
|
server._oui_identity_cache = None
|
||||||
|
with mock.patch.object(server, 'OUI_DATA_PATHS', []):
|
||||||
|
value = server.oui_identity('B8:27:EB:00:00:00')
|
||||||
|
self.assertEqual(value['manufacturer'], 'Raspberry Pi')
|
||||||
|
self.assertEqual(value['source'], 'builtin')
|
||||||
|
self.assertIsNone(value['model'])
|
||||||
|
|
||||||
def test_band_of_frequencies(self):
|
def test_band_of_frequencies(self):
|
||||||
self.assertEqual(server.band_of(2412), '2.4')
|
self.assertEqual(server.band_of(2412), '2.4')
|
||||||
self.assertEqual(server.band_of(5200), '5')
|
self.assertEqual(server.band_of(5200), '5')
|
||||||
|
|||||||
Reference in New Issue
Block a user