Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
updates:
# Dependabot version updates for GitHub Actions workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
target-branch: "develop"
commit-message:
prefix: "ci"
include: "scope"
open-pull-requests-limit: 5
groups:
github-actions:
patterns:
- "*"
labels:
- "dependencies"
- "github-actions"
7 changes: 4 additions & 3 deletions .github/workflows/build-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,22 @@ jobs:
- MsChapCapture
- WpsCapture
- KarmaResponder
- EspNowCapture

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.x'

- name: Install PlatformIO
run: pip install platformio intelhex

- name: Cache PlatformIO packages
uses: actions/cache@v4
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('**/platformio.ini') }}
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: "CodeQL Analysis"

on:
push:
branches: [develop, main]
pull_request:
branches: [develop]
schedule:
- cron: '30 2 * * 1'

jobs:
analyze:
name: Analyze (C/C++)
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.x'

- name: Cache PlatformIO packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('**/platformio.ini') }}
restore-keys: pio-${{ runner.os }}-

- name: Install PlatformIO
run: pip install platformio intelhex

- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: c-cpp
queries: security-extended

- name: Build C++ sources with PlatformIO
run: |
pio run -d examples/DynamicControl
pio run -d examples/ExportFormats
pio run -d examples/StorageAndNVS
pio run -d examples/FuzzingAndInjection

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
category: "/language:c-cpp"
6 changes: 3 additions & 3 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Install Doxygen
run: |
Expand All @@ -34,7 +34,7 @@ jobs:
doxygen Doxyfile

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v5
with:
path: 'docs/html'

Expand All @@ -48,4 +48,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ docs/
# Internal documentation
COOKBOOK.md
/tools/
AGENTS.md
agents.md

29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,14 @@ engine.begin(cfg); // begins normally; begin() clamps automatically

`validateConfig()` is a free inline function in the `politician` namespace — zero allocations, zero dependencies.

`begin()` rejects a smart-hopping configuration whose minimum dwell cannot be
represented after the `min + 50 ms` correction. Calling `begin()` on an already
initialized engine stops the previous runtime first, so rebegin is deterministic.
Calling `stop()` is safe more than once; it disables capture, unregisters the
instance, and releases the worker resources. The destructor also stops an active
engine. WiFi driver ownership remains process-wide and is reused by later
instances.

### Configuration

```cpp
Expand Down Expand Up @@ -291,6 +299,26 @@ void setPacketLogger(PacketCb cb); // Raw promiscuous-mode frames
void setLogger(LogCb cb); // Redirect internal log output
```

Callbacks run on the engine worker task while the engine state is protected.
They should do short, non-blocking work and must not call blocking Politician
methods or retain references to callback records. In particular,
`EspNowRecord::payload` points into the current receive buffer and is valid only
for the duration of the callback; copy it when it must outlive the callback.

ESP-NOW capture is enabled by default unless `POLITICIAN_NO_ESPNOW` is defined.
The parser accepts Espressif vendor-specific action frames and reports the
source, destination, channel, RSSI, timestamp, and payload through
`setEspNowCallback()`.

### Regulatory domain

Regulatory-domain selection is intentionally not implemented yet. The current
channel helper contains common 2.4 GHz and 5 GHz channels but has no authoritative
country source, DFS/CAC state, or API for selecting a country. A future change
should add an explicit country/domain configuration backed by the ESP-IDF WiFi
country API, filter channels from that result, and define behavior for DFS and
unsupported regions before exposing it publicly.

### Engine Control

```cpp
Expand Down Expand Up @@ -1127,6 +1155,7 @@ beaconFlood(ssids, ssidCount, 6, 5000);
| `MsChapCapture` | Bare EAP-MSCHAPv2 credential capture (hashcat -m 5500) |
| `KarmaResponder` | KARMA rogue AP responder with runtime Serial toggle |
| `PassiveSensing` | RSSI-based human presence and motion detection |
| `EspNowCapture` | ESP-NOW action frame capture and payload parsing |

## Legal & Ethical Use

Expand Down
46 changes: 46 additions & 0 deletions docs/delivery/ChangeSummary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Change Summary

## Changes

- Added `EspNowCapture` to the example build matrix and restored its example.
- Added the ESP-NOW callback record/parser contract.
- Added deterministic host-side `Config` validation tests.
- Added idempotent stop/rebegin cleanup for per-instance task, ring buffer, and
mutex resources.
- Made `begin()` transactional after instance registration, including Wi-Fi
driver teardown when that attempt initialized Wi-Fi.
- Added an ISR-safe shutdown barrier: registry removal and shutdown marking are
serialized, in-flight callbacks drain, and the worker exits before its
ringbuffer and mutex are destroyed.
- Removed the duplicate `[env:esp32]` section from `EspNowCapture`.
- Masked ESP-NOW MACs and disabled payload printing by default in the example;
complete payload diagnostics are compile-time opt-in.
- Documented lifecycle and callback contracts.

## Breaking Changes

None. Existing public methods and callback signatures remain compatible.

## Migration Scope

None. ESP-NOW is available by default and can be excluded with
`POLITICIAN_NO_ESPNOW`.

## Rollback Strategy

Revert the commits in reverse order. No database, persistent storage, or
external state migration is introduced.

## Known Limitations

- PlatformIO is not installed in the local environment, so ESP32 builds were
not run locally.
- Regulatory-domain selection is documented as a proposal only.
- The host test covers public configuration validation, not ESP32 FreeRTOS
scheduling or WiFi-driver behavior.
- PlatformIO is unavailable in the local environment; the native test was
compiled and executed directly with Clang, while the ESP32 example build was
not run locally.
- `stop()` must not be called from a Politician callback running on the worker
task, because waiting for that same worker would deadlock. Callbacks should
signal application state and let the main/application task perform stop.
26 changes: 26 additions & 0 deletions docs/delivery/ImplementationPlan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Implementation Plan

Base: `upstream/main`
Branch: `feat/core-reliability`

1. Add the existing ESP-NOW capture example to the PlatformIO CI matrix.
2. Add deterministic host-side coverage for public `Config` validation without
including Arduino or ESP32 runtime headers.
3. Preserve the public API while making `begin()` validation reject an unsafe
dwell correction and making stop/rebegin release per-instance runtime
resources.
4. Document lifecycle, callback lifetime/threading constraints, compatibility,
and the regulatory-domain proposal.

Security remediation completed on the current branch:

5. Make `begin()` roll back registry, Wi-Fi, mutex, ringbuffer, and worker
resources on every post-registration failure.
6. Make `stop()` mark shutdown and unregister under an ISR-safe critical
section, wait for in-flight callbacks, then wait for the worker before
deleting the ringbuffer and mutex.
7. Keep the ESP-NOW example's MAC output masked and payload output disabled by
default. Full payload diagnostics require `ESP_NOW_DIAGNOSTICS=1`.

Regulatory-domain behavior remains intentionally out of scope until a country
source and DFS policy can be defined safely.
12 changes: 12 additions & 0 deletions examples/EspNowCapture/platformio.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[platformio]
default_envs = esp32

[env:esp32]
platform = espressif32 @ ~6.13.0
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
symlink://../..
build_flags =
-D CORE_DEBUG_LEVEL=5
48 changes: 48 additions & 0 deletions examples/EspNowCapture/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <Arduino.h>
#include <Politician.h>

using namespace politician;

Politician engine;

void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n\n--- Politician ESP-NOW Capture Example ---");

Config cfg;
// We don't necessarily need to capture beacons for ESP-NOW, but it helps populate the AP cache
cfg.capture_filter = LOG_FILTER_BEACONS | LOG_FILTER_PROBES;

// Initialize the engine
if (engine.begin(cfg) != politician::OK) {
Serial.println("Failed to start Politician engine");
while (1) delay(100);
}

// Set the callback to process ESP-NOW payloads
engine.setEspNowCallback([](const EspNowRecord &rec) {
Serial.printf("[ESP-NOW] ch=%d rssi=%d src=%02x:%02x:%02x:%02x:%02x:%02x dst=%02x:%02x:%02x:%02x:%02x:%02x len=%d payload=[",
rec.channel, rec.rssi,
rec.src[0], rec.src[1], rec.src[2], rec.src[3], rec.src[4], rec.src[5],
rec.dst[0], rec.dst[1], rec.dst[2], rec.dst[3], rec.dst[4], rec.dst[5],
rec.length);

// Print the first few bytes of the payload in hex
int printLen = rec.length > 16 ? 16 : rec.length;
for (int i = 0; i < printLen; i++) {
Serial.printf("%02x ", rec.payload[i]);
}
if (rec.length > 16) Serial.print("...");
Serial.println("]");
});

// Start hopping to discover ESP-NOW devices on any channel,
// or use engine.lockChannel(1) if you know their specific channel.
engine.startHopping();
Serial.println("Hopping started. Waiting for ESP-NOW traffic...");
}

void loop() {
engine.tick();
}
Loading
Loading