docs: plan recon and PineAP reliability work

This commit is contained in:
2026-08-20 16:25:42 -05:00
parent 00cbc52254
commit 377cc83060
@@ -0,0 +1,365 @@
# Recon, Reports, Enterprise Certificates, and PineAP Stability 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:** Make Recon status truthful, enrich offline HTML reports with signal maps, move Handshakes under PineAP, add self-signed Evil Enterprise certificate generation, and contain known `pineapd` crash loops.
**Architecture:** Preserve the existing single-file Python backend and vanilla-JavaScript SPA conventions. Add small pure helpers around existing Recon, report, enterprise, and health boundaries, with backend-generated state replacing frontend inference. Implement each subsystem test-first and deploy only after the complete local suite passes.
**Tech Stack:** Python 3.11 standard library and device `python3-light`, BusyBox/OpenWrt commands, OpenSSL CLI, vanilla JavaScript, inline CSS/SVG, `unittest`.
## Global Constraints
- Support Pineapple Pager firmware `24.10.1` and daemon SHA-256 `e2cf0453d7e7d2e08bf31a242289e480d44649923a5199eed954d73df0cf1da5`.
- Do not add Python packages, frontend packages, a build system, remote assets, or inline report JavaScript.
- Do not patch or replace `/usr/sbin/pineapd`.
- Keep pool broadcast disabled on known affected and unknown builds.
- Preserve existing authentication, same-origin checks, and API error conventions.
- Use argv command lists, never shell interpolation, for user-supplied certificate fields.
- Preserve unrelated worktree changes and do not stage `loot/` or untracked existing certificate files unless a task explicitly requires them.
---
### Task 1: Truthful Recon Coverage State
**Files:**
- Modify: `payload/user/remote_access/pager-webui/server.py:66-79, 1335-1765`
- Modify: `payload/user/remote_access/pager-webui/www/js/views.js:1680-1910, 2672-2783`
- Test: `tests/test_recon.py:165-512`
- Test: `tests/test_env_check.py:202-324`
**Interfaces:**
- Produces: `_recon_coverage(iface, active) -> dict` with `state`, `reason`, and `channel`.
- Produces: scan-state keys `baseline_scan_id`, `scan_id`, `settling_until`, and `result_warning`.
- Produces: `/api/recon/status` fields `coverage_24`, `scan_id`, and `result_warning`.
- Consumes: existing `_recon_hopper_preflight`, `_iface_associated`, `_dummy_sta_borrowable`, `recon_scans_data`, and `_recon_scan_state` lock.
- [ ] **Step 1: Add failing backend coverage tests**
Add tests that assert an enabled but unassociated dummy STA is not diagnosed as starvation, a successfully borrowed dummy reports `full`, a failed busy probe reports `current_channel_only` or `pinned` with its observed reason, and hopper interfaces are empty after loop cleanup. Mock `_iface_associated`, `_iface_admin_up`, `_run`, and channel state so each test controls runtime evidence rather than UCI alone.
- [ ] **Step 2: Run the focused coverage tests and verify failure**
Run: `python3 -m unittest tests.test_recon.ReconHopperTest tests.test_env_check.EnvCheckTest -v`
Expected: new assertions fail because status has no `coverage_24` object and completed hopper state retains interfaces.
- [ ] **Step 3: Implement backend coverage state**
Add a pure status helper that reports `idle` when no scan is active, `full` only when `wlan0mon` is in the active hopper, `unavailable` when the monitor is absent/down, and otherwise uses the preflight skip reason/current channel to report `pinned` or `current_channel_only`. Store preflight per-interface outcomes in `_recon_hop_state`; do not derive the result solely from `_sta_uplink_enabled()`.
Clear `_recon_hop_state['ifaces']` in the hopper loop's `finally` block while preserving the last coverage reason long enough for final status polling.
- [ ] **Step 4: Add failing scan identity tests**
Add tests that mock the existing maximum scan ID before start, return a newer row after start, and assert status tracks that exact ID. Add a timeout test using a mocked clock that advances 15 seconds beyond duration and asserts `result_warning` is set instead of selecting the prior scan.
- [ ] **Step 5: Run scan-state tests and verify failure**
Run: `python3 -m unittest tests.test_recon.ReconScanStateTest -v`
Expected: failures because `_recon_scan_state` has no baseline/result identity or settlement state.
- [ ] **Step 6: Implement result association and settlement**
Before native start, query the maximum scan ID. After acceptance, store it as `baseline_scan_id`. During watchdog/status updates, query for the first row with a greater ID and set `scan_id`. At elapsed duration, allow a 15-second settlement window; after it expires without a row, set `result_warning` and end the run. Reset all identity fields on explicit reset/start.
- [ ] **Step 7: Update Recon frontend state**
Render backend `coverage_24` copy instead of the `wlan0Sta && !hop24` fallback. During auto-follow, select `status.scan_id` even when its counts are zero. Show completion only after a tracked result exists, or show the backend result warning after settlement failure. Keep existing styling and ASCII copy.
- [ ] **Step 8: Run Recon and environment tests**
Run: `python3 -m unittest tests.test_recon tests.test_env_check -v`
Expected: PASS.
- [ ] **Step 9: Commit Recon correctness**
```bash
git add payload/user/remote_access/pager-webui/server.py payload/user/remote_access/pager-webui/www/js/views.js tests/test_recon.py tests/test_env_check.py
git commit -m "fix: report observed recon coverage and scan results"
```
---
### Task 2: Firmware-Gated pineapd Containment
**Files:**
- Modify: `payload/user/remote_access/pager-webui/server.py:4432-4616, 4667-4814, 6982-6992`
- Modify: `payload/user/remote_access/pager-webui/www/js/app.js:516-546`
- Modify: `payload/user/remote_access/pager-webui/README.md:112-130`
- Modify: `README.md:54-61, 112-130`
- Test: `tests/test_health.py`
- Test: `tests/test_env_check.py`
- Test: `tests/test_robustness.py`
**Interfaces:**
- Produces: `_pineapd_build() -> dict` with `firmware`, `sha256`, and `policy` (`affected` or `unknown`).
- Produces: `_reconcile_pineapd_safety() -> dict` listing changed keys and errors.
- Produces: health-state keys `events`, `quarantined`, `stable_since`, `build`, and `last_reconcile`.
- Produces: `GET /api/health/diagnostics` as a JSON download with no secrets.
- [ ] **Step 1: Add failing build-policy and reconciliation tests**
Test the exact affected firmware/hash pair, an unknown hash, the five required safe UCI values, no unrelated UCI writes, one commit only when values changed, and no command-socket ping. Assert unknown builds are reported but not automatically rewritten.
- [ ] **Step 2: Run focused tests and verify failure**
Run: `python3 -m unittest tests.test_health tests.test_env_check -v`
Expected: failures because build policy and production reconciliation do not exist.
- [ ] **Step 3: Implement build detection and safe reconciliation**
Read firmware through the existing release parser and hash `/usr/sbin/pineapd` with `sha256sum`. Match the exact affected tuple. On affected builds, compare and set only:
```text
pineapd.@ssidpool[0].disable=1
pineapd.wlan2mon.disable=1
pineapd.wlan2mon.hop=0
pineapd.wlan1mon.bands=5
pineapd.wlan0mon.bands=2
```
Commit `pineapd` once if needed. Invoke reconciliation before Mark VIII explicitly starts/restarts the daemon, not continuously while a healthy daemon is running.
- [ ] **Step 4: Add failing circuit-breaker tests**
Use a mocked clock and process/SIGSEGV samples to assert three crashes in two minutes enter quarantine, Mark VIII issues no further restart, five stable minutes clear quarantine, and explicit recovery reruns reconciliation. Add pool tests for preserving 64 entries/4096 bytes and quarantining then clearing data above either limit only during a correlated crash loop.
- [ ] **Step 5: Implement bounded health recovery**
Track timestamped PID/SIGSEGV events in a bounded in-memory list. Enter quarantine at three crashes in 120 seconds. Stop Mark VIII restart calls while quarantined. Clear after 300 stable seconds or explicit recovery. For an oversized pool during the threshold-crossing event, write a timestamped diagnostic copy under `/root/loot/pineapd-diagnostics/`, clear through the existing safe pool command, and make one reconciled restart attempt.
- [ ] **Step 6: Add diagnostics endpoint tests**
Assert the download contains firmware/hash, policy, relevant UCI, monitor state, pool metrics, bounded event history, and health actions. Assert it excludes certificate private-key contents, enterprise secrets, cookies, and daemon binary data.
- [ ] **Step 7: Implement diagnostics and UI health detail**
Register `GET /api/health/diagnostics`. Reuse `Download` with JSON content and attachment naming. Expand `/api/health` with build/quarantine/reconcile data and display concise affected/unknown/quarantined state in the existing top-bar health treatment.
- [ ] **Step 8: Correct stability documentation**
Document automatic affected-build reconciliation, unknown-build behavior, passive checks, thresholds, and diagnostic preservation. Remove claims that normal health checks always clear the pool.
- [ ] **Step 9: Run health, environment, and robustness tests**
Run: `python3 -m unittest tests.test_health tests.test_env_check tests.test_robustness -v`
Expected: PASS.
- [ ] **Step 10: Commit containment changes**
```bash
git add payload/user/remote_access/pager-webui/server.py payload/user/remote_access/pager-webui/www/js/app.js payload/user/remote_access/pager-webui/README.md README.md tests/test_health.py tests/test_env_check.py tests/test_robustness.py
git commit -m "fix: contain affected pineapd crash loops"
```
---
### Task 3: Self-Contained Report Signal Maps
**Files:**
- Modify: `payload/user/remote_access/pager-webui/server.py:1921-2174`
- Test: `tests/test_recon.py:991-1191`
**Interfaces:**
- Produces: `_report_channel_maps(aps) -> str` containing zero or more escaped inline SVG sections.
- Produces: `_report_channel_map(aps, band) -> str` for one of `2.4`, `5`, or `6`.
- Consumes: AP dictionaries from `recon_scan_data` and existing `_esc_html`, `band_of`, and report CSS.
- [ ] **Step 1: Add failing report-map tests**
Add representative 2.4, 5, and 6 GHz APs and assert one SVG per populated band, expected labels, all four signal colors, raised-cosine path data, and escaped SSID/vendor metadata inside `<title>`. Assert no `<script>`, remote URL, or malformed SVG for missing channel/frequency.
- [ ] **Step 2: Run report tests and verify failure**
Run: `python3 -m unittest tests.test_recon.ReconReportTest tests.test_recon.ReconArchivesTest -v`
Expected: failures because reports contain occupancy tables but no signal-map SVG.
- [ ] **Step 3: Implement pure SVG helpers**
Port the existing channel-to-frequency, band-range, -100/-30 dBm scaling, +/-10 MHz width, and raised-cosine sampling math to Python. Clamp signal values to the graph range. Generate deterministic `viewBox` SVG with axes, labels, paths, and escaped titles. Omit bands with no plottable APs.
- [ ] **Step 4: Integrate maps and print styling**
Add responsive `.signal-map`, `.map-grid`, axis, and print CSS to `REPORT_CSS`. Insert maps after stat cards and before breakdown tables in `_recon_html_download`. Label live GPS as a report-generation-time fix and preserve archive omission.
- [ ] **Step 5: Run report tests**
Run: `python3 -m unittest tests.test_recon.ReconReportTest tests.test_recon.ReconArchivesTest -v`
Expected: PASS.
- [ ] **Step 6: Commit report maps**
```bash
git add payload/user/remote_access/pager-webui/server.py tests/test_recon.py
git commit -m "feat: add signal maps to HTML recon reports"
```
---
### Task 4: Move Handshakes Under PineAP
**Files:**
- Modify: `payload/user/remote_access/pager-webui/www/js/app.js:94-99, 411-436`
- Modify: `payload/user/remote_access/pager-webui/www/js/views.js:330-363, 944-948, 1747-1750, 2905-2908`
- Test: `tests/test_recon.py`
**Interfaces:**
- Produces: canonical route `#/pineap/handshakes` rendering `views.recon_handshakes` within `pineapShell`.
- Preserves: redirect `#/recon/handshakes` to the canonical route.
- [ ] **Step 1: Add failing static route tests**
Assert `PINEAP_TABS` contains `#/pineap/handshakes`, `RECON_TABS` does not contain Handshakes, the router registers the canonical route, the legacy redirect exists, and the Recon card links to the canonical route.
- [ ] **Step 2: Run route tests and verify failure**
Run: `python3 -m unittest tests.test_recon -v`
Expected: new navigation assertions fail.
- [ ] **Step 3: Move the route and shell**
Add Handshakes to `PINEAP_TABS`, remove it from `RECON_TABS`, register `#/pineap/handshakes`, redirect the old hash, update the summary-card link, and render the existing Handshakes body inside `pineapShell(root, '#/pineap/handshakes')` without changing data APIs.
- [ ] **Step 4: Run Recon tests**
Run: `python3 -m unittest tests.test_recon -v`
Expected: PASS.
- [ ] **Step 5: Commit navigation**
```bash
git add payload/user/remote_access/pager-webui/www/js/app.js payload/user/remote_access/pager-webui/www/js/views.js tests/test_recon.py
git commit -m "ui: move handshakes into PineAP"
```
---
### Task 5: Evil Enterprise Self-Signed Certificates
**Files:**
- Modify: `payload/user/remote_access/pager-webui/server.py:3462-4151, 6673-6683`
- Modify: `payload/user/remote_access/pager-webui/www/js/views.js:1386-1628`
- Modify: `payload/user/remote_access/pager-webui/www/css/app.css:501-620`
- Modify: `payload/user/remote_access/pager-webui/pagerwebui.init:15-25`
- Test: `tests/test_attacks.py:204-319, 434-455`
- Test: `tests/test_pineap_enterprise.py`
**Interfaces:**
- Produces: `_validate_cert_request(fields) -> dict` normalized subject/SAN/validity data or raises `ValueError`.
- Produces: `_ent_cert_status() -> dict` with validity, subject, SANs, fingerprint, dates, key type/size, and key-match state.
- Produces: `_generate_ent_certificate(fields) -> dict` with install/restart/rollback result.
- Produces: `GET /api/pineap/enterprise/certificate` and `POST /api/pineap/enterprise/certificate/generate`.
- [ ] **Step 1: Add failing validation and status tests**
Test country length, control-character rejection, 128-byte subject limits, required common name, 1-3650 validity, no more than 20 SANs, ASCII DNS syntax/253-character limit, and normalization of comma/newline-separated SAN input. Mock OpenSSL status commands and assert private-key content is never returned.
- [ ] **Step 2: Run enterprise tests and verify failure**
Run: `python3 -m unittest tests.test_attacks tests.test_pineap_enterprise -v`
Expected: failures because certificate request/status helpers and routes do not exist.
- [ ] **Step 3: Implement validation and certificate inspection**
Use strict subject-field validation and DNS-label parsing. Inspect certificate metadata with bounded OpenSSL argv calls. Compare certificate/key public-key digests without exposing them through the API. Return clear invalid/missing state rather than treating nonempty files as valid.
- [ ] **Step 4: Add failing atomic generation tests**
Mock temporary-directory creation and OpenSSL to assert `req -x509 -newkey rsa:2048 -sha256 -nodes`, SAN/serverAuth extensions, mode `0600` for key, `0644` for certificate, validation before replacement, backup restoration on failure, and cleanup of temporary files.
- [ ] **Step 5: Implement atomic generation**
Generate under a mode-`0700` directory inside `/root/loot/enterprise`. Validate parseability, current validity, key match, subject, and SANs. Back up active files, atomically replace them, and restore backups on install failure. Replace `_ensure_ent_certs` shared-bundle copying with generation through this path when valid active files are absent.
- [ ] **Step 6: Add failing active-restart and rollback tests**
When `_ent_running()` is true, assert the existing deployment state is copied, the new certificate is installed, `_deploy_enterprise` is invoked with unchanged settings, and success requires enabled hostapd. Force deployment failure and assert old certificate restoration plus one previous-state redeploy attempt; include rollback outcome in HTTP 502 data.
- [ ] **Step 7: Implement serialized restart and rollback**
Use the attack/deploy lock for certificate generation. Keep old certificate backups until redeploy succeeds. On failure, stop partial enterprise state, restore files, and attempt previous deployment exactly once. Do not recursively call certificate generation from deployment fallback.
- [ ] **Step 8: Add certificate APIs and matching UI card**
Register authenticated GET/status and POST/generate routes. Add an Evil Enterprise card using existing `.settings-form-grid`, `.pineap-infobox`, `runAction`, and button patterns. Include the requested fields, installed metadata, generation busy state, and a warning that active Enterprise will restart automatically. Refresh attack and certificate status after completion.
- [ ] **Step 9: Fix secret permissions**
Replace `chmod -R 755 "$PAGER_WEBUI_DIR"` with directory/file-specific safe modes that preserve executable scripts while keeping bundled/runtime keys `0600`. Explicitly chmod runtime `server.key`, `eap_users`, and enterprise state to `0600` after writes.
- [ ] **Step 10: Run enterprise tests**
Run: `python3 -m unittest tests.test_attacks tests.test_pineap_enterprise -v`
Expected: PASS.
- [ ] **Step 11: Commit certificate flow**
```bash
git add payload/user/remote_access/pager-webui/server.py payload/user/remote_access/pager-webui/www/js/views.js payload/user/remote_access/pager-webui/www/css/app.css payload/user/remote_access/pager-webui/pagerwebui.init tests/test_attacks.py tests/test_pineap_enterprise.py
git commit -m "feat: generate Evil Enterprise certificates"
```
---
### Task 6: Integration Verification and Pager Deployment
**Files:**
- Modify if required by verified defects: files from Tasks 1-5 only
- Build artifact: `build/pager-webui/payload-cGFnZXItd2VidWk.zip`
**Interfaces:**
- Consumes: all completed task interfaces.
- Produces: verified local suite, payload build, live Pager deployment, and browser/device evidence.
- [ ] **Step 1: Run every unit-test module in isolated processes**
Run:
```bash
for f in tests/test_*.py; do python3 -m unittest "tests.$(basename "$f" .py)" -v || exit 1; done
```
Expected: every module reports `OK`.
- [ ] **Step 2: Build the payload**
Run the repository's existing payload build/deploy script in its build-only or pre-upload stage. Verify the staged payload contains updated source and no `loot/`, temporary private keys, test caches, or unrelated untracked files.
- [ ] **Step 3: Deploy to the authorized Pager**
Use `scripts/deploy.sh --password 'Bryce9205'` against `root@172.16.52.1`. Record whether safe UCI reconciliation changes the five affected keys. Do not enable pool broadcast.
- [ ] **Step 4: Verify daemon and APIs**
Confirm `pidof pineapd`, `/api/health`, affected build/hash, non-quarantined status, safe UCI values, both monitor interfaces expected by hardware, and diagnostic download. Watch logs long enough to cover multiple historical 15-second crash intervals.
- [ ] **Step 5: Verify Recon behavior**
Run a controlled timed scan with enabled but unassociated `dummy_radio0`; verify no false starvation message, tracked scan ID, and 2.4 GHz results. Then use an existing legitimate AP/STA pin scenario and confirm channel-limited copy is accurate.
- [ ] **Step 6: Verify reports and navigation in browser**
At desktop and mobile viewports, verify Recon has Scanning/Reports only, PineAP has Handshakes, the old hash redirects, and generated live/archive HTML reports contain printable maps for all available bands.
- [ ] **Step 7: Verify certificate generation and restart**
Generate a certificate with representative attributes, inspect it with `openssl x509 -text`, verify file modes, deploy Enterprise, generate a second certificate while active, and confirm automatic restart uses the new fingerprint. Exercise a controlled mocked/local failure for rollback if forcing hostapd failure on the live device would risk connectivity.
- [ ] **Step 8: Review final diff and status**
Run `git status --short`, `git diff --check`, and inspect the cumulative diff from `00cbc52`. Confirm `loot/` and pre-existing untracked `certs/` remain unstaged.
- [ ] **Step 9: Commit only verified integration fixes if any**
If verification required code changes, stage only those files and commit with a focused message. Do not create an empty commit.