mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-10-02 02:35:05 -04:00
Address jokob-sk review: dedupe rogue detections, add iw to remaining Dockerfiles
check_trusted_aps() evaluated a rogue AP once per trusted_aps entry sharing its SSID, so the documented main-AP+extender pattern (same SSID, two entries) produced duplicate (bssid, motor) rows for a real clone - a problem once next_release's per-plugin identity-hash dedup guard lands in main, since it drops a plugin's entire batch on any internal duplicate. Fixed by deduping once in main() after collecting from all check_* functions. Also added iw + its setcap to Dockerfile.debian and .devcontainer/Dockerfile, which the original PR missed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011meLPKCzVpdZyAUfv5U6mm
This commit is contained in:
1 parent
f7a0c5861e
commit
41efcbc971
4 files changed
+104
-1
No files matched your search
@@ -134,7 +134,7 @@ ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
|
||||
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap fping \
|
||||
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss iw nmap fping \
|
||||
nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
|
||||
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 py3-psutil envsubst \
|
||||
nginx supercronic shadow su-exec jq && \
|
||||
@@ -178,6 +178,7 @@ RUN for vfile in .VERSION; do \
|
||||
apk add --no-cache libcap && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/arp-scan && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/sbin/iw && \
|
||||
setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip /usr/bin/nbtscan && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/traceroute && \
|
||||
setcap cap_net_raw,cap_net_admin+eip "$(readlink -f ${VIRTUAL_ENV_BIN}/python)" && \
|
||||
|
||||
@@ -120,6 +120,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
net-tools \
|
||||
python3 \
|
||||
iproute2 \
|
||||
iw \
|
||||
nmap \
|
||||
fping \
|
||||
zip \
|
||||
@@ -187,6 +188,7 @@ RUN for vfile in .VERSION .VERSION_PREV; do \
|
||||
# Set capabilities for raw socket access
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/sbin/arp-scan && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/sbin/iw && \
|
||||
setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip /usr/bin/nbtscan && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/traceroute.db && \
|
||||
# Note: python path needs to be dynamic or verificed
|
||||
|
||||
@@ -72,6 +72,7 @@ def main():
|
||||
detections += check_global_signatures(aps)
|
||||
detections += check_trusted_aps(aps, trusted_aps)
|
||||
detections += check_duplicate_ssid(aps, trusted_aps)
|
||||
detections = dedupe_detections(detections)
|
||||
escalate_known_devices(detections)
|
||||
|
||||
for det in detections:
|
||||
@@ -352,6 +353,22 @@ def check_duplicate_ssid(aps, trusted_aps):
|
||||
return found
|
||||
|
||||
|
||||
def dedupe_detections(detections):
|
||||
"""Collapse detections sharing the same (bssid, motor) identity -
|
||||
check_trusted_aps() can otherwise flag one rogue clone once per
|
||||
WIFICANARY_trusted_aps entry sharing its SSID (e.g. a main AP + range
|
||||
extender pair). Keeps the first occurrence of each identity."""
|
||||
seen = set()
|
||||
deduped = []
|
||||
for det in detections:
|
||||
key = (det['bssid'], det['motor'])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
deduped.append(det)
|
||||
return deduped
|
||||
|
||||
|
||||
def escalate_known_devices(detections):
|
||||
"""Motor 10 (see the addendum on issue #1789): a BSSID this plugin just
|
||||
flagged might not be a stranger's radio at all - it might be a device
|
||||
|
||||
@@ -488,6 +488,51 @@ def test_duplicate_ssid_flags_oui_not_among_multiple_trusted():
|
||||
assert found[0]['bssid'] == '11:22:33:44:55:66'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dedupe_detections()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_dedupe_detections_collapses_same_bssid_and_motor():
|
||||
detections = [
|
||||
_detection(bssid='aa:bb:cc:11:22:33', motor='evil_twin'),
|
||||
_detection(bssid='aa:bb:cc:11:22:33', motor='evil_twin'),
|
||||
_detection(bssid='aa:bb:cc:11:22:33', motor='duplicate_ssid_diff_vendor'),
|
||||
]
|
||||
deduped = wificanary.dedupe_detections(detections)
|
||||
assert len(deduped) == 2
|
||||
assert {d['motor'] for d in deduped} == {'evil_twin', 'duplicate_ssid_diff_vendor'}
|
||||
|
||||
|
||||
def test_dedupe_detections_keeps_distinct_bssids():
|
||||
detections = [
|
||||
_detection(bssid='aa:bb:cc:11:22:33', motor='evil_twin'),
|
||||
_detection(bssid='ff:ee:dd:99:88:77', motor='evil_twin'),
|
||||
]
|
||||
assert wificanary.dedupe_detections(detections) == detections
|
||||
|
||||
|
||||
def test_check_trusted_aps_flags_rogue_clone_twice_when_two_entries_share_ssid():
|
||||
# Reproduces the real gap jokob-sk found on PR #1809: a rogue AP cloning
|
||||
# a protected SSID gets evaluated once per WIFICANARY_trusted_aps entry
|
||||
# sharing that SSID - including the plugin's own documented range-
|
||||
# extender pattern (main AP + extender, same SSID, each its own entry).
|
||||
# check_trusted_aps() alone still produces the duplicate - dedupe_detections()
|
||||
# is what main() uses to collapse it, tested at the main() level below.
|
||||
trusted = [
|
||||
{'ssid': 'HomeWiFi', 'bssid': 'aa:bb:cc:11:22:33', 'security_set': {'wpa3'}},
|
||||
{'ssid': 'HomeWiFi', 'bssid': '44:55:66:aa:bb:cc', 'security_set': {'wpa2'}},
|
||||
]
|
||||
aps = [
|
||||
_ap('aa:bb:cc:11:22:33', 'HomeWiFi', 'wpa3'),
|
||||
_ap('44:55:66:aa:bb:cc', 'HomeWiFi', 'wpa2'),
|
||||
_ap('ff:ee:dd:99:88:77', 'HomeWiFi', 'open'),
|
||||
]
|
||||
found = wificanary.check_trusted_aps(aps, trusted)
|
||||
rogue_hits = [d for d in found if d['bssid'] == 'ff:ee:dd:99:88:77']
|
||||
assert len(rogue_hits) == 2
|
||||
assert {d['motor'] for d in rogue_hits} == {'evil_twin'}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_security_set()
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -670,5 +715,43 @@ def test_main_end_to_end_one_detection():
|
||||
wificanary.plugin_objects.write_result_file.assert_called_once()
|
||||
|
||||
|
||||
def test_main_dedupes_rogue_clone_across_two_trusted_entries_sharing_ssid():
|
||||
# Regression for jokob-sk's PR #1809 review: a main AP + range extender
|
||||
# (same SSID, each its own trusted_aps entry - the plugin's own
|
||||
# documented pattern) must not turn one rogue clone into two identical
|
||||
# (bssid, motor) rows - that pair is the plugin_objects identity NetAlertX
|
||||
# core's own dedup guard hashes per run, so a real duplicate here would
|
||||
# get the whole run's batch silently dropped once that guard lands.
|
||||
settings = {
|
||||
'WIFICANARY_IFACE': 'wlan0',
|
||||
'WIFICANARY_RUN_TIMEOUT': 60,
|
||||
'WIFICANARY_trusted_aps': [
|
||||
_encode_trusted_entry('HomeWiFi', 'aa:bb:cc:11:22:33', ('wpa3',)),
|
||||
_encode_trusted_entry('HomeWiFi', '44:55:66:aa:bb:cc', ('wpa2',)),
|
||||
],
|
||||
}
|
||||
aps = [
|
||||
_ap('aa:bb:cc:11:22:33', 'HomeWiFi', 'wpa3'),
|
||||
_ap('44:55:66:aa:bb:cc', 'HomeWiFi', 'wpa2'),
|
||||
_ap('ff:ee:dd:99:88:77', 'HomeWiFi', 'open'),
|
||||
]
|
||||
|
||||
with patch.object(wificanary, 'get_setting_value', side_effect=lambda k: settings.get(k)):
|
||||
with patch.object(wificanary, 'scan', return_value=aps):
|
||||
with patch.object(wificanary, 'DeviceInstance') as MockDeviceInstance:
|
||||
MockDeviceInstance.return_value.getAllByMacs.return_value = {}
|
||||
wificanary.plugin_objects.add_object = MagicMock()
|
||||
wificanary.plugin_objects.write_result_file = MagicMock()
|
||||
wificanary.main()
|
||||
|
||||
# The rogue AP legitimately trips two distinct motors (evil-twin clone AND
|
||||
# duplicate-SSID/different-vendor, same as test_main_end_to_end_one_detection)
|
||||
# - dedupe_detections() must not collapse those, only a repeated identity.
|
||||
identities = [(c.kwargs['primaryId'], c.kwargs['secondaryId'])
|
||||
for c in wificanary.plugin_objects.add_object.call_args_list]
|
||||
assert len(identities) == len(set(identities)), f"duplicate (bssid, motor) row: {identities}"
|
||||
assert identities.count(('ff:ee:dd:99:88:77', 'evil_twin')) == 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(pytest.main([__file__, '-v']))
|
||||
Reference in new issue
Block a user