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.
Verifying this release
The bitscanner release directory ships the binaries, SHA256SUMS, a cosign signature over
that manifest, and — per binary — a .sig, a .att attestation and a .sbom.json. Verify
the signature over the manifest first, then the checksums:
cosign verify-blob --key cosign.pub --bundle SHA256SUMS.sig \
--insecure-ignore-tlog=true SHA256SUMS
# WARNING: Skipping tlog verification is an insecure practice […]
# Verified OK
sha256sum -c SHA256SUMS
# bitscanner-linux-amd64: OK
# bitscanner-linux-arm64: OK
Both commands above were run against the shipped 0.1.3-ga artefacts, and so was the
negative control: altering one byte of SHA256SUMS makes the same verify-blob command
exit non-zero with invalid signature when validating ASN.1 encoded signature. A
verification step you have never seen fail is not a verification step.
bitscanner is signed with the bits family key — the one bitcollector and bitenforcer use, and the one published at cosign.pub. If you already verified a bits release, you already have the trust anchor, and it should not have changed. Why the key check is the load-bearing step, including the fingerprint cross-check, is worth reading once.
As of 0.1.3-ga, bitscanner ships the same artefact set as the rest of the family — a
.sig, a .att attestation and a .sbom.json beside every binary, plus SHA256SUMS.sig
over the manifest — so every step of that page applies here, including steps 4 and 5.
Earlier releases shipped only the signed manifest; if you are verifying 0.1.2-ga or older,
those two steps have nothing to check against.
The full public key is published as a TXT record, which is useful in an install script:
dig +short TXT _cosign-key.cert-ix.com | tr -d '"' | sed 's/.*key=//' \
| base64 -d | openssl pkey -pubin -inform DER -out cosign.pub
DNS is a different system from the web server, so a key obtained this way and one downloaded
from the site are two independent sources that must agree — which is exactly the cross-check
verifying your downloads asks you to make. The companion record
_cosign.cert-ix.com carries the fingerprint alone if you only want to compare that.
Platforms
0.1.3-ga publishes two signed binaries, and the gap is deliberate:
| Platform | Published | Why |
|---|---|---|
Linux amd64 | Yes | |
Linux arm64 | Yes | |
macOS amd64/arm64 | No | Withheld — see below |
Windows amd64 | No | Withheld — see below |
bitscanner is Linux-only because the readers underneath it are Linux implementations. The
two records it produces with every probe switched off — neighbor_table and network_state —
come from the kernel's own ARP/NDP cache and routing table as Linux exposes them. That is what
makes the shipped default able to report something real while putting zero packets on the
network, and it is the property the whole two-switch arming model is built on.
macOS and Windows expose the same information, through entirely different interfaces. Porting those readers is a real piece of work with its own testing burden, and it has not been done. A build for those platforms would compile and then report an empty neighbour table on a host that has neighbours — indistinguishable, to anyone reading the output, from a quiet segment. An empty result that means "not implemented here" is the most dangerous output this agent could produce, because the entire point of it is to tell you when something is on the wire that your inventory does not explain.
So there is no macOS or Windows build to download. If you need to see a segment that only has macOS or Windows hosts on it, put bitscanner on any Linux host attached to it — it reports on the segment, not on itself, so one Linux host is enough to cover the broadcast domain.
Next steps
- Scanning, and the two switches that arm it — the probe ladder, exactly what each probe puts on the wire, why the acknowledgements can only be written in a file, and the guard that decides at the moment the packet leaves.
- What leaves the host — egress, enrolment, secret files, and the honest limits this release ships with.
- Verifying your downloads — the trust anchor, and why it matters more than the signature.
- bitcollector — the inward-facing half of the answer.
- What is bits? — how the four jobs fit together.
¿Te resultó útil esta página?