Skip to content

Add CVE enrichment for WiFi and BLE scans (ADR-0010) - #1

Merged
ry-ops merged 4 commits into
mainfrom
claude/code-review-cve-scanner-stick-s3-x85g8y
Oct 5, 2026
Merged

ry-ops merged 4 commits into
mainfrom
claude/code-review-cve-scanner-stick-s3-x85g8y

Conversation

@ry-ops

@ry-ops ry-ops commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Implements automatic vendor and CVE enrichment for wifi.scan and ble.scan results on ingest, providing security leads without blocking strike capture.

Summary

This change adds a best-effort enrichment pipeline that attaches vendor information and CVE metadata to scanned access points and BLE devices. The enrichment uses live MAC-OUI lookups and CVE-search API queries, with graceful degradation if external services are unavailable.

Key Changes

  • New modules:

    • oui.py: MAC OUI → vendor name resolution via macvendors.com API with per-OUI caching and rate limiting (~1 req/s)
    • cve.py: Vendor-level CVE lookup against cve-search API with intelligent slug candidate generation and product matching heuristics
    • enrichment.py: Orchestrates OUI + CVE lookups and mutates scan payloads with a security block per item
  • Collector integration:

    • Collector now accepts an optional Enricher instance and applies enrichment to wifi.scan and ble.scan payloads before storage
    • Extracted common station-lookup + capability + disabled-flag logic into _gate() method
    • Enrichment failures are caught and logged but never block strike capture (best-effort design)
  • Emit dispatch improvements:

    • Added _armed dict to track pending emit tokens per station, binding tokens to the tool they were armed for
    • Token validation now checks both token existence and tool consistency
    • Disabled tools cannot be armed or fired, even with a valid token
  • Station parameter validation:

    • serial_station.py now clamps oversized ms budgets to prevent unbounded request lock holds
    • Added validation for non-numeric ms parameters
  • Store optimization:

    • store.py now computes strike counts via SQL JOIN instead of separate queries

Implementation Details

  • Vendor slug resolution: Instead of a hand-curated mapping table, candidate slugs are derived from vendor names (stripping legal-entity noise words) and probed against cve-search; the first match is cached for 24 hours
  • Product matching: Device labels (SSID/BLE name) are matched against vendor product slugs using token-based heuristics that strip noise words and require meaningful token overlap, avoiding false positives on short generic strings
  • Locally-administered MACs: Randomized/private addresses (U/L bit set) skip enrichment entirely rather than reporting misleading "vendor unknown"
  • Error resilience: Network failures, timeouts, and API errors are logged but don't propagate; enrichment degrades gracefully to partial or no security metadata

Testing

Comprehensive test suite added (test_enrichment.py) covering:

  • OUI resolution with known/unknown vendors and randomized MACs
  • CVE vendor slug candidate generation and product matching
  • End-to-end enrichment for wifi.scan and ble.scan with various failure modes
  • Graceful degradation when external APIs are unavailable

https://claude.ai/code/session_01FzVTmtSkMcpzidmwtye9JJ

… fixes

CVE enrichment (ADR-0010, sferic-sticks3):
- oui.py: MAC OUI -> vendor via macvendors.com, cached per-OUI, skips
  locally-administered/randomized addresses (no IEEE vendor to find).
- cve.py: vendor -> CVEs via CIRCL's cve-search API. Vendor slugs are
  probed (candidate slugs derived from the vendor name, first
  non-empty /browse/<slug> wins) rather than hand-mapped, so it
  self-corrects as cve-search's own vendor list changes. Product
  matching is token-based against the device's own SSID/name, with
  cve-search's generic slug suffixes ("_firmware") stripped first.
- enrichment.py: wires both into Collector.dispatch for wifi.scan and
  ble.scan, mutating each ap/device entry with a `security` block
  before the strike is stored. Any lookup failure degrades to no
  enrichment -- never fails the underlying strike.

Fixes found in review of the existing emit two-step (ADR-0008 §1):
- emit_dispatch had no disabled-tool check, unlike dispatch/
  stream_dispatch -- a disabled emit tool (ir.tvbgone, ir.replay,
  ble.hid, usb.hid) could still be armed and fired. Factored the
  shared station/capability/disabled gate into Collector._gate() so
  all three dispatch paths use the same check.
- emit_dispatch didn't verify a fire call's confirm_token was actually
  armed for the tool the call claims -- harmless on the device itself
  (it fires whatever was armed, not what the caller names), but the
  collector would store the resulting strike under the wrong tool.
  Collector now tracks (station -> armed tool, token) and rejects a
  fire whose tool doesn't match with a new `tool_mismatch` error.
- SerialStation.run()'s per-call `ms` budget was unbounded (unlike the
  streaming tools' clamped duration_ms) and unvalidated, so a huge or
  non-numeric value could hold the station's single request lock
  indefinitely or raise an unhandled ValueError. Clamped to 120000 and
  now raises a typed StationToolError("bad_params", ...) on bad input.
- store.flashes() did one COUNT(*) query per flash row; replaced with
  a single GROUP BY join.

Adds httpx as a new dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzVTmtSkMcpzidmwtye9JJ
ruff format --check failed on cve.py's two hand-wrapped set literals
(_PRODUCT_NOISE_WORDS, _SUFFIX_WORDS) -- reproduced locally and applied
`ruff format`, no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzVTmtSkMcpzidmwtye9JJ
main independently landed an equivalent fix for the same emit_dispatch
gaps this branch fixed (missing disabled-check, unscoped confirm_token) --
a different session apparently reviewed the same code concurrently.
Reconciled to main's (station, tool)-keyed _armed dict and its uniform
`bad_token` error contract (dropping this branch's separate `tool_mismatch`
code, since main's keying already makes a wrong-tool fire attempt a no-op
against the real armed entry rather than needing a distinct error path),
while keeping this branch's `_gate()` helper -- which already covers the
disabled-check main added inline -- and its CVE-enrichment wiring in
dispatch(), untouched by main's changes.

Test suite reconciled the same way: kept main's test names/coverage where
they duplicated this branch's, kept this branch's one test main didn't
have (disabling a tool after it's already armed, before firing), dropped
the redundant duplicate, and fixed the surviving wrong-tool test's
expected error code (tool_mismatch -> bad_token).

45 tests pass, ruff format/check clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzVTmtSkMcpzidmwtye9JJ
main landed Milestone 3 (WebSocket station transport, capability
auto-routing with optional `station`, `sferic.recall` full-text search,
read-only dashboard, fake-station dev harness) since this branch's last
sync. Reconciled:

- collector.py: rewrote _gate() to wrap main's new _resolve() (named or
  auto-routed station lookup, raising no_capable_station/ambiguous_station)
  instead of the old required-station lookup, keeping the disabled-check
  and CVE-enrichment call in dispatch() this branch added. All three
  dispatch paths (dispatch/stream_dispatch/emit_dispatch) now take an
  optional station like main's, gated through the one _gate() helper.
- pyproject.toml: unioned the new dependencies (httpx for CVE/OUI lookups,
  starlette/uvicorn/websockets for the WS listener + fake station); dropped
  the now-redundant httpx entry from the dev group since it's a core dep.
- README.md: merged both branches' additions to the Layout file listing.
- uv.lock: regenerated with `uv lock` against the merged pyproject.toml
  rather than hand-resolving the conflict markers.

82 tests pass (44 from this branch + main's new WS/routing/recall/dashboard
suites), ruff format/check clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FzVTmtSkMcpzidmwtye9JJ
@ry-ops
ry-ops merged commit 1ab46bd into main Oct 5, 2026
2 checks passed
@ry-ops
ry-ops deleted the claude/code-review-cve-scanner-stick-s3-x85g8y branch October 5, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants