Files
NetAlertX/test/server/test_plugin_object_mac_normalization.py

100 lines
4.2 KiB
Python

"""
Tests for centralized MAC-case normalization in plugin_object_class.
Regression coverage for https://github.com/netalertx/NetAlertX/issues/1775:
plugin authors are responsible for calling normalize_mac() themselves before
writing a MAC as primaryId, and it's easy for one parsing branch in one
plugin to forget (see server/plugins/snmp_discovery) - producing a device
reported under two different MAC cases and spurious connect/disconnect
events, since plugin_object_class.idsHash (used to detect new/missing
objects across scan cycles) is a case-sensitive hash of primaryId.
plugin_object_class now normalizes primaryId itself as a generic safety
net, via utils.plugin_utils.primary_id_is_mac(), whenever the owning
plugin's config.json marks objectPrimaryId as a MAC (true for every
device-scanning plugin - arp_scan, snmp_discovery, sync, etc. - and false
for publishers/exporters/other non-scanning plugins, whose primaryId is not
a MAC and must not be silently rewritten).
Run from inside the NetAlertX container - server/plugin.py isn't importable
standalone outside it (real conf/database/api imports).
pytest "test/server/test_plugin_object_mac_normalization.py" -v
"""
import os
import sys
# ---------------------------------------------------------------------------
# Path setup
# ---------------------------------------------------------------------------
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
sys.path.extend([f"{INSTALL_PATH}/server/plugins", f"{INSTALL_PATH}/server"])
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from db_test_helpers import make_plugin_event_row # noqa: E402
from plugin import plugin_object_class # noqa: E402
from utils.plugin_utils import primary_id_is_mac # noqa: E402
PREFIX = "TESTPLG"
def _scanner_plugin(prefix=PREFIX):
"""Shaped like a real device-scanning plugin's config.json
(e.g. snmp_discovery, arp_scan): objectPrimaryId marked as a MAC."""
return {
"unique_prefix": prefix,
"settings": [
{"function": "WATCH", "value": ["watchedValue1", "watchedValue2"]},
],
"mapped_to_table": "CurrentScan",
"database_column_definitions": [
{"column": "objectPrimaryId", "mapped_to_column": "scanMac", "type": "device_mac"},
{"column": "objectSecondaryId", "mapped_to_column": "scanLastIP", "type": "device_ip"},
],
}
def _non_scanner_plugin(prefix=PREFIX):
"""Shaped like a non-scanning plugin (publisher/exporter): no
CurrentScan mapping, primaryId is not a MAC."""
return {
"unique_prefix": prefix,
"settings": [],
}
class TestScannerPluginNormalizesMacCase:
def test_uppercase_primary_id_is_normalized(self):
row = make_plugin_event_row(PREFIX, "AA:BB:CC:DD:EE:FF")
obj = plugin_object_class(_scanner_plugin(), row)
assert obj.primaryId == "aa:bb:cc:dd:ee:ff"
def test_hyphenated_primary_id_is_normalized_to_colon_form(self):
row = make_plugin_event_row(PREFIX, "AA-BB-CC-DD-EE-FF")
obj = plugin_object_class(_scanner_plugin(), row)
assert obj.primaryId == "aa:bb:cc:dd:ee:ff"
def test_idshash_agrees_across_case_variants(self):
"""The actual bug: two readings of the same device that differ only
in MAC case must produce the same idsHash, or the scan-cycle diff
engine treats them as different objects (spurious connect/disconnect)."""
upper = plugin_object_class(_scanner_plugin(), make_plugin_event_row(PREFIX, "AA:BB:CC:DD:EE:FF"))
lower = plugin_object_class(_scanner_plugin(), make_plugin_event_row(PREFIX, "aa:bb:cc:dd:ee:ff"))
assert upper.idsHash == lower.idsHash
class TestNonScannerPluginLeavesPrimaryIdAlone:
def test_primary_id_untouched(self):
"""A publisher/exporter's primaryId isn't a MAC - must not be run
through normalize_mac(), which would silently mangle it."""
row = make_plugin_event_row(PREFIX, "Some-Mixed-Case-ID")
obj = plugin_object_class(_non_scanner_plugin(), row)
assert obj.primaryId == "Some-Mixed-Case-ID"
def test_helper_detects_scanner_vs_non_scanner_plugins():
assert primary_id_is_mac(_scanner_plugin()) is True
assert primary_id_is_mac(_non_scanner_plugin()) is False