bitscanner
bitscanner finds the devices your inventory does not contain.
bitcollector reads the host it runs on. bitscanner looks outward from that host at
the segment around it — and the answer that matters is the one nothing on your asset list
explains.
| Release | 0.1.3-ga, commit 7db3d4c, built with go1.25.12 |
| Platforms | Linux amd64/arm64 only — macOS and Windows are not published, and why |
| Supply chain | Reproducible build (built twice, refused unless byte-identical), CycloneDX SBOM and cosign attestation per binary, vulnerability gate, cosign signature per binary and over SHA256SUMS |
| Inbound network surface | No service listens. No health, metrics or admin port, and no key that opens one. Measured with scanning off: zero listening sockets. service_discovery, when armed, uses short-lived unbound UDP sockets; a datagram arriving on one becomes an inventory record only if it answers the query just sent and comes from inside allowed_cidrs — not checked in 0.1.3-ga and earlier, see below |
| Outbound | https only, to the endpoints you configure. No redirect is ever followed |
| Writes to the host | Its own data directory (agent.data_dir), which holds the enrolment key and agent token, 0600 inside a directory forced to 0700 |
| Packets on the network by default | Zero. Both arming switches ship off |
Before you run it, verify the download — the bitscanner-specific steps are below.
Two things here are large enough to have their own pages: scanning and the two-switch arming model — the probe ladder, what each probe puts on the wire, and the guard that authorises each packet — and what leaves the host, including enrolment, egress and the limits this release ships with.
The question bitcollector cannot answer
An inventory assembled from agents can only ever contain machines somebody installed an agent on. The printer, the lab switch, the contractor's laptop, the VM a team span up for a migration and never told anyone about — none of them will report themselves, and none of them are missing because of a technical failure. They are missing because nobody knew to look.
So the two binaries answer two different questions, and the boundary is the direction they face:
bitcollector | bitscanner | |
|---|---|---|
| Faces | Inward — the host it runs on | Outward — the segment that host sits on |
| Answers | "What is this machine, and what state is it in?" | "What else is on this wire, and is any of it unaccounted for?" |
| Sees a device only if | an agent is installed on it | it is visible from a host that has one |
| Sends packets | Never — it reads the host | Only if you arm it, probe by probe |
Neither replaces the other. A device bitscanner discovers is a lead: an address, a MAC, a vendor, a first-seen time. Turning that lead into an asset with an owner is work you do in Asset Management — bitscanner's job is to make sure the lead exists at all.
What it reports with everything switched off
The default configuration arms no probe, and the agent still has something to say, because the host's own kernel already knows its neighbours.
Measured, on an ordinary Linux host with active scanning fully disabled — the shipped default:
INFO scanguard active scanning is disabled; every probe will be denied
INFO network_collector network intelligence collector starting {"interval": "20s"}
INFO network_collector collection cycle complete
{"queued_for_delivery": ["network_state", "neighbor_table"]}
Both of those come from files the host already has — /proc/net/arp and the routing table —
so this state emits no packets at all.
| Record | What it carries | Needs |
|---|---|---|
network_state | The routing table: destination, gateway, interface, metric, flags | nothing |
neighbor_table | The kernel ARP/NDP cache: address, MAC, interface, state | nothing |
neighbor_discovery | The enriched neighbour inventory: address, MAC, vendor, device type, reachability, first/last seen — plus the subnets they were found on | scanning.probes.neighbor_discovery |
gateway_discovery | The first hops out of this host, with MAC attribution | scanning.probes.gateway_discovery |
service_discovery | mDNS/SSDP responders, attributed to the neighbour that answered | scanning.probes.service_discovery |
Every record is wrapped in the same envelope — schema_version, type, agent_id,
timestamp, sequence, payload, checksum — and the checksum is a SHA-256 integrity
check over schema_version, type, agent_id, timestamp and payload — not over sequence, and not a signature. bitscanner does not have
bitcollector's signed, hash-chained evidence
chain; if you need an artefact an auditor can verify offline,
that is the collector's job, not this one's.
Running it
bitscanner is a single static binary. Verify it first, then:
# What you have
bitscanner version
# BitScanner 0.1.3-ga (commit: 7db3d4c, built: 2026-08-09T09:54:40Z, go1.25.12, linux/amd64)
# Write a fully commented starting configuration
bitscanner config init --config /etc/bitscanner/config.yaml
# Check it BEFORE you start anything
bitscanner config validate --config /etc/bitscanner/config.yaml
# Configuration is valid.
# Run in the foreground
bitscanner run --config /etc/bitscanner/config.yaml
# Turn up the detail while you are setting it up
bitscanner run --config /etc/bitscanner/config.yaml --log-level debug --log-format console
config init writes the same annotated file this page is describing: every scanning key,
what each probe actually sends, and why the defaults are what they are. It is meant to be
read.
--log-level and --log-format are flags, and only flagsThere is no logging: block. There used to be — level, format, output_path,
max_size, max_backups, max_age — and not one of those keys was read by any code, so
the logger was configured entirely from the command line whatever the file said. The block
is gone, and a config that still contains it is refused by name:
Error: configuration validation failed: failed to load config: logging is set but no
longer exists: the whole `logging` block was parsed and read by nobody […] Use the
flags, which do work: `--log-level` (debug|info|warn|error) and `--log-format`
(json|console). There is no replacement for output_path or for the rotation keys — log
rotation was never implemented […]
The same treatment applies to control_plane.retry_interval, control_plane.max_retries,
security.allow_root_only, security.secure_bootstrap, security.audit_log_path and
security.encrypt_local_data. Two of those defaulted to true, so the shipped file read as
a secured bootstrap and encryption at rest while neither existed. A key that does nothing is
not shipped, and its removal is announced to the person whose file contains it rather than
silently ignored.
Configuration
bitscanner is configured by a YAML file, conventionally /etc/bitscanner/config.yaml.
You do not write it from scratch — bitscanner config init generates a fully annotated
starting configuration that documents every scanning key, what each probe actually sends,
and why each default is what it is.
bitscanner config init --config /etc/bitscanner/config.yaml # write an annotated config
bitscanner config validate --config /etc/bitscanner/config.yaml # check it before starting
bitscanner run --config /etc/bitscanner/config.yaml
Every probe ships disabled, so a freshly configured bitscanner puts zero packets on the network until you arm probes deliberately — see Running it for the two-switch arming model, and Scanning for what each probe sends.
Logging is set by flags only (--log-level, --log-format); there is no logging: block in
the file. The enrolment token lives in a separate file referenced by enrollment_token_file,
not inline in the configuration.
What it costs on the host
- No inbound service. There is no health port, no metrics port, and no configuration key
that opens one. Measured on a running agent with scanning off: the process owns zero
listening sockets.
One honest qualification: with
service_discoveryarmed, the agent opens ephemeral, unbound UDP sockets to send mDNS and SSDP queries and read the replies. They exist only for the duration of a probe — but "zero listening sockets" is true of the default configuration, not of every configuration. - Cheap when it is quiet. On a host with 118 ARP neighbours across 27 attached subnets,
a full neighbour-discovery pass took 71–189 ms per cycle across four consecutive
cycles. The default
modules.network_intelligence.intervalis1m. - A cycle is one scan cycle. The per-cycle host budget and the wall-clock scan deadline reset at the start of a collection and nowhere else, so a slow or tarpitted segment cannot let one cycle's probing run into the next.
- No store-and-forward, and no retry. A batch that cannot be delivered is logged as an
error and dropped — it is not spooled to disk and it is not retried. If the endpoint is
unreachable for ten minutes, that is ten minutes of observations you do not have. A
queued_for_deliveryline is logged every cycle, whether or not scanning is on, so a delivery failure cannot be mistaken for a collector that is not running — but "queued" is not "delivered", and the agent cannot tell you more than the destination told it.
0.1.3-ga and earlierThis page used to say the discovery sockets "accept nothing you did not solicit". That check
was never implemented. The sockets are a wildcard bind, so the kernel hands them any datagram
that reaches the port, and nothing compared the sender or the contents against the query that
had just gone out — an SSDP datagram became a device on the strength of containing the text
ST:. Anything able to reach that port could add hosts that do not exist to your inventory, at
addresses of its choosing, recorded as observations this agent made.
The check now exists in source, and it is two questions. From whom: the sender must be
inside your scanning.allowed_cidrs and pass every other rule the guard applies to a probe
target. What: the datagram must answer the query just sent — for mDNS, from port 5353 with
the response bit set and the unpredictable transaction ID this agent generated; for SSDP, an
HTTP 200 M-SEARCH reply carrying both ST and USN, never an unsolicited NOTIFY.
It is not in a released binary yet. If you are running 0.1.3-ga or earlier, treat
service_discovery results as unauthenticated leads: they are attributable to whoever sent the
packet, which is not necessarily the device named. Every other probe is unaffected — those
record what a connection this agent opened returned.
Unlike bitcollector, bitscanner has no
max_memory_mb / max_cpu_percent key. A periodic health check logs allocation and
goroutine counts and warns above 50 MB and 1,000 goroutines, and that is the whole of it.
Bound it with your init system's own controls (MemoryMax=, CPUQuota= in a systemd unit)
if you need a hard limit on a host that matters.