Skip to main content
Version: 1.0.0

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.

Release0.1.3-ga, commit 7db3d4c, built with go1.25.12
PlatformsLinux amd64/arm64 only β€” macOS and Windows are not published, and why
Supply chainReproducible 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 surfaceNo 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
Outboundhttps only, to the endpoints you configure. No redirect is ever followed
Writes to the hostIts 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 defaultZero. 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:

bitcollectorbitscanner
FacesInward β€” the host it runs onOutward β€” 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 ifan agent is installed on itit is visible from a host that has one
Sends packetsNever β€” it reads the hostOnly 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.

RecordWhat it carriesNeeds
network_stateThe routing table: destination, gateway, interface, metric, flagsnothing
neighbor_tableThe kernel ARP/NDP cache: address, MAC, interface, statenothing
neighbor_discoveryThe enriched neighbour inventory: address, MAC, vendor, device type, reachability, first/last seen β€” plus the subnets they were found onscanning.probes.neighbor_discovery
gateway_discoveryThe first hops out of this host, with MAC attributionscanning.probes.gateway_discovery
service_discoverymDNS/SSDP responders, attributed to the neighbour that answeredscanning.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 flags

There 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_discovery armed, 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.interval is 1m.
  • 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_delivery line 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.
These sockets accepted anything, in 0.1.3-ga and earlier

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

This release has no configurable memory or CPU ceiling

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.

It is the same key as the rest of the family

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.

You can fetch the key over DNS instead of HTTPS

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:

PlatformPublishedWhy
Linux amd64Yes
Linux arm64Yes
macOS amd64/arm64NoWithheld β€” see below
Windows amd64NoWithheld β€” 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​

Was this page helpful?