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 rules list [--json] / ble-doctor rules explain <id> / ble-doctor why <id> [--report FILE]

Browse the per-check rule catalog: list shows every check id, explain <id> prints that check's what/why/fix/verify doc, and why <id> [--report FILE] prints that doc and, with --report, the finding(s) for that id from a saved scan --json / analyze --json report.

ble-doctor mcp

Serves analyze and scan to an MCP client over stdio (pip install 'ble-doctor[mcp]' first; without the extra it prints how to install it and exits 2). Each tool returns the same JSON as the matching --json command. probe-writes is not exposed: it writes to a device, so an agent cannot trigger it.

ble-doctor fix <rule-id> --report FILE [--agent claude|codex|cursor] [--skip-approvals]

Prints a fix prompt for one failed finding from a saved scan --json / analyze --json report: the rule text, the finding as untrusted data inside a fence (control and bidi characters stripped, backticks replaced so nothing can close the fence), the task, and how to verify. With --agent it launches that coding agent with the prompt. The agent keeps its own approval prompts unless you pass --skip-approvals (or set BLE_DOCTOR_HANDOFF_SKIP_APPROVALS=1), because the prompt contains text from a device or capture. The per-agent flags behind --skip-approvals are copied from the sibling tool pcap-doctor and have not been verified against every version of each CLI; check yours. Inside a coding agent (CLAUDECODE, CODEX_THREAD_ID, CODEX_SANDBOX, CURSOR_SANDBOX, or BLE_DOCTOR_AGENT=1) nothing is launched.

Gating a build: --baseline and --fail-on

scan and analyze accept --baseline FILE (a report saved with --json) and --fail-on info|low|medium|high. With --baseline, only failed findings that are new since that report count; a finding keeps its identity across runs (the same fingerprint as the SARIF output: the check, its severity and its text, with only the frame counts and durations masked, so the same weakness on a longer capture is not new while a different UUID or address is). The run exits 3 when a counted finding is at or above --fail-on; exit 1 stays an error (for example an unreadable baseline, which is never treated as a pass). The gate sees only the checks that ran: a scan whose GATT enumeration did not run has no GATT findings to gate on, and says so on stderr. --json output gains a baseline object (new_findings, known_failed) only when --baseline is given, so the default shape is unchanged.

GitHub Action

- uses: actions/checkout@v4
  with: {fetch-depth: 0}
- uses: Vaibhav91one/ble-doctor@v0.2.0
  with:
    captures: "captures/**/*.pcap"
    fail-on: high

On a pull request the action analyzes each matching capture, compares it with the same file at the base commit, reports and gates only the findings that are new (the exit-3 gate of --baseline/--fail-on), posts one summary comment, and uploads SARIF to code scanning. fail-on: none reports without gating. Needs pull-requests: write for the comment and security-events: write for the upload. The v0.2.0 ref in the example is the release that ships this action: it exists once that release is cut, so pin a branch or a commit until then. Notes: the SARIF upload contains every failed finding of each capture, not only the new ones (code scanning de-duplicates by fingerprint); a capture that is new in the pull request, or renamed, has no base copy, so all its failed findings count as new; on a push event there is no base commit, so every failed finding gates; a captures pattern that matches nothing passes with a warning. The gate logic is tested locally; the comment and SARIF-upload steps are exercised on a hosted runner by the action-selftest workflow.

ble-doctor ci install

ble-doctor ci install writes .github/workflows/ble-doctor.yml, a pull-request workflow that runs the GitHub Action above on the captures in your repository. Options: --captures (space-separated globs, default **/*.pcap **/*.pcapng **/*.btsnoop), --fail-on none|info|low|medium|high (default high), --ref (the action ref to pin; default this version's tag v<version>, which exists once that release is cut), --print-only (print, write nothing), --force (overwrite a workflow that differs; without it a differing file is kept and the command exits 1) and --dir (repository root). The globs and the ref are checked before they are written into the YAML, and a symlinked .github or workflow file is refused.

SARIF output

ble-doctor scan --sarif PATH and ble-doctor analyze <capture_file> --sarif PATH also write the failed findings as a SARIF 2.1.0 file for code-scanning tools, with a stable partialFingerprints entry per finding. stdout is unchanged (--json still prints exactly one report). A live scan has no file location, so its results carry a logical BLE device location; GitHub code-scanning upload has not been verified yet.

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.2.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.2.0
File Size Uploaded
ble_doctor-0.2.0.tar.gz 79.2 kB Details

Built distribution (wheel)

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

Total release size: 168.6 kB

Release files / ble_doctor-0.2.0.tar.gz

Download URL ble_doctor-0.2.0.tar.gz
Size 79.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f49a06fa7beb880fcc549c098a6ca675d3296693204fd5d66ddcc6f19f3fdb6d
BLAKE2b-256 checksum
How to use checksums
34bb0f73583d8b569e213895789bb779bdfa21d2bec53409c9a996a15d8ce443
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.2.0-py3-none-any.whl

Download URL ble_doctor-0.2.0-py3-none-any.whl
Size 89.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
19305efb28422c39207a8681d436928e180c9e33dfaab18a8c4a9561c16c1d7c
BLAKE2b-256 checksum
How to use checksums
b3dcf3229fcf5f699abde423a3d418fb47d7a873b9445405436a7bbc6d548136
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

This release

0.2.0 This release

2 release files

0.1.0

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