comalice/usbatlas: Simple CLI showing who/what is using USB devices on a machine. - comalice/usbatlas

github.com · 2026-07-31 · by Long Nguyen ·github
Simple CLI showing who/what is using USB devices on a machine. - comalice/usbatlas

comalice/usbatlas

Simple CLI showing who/what is using USB devices on a machine. - comalice/usbatlas

1 stars · 0 forks · 0 watchers

README

USBAtlas

USBAtlas is a greenfield Go CLI for inspecting USB device topology and provenance on Linux. Current builds read USB topology from sysfs, enrich device names from usb.ids when available, optionally enrich USB/IP provenance from usbip port, and render the normalized graph as a tree, table, detail view, or JSON. Status USBAtlas is early development software. It currently supports local sysfs discovery, usb.ids name enrichment, and opt-in USB/IP provenance enrichment. Not implemented yet:

live monitoring TUI or GUI Graphviz or Mermaid output VM or container detection USB authorization or device control

Requirements

Linux Go 1.22 or newer readable /sys/bus/usb/devices readable /proc for opener discovery (partial results without root)

usb.ids is optional. If it is missing, USBAtlas still runs and includes numeric vendor/product IDs. USB/IP enrichment is optional and opt-in with --usbip. If enabled, USBAtlas runs usbip port; missing or unsupported usbip output is non-fatal and is reported in JSON warnings. Usage Run from the repository checkout: go run ./cmd/usbatlas Default tree view: go run ./cmd/usbatlas List devices with origin and driver summaries: go run ./cmd/usbatlas list List with process attribution for devices that have an open file descriptor on a /dev node (requires --blame, list-only): go run ./cmd/usbatlas list --blame Show details for a node ID: go run ./cmd/usbatlas show 1-2.3 Emit the normalized graph as JSON: go run ./cmd/usbatlas json Use a fixture or alternate sysfs root: go run ./cmd/usbatlas --sysfs ./internal/app/testdata/sysfs/basic json Use a specific usb.ids file: go run ./cmd/usbatlas --usbids /usr/share/hwdata/usb.ids list Override the /proc root for opener discovery (useful with fixtures): go run ./cmd/usbatlas --proc ./internal/app/testdata/proc/empty list Attempt USB/IP provenance enrichment: go run ./cmd/usbatlas --usbip json Flags may be combined: go run ./cmd/usbatlas --sysfs ./internal/app/testdata/sysfs/basic --usbids ./internal/app/testdata/usb.ids --usbip json Show command help: go run ./cmd/usbatlas --help Help output does not require sysfs discovery. Invalid commands, unknown flags, and missing required flag values print a specific error followed by usage text and exit non-zero. Print non-fatal warnings to stderr for human commands (tree, list, show): go run ./cmd/usbatlas --verbose --verbose has no effect on json, which already includes warnings in its output. Default human output stays quiet. Build & Install Build a local binary with the standard Go toolchain: go build -o usbatlas ./cmd/usbatlas or with the provided Makefile target: make build This produces a usbatlas binary in the repository root. Run it from there: ./usbatlas ./usbatlas list ./usbatlas --help Install to your GOBIN (or $GOPATH/bin) with: go install ./cmd/usbatlas USBAtlas is Linux-only and reads /sys/bus/usb/devices at runtime, so a built binary must be run on a Linux host. Tagged releases are cross-compiled for linux/amd64, linux/arm64, and linux/arm. Output Model USBAtlas builds a normalized graph with:

usb_device nodes for root hubs, hubs, and leaf devices usb_interface nodes for USB interfaces connected_to edges for topology has_interface edges for device-to-interface relationships

Human renderers hide interface nodes in the default tree. JSON output includes the full graph. The default tree annotates USB/IP-origin devices inline: [usbip] when the remote host is unknown, [usbip -> host] for an authoritative remote host, and [usbip -> host (inferred)] when the remote host is inferred from TCP state. Use show <id> for the full remote metadata block. For USB-to-serial adapters (FTDI, CP210x, CH340, PL2303, CDC-ACM, etc.), USBAtlas maps each interface to its host /dev/ttyUSB* or /dev/ttyACM* node via /sys/class/tty. More generally, USBAtlas enumerates host /dev character nodes per USB interface by walking /sys/class/tty, /sys/class/hidraw, and /sys/class/input, requiring a dev major/minor attribute, and finding the USB interface in each entry's resolved device ancestry.

Tree: full /dev/... paths appear as child leaves beneath the owning device, ordered as TTY, hidraw, then input. Raw /dev/bus/usb/... nodes are omitted to keep the topology readable. list: conditional TTY, Hidraw, and Input columns shown only when at least one device in the graph has that kind of node. show <id>: TTY, Hidraw, and Input lines when present, followed by a Raw Device line giving the device's raw /dev/bus/usb/<bus>/<dev> path (zero-padded to 3 digits each, derived from bus/device numbers). json: nodes carry dev_nodes on each interface and raw_dev_node on each USB device.

USB device serial numbers (from sysfs) appear in the default views when present: the tree appends [serial: …] to a device line, and the list table includes a Serial column (empty when a device exposes none). show <id> and json already include the serial. USBAtlas walks /proc//fd/ to detect which processes hold open file descriptors on a device's /dev nodes (raw /dev/bus/usb/..., TTY, hidraw, or input). When at least one opener is found:

Tree: the device line gets a yellow [in use] mark before any USB/IP annotation. show <id>: an In Use: yes line appears after Raw Device. list --blame: adds an Openers column showing pid:comm,... for matched devices, - when a device has a /dev node but no openers, or n/a when the device has no /dev node to match.

Devices without a /dev node (e.g. hubs without a usable raw device path) cannot be matched and show n/a under --blame. Unreadable /proc/<pid>/fd directories are skipped; with --verbose a count-based summary warning is printed to stderr. Run as root for full coverage. Color Tree, list, and detail output use ANSI color when writing to a terminal, to make hubs, USB/IP-origin devices, and serial/device-node values easier to pick out:

Hubs are bold. USB/IP-origin devices and their [usbip …] annotations are cyan when the remote metadata is authoritative, and dim cyan when inferred. Serial, TTY, hidraw, input, and raw-device values are magenta. In-use marks and opener values are yellow. Warnings printed with --verbose use a red warning: prefix.

Color is automatically disabled when output is piped or redirected (not a TTY), when the NO_COLOR environment variable is set (https://no-color.org), or when --no-color is passed. JSON output is never colored. Override color detection with: go run ./cmd/usbatlas --no-color list NO_COLOR=1 go run ./cmd/usbatlas list USB/IP Permissions Authoritative USB/IP remote metadata comes from usbip port, which reads per-port records under /run/vhci_hcd (or /var/run/vhci_hcd). On common hosts this directory is 0700 root:root, so unprivileged users cannot read it and usbip port emits libusbip: error: fopen / libusbip: error: read_record. When that happens USBAtlas falls back to inferring the remote host from /proc/net/tcp (ESTABLISHED connections to port 3240) and marks it (inferred). Authoritative vs inferred:

Authoritative — read from usbip port records. Includes host, port, and remote bus ID. Inferred — host only, derived from TCP state. Single matching connection is required; zero or multiple candidates leave the host unknown. Inferred data is marked in tree, detail, and JSON (confidence: "inferred").

Practical options for authoritative metadata:

sudo — the simplest check: sudo usbatlas --usbip json This runs the whole tool privileged; only needed when --usbip is requested.

Group / tmpfiles / service overrides — admins can grant a non-root group read access to /run/vhci_hcd via a systemd-tmpfiles snippet or a vhci_hcd service unit override, similar to how cdrom or dialout groups expose device nodes. USBAtlas does not perform or require any privilege escalation itself. Do not make the directory world-readable or blanket-chmod the runtime state. A copy-pasteable group-based recipe:

1. Create a dedicated group for unprivileged USB/IP readers.

sudo groupadd -r usbip

2. Add the user that will run USBAtlas.

sudo usermod -aG usbip "$USER"

Re-login or `newgrp usbip` for the new membership to take effect.

Then grant the group read+execute on the runtime directory using systemd-tmpfiles so the rule persists across reboots and module reloads:

/etc/tmpfiles.d/vhci_hcd.conf

Type Path Mode User Group Age Argument

d /run/vhci_hcd 0750 root usbip - - Apply immediately without rebooting: sudo systemd-tmpfiles --create /etc/tmpfiles.d/vhci_hcd.conf On distributions where vhci_hcd is loaded by a service unit rather than tmpfiles, apply the same group to that unit instead: sudo systemctl edit vhci-hcd.service [Service] Group=usbip

Keep the runtime directory group-readable after the module creates it.

ExecStartPost=/bin/chmod 0750 /run/vhci_hcd ExecStartPost=/bin/chgrp usbip /run/vhci_hcd After this, an unprivileged member of usbip can run: usbatlas --usbip json and receive authoritative remote metadata instead of the inferred TCP fallback.

Caveats:

/run/vhci_hcd is runtime state and may not persist across reboots or kernel module reloads; any permission setup must be applied by a persistent mechanism (tmpfiles, service override), not a one-off chmod. Group/tmpfiles rules only take effect after the vhci_hcd module creates /run/vhci_hcd; on hosts where the module is loaded on demand, trigger systemd-tmpfiles --create after the first USB/IP attach or reload vhci_hcd. Granting group read access exposes remote-attach metadata (host, port, bus ID) to non-root users; only add trusted users to the usbip group. Inferred host data only covers the remote host, not port or bus ID, and only when a connection to port 3240 is currently ESTABLISHED.

Development Run the full local verification suite with: make test This runs go test ./... and fixture-backed CLI smoke checks for tree, list, detail, and JSON output. The smoke checks use checked-in synthetic fixtures and do not depend on host USB hardware. Run only Go tests with: go test ./... CI checks The same checks that run in CI can be run locally: ./scripts/ci.sh This runs gofmt, go vet, golangci-lint, and make test. golangci-lint is auto-installed via go install if missing. The CI workflow (.github/workflows/ci.yml) does nothing beyond setting up Go and calling this script, so local and CI results match. Tagged releases are built by: ./scripts/release.sh This cross-compiles linux/amd64, linux/arm64, and linux/arm binaries, packages them as tarballs, and generates a SHA256 checksums file. Pushing a v* tag triggers the release workflow, which builds artifacts and creates a GitHub release. Relevant project docs live under docs/:

docs/mvp.md docs/architecture.md docs/data-model.md docs/roadmap.md

Work is tracked with Markdown tickets:

docs/backlog/ for proposed or future work docs/todo/ for scoped work ready to pick up docs/completed/ for completed work records

source

github.com · 1 images · 99 links

Comments (0)

No comments yet