feat: associate recon clients with confirmed networks

This commit is contained in:
2026-08-21 08:26:58 -05:00
parent 2c9fa8d137
commit a67faecee9
3 changed files with 149 additions and 14 deletions
@@ -1341,17 +1341,79 @@ def recon_scan_data(scan_id, _timeout=20, _limit=None, db=None):
'encryption': decode_encryption(r.get('encryption')),
'band': band_of(r.get('freq')),
'vendor': oui_vendor(fmt_mac(r.get('bssid'))),
'device_identity': oui_identity(fmt_mac(r.get('bssid'))),
'first_seen': lo,
'last_seen': hi})
aps.sort(key=lambda row: row['signal'] if row['signal'] is not None else 0)
devices = [r for r in rows if r.get('kind') == 'device']
mac_of = {r['row_id']: fmt_mac(r.get('mac')) for r in devices}
ssid_of = {}
for r in (row for row in rows if row.get('kind') == 'ap'):
bssid = _norm_mac(r.get('bssid'))
if bssid and bssid not in ssid_of:
ssid_of[bssid] = decode_ssid(r.get('ssid'))
evidence = {}
def add_association(client_mac, ssid, bssid=None, source=None, **timestamps):
client_mac = _norm_mac(client_mac)
bssid = _norm_mac(bssid) if bssid else None
ssid = decode_ssid(ssid)
if not client_mac or not ssid or not source:
return
key = (client_mac, bssid, ssid)
association = evidence.get(key)
if association is None:
association = {'ssid': ssid, 'sources': []}
if bssid:
association['bssid'] = bssid
evidence[key] = association
if source not in association['sources']:
association['sources'].append(source)
for name, value in timestamps.items():
if value is not None:
association[name] = value
for r in (row for row in rows if row.get('kind') == 'handshake'):
client = mac_of.get(r.get('stahash'))
bssid = mac_of.get(r.get('aphash'))
add_association(client, ssid_of.get(_norm_mac(bssid)), bssid, 'handshake')
try:
hostap_rows = _db_rows(
db, 'SELECT mac, ssid, connected_time, disconnected_time '
'FROM hostap_client WHERE scan = %d ORDER BY connected_time, id' % scan_id,
timeout=_timeout)
except Exception:
hostap_rows = []
for r in hostap_rows:
add_association(r.get('mac'), r.get('ssid'), source='hostap_client',
connected_time=r.get('connected_time'),
disconnected_time=r.get('disconnected_time'))
associations_by_client = {}
for key, association in evidence.items():
associations_by_client.setdefault(key[0], []).append(association)
for ap in aps:
ap['clients'] = []
ap['client_count'] = 0
ap_by_bssid = {_norm_mac(ap['bssid']): ap for ap in aps}
clients = []
for r in sorted(devices, key=lambda row: row.get('time') or 0):
if (r.get('mac') or '').strip().upper() in ap_macs:
continue
clients.append({'mac': fmt_mac(r.get('mac')), 'signal': r.get('signal'),
'freq': r.get('freq'), 'packets': r.get('packets')})
mac_of = {r['row_id']: fmt_mac(r.get('mac')) for r in devices}
mac = _norm_mac(r.get('mac'))
client_associations = associations_by_client.get(mac, [])
client = {'mac': fmt_mac(r.get('mac')), 'signal': r.get('signal'),
'freq': r.get('freq'), 'packets': r.get('packets'),
'vendor': oui_identity(fmt_mac(r.get('mac'))),
'associations': client_associations}
clients.append(client)
for association in client_associations:
ap = ap_by_bssid.get(_norm_mac(association.get('bssid')))
if ap is None:
continue
if not any(item['mac'] == client['mac'] for item in ap['clients']):
ap['clients'].append(client)
ap['client_count'] += 1
handshakes = []
for r in (row for row in rows if row.get('kind') == 'handshake'):
handshakes.append({'ap': mac_of.get(r.get('aphash'), '--'),