Metadata-Version: 2.5
Name: ble-doctor
Version: 0.2.0
Summary: Score the security posture of a BLE device from a capture or live GATT enumeration. Defensive / CTF / own-gear use.
Project-URL: Homepage, https://github.com/Vaibhav91one/ble-doctor
Project-URL: Repository, https://github.com/Vaibhav91one/ble-doctor
Project-URL: Issues, https://github.com/Vaibhav91one/ble-doctor/issues
Project-URL: Changelog, https://github.com/Vaibhav91one/ble-doctor/blob/main/CHANGELOG.md
Author-email: Vaibhav91one <108279746+Vaibhav91one@users.noreply.github.com>
License: MIT
License-File: LICENSE
Keywords: audit,ble,bluetooth,ctf,gatt,pcap,pentest,security
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Requires-Dist: bleak>=0.22
Provides-Extra: dev
Requires-Dist: jsonschema>=4; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp<2,>=1.2; extra == 'mcp'
Description-Content-Type: text/markdown

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Vaibhav91one/ble-doctor/main/docs/assets/logo-dark.svg">
  <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Vaibhav91one/ble-doctor/main/docs/assets/logo-light.svg">
  <img alt="ble-doctor" src="https://raw.githubusercontent.com/Vaibhav91one/ble-doctor/main/docs/assets/logo-light.svg" width="360" height="56">
</picture>

[![CI](https://github.com/Vaibhav91one/ble-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/Vaibhav91one/ble-doctor/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/ble-doctor?style=flat&color=000000&labelColor=000000&logo=pypi&logoColor=white)](https://pypi.org/project/ble-doctor/)
[![npm](https://img.shields.io/npm/v/ble-doctor?style=flat&color=000000&labelColor=000000&logo=npm&logoColor=white)](https://www.npmjs.com/package/ble-doctor)
[![Python](https://img.shields.io/badge/python-3.10%2B-000000?style=flat&logo=python&logoColor=white)](https://github.com/Vaibhav91one/ble-doctor/blob/main/pyproject.toml)
[![License: MIT](https://img.shields.io/badge/license-MIT-000000?style=flat)](https://github.com/Vaibhav91one/ble-doctor/blob/main/LICENSE)
[![Telemetry: none](https://img.shields.io/badge/telemetry-none-000000?style=flat)](#known-limits)

> **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.**

```bash
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

```bash
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

```bash
ble-doctor analyze capture.pcap
```

```text
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

```bash
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)

```bash
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

```bash
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

```yaml
- 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`:

```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](https://github.com/Vaibhav91one/ble-doctor/issues/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](https://github.com/Vaibhav91one/ble-doctor/issues/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](https://github.com/Vaibhav91one/ble-doctor/issues).

## Testing

```bash
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](AGENTS.md) for the contributor contract.

## License

[MIT](LICENSE)
