diff --git a/docs/superpowers/plans/2026-08-21-recon-client-associations-device-identity.md b/docs/superpowers/plans/2026-08-21-recon-client-associations-device-identity.md new file mode 100644 index 0000000..8ed9f0d --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-recon-client-associations-device-identity.md @@ -0,0 +1,281 @@ +# Recon Client Associations and Device Identity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add confirmed client-to-SSID/AP associations and best-available manufacturer identity to Recon APIs, tables, focus details, exports, and reports. + +**Architecture:** Keep `recon_scan_data()` as the canonical enrichment boundary. Add cached local OUI resolution and scan-scoped evidence joins in `server.py`, then consume the enriched object shape in existing JSON/CSV/HTML serializers and `views.recon`. Handshake evidence may resolve BSSID; `hostap_client` evidence remains SSID-only because its schema has no BSSID. + +**Tech Stack:** Python 3 standard library, SQLite read-only queries, vanilla JavaScript, existing test suite, no frontend build step. + +## Global Constraints + +- Associations must never be inferred from proximity, channel, timing, or probe requests. +- `ssid.type = 5` directed probes are not associations. +- `hostap_client` rows create confirmed SSID-only associations and never claim an AP BSSID. +- OUI resolution uses Nmap, macchanger, then the built-in map, then `Unknown`. +- Missing OUI files or optional association data must not fail Recon. +- No external network lookup or wireless/PineAP configuration change is allowed. +- Preserve unrelated existing changes in `server.py`, `tests/test_recon.py`, `loot/`, and certificate files. + +--- + +### Task 1: Add Cached OUI Identity Resolution + +**Files:** +- Modify: `payload/user/remote_access/pager-webui/server.py` near `OUI_VENDORS`, `_oui_prefix`, and `oui_vendor` +- Test: `tests/test_recon.py` in `DecodersTest` + +**Interfaces:** +- Produces `oui_identity(mac)` returning a JSON-safe dictionary with `manufacturer`, `model`, `oui`, and `source`. +- Keeps `oui_vendor(mac)` behavior compatible for existing callers. + +- [ ] **Step 1: Write failing tests** + +Add tests that patch `server.OUI_DATA_PATHS` and `server.open`, reset the identity cache, and assert Nmap wins over macchanger and built-in fallback. Add tests for missing files, unknown global MACs, locally administered MACs, and `model is None`. + +```python +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_handles_local_and_unknown(self): + server._oui_identity_cache = {} + self.assertEqual(server.oui_identity('02:11:22:33:44:55')['manufacturer'], + 'Local/Randomized') + self.assertEqual(server.oui_identity('AA:BB:CC:00:00:01')['manufacturer'], + 'Unknown') +``` + +- [ ] **Step 2: Run the focused tests and verify failure** + +Run: `python3 -m unittest tests.test_recon.DecodersTest -v` + +Expected: FAIL because `OUI_DATA_PATHS`, `_oui_identity_cache`, and +`oui_identity()` do not yet exist. + +- [ ] **Step 3: Implement the smallest resolver** + +Add the two device paths, a process-level cache, parsers for the first six +hexadecimal characters in each local database line, source labels `nmap` and +`macchanger`, and fallback to `OUI_VENDORS`. Return `model: None` for every +current source. Detect the locally administered bit before file lookup. + +- [ ] **Step 4: Run focused and regression tests** + +Run: `python3 -m unittest tests.test_recon.DecodersTest -v` + +Expected: PASS, including the pre-existing `oui_vendor` assertions. + +- [ ] **Step 5: Commit** + +```bash +git add payload/user/remote_access/pager-webui/server.py tests/test_recon.py +git commit -m "feat: resolve recon device manufacturers locally" +``` + +### Task 2: Enrich Scan Associations and APs + +**Files:** +- Modify: `payload/user/remote_access/pager-webui/server.py` in `recon_scan_data()` and its nearby SQL helpers +- Test: `tests/test_recon.py` in `ReconDataTest` + +**Interfaces:** +- `recon_scan_data(scan_id, _timeout=20, _limit=None, db=None)` returns existing fields plus AP `device_identity`, `clients`, `client_count`, and client `vendor`, `associations`. + +- [ ] **Step 1: Extend the fixture and write failing tests** + +Add `hostap_client` rows, a second AP/client handshake pair, and a type-5 probe row to `make_db()`. Assert handshake association has SSID/BSSID/source, host-AP association has SSID and no BSSID, type-5 does not associate, duplicate evidence merges sources, AP counts are unique, and client/AP vendors are present. + +```python +def test_scan_detail_associations_are_confirmed_only(self): + data = server.recon_scan_data(1) + client = next(c for c in data['clients'] if c['mac'] == 'AE:77:C0:EB:31:41') + self.assertEqual(client['associations'][0]['sources'], ['handshake']) + self.assertEqual(client['associations'][0]['ssid'], 'Anderson-5') + self.assertEqual(client['associations'][0]['bssid'], 'C8:9E:43:64:80:80') + self.assertEqual(data['aps'][0]['client_count'], 1) + self.assertNotIn('ProbeOnlySSID', [a['ssid'] for a in client['associations']]) +``` + +- [ ] **Step 2: Run the focused test and verify failure** + +Run: `python3 -m unittest tests.test_recon.ReconDataTest -v` + +Expected: FAIL because enriched association fields do not exist. + +- [ ] **Step 3: Add bounded evidence queries** + +Load handshake pairs from the selected scan and resolve AP/client MACs from the +already loaded `wifi_device` rows. If `hostap_client` exists, query only rows +for the selected scan inside a guarded `try` block; if the table is absent, +use an empty list. Do not join type-5 rows into the association map. + +- [ ] **Step 4: Build deterministic deduplicated associations** + +Normalize MACs and use `(client_mac, bssid, ssid)` as the association key, +where a missing BSSID is represented separately from any AP row. Merge source +names in stable order `handshake`, then `hostap_client`, and retain host-AP +timestamps. Attach BSSID-backed associations to matching APs only; attach +SSID-only host-AP evidence to clients only. + +- [ ] **Step 5: Add identity and AP/client projection** + +Call `oui_identity()` for every AP BSSID and client MAC. Add `clients` and +`client_count` to AP objects, preserving current AP ordering and existing +fields. Keep clients with no association and set `associations: []`. + +- [ ] **Step 6: Run the complete Recon tests** + +Run: `python3 -m unittest tests.test_recon -v` + +Expected: PASS for all existing and new tests. + +- [ ] **Step 7: Commit** + +```bash +git add payload/user/remote_access/pager-webui/server.py tests/test_recon.py +git commit -m "feat: associate recon clients with confirmed networks" +``` + +### Task 3: Propagate Enriched Data Through Exports + +**Files:** +- Modify: `payload/user/remote_access/pager-webui/server.py` in Recon CSV/HTML builders and download handlers +- Test: `tests/test_recon.py` in `ReconReportTest` + +**Interfaces:** +- Existing JSON downloads preserve the enriched detail object. +- Existing CSV and HTML downloads include identity, client counts, and confirmed association details. + +- [ ] **Step 1: Write failing export assertions** + +Assert JSON contains `device_identity` and `associations`, CSV headers/rows +contain `Device Identity`, `Client Count`, and semicolon-separated confirmed +SSIDs, and HTML contains a `Confirmed Clients` section while excluding the +type-5 probe SSID. + +- [ ] **Step 2: Run report tests and verify failure** + +Run: `python3 -m unittest tests.test_recon.ReconReportTest -v` + +Expected: FAIL because serializers currently omit the enrichment. + +- [ ] **Step 3: Implement deterministic flattening and report sections** + +Keep JSON unchanged apart from its enriched source object. Add CSV columns +using a stable display identity and `'; '.join()` for multiple association +SSIDs. Add AP identity/client count to the existing AP table and a confirmed +client table to HTML, escaping all values through the existing HTML helpers. + +- [ ] **Step 4: Run report and full Python tests** + +Run: `python3 -m unittest tests.test_recon tests.test_health tests.test_ws -v` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add payload/user/remote_access/pager-webui/server.py tests/test_recon.py +git commit -m "feat: include recon identity in exports" +``` + +### Task 4: Update Recon Tables and Focus Details + +**Files:** +- Modify: `payload/user/remote_access/pager-webui/www/js/views.js` around `reconDefaultCols`, `colDefs`, table column definitions, and `renderFocus` +- Modify: `payload/user/remote_access/pager-webui/www/css/app.css` for compact identity/association detail styling if needed +- Test: `tests/test_recon.py` source assertions or existing frontend smoke harness + +**Interfaces:** +- Existing `views.recon` consumes enriched AP/client objects without new endpoints. +- `pw_recon_cols` migration preserves existing values and adds new defaults. + +- [ ] **Step 1: Add source-level failing assertions** + +Assert the source contains default AP `identity` and `clients` columns, client +`vendor` and `associated_ssid` columns, association search values, and a +`Confirmed Clients` focus section. + +- [ ] **Step 2: Implement client/AP display helpers** + +Add a helper that formats `device_identity` as manufacturer plus model when +model is non-null, otherwise manufacturer plus OUI for unknown values. Add a +helper that formats one association SSID or `first SSID +N` and sets the full +association summary in the cell `title`. + +- [ ] **Step 3: Update column defaults and settings** + +Replace AP `vendor` with `identity`, add AP `clients`, and add client `vendor` +and `associated_ssid` defaults. When loading old settings, merge missing keys +from `reconDefaultCols()` rather than discarding the saved preferences. + +- [ ] **Step 4: Update filtering, sorting, and table rendering** + +Include formatted identity and association strings in searchable values, +retain numeric sorting for client counts, and render the new columns through +the existing table/paginator code. + +- [ ] **Step 5: Add confirmed clients to AP focus** + +Render MAC, vendor, and comma-separated evidence sources from `ap.clients`. +Show `No confirmed clients` for an empty list and do not list probe-only +records. + +- [ ] **Step 6: Run frontend/source and Python tests** + +Run: `python3 -m unittest discover -s tests -p 'test_*.py' -v` + +Expected: PASS. Then run the project’s existing browser smoke harness if +available and verify desktop/mobile Recon rendering without changing device +configuration. + +- [ ] **Step 7: Commit** + +```bash +git add payload/user/remote_access/pager-webui/www/js/views.js payload/user/remote_access/pager-webui/www/css/app.css tests/test_recon.py +git commit -m "feat: show recon client identities and associations" +``` + +### Task 5: On-Device Read-Only Verification + +**Files:** +- No source changes expected. +- Evidence: local command output only; do not add credentials or device dumps to git. + +- [ ] **Step 1: Deploy through the project’s normal development/deploy path** + +Use the existing script documented in `README.md`; do not alter wireless or +PineAP settings. + +- [ ] **Step 2: Compare API data with read-only SQLite evidence** + +Query `scan`, `wifi_device`, `ssid`, `handshake`, and `hostap_client` using the +read-only SQLite URI. Confirm handshake BSSID/client pairs match API +associations, host-AP entries are SSID-only, and type-5 rows are absent from +associations. + +- [ ] **Step 3: Verify identity and UI behavior** + +Confirm AP identity resolves from an installed local database or fallback, +unknown/local MAC labels are honest, Access Points shows client counts and +identity, Clients shows vendor/SSID, and the focus sidebar shows confirmed +clients at desktop and mobile widths. + +- [ ] **Step 4: Run final verification before claiming completion** + +Run: `git diff --check`, `python3 -m unittest discover -s tests -p 'test_*.py' -v`, and `git status --short`. + +Expected: no whitespace errors, all tests pass, and only intended source +changes plus pre-existing worktree changes are present.