This release is a pre-release and may not be stable for production use.
Probolos
Beta — feature-complete, still being tested. This is a research-stage project. The admission path has been exercised on real hardware and has automated regression coverage, but it has never been run against an actual attack: no BadUSB fixture (ATmega32u4, Raspberry Pi Zero, O.MG cable) has been put through the behavioural quarantine, so the claim that matters most is the one with the least evidence behind it. The feature set is frozen at 0.11.0 on the way to 1.0: until then only fixes, tests and documentation go in (
ROADMAP.md). Known open weaknesses are listed inSECURITY.md, "Known weaknesses". Do not rely on it as a security control on a machine you care about. Treat everything here as experimental and report anything that surprises you.
A deny-by-default USB admission gate for Linux.
Probolos (πρόβολος) — Greek for a jutting barrier: the thing set in front that must be got past first.
New USB devices are held for a decision. Identity checks run while blocked; optional behavioural and storage inspection temporarily activate the device. Remembered devices and configured safety exemptions may be admitted automatically.
identity · consistency · behaviour
The idea
Every USB defence has the same problem: the kernel binds a driver the moment a device is enumerated. By the time anything notices a keyboard is a BadUSB, it has already typed.
Probolos sets authorized_default=0 on every root hub, so a new device arrives
inert. It is then examined across four stages, and only a human decision
authorizes it.
| Stage | What it asks | Device state |
|---|---|---|
| 1 · Identity | What does it claim to be? | blocked |
| 2 · Consistency | Do its own claims agree with each other? | blocked |
| 3 · Behaviour | What does it do when switched on and gagged? | live, input grabbed |
| 4 · Contents | What is on the medium? | live, read-only, never mounted |
Stage 3 grabs evdev input after drivers bind, observes it, then re-blocks the
device before releasing the grabs. Keystrokes may escape before a grab
succeeds, including with --close-race-window: that legacy flag enables
experimental deferred binding, not race-free isolation. Newly discovered input
nodes are checked throughout observation; this still requires userspace to react.
Stage 4 also temporarily activates the device. Probolos itself never mounts the medium, but another service may do so. It is skipped unless every parsed configuration and alternate setting declares mass storage only (no HID, network, serial or vendor function), or if descriptors are incomplete.
The research subject is an admission gate combining descriptor, behavioural, and storage metadata evidence. Comparative claims about other tools require independent measurements of their actual configurations; the previous table asserting that other tools invariably decide after driver binding was removed.
Quick start
git clone https://github.com/capitan0n/probolos
cd probolos
sudo pacman -S python-pyudev # or your distro's package (python3-pyudev)
sudo python3 -m probolos --observe 3
Plug in a device. You will get a report and a prompt.
Keep a second way in while testing — SSH, or your built-in keyboard. A laptop keyboard on the PS/2 (i8042) controller is not USB and is never affected. Devices on internal (
removable=fixed) USB ports are not gated unless you pass--gate-fixed-ports; check before you rely on either.
Stop with Ctrl-C; the gate reopens on every exit path.
Recommended: privilege separation
sudo python3 -m probolos --privsep --agent --agent-user "$USER"
python3 -m probolos.agent # in your graphical session
--privsep runs the analyzer as nobody and routes privileged operations
through a separate gate. The trusted code also includes startup preparation,
protocol handling and cleanup; it is not a 150-line security boundary.
nobody is shared: any other process running as nobody can kill the
analyzer, and the gate then reopens (SECURITY.md, "Known weaknesses").
--privsep-user takes a dedicated account instead: the service runs the
analyzer as probolos, a system account install.sh creates, and a run by
hand can pass --privsep-user probolos once it exists.
--agent moves the prompt into a desktop dialog: one dialog for a plain
storage device, a second confirmation for anything that can type or carry
traffic or showed a warning, and a 10-second countdown before "Allow anyway"
for a critical finding (a refused device returning, a changed identity).
Run it in the background
sudo ./install.sh # install and start; re-run after pulling to update
sudo ./install.sh --uninstall # stop and remove (history is kept)
This installs the code to /opt/probolos (root-owned), a probolos command
(sudo probolos --history), the gate as a system service with --privsep, and
the desktop prompt as a user service for the account that ran sudo.
Stop automount racing the scan
Stage 4 must briefly authorize the device for its block node to appear, and udisks2 may automount the medium inside that window. Probolos itself only ever reads raw sectors and never mounts anything, but a desktop session will.
The simplest answer is --no-storage-scan, which skips stage 4 entirely and
removes the window. If you want stage 4, tell udisks not to automount USB block
devices:
sudo tee /etc/udev/rules.d/60-probolos-inhibit-automount.rules <<'EOF'
# Probolos stage 4 authorizes a storage device just long enough to read its
# partition table. udisks2 will automount it in that window unless told not to.
SUBSYSTEM=="block", ENV{ID_BUS}=="usb", ENV{UDISKS_AUTO}="0"
EOF
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=block
Know what this costs. The rule is system-wide and permanent: every USB block device stops automounting, including ones Probolos has approved and ones plugged in while it is not running. You mount them by hand afterwards. Remove the file and reload to undo it.
It also only constrains udisks. Any other automounter on the machine is unaffected, so verify the behaviour on your own system rather than assuming the window is closed.
Card readers: the card is not gated
A card reader is the USB device; the card is a medium inside it. Inserting, removing or swapping a card causes no USB re-enumeration, so once a reader is admitted every later card enters without passing the gate. Stage 4 only sees a card that was already in the reader when it was plugged in.
--watch-media adds a separate detection layer for that: it listens for the
block-layer change on admitted storage hosts (and ones present at startup),
reads each new medium the same way stage 4 does — raw, read-only, never
mounted — and reports its layout, EFI system or hidden partitions, and drift
from the first medium seen in that slot. It does not gate the card: there
is no per-medium authorized switch. The only enforcement is
--media-policy deauthorize, which switches the whole reader off on a
CRITICAL finding.
Two things decide how much a report is worth, and each one says which case applied:
- Automount. udisks2 mounts on the same event. Without the udev rule above, the card may already be mounted when it is read, and the report is post-hoc alerting, not prevention.
- Latency. Media detection rides on the kernel's disk-event polling, typically 1–2 s. Some readers do not report media changes at all; such a slot is flagged when first seen.
Under --privsep this widens the gate: the analyzer may open the whole disks
of watched readers read-only and switch those readers off. See SECURITY.md.
Common options
| Flag | Effect |
|---|---|
--observe SEC |
length of the behavioural quarantine (0 disables stage 3) |
--privsep |
run the analyzer as nobody behind a separate root gate |
--privsep-user USER |
the analyzer's account instead of nobody (the service: probolos) |
--agent |
ask via a desktop dialog instead of the terminal |
--dry-run |
report everything, change nothing |
--list |
read-only inventory of attached devices; never closes the gate |
--trusted / --remove-trusted N|all |
list and revoke remembered devices (history is kept) |
--history [-v] |
every device seen, its decisions and descriptor drift |
--remove-all |
clear remembered devices and history; asks first, refuses while the daemon runs |
--no-storage-scan |
skip stage 4 entirely |
--watch-media |
inspect and alert on cards inserted into admitted readers (detection only) |
--media-policy log|deauthorize |
with --watch-media: on a CRITICAL media finding, log (default) or drop the whole reader |
--allow-port PORT |
keep a rescue port always open |
--capture-payload |
reconstruct what a quarantined device typed (opt-in) |
--release |
reopen the gate after a crash |
If something goes wrong
The gate restores on exit, on signals, and via atexit. If a device is still
blocked:
sudo python3 -m probolos --release # from the checkout
sudo probolos --release # after install.sh
--release refuses while a gate is still running. If Probolos itself is
wedged, the panic file forces the gate open from another TTY or over SSH:
sudo touch /run/probolos.panic
It must be root-owned — a panic file anyone could create would be a way for any local account to switch the tool off. Last resort, one line:
for hub in /sys/bus/usb/devices/usb*/authorized_default; do echo 1 | sudo tee "$hub"; done
That only affects devices attached from then on: replug anything still
blocked, or authorize it with echo 1 | sudo tee /sys/bus/usb/devices/<name>/authorized.
Running as a service
sudo ./install.sh does all of it. See systemd/README.md
for what it sets up by hand: the two units (a system
service for the gate, a user service for the agent) and what the sandboxing
does.
Requirements
- Linux with sysfs USB authorization (
/sys/bus/usb/devices/*/authorized) - Python 3.10+
pyudevfor the event loop- Optional:
PyYAML, only for--rules FILE - Optional:
kdialog,zenity, ortkinterfor the desktop agent
No python-evdev: the quarantine talks to the kernel directly through one
EVIOCGRAB ioctl.
Development
python3 -m unittest discover -b -s tests -t . # the whole suite
python3 -m tests.run_all # same, with AF_UNIX skips
Both collect the same tests. run_all exists only to skip the handful that
need a listening AF_UNIX socket, which some restricted containers refuse; use
it there and the plain command everywhere else. If the two ever report
different totals, that difference is a bug — it was one before, when the
defensive-parsing checks were bare module-level functions that unittest
discovery does not collect and only run_all picked up.
ruff check . # lint (config in pyproject.toml)
python3 -m mypy # types, root side (config in pyproject.toml)
CI runs these tools at exact versions, pinned in .github/requirements-*.txt
(pip install -r .github/requirements-ci.txt -r .github/requirements-typecheck.txt
gets the same ones locally), and
holds coverage of the root-side code to a floor (.github/workflows/coverage.yml).
The suite needs only the standard library: no root, no USB hardware and no
pyudev (a few tests skip themselves when an optional piece such as PyYAML
is missing).
tests/ holds one module per area of the code, and each file's docstring
names the modules it covers: a bug in probolos/trust.py gets its regression
test in tests/test_trust.py, probolos/storage.py in test_storage.py, and
so on. The regression tests from the security reviews live there too, as
classes named after the defect they pin (AnalyzerHasNoControllingTerminal,
TheDecoyFromTheReport) rather than after the review that found it.
Builders and fixtures used by more than one file are in tests/_support.py.
| File | Covers |
|---|---|
test_agent.py |
agent, agentlink, dialogs |
test_boundary.py |
the refusal and error paths of privsep, gate_client, gate_server |
test_cli.py |
__main__ |
test_daemon.py |
daemon, session |
test_descriptors.py |
descriptors, descriptors_safe, usbclass |
test_gate.py |
gate, safety |
test_interrogate.py |
interrogate, the study's offline half (probe order, --gentle, CSV rows) |
test_ledger.py |
ledger, history |
test_privsep.py |
privsep, gate_server, gate_client, protocol |
test_properties.py |
property-based (Hypothesis), across modules: descriptor, medium and protocol parsing, textsafe, the state files, the agent socket |
test_quarantine.py |
quarantine, payload, deferred_bind |
test_rules.py |
rules, analyzers, report |
test_service.py |
the shipped service: systemd/probolos.service, systemd/probolos.sysusers, install.sh |
test_storage.py |
storage, storage_hardening, mediawatch |
test_sysfs.py |
sysfs |
test_testbed.py |
testbed.spawn, testbed.emulate: every preset builds and raises what testbed/README.md promises (for the drift pair: same identity, different fingerprint) |
test_textsafe.py |
textsafe |
test_trust.py |
trust, atomicio |
testbed/ emulates USB devices in software via dummy_hcd + raw_gadget,
with presets for BadUSB, descriptor drift and overpowered devices — so the
CRITICAL paths can be exercised with no hardware.
Scope
CAPABILITIES.md is the authoritative list of what Probolos
does, what it does not do, and what may be built later. Read it first — it
distinguishes between code that runs and code that merely exists in the tree.
SECURITY.md covers the threat model and is explicit about what
Probolos does not stop: USB stack vulnerabilities, a patient attacker,
descriptor forgery, Thunderbolt/DMA, and wireless gateways.
Short version:
- Does — deny-by-default admission, descriptor consistency rules,
pre-authorization quarantine with
EVIOCGRABand timing analysis, unmounted storage-metadata inspection, cross-session ledger and trust store, privilege separation with kernel-derived scope, lockout safety. - Does not — anything before the kernel finishes enumerating, anything after
you approve the device, Thunderbolt/DMA, USB-PD, wireless, or file contents.
A card inserted into an admitted reader is not gated;
--watch-mediaonly inspects and alerts on it. - Off by default —
--close-race-windowexperiments with deferred binding using a bus-wide switch. It does not remove the input race. SeeSECURITY.md. - No early activation — use
--observe 0 --no-storage-scanwhen holding unknown devices blocked is more important than behavioural/storage evidence. - Not yet — no HID report descriptor analysis: the parser for it is written
and hardened, but the report descriptor is not in the sysfs blob and has no
source wired to it.
CAPABILITIES.md§2.2 and §3.2.
Status: beta, 1.0.0b1, feature-frozen since 0.11.0 on the way to 1.0
(ROADMAP.md). The tree has been through several security
review passes, the latest on 2026-10-04 (docs/QA-LOG.md);
each fixed finding has a regression test named after the defect, under
tests/, and the ones still open are listed in SECURITY.md.
Verified on real hardware. Closing and restoring authorized_default on
all five root hubs of the reference laptop. A Kingston DataTraveler 3.0 through
stage 1 and stage 4 — identity, MBR parse, filesystem signature — with the
device re-blocked before the prompt. Descriptor drift across visits, including
the false positive that the same stick produces when moved between a USB 2 and
a USB 3 controller, which is why the ledger fingerprint ignores bus-negotiated
fields. Both prompt paths, both answers, and gate restoration on SIGINT.
Not verified on real hardware. Stage 3: no BadUSB or HID fixture has been
run against the quarantine, so the EVIOCGRAB path and the exposure-window
measurement rest on emulation only. Nor has --privsep, --close-race-window,
or either systemd unit. A passing test does not prove USB isolation on a real
kernel, and these are the claims most worth distrusting until somebody plugs an
ATmega32u4 in and watches what happens.
Treat every real-world result as data rather than a guarantee, and report
anything that surprises you. What is not done is listed in
CAPABILITIES.md §2.2 and §3, the plan to 1.0 is
ROADMAP.md, and defects found since the freeze are logged in
docs/QA-LOG.md.
License
GPL-3.0-or-later: Probolos is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at your
option) any later version. See LICENSE.
Note the warranty disclaimer in particular: this is beta, security-relevant software provided as-is. You are responsible for what you run it on.
Metadata
Release files for probolos 1.0.0b1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| probolos-1.0.0b1.tar.gz | 514.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| probolos-1.0.0b1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 778.6 kB
Release files / probolos-1.0.0b1.tar.gz
| Download URL | probolos-1.0.0b1.tar.gz |
|---|---|
| Size | 514.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6cdc8c1f6e84a41ad90cce99039c6232481865708c8db613b74b80c367fad6af
|
|
BLAKE2b-256 checksum How to use checksums |
6071f9bcc6ddf06ace99537ebe9d2ec91ee2616320c8c86b89e5df2ea488db59
|
| 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 9, 2026.
Transparency logRelease files / probolos-1.0.0b1-py3-none-any.whl
| Download URL | probolos-1.0.0b1-py3-none-any.whl |
|---|---|
| Size | 264.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
70de47aa176809992debd177730d9831fa50afe4603e0394761d45f13afd4769
|
|
BLAKE2b-256 checksum How to use checksums |
bd6434d46563c2d6e958ff3365571aaab9c4872c48a552556a396a6d99720427
|
| 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 9, 2026.
Transparency log