The phone/wearable gate used a manual OUI check that missed privately-addressed
MACs (iOS 14+/Android 10+ randomize per network). Any phone not pre-configured
in device_labels was silently enrolled with no alert, causing ARRIVED/OCCUPIED
to never fire. Replacing the gate with is_personal_device() catches OUI, labels,
and LAA bit uniformly.
ARP scan interval drops from 30s to 15s so worst-case arrival detection is 15s.
Watchdog timeout and departure debounce were both 900s — stacked,
worst-case VACANT alert was 30 minutes after leaving. With active ARP
scanning every 30s we can confirm absence quickly, so both timers drop
to 180s. Override via NET_ALERTER_WATCHDOG_TIMEOUT and
NET_ALERTER_DEPARTURE_DEBOUNCE env vars.
Non-phone OUIs (IoT, thermostats, TVs, etc.) are silently enrolled
for presence tracking but never trigger Matrix alerts. Only devices
matching MOBILE_DEVICE_OUIS or with an explicit label in device_labels
produce alerts.
Passive ARP sniffing fails on modern WiFi because the AP intercepts
ARP broadcasts and responds on behalf of clients. Active scanning
sends who-has to all 254 /24 addresses and collects replies directly,
making presence detection reliable regardless of AP configuration.
Also adds monitor mode sniffer thread that activates automatically if
a second WiFi interface is present (e.g. USB dongle). Reads raw 802.11
frames, bypassing the AP stack entirely for station-transmitted data
and management frames.
When systemd sends SIGTERM, asyncio cancels the scan task before
BleakScanner.__aexit__ runs. BlueZ already stopped discovery by then,
so stop() throws 'No discovery started'. This was crashing the service
and triggering a 22-restart loop before systemd settled. Catch the
specific error string and treat it as a debug-level no-op.
is_labeled compared raw src_mac against normalized device_labels keys,
always evaluating False for labeled devices. ARP replies (opcode=2) from
personal MACs were never trusted, so on_arrival only fired on ARP
requests (opcode=1) which are rare. Labeled devices now correctly pass
the opcode=2 gate, triggering arrival alerts with their display name.
ble_alerter.py: probe Apple BLE devices via GATT characteristic 0x2A00
to discover device names (iPhone, Apple TV, etc). Names persist to
ble_names.json with last_seen timestamps. GATT_PROBE=1 enables active
probing; without it, Apple device last_seen timestamps still tracked.
net_alerter.py: on WiFi device arrival, read ble_names.json and log
any Apple BLE devices seen within the last 60s as correlation candidates.
Gives operator visibility into which BLE MACs co-arrived with a WiFi device.
Shared state file: /opt/net_alerter/ble_names.json
iOS sends 'My iPhone' via DHCP option 12 regardless of the actual device
name set on the phone. Operator-set labels are authoritative and should
not be overridden by privacy-obscured DHCP hostnames.
DHCP triggers on_arrival immediately but iPhone sends mDNS frames
milliseconds later. Without the delay the alert fires with no hostname
and falls back to the device label. Timer reads from known_devices at
fire time so mDNS-updated hostname is used.
Label in device_labels is for tracking/identification. When mDNS reveals
the real device name, use it. Label serves as fallback when no hostname
has been discovered yet.
_extract_mdns_hostname scans A records in mDNS payload before on_arrival fires,
so the ARRIVED alert shows the device's actual name instead of the IP.
_parse_mdns_for_names also now updates known_devices hostname on subsequent frames,
so departure alerts get the real name even if the first frame had no A record.
Operator needs ARRIVED for any phone or wearable on site, including
unknown devices. Silent seeding only applies to infrastructure and
non-personal devices.
seed_from_arp_cache was adding labeled personal devices to known_devices at startup,
causing all subsequent arrivals to be ignored (already_known=True). Now skips them
so on_arrival fires normally when they rejoin.
ARP opcode=2 (replies) are now treated as genuine signals for explicitly labeled devices.
iPhones with privacy MACs frequently emit only opcode=2 after initial association — the
AP proxy forgery concern doesn't apply to devices the operator has explicitly named.
Phone (c6:37:b0:b2:07:30) was cycling through arrivals/departures faster
than the 30-min churn window threshold, causing _check_churn to add its
IP to infrastructure_ips. All subsequent on_arrival calls were silently
dropped by the infrastructure IP check, so no ARRIVED alerts fired.
Two fixes:
- _check_churn: skip churn suppression entirely for MACs in device_labels
or personal_devices — labeled devices are never infrastructure
- on_arrival already-known path: upgrade ip from '0.0.0.0'/'unknown'
(ARP probe placeholder) to real IP when netlink delivers the actual address
Travel routers and other LAA-bit devices get misclassified as personal phones
because the LAA heuristic fires on any MAC with bit 1 set (b6:cc is 0xb6).
Add infrastructure_macs set checked before the LAA heuristic in is_personal_device().
NET_ALERTER_INFRA_MACS env var (comma-separated MACs) populates the set at startup.
b6:cc was mislabeled as 'Dane iPhone' causing it to be treated as a
personal device. Removing the label fixes the problem without needing
any Tailscale-specific code. Keep mDNS and ARP arrival fixes.
tailscale status --json uses CurAddr for the active direct endpoint,
not Endpoints (which is always null/absent). Without this fix the
Tailscale exclusion set was always empty.
Tailscale peers (travel router b6:cc) were kept perpetually REACHABLE in the
kernel ARP cache by OPi's WireGuard keepalives, refreshing last_seen and
blocking departure alerts. seed_tailscale_peers() excludes their MACs.
mDNS was only refreshing the watchdog timer for already-known devices. Apple
devices send mDNS on WiFi join as the primary signal — now calls on_arrival()
so first-seen mDNS triggers ARRIVED alert and occupancy update.
ARP opcode 1 for personal devices never called on_arrival(), so devices that
joined without DHCP (c6:37 My iPhone at 01:25) got no alert. Now fires
on_arrival() for personal devices not yet in known_devices.
Repeated watchdog calls for a device already in departing=True state
were feeding the churn suppressor, which misclassified the iPhone's IP
as infrastructure after 3 calls within 30 min. This blocked all
subsequent departure processing and caused a 31-min alert delay.
Fix: gate the on_departure() call on a should_depart flag set only when
the device is known and not already departing.
Watchdog-triggered departures set departing=True after 15 min of silence.
If the device returned during the 15-min debounce window, on_arrival()
treated it as a rapid flap and suppressed the ARRIVED alert — even though
the device had been genuinely absent for 15+ minutes.
Store depart_initiated timestamp when setting departing=True. On re-arrival,
check the gap: < 120s = true rapid flap (transient Netlink/DHCP event),
suppress as before. >= 120s = real absence confirmed by watchdog, pop the
device from known_devices and let it fall through to the new-device path
so the ARRIVED alert fires.
Also removes debug traceback instrumentation left in _update_last_seen
from the prior investigation session.
Without this, last_seen is empty after every restart and the watchdog
skips all devices (ts is None → continue). Devices that are genuinely
active refresh last_seen via ARP requests or mDNS. AP proxy ghosts only
send ARP replies (now filtered), so they go silent and the watchdog
fires departure after WATCHDOG_TIMEOUT_SEC.
AP ARP proxy sends REPLY frames with the disconnected client's MAC as
the Ethernet source. Treating those as genuine device signals kept
last_seen fresh, preventing the watchdog from detecting departure.
Only ARP REQUEST (opcode 1) frames originate from the device itself.
Skip _update_last_seen() for opcode 2 (REPLY) frames.
ARP proxy on WiFi APs causes the kernel neighbor table to show devices
as REACHABLE even after they disconnect. This made last_seen refresh
continuously from on_arrival() (netlink path), preventing the watchdog
from ever detecting departure.
Two fixes:
1. Remove _update_last_seen() from on_arrival() — netlink events are
not reliable as a genuine-traffic source on ARP-proxy networks.
last_seen is now only updated by the raw packet capture path (ARP
requests, mDNS, DHCP) where src_mac must be the device itself.
2. Watchdog now covers all is_personal_device() entries in known_devices,
not just the personal_devices enrollment set. device_labels entries
(like named phones) were previously unmonitored by the watchdog.
Kernel fires RTM_NEWNEIGH for NUD_STALE, NUD_DELAY, and NUD_PROBE states
as it probes a potentially-departed device. Each probe was calling on_arrival()
which refreshed last_seen, preventing the watchdog from ever detecting departure.
Only NUD_REACHABLE confirms a device is actually on the network.
Garmin and Fitbit wearables use globally-assigned OUI MACs (no private addressing)
so the LAA bit check misses them. Added their OUIs to MOBILE_DEVICE_OUIS.
Consolidated all three detection signals into is_personal_device():
1. Explicit config (device_labels, personal_devices)
2. OUI table (phones + Garmin/Fitbit)
3. LAA bit (iOS 14+/Android 10+ private MACs)
Occupancy _is_personal now delegates to is_personal_device instead of duplicating logic.
Modern phones use private/random MACs (iOS 14+, Android 10+) which
always have the locally-administered bit set (bit 1 of first octet).
TVs, gateways, and smart home devices use globally-assigned OUI MACs
where this bit is clear.
No configuration needed — any LAA MAC counts toward occupancy.
Explicit device_labels/personal_devices still work as fallback.
Previously counted every non-infrastructure device toward OCCUPIED state,
causing TVs and other stationary equipment to falsely indicate occupancy.
Now only MACs in device_labels or personal_devices count. Everything else
is observed but ignored for presence purposes.
device_labels keys are normalized (no colons, uppercase) but known_devices keys
are colon-lowercase. device_labels.get(mac) always returned None.
Also extend label display to arrival and departure alerts — if a MAC has a
configured label, show that instead of vendor/hostname.
NET_ALERTER_DEVICE_LABELS=MAC=Name,MAC=Name maps known devices to
friendly names. OCCUPIED alerts now show which devices are present.
Falls back to vendor string then MAC tail if no label configured.
Previously the service would suppress the startup OCCUPIED alert, meaning
if phones were already present when the service started, no Matrix alert
ever fired until a departure+return cycle. Users never received confirmation
the sensor was working.
Changes:
1. mDNS as separate signal category: Map 'mdns' signal_type to its own signal_category (not grouped with 'wifi'). Enrollment now triggers when EITHER (a) len(signal_types) >= 2 OR (b) 'mdns' in signal_types.
2. Active probes on device discovery: When new identity is created, fire non-blocking probes to collect additional signals. Uses arping or ping (no new deps) to probe IP if available. Runs in daemon thread to avoid stalling correlation loop.
This allows devices announcing via mDNS to be enrolled immediately without waiting for a second signal type, while still collecting additional signals for richer device profiles.
Strategy 0 now checks if a MAC already exists in any identity's wifi_macs/ble_macs
sets before creating a new record. Prevents duplicate identity creation on repeated
mDNS/ARP signals.
Strategy 3 now conditionally updates existing records instead of unconditionally
overwriting them. Preserves signal_count and signal_types accumulation needed for
auto-enrollment (2+ signal types).
Fixes issue where devices seen only via mDNS/ARP never reached enrollment threshold.
Wrap BleakScanner context manager in try-except to detect adapter
initialization errors. If 'No Bluetooth adapters found' or similar, log
warning and set shutdown event to trigger graceful exit with code 0.
Prevents systemd restart loop on hardware without BT adapter (e.g. OPi).
- Add detect_wifi_config_type() to identify config format (netplan vs wpa_supplicant)
- Add read_wpa_supplicant() to parse wpa_supplicant-wlan0.conf
- Add write_wpa_supplicant() to write updated config with proper formatting
- Update run_interactive(), run_list(), run_add() to detect and dispatch to correct handler
- Both formats now auto-detected; existing netplan paths continue to work
- wpa_supplicant format used by OPi Zero 3 / Armbian defaults
P1 Fixes:
- Signal count enrollment logic: Changed from broken signal_count increment to tracking distinct signal types (BLE vs WiFi) using a set. Device enrolls when len(signal_types) >= 2, ensuring multi-source correlation.
- DNS mDNS pointer endianness: Added bounds check to prevent out-of-bounds reads when following DNS compression pointers. Checks pointer_offset < offset and pointer_offset < len(payload) before recursing.
- Nested RLock fragility: Refactored enrollment callback to not acquire lock (caller _ingest_signal holds it). Renamed _on_device_enrolled() to _fire_enrollment_callback() and removed lock acquisition.
P2 Fixes:
- BLE Handoff parsing: Implemented full HCI packet parsing to extract Apple Company ID (0x004C), Handoff message type (0x0C), and sequence number (bytes 4-5, big-endian). Calls _ingest_signal() with handoff_seq parameter.
- DNS record count overflow: Capped total_records at 1000 to prevent unbounded loop DoS on crafted mDNS packets.
- device_store unbounded growth: Added simple eviction when store exceeds 500 entries - evicts 100 oldest by first_seen timestamp. No LRU needed for MVP.
All 40 existing tests continue to pass.
Track devices across MAC rotations using BLE Handoff sequence numbers
and DHCP Option 55 fingerprinting. Auto-enroll personal devices when
seen on 2+ independent signal types. Handle iOS MAC rotation silently.
Confidence-based departure only fires for enrolled devices.
Changes:
- New device_store dict for stable device identity tracking (keyed by
handoff seq anchor or DHCP fingerprint hash)
- _dhcp_fingerprint_hash() computes stable DHCP Option 55 fingerprint
- _create_identity_record() initializes device records
- _on_device_enrolled() triggers silent enrollment when signal_count >= 2
- _correlate_or_create_identity() finds or creates identities via BLE seq
or DHCP fingerprint, handles MAC rotation detection
- _ingest_signal() ingests signals from multiple sources (BLE, DHCP, ARP,
mDNS) and triggers enrollment when criteria met
- ble_sniffer() thread for Apple Continuity Protocol parsing (HCI socket +
hcidump fallback); graceful failure on missing hardware
- Modified parse_frame() to extract DHCP Option 55 and ingest signals for
ARP, mDNS, DHCP packets
- Updated main() to start BLE sniffer thread and log device_store stats
Implement passive observation tracking for iOS devices:
- Add last_seen dict and lock for tracking passive frame observations
- Parse ARP frames (ethertype 0x0806) for MAC observation
- Parse mDNS frames (UDP port 5353) for device name auto-discovery
- Rename parse_dhcp to parse_frame to reflect broader scope
- Add personal_names set for device name matching
- Parse NET_ALERTER_PERSONAL_NAMES env var (comma-separated device names)
- Add mDNS DNS message parsing to extract PTR/SRV/A record names
- Auto-add MACs to personal_devices when mDNS name matches personal_names
- Call _update_last_seen on all frame types (ARP, mDNS, DHCP)
- Add personal_watchdog thread to fire on_departure after WATCHDOG_TIMEOUT_SEC
- Add NET_ALERTER_WATCHDOG_TIMEOUT env config (default 900s)
- Fix load_personal_macs_from_env logging to show counts instead of MACs
Changes are minimal and focused:
- No socket changes (existing AF_PACKET socket already sees all frames)
- No new dependencies (stdlib only)
- Deadlock prevention (watchdog does not hold locks when calling on_departure)
- Backward compatible (personal_names optional, watchdog is passive)
on_departure() holds known_lock when calling _check_churn(). When churn
threshold is hit, the nested `with known_lock:` inside _check_churn tried
to reacquire an already-held non-reentrant lock → permanent deadlock.
This killed the production process on the OPi Zero 3 after ~15 hours of
runtime on 2026-04-12 at ~02:12 UTC. infrastructure_ips.add() is safe
without the inner lock since the caller already holds known_lock.
Also: reset _churn_tracker between tests to prevent cross-test state
accumulation that triggered the churn threshold in test isolation.
Fix#605: Remove MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_ROOM_ID from module-level globals.
Credentials are now read at call time in send_alert() via _read_env_value() to minimize
credential lifetime in process memory.
Fix#606: Update deploy.sh to use Infisical creds CLI instead of .secrets file.
Fetches NET_ALERTER_PASSWORD from matrix folder in Infisical vault, then generates
access token at deploy time.
All 16 net_alerter tests pass when run individually or in groups.
Two P1 bugs fixed:
1. Timer cancel deadlock: timer.cancel() was called while holding locks, which would deadlock if the callback was running/paused waiting for those same locks. Fixed by extracting timers outside of all locks before calling cancel().
2. MAC validation: _normalize_mac() now validates format with regex and returns empty string for invalid MACs. Callers (load_personal_macs_from_env, is_personal_device) skip empty results and log warnings.
All 16 tests pass.
- P1-1: Fix lock ordering deadlock by establishing canonical order (known_lock → departure_lock → occupancy_lock). Refactor on_arrival() to acquire locks in correct order. Fix send_departure_alert() callback to hold occupancy_lock while updating state.
- P1-2: Add _normalize_mac() helper to strip colons/hyphens, apply to load_personal_macs_from_env() and is_personal_device() for consistent MAC comparison.
- P1-3: Remove occupancy_lock acquisition from _update_occupancy_state_unlocked() - callers must hold lock. Update all callers to acquire occupancy_lock before calling.
All individual tests pass. Fixes allow on_arrival() and timer callbacks to execute without deadlock.
- Add MOBILE_DEVICE_OUIS set with 55+ OUI prefixes for personal devices
- Add is_personal_device() function to detect personal devices by OUI or manual config
- Add load_personal_macs_from_env() to parse NET_ALERTER_PERSONAL_MACS
- Add location_occupancy state tracking (VACANT/OCCUPIED/UNKNOWN)
- Add update_occupancy_state() function with thread-safe alerts on transitions
- Add _update_occupancy_state_unlocked() helper to prevent deadlock
- Fix deadlock in on_arrival() by using unlocked version when holding known_lock
- Update test suite with 5 new occupancy tests
- Fix test assertions to exclude occupancy alerts for device arrival tests
Tests now call cleanup_flap_state() at the start of each test to properly
reset the interface flap detection state (_flap_window, _interface_recovering,
_recovery_timer) between tests, preventing state carryover failures.
- Fix 1: Interface flap detection suppresses departure storms when wlan0 briefly loses association and triggers simultaneous RTM_DELNEIGH events for all cached neighbors
- Tracks 3-second window of departure MACs
- Locks recovery state for 60 seconds if 3+ departures occur in rapid succession
- Prevents false positive alerts during interface recovery
- Fix 2: Additional infrastructure IPs via NET_ALERTER_INFRA_IPS env var
- Allows operator to suppress alerts for devices like access points that cycle regularly
- Comma-separated list of IP addresses to be seeded as infrastructure at startup
- Example: NET_ALERTER_INFRA_IPS=10.0.0.0,10.0.0.0
Addresses DevTrack #467 (net_alerter false positive alerts)
sensor.service running at 10.0.0.0. ARP + DHCP sensors active.
Probe sniffer degraded (no wlan1). BLE degraded (BlueZ too old for or_patterns).
Untracked audit files committed for reference.
- P1 #551: Add STATUS_PORT env var (default 9191) for configurable HTTP status server port
- P1 #552: Fix SQLite connection leak — wrap all sqlite3.connect() with try/finally in lookup_person(), seed_persons_from_db(), log_signal(), log_presence_change()
- P1 #553: Fix race condition in update_person_state() — compute certainty snapshot under devices_lock, determine state transition under persons_lock, send alert outside locks to prevent stale snapshot
- P2 #559 (included): Add WAL mode + pragmas to init_db() for concurrent write safety
- Database path: Align to /opt/sensor/presence.db in both daemon and install.sh
- Increase connection timeout from 1s to 10s across all functions
- install.sh: Document PRESENCE_STATUS_PORT and PRESENCE_MATRIX_WEBHOOK env vars
Implements DevTrack #530: Person Presence Intelligence daemon for BigBrother drop implant.
Core components:
- presence_daemon.py: 4 sensor threads (probe, ARP, DHCP, BLE) with exponential decay
- presence_schema.sql: SQLite schema for persons, devices, signals, occupancy log
- install.sh: Deployment script (copies to /opt/sensor/, creates systemd service)
- AUDIT_impl.md: Implementation notes, limitations, audit checklist
Features:
- Exponential decay model (lambda=0.08/min) for signal certainty
- Presence anchor (45-min hold time prevents false VACANT)
- Person state machine: UNKNOWN → PRESENT ↔ ABSENT
- Multi-signal fusion: max + 0.08×min for device aggregate
- Thread-safe in-memory state with SQLite persistence
- Matrix webhook alerting (if configured)
- HTTP status endpoint on 127.0.0.1:9191
- Graceful BLE degrade if bleak unavailable
- No tool fingerprints in output or service names
- Reject dash/colon MAC-format device names (e.g. 45-48-39-7F-73-C7)
from open mode — were slipping past is_generic_name()
- Add exponential backoff (1s, 2s) on Matrix 429/5xx, max 3 attempts —
startup burst was rate-limiting and dropping alerts
- Commit active scanner flag (scanning_mode=active) that was in working
tree but not in prior commit c0b91e9 despite the commit message
- Add 3 tests: MAC format filter, 429 retry success, 429 give-up
Fixes DevTrack #518
Passive mode fails on Debian bookworm bleak 0.20.2 without or_patterns filter.
Active mode is functionally equivalent for building-level presence detection
and actually yields more complete advertisement data including device names.
When on_departure popped the device from known_devices, rapid RTM_NEWNEIGH
events during ARP cache rebuild each triggered on_arrival as if the device
was brand new, flooding the Matrix room with 5-10+ duplicate ARRIVED alerts.
Fix: keep device in known_devices with departing=True flag. on_arrival
silently cancels pending departure timers and returns without alerting when
it sees this flag. Timer callback checks flag before sending the alert and
only pops the device when departure is confirmed after 15 minutes.
- Bug: device departs → timer starts, but last_departed_time not set yet
If device re-arrives before timer fires, timer never cancelled
Result: false DEPARTED alert sent after re-arrival debounce expires
- Fix: Cancel any pending departure timer immediately on re-arrival,
before checking last_departed_time
- Test: Added regression test that verifies timer is cancelled when
device re-arrives before 15-min debounce expires, preventing false alerts
- All 10 tests passing
- lookup_oui() now tries SQLite first (/opt/net_alerter/oui.db)
- Falls back to inline OUI_DICT if DB missing or query fails
- No network calls, local SQLite reads only
- Debug-level logging for failures (no output noise)
- Infrastructure IPs (gateway, self, broadcast) silently ignored in
on_arrival() and on_departure(); seeded at startup from 'ip route'
- 15-minute departure debounce: alert only fires if device is still
absent after 900s, eliminating ARP cache flush false positives
- 5-minute re-arrival suppression: ARRIVED alert skipped when device
returns within 5 min of a recorded departure (covers interface flap)
- DHCP renewal dedup: known device re-announcing within 30 min skips
ARRIVED alert
- Tests added for all four suppression paths (9 total, all passing)
- Replace broken OUI file lookup with inline dict of 200+ common vendors (works on Armbian)
- Fix hostname/IP duplication when reverse DNS fails (show IP once, not twice)
- Add format_arrival() and format_departure() helpers for consistent alert formatting
- Update on_arrival/on_departure to use new formatting functions
- Add comprehensive tests for hostname dedup and OUI lookup
Replace all sensor.* logger namespaces with __name__ (generic module
identifiers instead of discoverable 'sensor.*' prefixes).
Change hardcoded 'bb' API user to 'admin' in config and code defaults.
Change hardcoded relay_user 'bb' to 'operator' — prevents network
profiling from exposing tool identity via SSH config.
Fixes#457, #458, #459
- Removed get_local_subnets() and scan_subnet() — no longer needed
- Added read_arp_cache() to read kernel ARP table from /proc/net/arp
- Added fallback to 'ip neigh show' if /proc/net/arp unavailable
- Added lookup_oui() to query system /usr/share/hwdata/oui.txt for vendor names
- Removed subprocess nmap call and re import (no longer needed)
- run() now directly reads ARP cache instead of scanning subnets
- Zero active network traffic — purely passive monitoring
- Removed sudo requirement
Fixes interface auto-detection failures on Pi Zero 2W where wlan0 doesn't exist
at boot. Implementation:
- Added detect_interface_with_retry() function with 3x retry fallback chain
- Config-supplied interface preferred, then /proc/net/route, then UP interface
- Exponential backoff: 5s, 10s, 20s delays between retries
- Filters excluded prefixes: lo, tailscale*, wg*, tun*, docker*, veth*, br-*
- Used in Engine.__init__(), PacketCapture.start(), MacManager.start()
- Full test coverage: 10 tests for detection, backoff, filtering
Addresses issue where network stack initializes after boot and interfaces
become available after 5-10 second delay.
Parse nmap vendor name from MAC Address line (filtering out 'Unknown').
Fall back to socket.gethostbyaddr for IPs nmap didn't resolve a hostname
for. Alert label is now: 'hostname (vendor)' | 'hostname' | 'vendor' |
'unknown', in that priority order.
urllib's default Python-urllib user agent is flagged by Cloudflare Bot
Fight Mode (error 1010) when m.example.org homeserver sits behind
Cloudflare. Add a browser-like User-Agent to Matrix PUT/POST requests so
Cloudflare passes the traffic through.
- Remove 20-wifi.yaml netplan block (Bug 1): causes netplan to spawn its own wpa_supplicant instance, which claims ctrl_iface and blocks wpa_supplicant@wlan0.service. The per-interface systemd service is the correct approach.
- Add cleanup line to remove any pre-existing 20-wifi.yaml from the Armbian image.
- Patch 10-dhcp-all-interfaces.yaml to suppress default route on ethernet (Bug 2): without this, ethernet DHCP default route metric 100 beats WiFi metric 1024, routing internet traffic to ethernet which has no upstream. Causes Tailscale and firstboot to fail.
- Force IPv4 resolution in Tailscale curl (Bug 3): some homelab/ISP networks lack IPv6 internet routes. IPv6 DNS resolution for tailscale.com causes immediate curl failure on first boot.
- Remove wpa_supplicant@wlan0.service enablement: Netplan spawns its own
wpa_supplicant instance, enabling the @wlan0 unit creates a second one
that fights for the socket, causing WiFi instability and SSH drops
- Write /etc/nftables.conf to card at flash time: Armbian bookworm default
nftables policy drops all input; SSH was unreachable despite sshd running.
Ruleset allows established, loopback, ICMP, and TCP/22 only
Fixes DevTrack #317 and #295
- Set HOME/GOPATH/GOMODCACHE before go install kerbrute on arm64 to fix
'module cache not found' abort that prevented --enable-service from running
- Enable bigbrother-capture, bigbrother-watchdog, and bettercap.service
in --enable-service block (previously only bigbrother-core was enabled)
- Add services/bettercap.service: runs passive_recon.cap with net.recon +
net.sniff + events.stream instead of idle api.rest only
- bettercap.service depends on bigbrother-core to ensure engine is up first
Fixes DevTrack #311 and #312
- Remove generic wpa_supplicant.service symlink when wpa_supplicant@wlan0 is enabled — both services fight over wlan0 causing WiFi instability and SSH drops
- Add udev rule to disable WiFi power saving (aw859a drops connections under load with power save on)
- Drop set -e from firstboot script so a failing step does not kill the whole run
- Guard setup.sh call — skip gracefully if not present (dev/test scenario)
Fixes#295 and #296
Armbian minimal uses systemd-networkd+wpa_supplicant via Netplan, not NM.
Write Netplan YAML, wpa_supplicant-wlan0.conf, and networkd .network file.
Enable wpa_supplicant@wlan0.service via symlink if template exists.
WiFi, SSH keys, root password, firstboot service were all being written to
the FAT32 boot partition (p1) instead of the ext4 rootfs (p2). Detect the
ext4 partition by filesystem type and mount that as MOUNT_POINT.
mktemp creates the file before ssh-keygen runs — keygen sees it exists
and prompts. Switch to mktemp -d so keygen writes a fresh file.
When run via sudo SUDO_USER is set but HOME=/root. Resolve the real
home via getent so the key lands in the operator's ~/.ssh, not root's.
echo calls in summary/final sections were missing -e flag so ANSI
escape codes printed literally instead of rendering.
Infisical SSH key storage was targeting a non-existent 'bigbrother'
folder, silently failing to store the private key. Removed folder
argument so keys store at root where creds CLI can retrieve them.
Matrix doesn't use a simple webhook URL — needs homeserver, room ID,
and bot token separately. Uses PUT /_matrix/client/v3/rooms/.../send
with Bearer token auth. Room ID is bash-encoded (! → %21, : → %3A)
to avoid Python dependency pre-setup. Also adds ntfy and plain
webhook as options under a typed alerter menu.
Device ID is now a MAC-format hex string (12 chars) using the same
OUI as the randomly selected device profile. If the device gets an
Amazon OUI for its MAC, the device ID starts with fc65de. Hostname,
MAC, and device ID all point to the same vendor — no inconsistency
for anyone watching the network.
bb- prefix is a tool fingerprint — visible in Infisical key names,
alerter payloads, and config files on the device. Default is now
a plain 10-char hex string with no identifying prefix.
Tailscale handles NAT traversal automatically via DERP relay — more
reliable than WireGuard for devices landing on unknown networks.
Add reminder to use reusable non-expiring keys for long-term drops.
Blind drops need some way to locate the device when no VPN is selected.
Alerter now reports public IP (via ipify) with fallback to local IP,
plus hostname — gives enough info to SSH directly on same-network drops.
Generates a dedicated tunnel keypair during setup, prints public key
for operator to pre-stage on server, writes private key to SD card.
On first boot: installs autossh, writes bb-tunnel.service with
correct RemoteForward and reconnect settings. Zero network traffic
until operator initiates connection through the tunnel port.
- Add prompt_default() helper: shows defaults, accepts Enter to use them
- WiFi: default=n (skip), keeps passphrase prompt loop
- Root password: auto-generate with openssl, show in summary if used, operator can override
- SSH key: default=1 (generate new)
- Device ID: auto-generate bb-<8-char-uuid>
- Boot mode: default=1 (passive)
- VPN: default=1 (none)
- Hostname: NEW section with auto-generated node-<6-char-uuid> default
- Alerter: default=n (disabled)
- Download: use curl -L (no -f), verify file exists and >50MB
- Config summary: show hostname, mask password (show auto-generated clearly)
- Apply config: write BB_HOSTNAME to /etc/hostname and /etc/hosts
- No eval with user input: use printf -v for variable assignment
ls glob on empty cache dir exits non-zero and kills script under
set -euo pipefail. Switched to find which exits 0 on no matches.
Same fix for grep|sed pipeline in device selection; added empty
DEVICE guard.
Single script covers full drop implant prep: image download and flash,
WiFi (with UUID fix), per-device SSH keygen to Infisical, mode selection,
VPN (Tailscale/WireGuard), network alerter webhook, and first-boot service.
Insert card, power on, walk away.
WiFi keyfiles require UUID or NM silently ignores them — that was why
WiFi never connected. Rewrote preconfig_sd.sh to:
- Generate UUID for NM keyfile
- Copy BB source to /root/bb-src/ on the card
- Install bb-firstboot.service that runs setup.sh --enable-service
after network-online.target, then disables itself
- Per-device keygen, private key to Infisical
Single command now produces a card that boots and self-installs.
Each device gets a unique ed25519 keypair generated at preconfig time.
Private key goes to Infisical (bigbrother/SSH_PRIVKEY_<ID>), public key
to the SD card only. No operator keys or identifying comments on device.
Copying authorized_keys verbatim onto a drop implant leaks internal hostnames,
usernames, and tool names in key comments. preconfig_sd.sh now strips all
comments (awk '{print $1,$2}') when writing keys to SD card. deploy.sh adds
the same strip step on the live device as a safety net.
build_data.py created 'profiles' but mac_manager queries 'mac_profiles'.
Columns category/model renamed to device_type/device_name. device_type
values aligned with _CATEGORY_MAP in mac_manager (streaming, smart_tv,
phone, tablet, smart_speaker, iot, printer, gaming).
ja3_fingerprints.db table renamed from 'fingerprints' to 'ja3_profiles'
and expanded with all columns ja3_spoofer queries (name, description,
cipher_suites, extensions, elliptic_curves, ec_point_formats as JSON).
StateManager.reset_module_status() clears stale 'running' entries on
startup so watchdog doesn't flag old-session PIDs as dead modules.
Engine.start_all() calls reset before launch and does a 3s post-startup
liveness check, logging any modules that crashed immediately after fork.
When wlan0 is the primary data interface (WiFi-only deployment), changing
its MAC via SIOCSIFHWADDR drops the AP association and puts all AF_PACKET
sockets into ENETDOWN permanently until wpa_supplicant re-associates.
- _apply_profile: skip set_mac if _eth_iface is a WiFi interface
- _apply_profile: skip WiFi set_mac if _wifi_iface == _eth_iface (same card)
- stop: mirror the same skip logic for MAC restoration
DHCP hostname + TCP stack tuning still applied for blending on WiFi-only nodes.
with conn: context manager in Python sqlite3 doesn't respect busy_timeout
for the implicit BEGIN; under concurrent subprocess writes the transaction
would fail immediately. Replace with explicit BEGIN IMMEDIATE + retry loop
(6 attempts, 0.5s back-off) to handle WAL write contention.
credential_sniffer and credential_db both wrote to the same credentials.db
with different schemas; credential_db's executescript failed when creating
the crack_status index on an existing table that lacked that column.
Fix: split credential_db's schema init into table DDL + migration + indexes
so missing columns are added before index creation runs. Migration also
handles any future schema drift.
state.py: add 30s timeout + busy_timeout PRAGMA to write connection so
concurrent subprocess writes stop failing with 'database is locked'.
capture_bus.py: reconnect on ENETDOWN instead of aborting the capture
loop — mac_manager temporarily brings the interface down during MAC
rotation which was silently killing all packet capture.
With multiprocessing fork, threads are not inherited. The parent's
CaptureBus reader thread runs in the parent but module subprocesses
subscribe AFTER fork, so their SubscriberQueue entries are added to
a child-local copy of _subscribers that the parent reader never sees.
Fix: reset and restart CaptureBus in each module subprocess so it
has its own reader thread on the inherited AF_PACKET socket. Each
module gets independent packet delivery. Linux AF_PACKET allows
multiple readers on the same interface.
Three bugs causing all modules to exit within seconds of startup:
1. _module_runner had no keep-alive loop after calling module.start().
All modules use a thread-spawning pattern where start() returns
immediately, so the subprocess exited and killed all daemon threads.
Added while module._running: sleep(1) loop to block until stop().
2. Engine injected capture_bus under key 'capture_bus' but 9 modules
(auth_flow_tracker, cloud_token_harvester, ldap_harvester, network_mapper,
rdp_monitor, quic_analyzer, smb_monitor, db_interceptor, vlan_discovery)
look for '_capture_bus'. Inject both keys so all modules get it.
3. mac_manager crashes on startup when innocuous_macs.db exists but has
no mac_profiles table (DB not yet seeded). Added OperationalError
handler to fall back to random OUI instead of crashing.
- Add secure_wipe_file() to utils/crypto.py for file overwrites + deletion
- Add secure_wipe parameter to StateManager.stop() method
- Wipes all SQLite WAL files (.db, .db-wal, .db-shm) with 3 passes
- Normal exit now calls state.stop(secure_wipe=True) to clear state database
- Falls back to regular delete if secure wipe fails
- Prevents state.db from persisting on disk after shutdown
- Add encrypt_credential_dict() and decrypt_credential_field() to utils/crypto.py
- Create utils/credential_encryption.py with encryption/decryption helpers
- Add get_credential_encryption_key() to retrieve key from Infisical at runtime
- Add emit_credential_found() wrapper for modules to use instead of direct bus.emit()
- Update all 15 modules emitting CREDENTIAL_FOUND to use encrypted wrapper
- Update 4 modules consuming CREDENTIAL_FOUND to decrypt payload before processing
- Sensitive fields (username, password, hash, token, etc.) encrypted with AES-256-GCM
- Falls back to plaintext if encryption key unavailable or encryption fails
- Move crash_callback execution outside of self._lock
- Callback is called after releasing the monitor lock
- Re-acquire lock only for restart decision logic
- Prevents deadlock if callback tries to acquire same lock
- Maintains thread safety for restart and process state updates
- Replace StrictHostKeyChecking=no with accept-new (verify on first connect)
- Require ssh_known_hosts_file config key pointing to operator's known_hosts
- Validate known_hosts file exists before attempting transfer
- Fail explicitly if known_hosts not configured (prevents silent MITM vulnerability)
- Uses -o UserKnownHostsFile=path to use pinned host keys
- Use jinja2.Environment with BaseLoader instead of Template
- Enable autoescape=True to escape all template variables
- Sanitize relay_targets by converting to strings: [str(ip) for ip in ...]
- Prevents template injection even if relay_targets contains malicious content
- set_parameter() now validates param name with regex (alphanumeric, dots, underscores)
- set_parameter() quotes value with repr() to prevent argument injection
- load_caplet() validates path exists, is absolute, and ends with .cap/.caplet
- load_caplet() quotes caplet path for safe interpolation into bettercap command
- Removed misplaced requires_capture_bus entries that broke indentation
- Added requires_capture_bus = True correctly after requires_root in all passive modules
- All passive modules now have valid Python syntax
- Engine.__init__ now initializes self.capture_bus = None
- Engine.start_all() instantiates CaptureBus before starting modules
- CaptureBus uses resolved interface from config
- Inject capture_bus into configs of all modules with requires_capture_bus=True
- Engine.stop_all() properly stops CaptureBus after stopping modules
- CaptureBus instantiation includes error handling with fallback
- BaseModule now has requires_capture_bus = False default
- All 17 passive modules set requires_capture_bus = True
- bigbrother.py imports CaptureBus from core.capture_bus
- Ready for Engine to instantiate and inject CaptureBus
- Engine.__init__ now detects actual interface when config has 'auto'
- Uses get_primary_interface() to find default route interface
- PacketCapture module updated to use detected interface with fallback
- Handles Pi Zero 2W (wlan0) and other non-eth0 systems
Removed spurious second IR-014 (Verbose Module Startup Logging) which did not exist in any agent output.
This finding was causing confusion between the deployment blocker IR-014 (missing get_hardware_tier() import, CRITICAL)
and the false entry. Corrects triage table consistency and ensures all findings are grounded in actual agent analysis.
- Add pi3b tier (700MB RAM limit, 8 passive/2 active modules, no bettercap/mitmproxy)
- Update detect_hardware_tier() to detect Raspberry Pi 3B by model string and BCM2837
- Add pi3b section to hardware_tiers.yaml
- Add systemd MemoryMax/CPUQuota/OOMScoreAdj to all three service files
- Fix bigbrother-watchdog.service ExecStart (was calling non-existent ResourceMonitor().run())
- Add scripts/run_watchdog.py as standalone health monitor daemon
apt-cache show returns true even when Candidate is (none). Use
apt-cache policy and grep for a real version string to correctly
skip aircrack-ng on Ubuntu arm64 where it has no install candidate.
Make aircrack-ng a conditional package install (same pattern as
proxychains/sshuttle) so setup.sh doesn't abort on distros where
the package has no install candidate.
Interactive and non-interactive CLI to detect, mount, and edit the
netplan 20-wifi.yaml on an armbi_root-labelled SD card without
needing to boot the implant.
Enables/disables/reports status for bigbrother-core, bigbrother-capture,
and bigbrother-watchdog. Supports enable (enable+start), disable (stop+disable),
and status commands. Works locally on-device and over SSH from operator machine.
WireGuard (primary C2, PersistentKeepalive=0 for zero idle traffic),
Tailscale (fallback mesh VPN), Bridge (transparent inline L2 bridge with
802.1X bypass and ebtables protocol suppression), WiFiClient (WPA2-PSK
and WPA2-Enterprise via wpa_supplicant), ReverseTunnel (autossh over
stunnel/websocket — never raw SSH on 443), CellularBackup (LTE modem
via AT commands/mmcli, OPi Zero 3+ only), BLEEmergency (GATT server
with PSK auth for local last-resort access), DataExfil (priority-based
exfil with IMMEDIATE/NIGHTLY/ON_DEMAND tiers through connectivity chain).
Bridge scripts: setup_bridge.sh (create br0, disable STP, suppress
CDP/LLDP/BPDU via ebtables) and teardown_bridge.sh (safe cleanup).
6 stealth modules implementing the OPSEC layer:
- MacManager: innocuous MAC profiles from DB, DHCP hostname/vendor class spoofing, TCP stack tuning (TTL, window, timestamps)
- ProcessDisguise: rename processes to system service names via prctl, auto-disguise on MODULE_STARTED/TOOL_RESTARTED events
- LogSuppression: rsyslog filters, auditd exclusions, journald rate limits, shell history/utmp/wtmp clearing with clean removal on stop
- EncryptedStorage: LUKS2 container with HKDF network-derived key (CPU serial + C2 component), fallback key, auto-format and subdirectory creation
- TmpfsManager: tier-aware RAM-backed mounts for working dirs and WAL files, power loss = instant evidence destruction
- Watchdog: periodic health checks, auto-restart (max 3), OOM priority assignment by module type, bus event monitoring
Complete rewrite showing exact expected output from every passive and active
module against a realistic small business dental office network. Includes
protocol-level explanations, realistic fake data, honest assessment of what
produces zero, and step-by-step DNS poisoning/ARP spoofing/Responder walkthroughs.
- New passive capture modules (stick to the module pattern in `modules/passive/`)
- Hardware support for additional SBCs
- Documentation improvements
## What's Not Welcome
- Active modules that target specific commercial services (keep it protocol-level)
- Features that reduce OPSEC (fingerprints, banners, hardcoded strings)
- Breaking changes to the state machine API without a migration path
## Module Pattern
New modules implement the `BaseModule` interface:
```python
classMyModule(BaseModule):
name="my_module"
tier="passive"# or "active"
requires=["root"]# capabilities needed
defstart(self):...
defstop(self):...
defstatus(self)->dict:...
```
Modules must not block the main event loop. Use threads or asyncio internally.
## Testing
```bash
python3 -m pytest tests/
```
Tests that require root or live network access are marked `@pytest.mark.requires_root` and skipped in CI. Write unit tests for logic that doesn't need hardware.
## Submitting Changes
1. Fork the repo
2. Branch from `main`
3. Write a test if the change has logic worth testing
4. Open a PR with a description of what changed and why
The BigBrother daemon has been running since March 25 but is completely non-functional. All modules fail silently. Root cause: 5 cascading issues blocking module startup.
## DevTrack Items Created
-#187: Issue 1 — Auto-detect active network interface
-#188: Issue 2 — Instantiate CaptureBus in Engine
-#189: Issue 3 — Seed database tables
-#190: Issue 4 — Track module startup state
-#191: Issue 5 — Diagnose LUKS encrypted storage
---
## Issue Analysis & Fix Strategy
### Issue 1: Wrong Network Interface (CRITICAL - Hard Blocker)
**Current State:**
-`CaptureBus.__init__` defaults to `"eth0"`
-`PacketCapture.DEFAULT_INTERFACE` = `"eth0"`
-`bigbrother.yaml` has `primary_interface: "auto"` but auto-detection doesn't propagate
- Condo Pi is on `wlan0` (10.0.0.0/24). `eth0` has no carrier → capture fails
**Root Cause:**
- No active interface detection function exists
- Hardcoded defaults override config values
- Config "auto" value is never resolved
**Files to Change:**
-`core/capture_bus.py` — Remove hardcoded "eth0", accept interface param in __init__
-`modules/passive/packet_capture.py` — Remove DEFAULT_INTERFACE, read from config
Network drop implant for silent, autonomous network surveillance. Deploy on any Debian host and get passive SOC-level visibility plus active exploitation capability over a C2 tunnel.
> **Legal notice:** This tool is for authorized penetration testing, red team engagements, and security research on networks you own or have written permission to test. Unauthorized use is illegal and unethical. You are responsible for your actions.
## What It Does
BigBrother turns a cheap SBC or any Debian host into a persistent network implant with two operational modes:
Captured credentials are encrypted before local storage and C2 transmission. Requires an Infisical-compatible secrets manager with a `CREDENTIAL_ENCRYPTION_KEY` secret:
```bash
export BB_CREDS_BIN=/path/to/your/creds-cli
```
The `creds` CLI must support: `creds get CREDENTIAL_ENCRYPTION_KEY`
If `BB_CREDS_BIN` is not set, credentials are stored in plaintext with a warning.
## Net Alerter
Standalone Matrix alert daemon for device presence monitoring. See `net_alerter/README.md`.
1.**[operator_setup.sh:707-712] Webhook and Matrix tokens written to `/root/bb-config.env` on SD card**
- PRIORITY: P1 (Secret in deployed artifact)
- Issue: Matrix tokens (`BB_ALERTER_MATRIX_TOKEN`), webhook URLs (`BB_ALERTER_WEBHOOK`), and Tailscale auth keys are written to `/root/bb-config.env` on the flashed SD card in plaintext
- Risk: Anyone with physical or filesystem access to the device can extract active alerter credentials
- Fix: Store these secrets in Infisical on the target device only. At deployment time, the first-boot script should retrieve them from Infisical CLI rather than from bb-config.env
2.**[setup.sh:266-271] .env file created with plaintext Matrix credentials at `/opt/net_alerter/.env`**
- PRIORITY: P1 (Secret in service environment file)
- Issue: Matrix tokens are written to `/opt/net_alerter/.env` during service setup. This file persists on the device and can be read by any user or process with filesystem access
- Risk: Compromised device filesystem leaks all alerter credentials
- Fix: Credentials should be retrieved from Infisical via `creds` CLI at service startup time, not cached to .env
## WARN
3.**[net_alerter/test_ble_alerter.py:113,130,149,166] Test file uses placeholder token `"syt_test"`**
- PRIORITY: P3 (Harmless test data)
- Detail: The test file correctly uses a placeholder token that is not a valid Matrix token. This is acceptable for unit tests as it does not represent a real credential
- Status: ACCEPTABLE — test tokens are explicitly non-secret and cannot authenticate to real servers
## PASS
- Secrets in source code: CLEAN — No GitHub tokens, AWS keys, Stripe keys, or bearer tokens found in .py, .sh, .yml, .yaml, or .json files
- Git history (recent commits): CLEAN — Only one commit in repo (initial public release), no secrets leaked in history
- CI/CD should have no step that writes secrets to disk
### Medium Priority
- Document the Infisical setup required for BigBrother deployments (which folders, which keys)
- Add a validation script that checks Infisical has all required keys before first boot
### Summary
The codebase demonstrates strong cryptographic and credential management patterns (credential_encryption.py is well-designed), but the deployment flow undermines this by writing alerter secrets to persistent plaintext .env files. The fix is to retrieve these secrets from Infisical at runtime instead of storing them on the device.
**Audit Scope:** Verification that sanitization work completed on bigbrother-public for public GitHub release. Focus: personal identifiers, hardcoded secrets, OPSEC issues, and deployment targets.
**VERDICT: CLEAN — All sanitization work verified complete.**
The codebase has been successfully sanitized for public release. All previously identified personal identifiers, hardcoded paths, and infrastructure-specific references have been either:
1. Removed entirely
2. Replaced with environment variable fallbacks
3. Generalized to template examples (e.g., `your-homeserver.example.com`)
No hardcoded credentials, API keys, or deployment targets were discovered. The code is ready for public release.
---
## Findings
### CRITICAL
**Count: 0** — No critical security issues found.
### HIGH
**Count: 0** — No high-severity security issues found.
**Status:** ✓ PASS — Operator must explicitly specify target.
### 4. Net Alerter Daemon ✓ VERIFIED CLEAN
**File:**`net_alerter/net_alerter.py`
**Checks:**
- **Hardcoded IPs:** No specific deployment targets (192.168.1.x with .1, .10, .202, etc.)
- **Matrix homeserver defaults:** Removed. Lines 880-883 require `MATRIX_HOMESERVER` env var to be set explicitly; function returns early if not configured
- **Hardcoded domains:** No `mealeyfamily.com` fallback in send_alert() function
**Status:** ✓ PASS — All hardcoded homeserver defaults removed.
### 5. BLE Alerter Daemon ✓ VERIFIED CLEAN
**File:**`net_alerter/ble_alerter.py`
**Checks:**
- **Matrix homeserver:** Lines 82-84 load from .env or env vars; no hardcoded default
- **Hardcoded domains:** None found; uses variable substitution throughout
| Test Data | Device names, identifiers | ✓ GENERIC |
---
## Recommendations
No mandatory changes required for public release. The codebase is clean.
### Optional Enhancements (Not Blocking)
1. Add `.gitignore` rule for `*.env` files (already excluded via env var pattern, but explicit is safer)
2. Consider replacing `/opt/` paths in docs with `/opt/{TOOL_NAME}/` to reduce path predictability
3. Consider documenting deployment paths as entirely operator-configurable in main README
---
## Conclusion
**Status: APPROVED FOR PUBLIC RELEASE**
All sanitization work has been verified complete. The bigbrother-public repository contains no personal identifiers, hardcoded credentials, or deployment-specific infrastructure references. The codebase is ready for GitHub public release.
**Signed:** Security Auditor
**Date:** 2026-06-26
**Confidence:** High (comprehensive source-level review + regex pattern matching)
**Issue:** Matrix tokens written to `/root/bb-config.env` on SD card in plaintext
**Severity:** P1 (Secret in deployed artifact)
**Status:** UNRESOLVED — Code still contains the issue
### AUDIT_ENV.md — P1 FAIL #2
**File:** `setup.sh:266-271`
**Issue:** Matrix tokens written to `/opt/net_alerter/.env` on device in plaintext
**Severity:** P1 (Secret in service environment file)
**Status:** UNRESOLVED — Code still contains the issue
---
## PM Acceptance Ruling (Noted)
The PM has provided the following reasoning for accepting these findings as design decisions (P3/P4 rather than blocking P1):
- **Autonomy:** BigBrother is a network drop implant without guaranteed access to central secrets manager (Infisical)
- **Operator-entered secrets:** Credentials are interactive input during deployment, not hardcoded in source
- **Standard pattern:** `/opt/net_alerter/.env` with `chmod 600` follows industry-standard self-hosted service patterns (Matrix Synapse, Grafana, etc.)
- **Source code clean:** No secrets committed to git; public repository contains only generic templates
This reasoning is **sound from a threat-modeling perspective** for a field implant operating independently. However:
1.**Audit findings remain in code** — the env validator explicitly recommends these secrets be retrieved from Infisical at runtime instead of cached to .env files
2.**Gate function:** The gate's role is to flag unresolved audit findings, not to re-evaluate audit severity
3.**Human decision required:** These are pre-existing environmental risk findings that require explicit PM acceptance; the gate documents that acceptance
---
## Security Audit Summary (CLEAN)
**AUDIT_security_1262.md: APPROVED FOR PUBLIC RELEASE**
- Sanitization verified complete
- No personal identifiers, hardcoded secrets, or OPSEC leaks
- All deployment targets and infrastructure references generalized
- No credential fingerprints or tool attribution risk
- Ready for GitHub public release from a security standpoint
PM re-confirms in writing that the env audit's P1 findings are accepted design decisions for a field implant. Gate can then issue APPROVED with documented risk acceptance.
- Move alerter secrets from .env files to Infisical-retrieved-at-runtime pattern
- Requires modifying `operator_setup.sh` and `setup.sh` to use `BB_CREDS_BIN` env var
- Effort: Medium (1-2 hours refactoring + re-testing)
**Option 3: Accept Technical Debt**
Proceed with code release and open a DevTrack item to address env findings in a follow-up maintenance cycle.
---
## Recommendation
The security audit is clean, and the PM's reasoning is valid for autonomous field deployment. However, the gate cannot approve work where AUDIT_ENV.md contains unresolved FAIL entries.
**Recommend:** PM explicitly confirms in a reply that the two P1 env findings are accepted design risks for this implant. Gate will then issue APPROVED with risk acceptance documented.
Alternatively, if time permits before release: refactor credential handling to use Infisical-at-runtime pattern (addresses env findings without breaking field autonomy).
---
## PM Formal Risk Acceptance — 2026-06-26
The two env audit P1 findings are **accepted design decisions** for the following reasons:
1. A network drop implant is not a web service. It operates autonomously in the field, potentially without any network path to an Infisical instance. Local credential storage is a functional requirement.
2. Credentials reach the device via interactive operator input during `operator_setup.sh`. They are never present in the source code or git history.
3.`/opt/net_alerter/.env` with `chmod 600` is the standard credential pattern for self-hosted services. The threat model for a service running on a device the operator physically deployed differs from a multi-tenant web application.
4. The public repo contains zero hardcoded secrets. Users supply their own Matrix homeserver and token.
These findings are **downgraded to P3** and accepted without code change. They are documented here for the record.
---
**Gate Decision: APPROVED**
**Decision maker:** PM
**Date:** 2026-06-26
**Basis:** Security audit clean; env findings accepted as P3 design decisions per threat model above.
The new `net_alerter.py` is a persistent systemd daemon that monitors device presence via passive network observation. It replaces the previous 5-minute cron-based implementation with three concurrent threads running indefinitely.
## Architecture
### Three Concurrent Threads
1.**DHCP Sniffer** (AF_PACKET raw socket)
- Captures DHCP packets passively on the network interface
- Extracts MAC, IP, and hostname from DHCP Discover/Request/Release messages
- Triggers `on_arrival()` for Discover/Request, `on_departure()` for Release
2.**RTM_NEWNEIGH Watcher** (Netlink socket)
- Kernel pushes RTM_NEWNEIGH events when ARP neighbors reach REACHABLE/STALE/DELAY/PROBE states
- Supplements DHCP sniffer for devices that don't use DHCP
- Triggers `on_arrival()` immediately
3.**RTM_DELNEIGH Watcher** (Netlink socket)
- Kernel pushes RTM_DELNEIGH events when ARP neighbors are deleted (5-10 minutes after last seen)
- Triggers `on_departure()` when neighbor is removed from kernel table
### Key Features
- **Zero Active Probing** — no nmap, no arp-scan, no ping. Only passively listens.
- **Stdlib Only** — no external dependencies (socket, struct, threading, time, logging, os)
- **Thread-Safe** — `known_devices` dict protected by `threading.Lock`
- **Persistent Daemon** — systemd service with auto-restart
- **Logging to File + Stdout** — `/opt/net_alerter/net_alerter.log`
- **Matrix Alerting** — sends alerts to configured Matrix room on arrival/departure
## Prerequisites
### Device Requirements
- Debian-based OS (Debian, Raspbian, Orange Pi OS)
- Python 3.6+
- Root access (AF_PACKET and Netlink require CAP_NET_RAW)
- Primary network interface (auto-detected from /proc/net/route)
### Configuration Files
The daemon reads `/opt/net_alerter/.env` at startup:
```bash
MATRIX_HOMESERVER=https://m.example.org
MATRIX_ACCESS_TOKEN=<token from Matrix login>
MATRIX_ROOM_ID=!REDACTED:example.org
```
If `.env` doesn't exist, it will log a warning and try environment variables instead.
@@ -119,7 +119,7 @@ This report consolidates 105 findings across 6 independent security audit agents
- Fix: Use /opt/.cache/bb/{random}/; environment-variable override
- Fix: Use /opt/.cache/bb/{random}/; environment-variable override
- *Severity note: CRITICAL in OA source. Path disclosure during post-incident forensics enables attribution to BigBrother framework. Combined with alert suppression capabilities identifies tool as intentional implant.*
- *Severity note: CRITICAL in OA source. Path disclosure during post-incident forensics enables attribution to BigBrother framework. Combined with alert suppression capabilities identifies tool as intentional implant.*
- *Severity note: CRITICAL in OA source. Domain leakage enables attribution to specific operator. Forensic analysis of Matrix logs reveals operator identity and infrastructure.*
- *Severity note: CRITICAL in OA source. Domain leakage enables attribution to specific operator. Forensic analysis of Matrix logs reveals operator identity and infrastructure.*
@@ -249,7 +249,7 @@ Raw agent findings merged into consolidated findings (29 consolidated from 105 r
- **Context:** Functions `lookup_person()`, `seed_persons_from_db()`, `log_signal()`, `log_presence_change()` don't close connections on exceptions; resource exhaustion under high-frequency signal logging
- **Fix:** Wrap all `sqlite3.connect()` in try/finally or use context manager (`with` statement)
- **Acceptance:** All four functions use context manager; no unclosed connection paths
- **Status:** FIXED — All four functions wrapped with try/finally and conn.close() in finally block; timeout increased to 10s
- [x] **[HIGH]** `presence_daemon.py:272-323` Race condition in `update_person_state()` — two-lock acquisition | Sources: code-auditor | Effort: S | DevTrack: #553
- **Context:** Lock released between `devices_lock` and `persons_lock`; `max_last_seen` snapshot becomes stale. Concurrent signal could change person state after certainty computed but before lock acquired, causing duplicate alerts or missed transitions.
- **Fix:** Acquire both locks atomically or compute entire state transition inside single critical section; snapshot under lock before computing state machine
- **Acceptance:** State machine transitions (UNKNOWN→PRESENT, PRESENT→ABSENT) protected by single lock; no log entries showing duplicate/missed transitions
- **Status:** FIXED — Certainty computed under devices_lock and snapshot held; state machine logic moved inside persons_lock; alert sending moved outside locks to minimize critical section
- [ ] **[HIGH]** `presence_daemon.py:384-404` Raw socket struct unpacking lacks full validation | Sources: security-auditor | Effort: S | DevTrack: #554
- **Context:** Netlink parsing checks `nlmsg_len < 16` but not actual buffer length; DHCP MAC extraction at `[28:34]` not validated. Crafted packets could cause silent truncation, incorrect MAC parsing, or exception flood.
- **Fix:** Add explicit length checks before struct.unpack_from() and MAC extraction:
```python
# Netlink: before struct.unpack_from('=IHHII', data, offset)
if len(data) < offset + 16:
logger.warning("Truncated netlink message")
return
# DHCP: before mac_bytes = dhcp[28:34]
if len(dhcp) < 34:
return
```
- **Acceptance:** All edge cases validated; unit tests (if added) should cover truncated packets
- **Status:** NOT FIXED (out of scope for this bugfixer task — only 4 P1 items assigned)
---
## P2 — Fix This Week (HIGH Code Quality or Significant Risk)
- [ ] **[HIGH]** `presence_daemon.py:289` Magic number in aggregate certainty calculation | Sources: code-auditor | Effort: XS | DevTrack: #555
- **Context:** Hardcoded `0.08` coefficient for secondary device weighting (line 289: `certainties[0] + 0.08 * certainties[1]`) should be named constant for clarity and maintainability
- **Context:** install.sh forces `/opt/sensor/sensor.db` (line 31) but daemon defaults to `/opt/presence/presence.db` (presence_daemon.py:29). Will create two separate databases.
- Update install.sh line 31: `PRESENCE_DB_PATH=/opt/presence/presence.db`
- Verify daemon defaults match or accept via env var override
- **Acceptance:** Single database file created; systemd EnvironmentFile sets consistent path; no duplicate databases on disk
- [ ] **[HIGH]** `presence_daemon.py:541-573` BLE scanner missing error recovery — event loop can die silently | Sources: security-auditor, db-auditor | Effort: M | DevTrack: #558
- **Context:** No retry loop or watchdog; malformed BLE packets could crash event loop, silently disabling BLE tracking. Also overlaps with seed_persons_from_db() error handling (db-auditor: missing recovery on empty persons table).
- **Fix:**
1. Add retry loop with exponential backoff to `ble_scanner()` thread:
- **Context:** No PRAGMA journal_mode=WAL; synchronous journaling under 6-thread concurrent write load will serialize and timeout (SQLITE_BUSY errors)
- **Fix:** Add to `init_db()` or presence_schema.sql:
```python
# In init_db(), after conn.execute(schema):
conn.execute('PRAGMA journal_mode=WAL')
conn.execute('PRAGMA synchronous=NORMAL')
conn.execute('PRAGMA foreign_keys=ON')
conn.commit()
```
- **Acceptance:**`PRAGMA journal_mode` reports WAL; no SQLITE_BUSY timeouts under sustained signal load; schema integrity enforced
- **Status:** FIXED — WAL pragmas added to init_db() at L148-150, committed at L151
- [ ] **[HIGH]** `presence_daemon.py:605` N+1 pattern in decay_loop — inefficient person state recomputation | Sources: db-auditor | Effort: M | DevTrack: #560
- **Context:** Every person state update re-computes aggregate certainty from full devices dict; should batch-compute all person states once per decay interval
- **Fix:** Refactor decay_loop to compute all person states in one pass:
```python
def decay_loop():
while True:
time.sleep(60)
try:
with devices_lock:
# Collect all person_ids
person_ids = set(d.person_id for d in devices.values() if d.person_id)
# Compute all person states in one critical section
decayed_devices = {mac: decay_device(dev) for mac, dev in devices.items()}
# State transitions outside lock
for person_id in person_ids:
update_person_state(person_id)
```
- **Acceptance:** Decay loop computes person states once per 60-second interval; no duplicate iterations per person
---
## P3 — Fix This Month (MEDIUM Code Quality or Noticeable Risk)
- [ ] **[MEDIUM]** `presence_daemon.py:468-538` High cyclomatic complexity in parse_dhcp() | Sources: code-auditor | Effort: S
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.