Skip to main content
ble-doctor

CI PyPI npm Python License: MIT Telemetry: none

Authorization and scope. ble-doctor is for your own devices, CTF targets, and systems you have written permission to test. It is a defensive analysis tool. probe-writes transmits to the target and is opt-in per characteristic. Do not point it at hardware you do not own or have explicit authorization to assess.

Hand it a BLE capture or a device address. It scores the security posture 0-100 and tells you what is weak: pairing method, unauthenticated writes, DFU exposure, address rotation.

ble-doctor analyze capture.pcap                       # offline: score a btmon/Ubertooth capture
ble-doctor scan --target AA:BB:CC:DD:EE:FF            # live read-only GATT enumeration + score
ble-doctor probe-writes AA:BB:CC:DD:EE:FF --char <uuid>   # opt-in: test one characteristic for open writes

Every subcommand takes --json for a stable machine-readable envelope. No telemetry; nothing leaves your machine.

Get started

1. Install

uvx ble-doctor analyze capture.pcap      # Python, via uv; nothing to install
pipx install ble-doctor
npx ble-doctor analyze capture.pcap      # Node launcher: runs the same version through uvx or pipx
pip install ble-doctor

From a clone: pip install -e .. Requirements:

Need For
Python 3.10+ everything
tshark (Wireshark) on PATH analyze (capture parsing, SMP pairing dissection)
bleak (installed with the package) and a BLE adapter scan, probe-writes
bluesploit (optional) an INFO cross-check inventory in scan; never moves the score

2. Score a capture

ble-doctor analyze capture.pcap
BLE Doctor — score: 85/100

[FAIL] mac_rotation (medium): ...
[PASS] pairing_method (info): ...

(Illustrative; real detail strings depend on the capture.) If the capture contains no SMP pairing exchange the pairing check is skipped and the reason is printed, not scored.

3. Enumerate a live device

ble-doctor scan --target AA:BB:CC:DD:EE:FF      # or set BLE_DOCTOR_TARGET

--target takes an address or advertised name; with none given and exactly one device in range, it uses that one. The connection is read-only.

4. Probe for unauthenticated writes (opt-in)

ble-doctor probe-writes AA:BB:CC:DD:EE:FF --char 0000ffe1-0000-1000-8000-00805f9b34fb

Connects without bonding and attempts one harmless write per named characteristic, then classifies what the device answered. Nothing is written unless you name characteristics (--char, --service) or pass --allow-all. Firmware-update characteristics are refused unless you also pass --allow-dangerous, which can brick a target.

5. Teach your coding agent

ble-doctor install                  # Claude Code skill + Cursor rule + AGENTS.md block in this repo
ble-doctor install --print-only     # show what would be written

CLI reference

ble-doctor scan [--target ADDR_OR_NAME] [--json]

Live checks: rig state, GATT enumeration, write surface, DFU, bluesploit inventory. --target defaults to $BLE_DOCTOR_TARGET.

ble-doctor analyze <capture_file> [--json]

Offline checks on a btmon/btsnoop/Ubertooth capture: advertising-address rotation and SMP pairing.

ble-doctor probe-writes <address> [flags]

Flag Meaning
--char UUID characteristic to probe; repeatable
--service UUID service whose characteristics may be probed; repeatable
--allow-all probe every writable characteristic (firmware-update ones still refused)
--allow-dangerous also probe firmware-update/erase characteristics. Real side effects.
--payload HEX probe payload in hex (default is a harmless one)
--json machine-readable output

ble-doctor install [--agent claude|cursor|codex] [--print-only] [--force] [--dir PATH]

Writes .claude/skills/ble-doctor/SKILL.md, .cursor/rules/ble-doctor.mdc and a managed block in AGENTS.md. --agent is repeatable (default: all). Existing differing skill/rule files are kept unless --force.

Exit codes: 0 ok, 1 the operation failed (missing tshark/bleak, unreadable capture), 2 bad arguments.


JSON output

analyze --json:

{
  "pairing_skipped": "no SMP pairing exchange in capture",
  "score": 100,
  "findings": [
    {"id": "mac_rotation", "severity": "info", "passed": true, "detail": "..."}
  ]
}

scan --json has the same score and findings, plus rig_state, gatt_checks_skipped (null or a reason), gatt_enumeration (depth, service_count, writable_char_count), bluesploit_skipped, bluesploit_recon, and, when enumeration succeeded, the raw gatt_info.

probe-writes --json: {"results": [...], "writable_chars": [...], "findings": {"score", "findings": [...]}}.

A non-null skip field means those checks did not run. It is never a pass.

The score

score = 100 x product(1 - weight/100) over failed checks, rounded half-up. Weights: high 30, medium 15, low 5, info 0. Monotonic (more or worse findings never raise it), with diminishing returns, and one HIGH leaves 70 rather than zero.

Check catalog

ID Severity What it measures
pairing_method high / medium / low / info Pairing association model inferred from the SMP exchange (Core Spec Vol 3 Part H Table 2.8). Just Works is weakest; undeterminable or reserved IO capability is medium.
unauthenticated_writes high / info Characteristics writable without bonding. scan uses a heuristic; probe-writes measures it.
dfu_entry_point high / info HIGH only when the DFU control point is writable without bonding or encryption. A DFU service that is bond-protected, or merely present, is INFO.
dfu_signing_enforced high / medium / info Reads a recorded signing probe. With no probe it is INCONCLUSIVE at INFO (0 points): a queued probe, not a pass.
dfu_surface_absent info States whether "no DFU surface" is backed by a complete enumeration or is unknown.
mac_rotation medium / info Whether the advertising address rotates, duration-aware; too short a capture says so instead of judging.
bluesploit_recon info Read-only bluesploit module inventory as a manual-reproduction pointer. Never moves the score.

Where a row lists several severities, the check picks one from the evidence; see detail.

Known limits

  • Validated only against an nRF Connect test double and synthetic fixtures, not a genuine third-party target. Treat results as unproven on real hardware until #40 lands.
  • requires_bonding from scan is a heuristic (vendor characteristics assumed bond-protected); only probe-writes measures it.
  • DFU signing is not probed live yet (#41): it stays INCONCLUSIVE.
  • scan cannot observe the pairing method (it is an SMP fact); use analyze on a capture.
  • bleak + BlueZ can wedge discovery with org.bluez.Error.InProgress; the tool retries and then tells you to sudo systemctl restart bluetooth.
  • Radio-layer only. Companion-app and cloud-side security are out of scope.
  • No SARIF, MCP server, or CI action yet: see the open issues.

Testing

pip install -e '.[dev]' && python -m pytest test/ -q

Capture tests need tshark and skip without it; CI installs it and fails if they skip. See AGENTS.md for the contributor contract.

License

MIT

Metadata

Release files for ble-doctor 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ble-doctor 0.1.0
File Size Uploaded
ble_doctor-0.1.0.tar.gz 63.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ble-doctor 0.1.0
File Interpreter ABI Platform
ble_doctor-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 132.8 kB

Release files / ble_doctor-0.1.0.tar.gz

Download URL ble_doctor-0.1.0.tar.gz
Size 63.5 kB
Tags Source
SHA-256 checksum
How to use checksums
ca0f7336f09fbc08568018dac5773c23e2f7c58059e582315c30d5cf5e723128
BLAKE2b-256 checksum
How to use checksums
9e8e2efd17492712b8be9493ec4f95213865ff86a9e528905442e986ed220d5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / ble_doctor-0.1.0-py3-none-any.whl

Download URL ble_doctor-0.1.0-py3-none-any.whl
Size 69.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ff3b7c10157c9e698b520bb3cc4604b7abf6807f9f0688e829a11fafca2bf7a2
BLAKE2b-256 checksum
How to use checksums
80e052fd64646f34e31c0c1bdce63612802f143ca3dbd7c415277a69a883eca5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page