From 5ea493b3f88cf79f16d0086a8783137346ee04ff Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Mon, 28 Apr 2025 11:52:45 -0500 Subject: [PATCH] Enhance SSID metrics extraction by adding client tracking and updating function signatures --- enrich.py | 3 +- enrichment/merge_ssid_summaries.py | 1 - enrichment/metrics_clients.py | 2 +- enrichment/metrics_ssid.py | 106 ++++++++++++++++------------- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/enrich.py b/enrich.py index 33840bf..3413b96 100755 --- a/enrich.py +++ b/enrich.py @@ -65,7 +65,8 @@ def analyze_pcap(pcapng_path, start_ts, end_ts, ap_bssid, ap_channel): ssid_signals, cisco_ssid_clients, cisco_reported_clients, - ssid_packet_counts + ssid_packet_counts, + ssid_clients ) = extract_ssid_metrics(filtered_packets) our_ssid = bssid_to_ssid.get(ap_bssid, None) diff --git a/enrichment/merge_ssid_summaries.py b/enrichment/merge_ssid_summaries.py index 13b27e7..12f0fe2 100644 --- a/enrichment/merge_ssid_summaries.py +++ b/enrichment/merge_ssid_summaries.py @@ -1,4 +1,3 @@ -from collections import defaultdict from statistics import mean def merge_ssid_summaries(summary_lists): diff --git a/enrichment/metrics_clients.py b/enrichment/metrics_clients.py index 56b0410..6fc46dd 100644 --- a/enrichment/metrics_clients.py +++ b/enrichment/metrics_clients.py @@ -2,7 +2,7 @@ from collections import defaultdict from enrichment.utils import get_channel_from_freq def get_clients_on_ap(capture, ap_bssid): - clients = defaultdict(list) + clients = defaultdict(int) ap_bssid = ap_bssid.lower() for packet in capture: diff --git a/enrichment/metrics_ssid.py b/enrichment/metrics_ssid.py index f02b722..9c50124 100644 --- a/enrichment/metrics_ssid.py +++ b/enrichment/metrics_ssid.py @@ -3,6 +3,7 @@ from collections import defaultdict from enrichment.utils import get_channel_from_freq def extract_ssid_metrics(packets): + ssid_clients = defaultdict(set) bssid_to_ssid = {} ssid_to_bssids = defaultdict(set) ssid_hidden_status = {} @@ -24,63 +25,75 @@ def extract_ssid_metrics(packets): continue packet_freq = int(radio.channel.freq) - get_channel_from_freq(packet_freq) # validate channel, or skip + get_channel_from_freq(packet_freq) # Validate channel or skip subtype = int(getattr(wlan, 'type_subtype', 0), 16) - if subtype not in (5, 8): # Beacon or Probe Response - continue - try: - mgt = packet.get_multiple_layers('wlan.mgt')[0] - tags = mgt._all_fields.get('wlan.tagged.all', {}).get('wlan.tag', []) - except Exception: - continue + # --- EXTRACT SSID from Management Frames --- + if subtype in (5, 8): # Beacon or Probe Response + try: + mgt = packet.get_multiple_layers('wlan.mgt')[0] + tags = mgt._all_fields.get('wlan.tagged.all', {}).get('wlan.tag', []) + except Exception: + continue - ssid = None - hidden_ssid = False + ssid = None + hidden_ssid = False - privacy_bit = mgt._all_fields.get('wlan_mgt.fixed.capabilities.privacy') - is_open = (str(privacy_bit) != '1') + privacy_bit = mgt._all_fields.get('wlan_mgt.fixed.capabilities.privacy') + is_open = (str(privacy_bit) != '1') - for tag in tags: - tag_number = tag.get('wlan.tag.number') - if tag_number == '0': - raw_ssid = tag.get('wlan.ssid', '') - if not raw_ssid: - hidden_ssid = True - ssid = '' - else: + for tag in tags: + tag_number = tag.get('wlan.tag.number') + if tag_number == '0': + raw_ssid = tag.get('wlan.ssid', '') + if not raw_ssid: + hidden_ssid = True + ssid = '' + else: + try: + ssid_bytes = bytes.fromhex(raw_ssid.replace(':', '')) + ssid = ssid_bytes.decode('utf-8', errors='replace') + except Exception: + ssid = None + if tag_number == '133': try: - ssid_bytes = bytes.fromhex(raw_ssid.replace(':', '')) - ssid = ssid_bytes.decode('utf-8', errors='replace') - except Exception: - ssid = None - if tag_number == '133': - try: - num_clients = int(tag.get('wlan.cisco.ccx1.clients')) - if ssid: - cisco_ssid_clients[ssid].append(num_clients) - cisco_reported_clients.append(num_clients) - except (TypeError, ValueError): - pass + num_clients = int(tag.get('wlan.cisco.ccx1.clients')) + if ssid: + cisco_ssid_clients[ssid].append(num_clients) + cisco_reported_clients.append(num_clients) + except (TypeError, ValueError): + pass - if not ssid: - continue + if not ssid: + continue - ssid_hidden_status[ssid] = hidden_ssid - ssid_encryption_status.setdefault(ssid, is_open) - ssid_packet_counts[ssid] += 1 + ssid_hidden_status[ssid] = hidden_ssid + ssid_encryption_status.setdefault(ssid, is_open) + ssid_packet_counts[ssid] += 1 + bssid = getattr(wlan, 'bssid', '').lower() + if not bssid or bssid == 'ff:ff:ff:ff:ff:ff': + continue + + bssid_to_ssid[bssid] = ssid + ssid_to_bssids[ssid].add(bssid) + + signal = getattr(radio, 'dbm_antsignal', None) + if signal: + ssid_signals[ssid].append(int(signal)) + + # --- CAPTURE CLIENTS on any frames --- + # This runs even on non-beacon frames bssid = getattr(wlan, 'bssid', '').lower() - if not bssid or bssid == 'ff:ff:ff:ff:ff:ff': - continue + sa = getattr(wlan, 'sa', '').lower() + da = getattr(wlan, 'da', '').lower() - bssid_to_ssid[bssid] = ssid - ssid_to_bssids[ssid].add(bssid) - - signal = getattr(radio, 'dbm_antsignal', None) - if signal: - ssid_signals[ssid].append(int(signal)) + if bssid in bssid_to_ssid: + ssid = bssid_to_ssid[bssid] + for mac in (sa, da): + if mac and mac != "ff:ff:ff:ff:ff:ff" and mac != bssid: + ssid_clients[ssid].add(mac) except Exception: continue @@ -93,5 +106,6 @@ def extract_ssid_metrics(packets): ssid_signals, cisco_ssid_clients, cisco_reported_clients, - ssid_packet_counts + ssid_packet_counts, + ssid_clients )