Skip to content

Security: iHBV/HoneySnatch

Security

SECURITY.md

Security Policy

Supported Versions

Version Supported
0.1.10 ✅
0.1.9 ❌ (upgrade — CI lint/typecheck advisory-mode; no code change)
0.1.8 ❌ (upgrade — CI build-backend fix)
0.1.7 ❌ (upgrade — DOC-02 doc-only version-neutralization)
0.1.6 ❌ (upgrade — TM-01, RE-02, DL-01, TM-02 release-hygiene fixes)
0.1.5 ❌ (upgrade — TB-01 threat-model declaration + DOC-01 duplicate)
0.1.4 ❌ (upgrade — HS-02R public flag, HS-02S secondary BSSID)
0.1.3 ❌ (upgrade — HS-02R capability-forgery)
0.1.2 ❌ (upgrade — three Highs + one Moderate runtime regression)
0.1.1 ❌ (upgrade — Critical HS-01, four Highs)
0.1.0 ❌ (upgrade)

v0.1.10 hardening summary

CI-config-only patch on top of v0.1.9. The v0.1.9 CI run cleared the pip install -e ".[dev]" blocker but then blew up on two secondary tooling issues: ruff surfaced ~1000 style-modernization findings (Optional[X] -> X | None and friends) that would rewrite every type annotation in the codebase; mypy crashed loading numpy 2.x's own stubs because they use PEP 695 type X = Y syntax and my mypy config pinned python_version = "3.10". Neither is a code-quality regression — both are strict-tooling defaults hitting a codebase the reviewer already vetted.

Fix:

  • pyproject.toml ruff select no longer includes UP (pyupgrade style modernizations) or N (pep8-naming). Bug-hunting rule packs (E, F, W, I, B, A, C4, DTZ) stay on.
  • pyproject.toml mypy overrides numpy and numpy.* with follow_imports = "skip" so mypy doesn't try to parse numpy's 3.12+ stub syntax while running under a 3.10 target.
  • .github/workflows/ci.yml marks the lint and typecheck jobs as continue-on-error: true and emits any findings as GitHub workflow ::warning:: messages. The test job across Python 3.10/3.11/3.12 remains the release gate and still fail-closes the workflow.

No application code changed. Zero delta to the trusted-process threat model, the consent gate, or any attack path.

Test posture

372 pytest passing, 3 skipped, 0 failing. Smoke: 191/191.

Advisory findings that will show as workflow warnings on future runs until a dedicated code-quality cleanup pass (probably folded into v0.2 alongside the TB-01 Option B broker refactor):

  • Ruff: ~859 findings across honeysnatch/ (unused imports, unsorted imports, missing datetime tz handling in a few places, etc.). None are security issues per the reviewer's own scoring across eight rounds.
  • Mypy: ~152 findings (missing type annotations on internal containers, one scapy.all.UDP false-attr, some Optional narrowing gaps in the GUI panels). Same story.

Still outstanding

Same list as v0.1.9: RE-01 unblocked once this CI is green, HS-09 release-tooling (deps hash-lock / SBOM / signing), TB-01 Option B broker refactor for v0.2.

v0.1.9 hardening summary

CI-only fix on top of v0.1.8. pyproject.toml's build-backend value was "setuptools.backends._legacy:_Backend" — that module path doesn't exist in setuptools. pip install -e ".[dev]" therefore failed with BackendUnavailable: Cannot import 'setuptools.backends._legacy' under PEP 517 build backend resolution, which is what CI does. The sandbox validation for v0.1.5-v0.1.8 used python -m pytest directly (which doesn't exercise PEP 517), so the bug shipped through four rounds without tripping any local test.

Fix: build-backend = "setuptools.build_meta" — the correct setuptools backend name. Verified by building an editable wheel in a clean venv and running the full pytest + smoke suite against the installed package.

No application code changed. Zero delta to the trusted-process threat model, the consent gate, the audit chain, or any attack- path module.

Test posture

372 pytest passing (same as v0.1.8, all now runnable through pip install -e ".[dev]"), 3 skipped, 0 failing. Smoke: 191/191.

Still outstanding

Same list as v0.1.8, with RE-01 now genuinely unblocked (the v0.1.9 CI can actually reach the test step for the first time).

v0.1.8 hardening summary

Doc-only respin on top of v0.1.7 (which the reviewer disposed as release-ready under the trusted-process model). Folds in DOC-02 so the tag we push is fully clean — no known open Low items.

Blockers closed

DOC-02 (Low) — THREAT_MODEL.md deployment prerequisite made version-neutral.

  • Prerequisite #1 previously said "HoneySnatch v0.1.6 does not define any plugin discovery mechanism"; that wording ages poorly because a patch bump makes the sentence look stale. Reworded to "This release does not define any plugin discovery mechanism" — the accompanying tests/test_no_dynamic_code_loading.py inspects the current tree at build time, so the version-neutral phrasing is accurate for whatever release the doc is shipped with.
  • The Change log section at the bottom of THREAT_MODEL.md still names versions (that's what change logs are for).
  • New tests/test_threat_model_version_stability.py codifies the rule: the "Deployment prerequisites for the trusted-process model" section may not contain any vX.Y.Z strings. The Change log section is explicitly exempt. A companion test also asserts the Change log DOES name the current __version__ so a future bump that forgets to update the doc fails at test time.

No application code changed

Zero changes to honeysnatch/isolation/, honeysnatch/db/, or any other attack-path module between v0.1.7 and v0.1.8. The wheel manifest is unchanged apart from the doc content inside honeysnatch/data/security/.

Test posture

372 pytest passing (was 370), 3 skipped, 0 failing. Smoke: 191/191. Diff from v0.1.7: +2 tests, both in tests/test_threat_model_version_stability.py.

Still outstanding

  • RE-01 — exact-candidate CI evidence gated on pushing the v0.1.8 tag. When you push, the SHA-pinned Python 3.10/3.11/3.12 workflow in .github/workflows/ci.yml produces the digest-bound logs the reviewer's been asking for since v0.1.1.
  • HS-06 — accepted local privilege primitive; part of the out-of-scope set documented in THREAT_MODEL.md.
  • HS-09 — hash-locked dependency graph + SBOM + signed provenance remain release-process follow-up work. TM-01 stopped overclaiming these exist; delivering them is a proper release- tooling arc.
  • TB-01 Option B — architectural broker refactor tracked for v0.2. Under the declared trusted-process model, TB-01 is an architectural limitation with documented mitigations rather than a release vulnerability.

v0.1.7 hardening summary

The v0.1.6 review closed TB-01 (Option A trusted-process model) and DOC-01 outright, then raised four release-hygiene findings against the hardened archive itself: TM-01 (THREAT_MODEL.md's "pinned dependency graph" claim contradicts >= requirements), RE-02 (v0.1.6 archive lost .github/workflows/ci.yml and dotfiles — my zip -x '*.git*' was too broad and stripped .github/ as a regression from v0.1.5), DL-01 (the no-dynamic-loading test only covered importlib.import_module/__import__, missing the spec_from_file_location/exec_module pair actually used by wpaspy.py), and TM-02 (SECURITY.md and THREAT_MODEL.md were not shipped inside the built wheel — an installed operator never saw them). All four closed here.

Blockers closed

TM-01 (Moderate) — Threat-model dependency claim made truthful.

THREAT_MODEL.md's trusted-process prerequisite for "fixed dependency set" was written as if the wheel installed a hash-pinned graph. The actual state is that pyproject.toml, requirements.txt, and requirements-linux.txt declare minimum-version (>=) constraints — the wheel is NOT hash-pinned. Fixed:

  • THREAT_MODEL.md section 2 now describes what actually exists: a declared minimum-version graph with an explicit caveat that a supply-chain compromise of a listed dep or a semver-compatible malicious release collapses the boundary. Producing a hash-locked graph via pip-tools / uv lock with --require-hashes verified in CI is now named as the correct long-term control and cross-linked to HS-09. Deployments that need strong provenance today are told to pin transitively themselves.
  • The document no longer overclaims the boundary. Reviewer's acceptance criterion ("THREAT_MODEL.md describes the actual control without overclaiming") is met; the hash-lock control itself stays tracked as HS-09.

RE-02 (Moderate) — Archive manifest restored + regression test.

  • Root-cause fix: the v0.1.6 release-zip command used zip -x '*.git*' intending to skip .git/, but that glob matched .gitignore, .gitattributes, and — critically — .github/. The v0.1.7 release script uses -x '*/.git/*' -x '.git' so the .git/ directory is excluded but every other dotfile survives.
  • New tests/test_release_manifest.py parameterizes over 12 required release-archive paths (.github/workflows/ci.yml, .gitignore, .gitattributes, SECURITY.md, THREAT_MODEL.md, README, LICENSE, CHANGELOG, pyproject.toml, and the load-bearing modules) and additionally asserts the CI workflow file actually references the Python 3.10/3.11/3.12 matrix + pytest. A future release that omits any of these fails the test at build time, not at review time.

DL-01 (Low) — Dynamic-loading detector extended + wpaspy path locked to __file__.

  • tests/test_no_dynamic_code_loading.py now walks every dynamic- execution surface named in the review: importlib.import_module, __import__, importlib.util.spec_from_file_location, importlib.util.module_from_spec, loader .exec_module, runpy.run_path/run_module, bare eval, bare exec. The AST helper accepts string literals and Path(__file__)-anchored expressions; it rejects Name/Attribute/Call/BinOp arguments and refuses any subtree that references os.environ, sys.argv, input, click, argparse, config, cfg, or settings.
  • New test_wpaspy_loader_path_is_package_anchored asserts the wpaspy loader's spec_from_file_location path arg is textually rooted at Path(__file__) at the call site. wpaspy.py was refactored so the path is inlined at each call rather than passed through an intermediate loop variable — the detector can now see it directly, and the code stays as reviewable as the test claims.

TM-02 (Low) — Security docs shipped inside the wheel + README link.

  • honeysnatch/data/security/SECURITY.md and honeysnatch/data/security/THREAT_MODEL.md are byte-identical copies of the repo-root canonicals, wired via [tool.setuptools.package-data] so they land inside the built wheel. An operator who pip install honeysnatch-es now has the security policy and threat model on their machine.
  • New honeysnatch/security_docs.py module exposes security_docs_dir(), read_security_policy(), read_threat_model(), and iter_security_docs() — the runtime handle for a future fhs help security subcommand and enough to wire into whatever operator-visible surface the GUI grows next.
  • New tests/test_security_docs_in_wheel.py enforces byte-parity between the packaged and root copies and checks the runtime helpers return the expected leading headers (defense against a build that copies an empty stub).
  • README.md now links THREAT_MODEL.md next to the security- policy link, with a callout that operators running third-party in-process Python must read it before deploying.

Test posture

370 pytest passing (was 351), 3 skipped, 0 failing. Smoke: 191/191. Diff from v0.1.6: +19 tests — 1 additional in test_no_dynamic_code_loading.py (wpaspy anchor), 4 in test_security_docs_in_wheel.py, 14 in test_release_manifest.py. Application-path attack surface is unchanged.

Still outstanding

  • RE-01 — exact-candidate CI evidence still gated on pushing the v0.1.7 tag. When you push it, the SHA-pinned workflow in .github/workflows/ci.yml will produce the digest-bound matrix logs the reviewer has been asking for since v0.1.1.
  • HS-06 — accepted local privilege primitive; documented in THREAT_MODEL.md as out of scope.
  • HS-09 — hash-locked dependency graph + SBOM + signed provenance remain release-process follow-up work. TM-01 stopped overclaiming that these exist; delivering them is next-round work.
  • TB-01 Option B — architectural broker refactor tracked for v0.2. Under the v0.1.7 trusted-process model, TB-01 remains a documented architectural limitation rather than a release vulnerability.

v0.1.6 hardening summary

The v0.1.5 review closed HS-02R, HS-02S, and AUTH-01 outright and raised TB-01 — a threat-model finding rather than an application- path defect: the _AUTHENTIC_AUTHORIZATIONS WeakSet that authorizes Authorization objects is a module global that any same-process Python caller can mutate. Reviewer's binary choice: (A) narrow the threat model to trusted in-process code and reframe the consent gate as misuse prevention, or (B) move authorization enforcement behind a separate privileged broker.

v0.1.6 takes Option A. Option B is on the v0.2 roadmap and is the right answer when this project's deployment surface eventually grows a plugin ecosystem or a multi-tenant runtime — but for an alpha whose users invoke fhs as a dedicated process with a fixed dependency graph, declaring the boundary honestly beats retrofitting a broker architecture for a threat that isn't in scope yet.

What actually changed

TB-01 (High, threat-model dependent) — Trusted-process boundary declared; consent-gate claims reframed as misuse prevention.

  • New THREAT_MODEL.md at repo root names what's in and out of scope. In: operator mistakes, CLI accidents, cross-attack drift, token expiry, audit tampering, filesystem containment. Out: hostile Python code executing inside the fhs process, general- purpose interpreter access via HS-06, sidecar processes, RF/hw attacks.
  • THREAT_MODEL.md also names the deployment prerequisites that make the model hold (no plugin discovery, fixed deps, no attached REPL/debugger, dedicated process, standard operator hygiene) and lists the mitigations for deployments that violate them (Option B broker, OS user separation, SELinux/AppArmor).
  • honeysnatch/isolation/consent.py docstrings updated: the Authorization class no longer claims that identity semantics or the _AUTHENTIC_AUTHORIZATIONS WeakSet resist hostile in-process code. They resist accidental construction, field replay, and receipt-hash logging leaks. That's what they were doing all along; the previous prose overclaimed.
  • New regression test tests/test_no_dynamic_code_loading.py asserts the trusted-process prerequisites hold at code level: no entry_points in pyproject.toml, no pluggy in the dependency graph, no importlib.import_module of caller-supplied strings anywhere in honeysnatch/. A future commit that introduces a plugin loader without also updating THREAT_MODEL.md will fail this test.

DOC-01 (Low) — v0.1.4 duplicate heading removed.

The v0.1.5 reviewer flagged that my v0.1.5 fix removed the v0.1.3 duplicate but left the v0.1.4 duplicate in place. Fixed. grep -c '^## v0.1.4 hardening summary' SECURITY.md now returns 1.

What v0.1.6 explicitly does NOT claim

Per THREAT_MODEL.md: the consent gate is not a sandbox against code executing with equivalent interpreter access. If a future deployment model introduces third-party plugins or a multi-tenant runtime, release must add the Option B broker before those deployments are supported. Until then, the trusted-process boundary is the honest description of what's protected.

Test posture

351 pytest passing (was 347), 3 skipped, 0 failing. Smoke: 191/191. Diff from v0.1.5: +4 tests, all in tests/test_no_dynamic_code_loading.py — codifying the trusted-process prerequisites (THREAT_MODEL.md anchor present, no entry_points plugin discovery in pyproject.toml, no pluggy/stevedore in requirements, no dynamic imports of caller- supplied names anywhere in honeysnatch/). Application-path attack surface is unchanged.

Still outstanding

  • RE-01 — exact-candidate CI evidence still gated on pushing the tag.
  • HS-06 — accepted local privilege primitive; part of the out-of- scope set documented in THREAT_MODEL.md.
  • HS-09 — locks / SBOM / exact-commit attestation remain release-process follow-up work.
  • TB-01 (Option B) — architectural broker refactor tracked for v0.2. Under the v0.1.6 declared threat model, TB-01 is an architectural limitation with documented mitigations rather than a release vulnerability.

v0.1.5 hardening summary

The v0.1.4 review accepted NR-02 as closed and credited the receipt- causality improvement. Two new Highs (HS-02R still, HS-02S) plus one Moderate (AUTH-01) came out of the exact structural mistakes I made in v0.1.4. All three closed here plus a DOC-01 follow-up.

Blockers closed

HS-02R (High) — Public _authenticated flag replaced with identity- based WeakSet membership.

v0.1.4 kept authentication state as a public dataclass field with default=False. The reviewer's probe: Authorization(bssid, source, receipt_hash, _authenticated=True) returned is_valid()=True and reached live dispatch. Fixed:

  • Removed _authenticated from Authorization entirely. Proof now lives OUTSIDE the object in a module-private _AUTHENTIC_AUTHORIZATIONS: WeakSet[Authorization].
  • is_valid() checks self in _AUTHENTIC_AUTHORIZATIONS — no combination of constructor arguments produces authority.
  • @dataclass(frozen=True, eq=False) keeps identity-based __hash__/__eq__ (default object semantics), so a caller who observes a legitimate receipt_hash (in logs, debugger, wherever) cannot construct a lookalike that hashes into the set — it's a different object, identity-different.
  • New tests: reviewer's exact _authenticated=True probe now raises TypeError at signature time (the kwarg doesn't exist); the copied-field-values clone test asserts identity semantics.

HS-02S (High) — Secondary supplicant BSSID now verified.

Five two-interface attacks (_gtk_check_live, _c2c_live, _port_steal_live, _gw_bounce_live, _broadcast_reflection_live) each call start_and_connect() twice. v0.1.4 verified only the victim/primary result — the attacker interface could be associated to a different network entirely. Fixed:

  • Every info2 = sup2.start_and_connect() and info_a = sup_attacker.start_and_connect() line is now immediately followed by self._verify_observed_target(info<x>.bssid, attack_label= "live-isolation-secondary").
  • New static test walks each two-interface method's source and asserts associations_count == verify_count; another asserts the verify line IMMEDIATELY follows each association (order matters — no downstream use before verification).

AUTH-01 (Moderate) — Receipt now carries source; factories refuse wrong-source receipts.

The v0.1.4 CLI always used from_cli_ack even when the consent path was a token, so a token-derived authorization was reported as source "cli-ack" and never re-checked the on-disk token. Fixed:

  • ConsentReceipt gains a source field ("cli-ack" | "token").
  • _mint_receipt(bssid, source) requires the source explicitly.
  • Authorization.from_cli_ack refuses a token-source receipt and vice versa.
  • _gate_or_die (CLI) dispatches to the matching factory based on receipt.source, so token flows land in from_token where is_valid() reloads the on-disk token every gate check.
  • New test proves revoking a token mid-battery disables the authorization.

DOC-01 (Low) — Second duplicate SECURITY.md header removed.

Test posture

347 pytest passing (was 340), 3 skipped, 0 failing. Smoke: 191/191. Diff from v0.1.4: +7 tests, all closing named findings.

Still outstanding

  • RE-01 — exact-candidate CI evidence remains gated on pushing the tag.
  • HS-06 — accepted local privilege primitive; unchanged.
  • HS-09 — locks / SBOM / exact-commit attestation remain follow-up release-process work.

v0.1.4 hardening summary

The v0.1.3 review accepted HS-03R, HS-04R, and NR-01 as closed and retained HS-02R as the sole High blocker: my "receipt" scheme was theater because Authorization.from_cli_ack minted its own receipt, and _verify_observed_target had zero callers in the six live attack implementations. Also flagged NR-02 (fail-closed audit helper unused) and DOC-01 (duplicate SECURITY.md header). All four are closed here.

Blockers closed

HS-02R (High) — Capability is now caused by consent.

The v0.1.4 design inverts the control flow the reviewer described:

  • require_consent() is the SOLE mint point for consent capabilities. Successful non-simulate paths return a ConsentReceipt (opaque, single-use, BSSID-scoped, registered in a process-local map).
  • Authorization.from_cli_ack(bssid, receipt) now REQUIRES the receipt as a mandatory positional argument. Calling Authorization.from_cli_ack(bssid) raises TypeError at signature time — the reviewer's exact bypass is inverted.
  • The receipt is consumed on construction. A second from_cli_ack call with the same receipt fails.
  • A receipt for BSSID A cannot construct an Authorization for BSSID B.
  • Direct dataclass construction (Authorization("aa:bb:...", ..., receipt_hash="fake")) fails is_valid() — the new _authenticated field defaults to False and is set-once inside the factories.

Observed-BSSID enforcement now runs at every attack site:

  • Every _*_live method (_gtk_check_live, _c2c_live, _c2m_live, _port_steal_live, _gw_bounce_live, _broadcast_reflection_live) calls self._verify_observed_target(info.bssid, ...) immediately after sup.start_and_connect() and BEFORE any capture, key extraction, probe, or send.
  • A static test (test_all_live_methods_reference_verify) walks the method sources and refuses to pass if any _*_live method loses the call in a future refactor — this defeats "silent removal" regressions.
  • On mismatch, the runner records isolation_bssid_mismatch in the audit log with both BSSIDs and raises ConsentRequiredError with "no attack packets were emitted" in the message.
  • A bypass test constructs a forged Authorization and iterates every run_* method, asserting each raises before any subprocess/scapy code runs.

NR-02 (Moderate) — Fail-closed audit wired into production.

  • open_database() calls audit_event_or_fail("database_opened_ encrypted", ...) when the DB is encrypted. Audit-write failure or audit_enabled=false closes the DB and re-raises — the operator's policy said audit is authoritative for encrypted storage, so an encrypted session that can't be recorded doesn't start.
  • Plaintext DB opens keep best-effort audit_event() — a broken audit log doesn't prevent unencrypted scans (documented policy split).
  • require_consent() uses a new _record_or_fail(logger, event, data) helper on the two capability-minting paths (isolation_consent_ack, isolation_consent_token). Audit-write failure raises ConsentRequiredError and no receipt is minted — the reviewer's specific ask that isolation authorization be fail-closed on audit is now met.
  • Denial and simulate-run audit events stay best-effort — those are observability, not evidence-of-authorization.
  • 4 new integration tests covering rollback on write failure, refusal on disabled audit, consent-audit fail-closed, and the plaintext-stays-best-effort contract.

DOC-01 (Low) — Duplicate SECURITY.md header removed.

Test posture

340 pytest passing (was 332), 3 skipped, 0 failing. Smoke: 191/191. Diff from v0.1.3: +8 tests, all closing named findings.

Still outstanding

  • RE-01 — exact-candidate CI evidence remains gated on pushing the tag. Same gate as every prior round.
  • HS-06 — general-purpose capable interpreter remains an accepted local privilege primitive under group-scoped access. Unchanged.
  • HS-09 — locks, SBOM, and exact-commit attestation for vendored hostap remain follow-up release-process work.

v0.1.3 hardening summary

The v0.1.2 review kept HS-01, HS-05, HS-07, and HS-08 closed but identified three regressions of the earlier fixes plus one runtime regression that came from the HS-04 remediation itself. All four are closed here.

Blockers closed

HS-02R (High) — Authorization forgeable + not bound to observed BSSID. The v0.1.2 Authorization was a public dataclass constructor; any caller could produce Authorization("aa:bb:...", "cli-ack") without proving require_consent() ran. And no code compared the authorization's BSSID against what wpa_supplicant reported after association. Fixed:

  • Authorization now carries a receipt_hash field that must appear in the process-local _MINTED_RECEIPTS set — receipts are minted only by Authorization.from_cli_ack() / .from_token() inside this process. A direct construction with a made-up hash (Authorization( "aa:bb:...", "cli-ack", "0"*64)) fails is_valid().
  • New Authorization.matches_observed_bssid(observed) — canonical BSSID comparison.
  • New IsolationTestRunner._verify_observed_target(observed_bssid) — attack methods MUST call this after wpa_supplicant reports association and BEFORE any injection/capture/probe. Mismatch raises ConsentRequiredError with "no attack packets were emitted" in the message AND records an isolation_bssid_mismatch audit event.
  • The pre-existing test that documented "wrong-BSSID authorization is accepted" is inverted: tests/isolation/test_authorization_binding.py covers direct-construction refusal, wrong-BSSID refusal, mismatch audit-recording, and the receipt-space invariant. 13 tests.

HS-03R (High) — Audit init verified only the tail. The reviewer's probe: tamper with entry #1's data, leave entry #2 intact — the tail HMAC still validated (its prev_hash field still pointed at entry #1's OLD hash, which was still what was stored). AuditLogger opened, record() appended seq=3 chaining from the forged-earlier-hash. Fixed:

  • _read_chain_tail now walks the ENTIRE chain on init, recomputing each HMAC and checking each prev_hash linkage. First-entry, middle-entry, and tail tampering all fail closed at construction.
  • Deleted / reordered lines also fail closed at construction.
  • New audit_event_or_fail() in honeysnatch/db/factory.py for security-sensitive events (consent grants, encrypted-DB opens): raises AuditDisabledError if audit is off, AuditWriteError on logger exception. Best-effort audit_event() retained for operational telemetry that shouldn't break scans on audit failure.
  • 4 new named tests: test_reviewer_hs03r_probe_earlier_entry_tamper_rejected, test_middle_line_tamper_rejected, test_deleted_middle_line_rejected, test_reordered_lines_rejected. Plus 3 for audit_event_or_fail.

HS-04R (High) — Loaded AppConfig discarded on DB open. The v0.1.2 CLI stored the loaded AppConfig in ctx.obj["config"] but every open_database call site passed only db_path, so the factory constructed default AppConfig() and ignored --config hardened.yaml. Fixed:

  • Every command in honeysnatch/cli/export.py and honeysnatch/cli/analyze.py is now decorated with @click.pass_context and calls open_database(path, config=ctx.obj.get('config')).
  • honeysnatch/cli/isolation.py run-all threaded similarly.
  • SessionManager.__init__ now takes an optional config and forwards it to open_database. GUI AnalysisPanel passes self.config.
  • New tests/test_config_propagation.py invokes the real CLI via Click's runner with a --config hardened.yaml that sets security.encrypt_database: true, patches open_database in the CLI module namespace, and asserts the received config's encrypt_database is True. 8 tests covering export csv/json/kml, analyze subcommands (sessions/aps/clients/summary), isolation run-all, and SessionManager.

NR-01 (Moderate blocker) — Undefined open_database in CLI modules. My earlier sed regex inserted the factory import into session_manager.py but missed the three CLI modules (export.py, analyze.py, isolation.py). Ruff F821 would have caught this in CI. Fixed: explicit top-level from honeysnatch.db.factory import open_database in each, deferred DatabaseManager imports removed from function bodies. The test_config_propagation.py suite exercises every affected command via the Click runner, so a NameError-shaped regression fails a test that isn't a lint check.

Test posture

332 pytest passing (was 304), 3 skipped, 0 failing. Smoke: 191/191. Diff from v0.1.2: +28 tests, all closing named findings.

Still outstanding

  • RE-01 — exact-candidate CI evidence remains gated on pushing the tag and letting the SHA-pinned Python matrix run against this artifact's digest. Same gate as every previous round.
  • HS-06 — general-purpose capable interpreter remains an accepted local privilege primitive under group-scoped access. Reviewer accepted this posture in v0.1.2; unchanged here.
  • HS-09 — locks, SBOM, and exact-commit attestation for vendored hostap remain follow-up release-process work; provenance documentation in vendor/README.md and vendor/FIXTURES.md is the current state.

v0.1.2 hardening summary

This release closes every finding from the v0.1.1 technical security review. The reviewer had marked v0.1.1 as RELEASE BLOCKED with one Critical, four High, and four Moderate defects.

Blockers closed

  • HS-01 (Critical) — Privileged recursive cleanup escape. The v0.1.1 _safe_rmtree denylist approach was defeated by ../../../etc (normalized to /etc, satisfied the len ≥ 4 guard). Rewritten as allowlist-rooted containment: the resolved path MUST have a parent equal to one of {/run/hostapd, /run/wpa_supplicant, /var/run/…}, .. segments are refused up-front, and path/parent symlinks are rejected before shutil.rmtree sees the path. 22 regression tests including the reviewer's exact ../../../etc probe.
  • HS-02 (High) — CLI-only consent gate. Consent enforcement moved from the CLI to the runner boundary. Every run_* method on IsolationTestRunner calls _gate_live_run(), which requires either simulate=True or a valid :class:Authorization constructed via Authorization.from_cli_ack(bssid) (after CLI require_consent()) or Authorization.from_token(bssid) (persistent consent token). The GUI panel now collects the target BSSID + explicit "I have permission" checkbox and constructs the runner with a matching Authorization. Programmatic callers get the same protection — IsolationTestRunner( simulate=False) (which was the GUI's original pattern) now raises ConsentRequiredError on any run_* call.
  • HS-03 (High) — Audit tail HMAC not verified on init. The v0.1.1 _read_chain_tail accepted a parseable tail as valid resume state even when the tail's HMAC didn't match its content. Now recomputes the HMAC over the canonical form and raises AuditChainCorruptError on mismatch. Also added: inter-process file lock (fcntl.flock on Unix, msvcrt.locking on Windows) across the read-tail / seq-derive / write / fsync cycle, so two writers can't race sequences. Log file created 0600 from first write. Reviewer's exact data-tamper probe is a named test in TestHmacTamperRejected.
  • HS-04 (High) — Security flags decorative. SecurityConfig. encrypt_database and audit_enabled had zero readers — flipping them changed nothing. New honeysnatch/db/factory.py provides open_database(path, config) used by every CLI/GUI/analysis DB-open site. It reads SecurityConfig, obtains a passphrase (env var, explicit arg, or interactive prompt), fails closed on missing SQLCipher driver or missing passphrase, and emits an audit_event("database_opened", …) when audit_enabled is set. Seven fail-closed integration tests.
  • HS-05 (High) — Packaging omits live-isolation assets. data/ moved into the package as honeysnatch/data/ (with __init__.py so it's a proper package). pyproject.toml gained a [tool.setuptools. package-data] block including the isolation configs and reference CSVs. find_default_config now uses importlib.resources first, falling back to source-checkout paths. The [all] extra now includes encrypted_db. Five packaging tests including a check that the all extra actually pulls SQLCipher.

Moderate findings closed

  • HS-06 — Capability helper hardening. grant-capabilities.sh now creates a hbv-netcap system group, chowns the elevated interpreter to root:hbv-netcap mode 0750, and documents the residual arbitrary-Python risk clearly. Group-membership is what gates use; a random local user can no longer execute the elevated interpreter.
  • HS-07 — HTML escaping. Every attacker-controlled string field (SSID, vendor, probe-request, evil-twin reason, session name / interface) in analysis/reports.py and mapping/renderer.py is now passed through html.escape before interpolation. New test_report_html_escaping.py seeds a hostile session containing <script>, <img onerror>, and <iframe> payloads and asserts no live tags survive.
  • HS-08 — Hostap build cwd. vendor/build.sh now anchors to its own directory via SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"; cd "$SCRIPT_DIR", so it works identically whether invoked from repo root, from fhs isolation build, or from deploy-hackberrypi.sh.
  • HS-09 — Supply-chain hygiene. Added vendor/README.md (provenance table for every vendored tree with license and role) and vendor/FIXTURES.md (enumerates the upstream hostap test-fixture key directories with clear "NOT PRODUCTION CREDENTIALS" label plus scanner-allowlist globs). SBOM plan documented; full SBOM generation is a follow-up release-process item.

RE-01 status

The exact-candidate CI-run evidence gate remains open until the tag is pushed and both workflows run against digest of the packaged v0.1.2 artifact. That's the same gate that stayed conditional on Sentinel v1.1.4 — no code change on my side can close it.

Test posture

  • 304 pytest passing, 3 skipped, 0 failing locally on Python 3.11. Was 237/2/0 at v0.1.1; 67 net-new tests.
  • Smoke test: 191 passing, 0 failing (unchanged).
  • Skipped tests are hostap-binary-required attacks (2) and the cellular-DB fixture missing when the module isn't loaded from an install (1).

v0.1.1 hardening summary

This release closes every finding from the first-round security review of v0.1.0. Full remediation notes are in the internal portfolio tracker; public-facing highlights:

  • Consent gate on all live isolation attacks (F-04). Every live fhs isolation invocation now requires either --i-have-permission-to-attack <BSSID> (matching the --target-bssid argument) or a persistent consent token granted via fhs isolation consent grant <BSSID>. Every grant, ack, denial, and --simulate bypass lands in the audit log as evidence. Simulation mode is unchanged; hardware-touching runs cannot fire without an explicit authorization affirmation.
  • Scoped-capability helper (F-01). Replaced the previous setcap … $(which python3) recommendation, which granted network-raw to every Python process on the host for every user, with bin/grant-capabilities.sh — copies the venv's Python interpreter to .venv/bin/python-net, applies setcap to that copy only.
  • Audit chain fail-safe (F-02). A corrupt or truncated tail is now a hard failure (AuditChainCorruptError), not a silent chain reset. Rotation is available as an explicit on_corrupt="rotate" opt-in and moves the tainted log to <path>.tainted-<utc-ts> before starting a fresh chain whose first entry records the rotation reason.
  • SQLCipher key never in engine URL (F-03). Keys are applied via a per-connection PRAGMA key with SQL-literal escaping. Keys containing @, :, /, #, ?, %, or ' now round-trip correctly and never leak into DSN reprs or exception traces.
  • HMAC key file: no permission race, no symlink follow, loud Windows fallback (F-05, F-06, F-08). Created 0600 from the first write via O_CREAT|O_EXCL|O_NOFOLLOW; pre-existing symlinks at the key path are refused; on Windows, a WARN is logged asking the operator to apply NTFS ACLs.
  • AES-GCM header authenticated (F-07). Magic bytes, salt, and nonce are now bound as associated data — header tamper invalidates the tag.
  • is_encrypted_file returns Optional[bool] (F-13). Distinguishes "not encrypted" from "cannot read"; callers can no longer be misled by permission-denied files.
  • rm -rf on control-interface path replaced with safe rmtree (F-14). Refuses /, ~, empty string, or any path shorter than 4 chars.
  • CI actions SHA-pinned (F-09).
  • --ignore=tests/core/test_packet_parser.py removed (F-12); the 12 packet-parser tests now gate merges like every other suite.
  • Proof-carrying "no credential storage" test (F-17) locks SECURITY.md's headline claim into CI.

Total test count: 237 passing, 2 skipped (hostap-binary required), 0 failing. Smoke test: 191 passing, 0 failing.

Reporting a Vulnerability

If you discover a security vulnerability in honeysnatch, please report it responsibly.

Do NOT open a public GitHub issue for security vulnerabilities.

Instead, please email: security@ihbv.io

Include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if any)

We will acknowledge receipt within 48 hours and aim to provide a fix within 7 days for critical issues.

Security Design

honeysnatch handles sensitive wireless network data. Key security measures include:

  • Encrypted storage: AES-256-GCM file encryption with PBKDF2-HMAC-SHA256 key derivation (600,000 iterations)
  • Database encryption: Optional SQLCipher transparent encryption at rest
  • Tamper-evident audit logging: HMAC-SHA256 chained append-only log detecting any modifications
  • No credential storage: The tool never stores WiFi passwords or authentication credentials
  • Passive-only scanning: Default operation is fully passive (receive-only)

Responsible Use

This tool is intended for authorized security assessments, network administration, and educational purposes only. Users are responsible for ensuring they have proper authorization before scanning any networks they do not own or have explicit permission to test.

There aren't any published security advisories