Collecting Sniffer Output with BigFix (Agentless / No Elastic Path)¶
Some network segments cannot reach the Elastic (or Splunk) ingest tier — a co-located forwarder has no route out of the enclave. On those segments, run the sniffer in sealed-file mode and let BigFix ferry the completed files off the host over its native relay chain, exactly as it already ships scan-result files. This removes the per-segment forwarder-to-Elastic path; only the BigFix relay root needs one path to the ingest tier.
This is the collection recipe. It pairs with the offline_output.rotation_mode: sealed
configuration documented in configuration.md.
Why sealed mode is required¶
The default circular rotation reuses fixed filenames (connections.N.ndjson) and
truncates them on wrap. A polling collector can lose the unread tail of a file that gets
overwritten between polls, and reused filenames cause duplicate re-ingestion. Sealed
mode writes to a single active file and, on rollover, atomically renames it to a
unique, immutable file that is never reopened or reused — safe to grab and delete.
Sniffer configuration¶
offline_output:
output_file: "/var/log/pqc-sniffer/connections.ndjson"
rotation_enabled: true
rotation_mode: "sealed"
max_size_mb: 100 # size trigger per sealed file
max_files: 10 # retention cap if the collector stalls (oldest pruned, warned)
seal_interval_seconds: 300 # also seal every 5 min so quiet/lab segments hand off promptly
seal_gzip: true # gzip each sealed file (~10:1) to cut relay/transfer load
Resulting files in /var/log/pqc-sniffer/:
| File | Meaning | Collect? |
|---|---|---|
connections.ndjson.active |
The live file being written | No |
connections-YYYYMMDD-HHMMSS-<seq>.ndjson |
Sealed (uncompressed) | Yes |
connections-YYYYMMDD-HHMMSS-<seq>.ndjson.gz |
Sealed (gzipped, when seal_gzip: true) |
Yes |
.connections-*.raw, .connections-*.gz.part |
Dot-prefixed compression intermediates | No |
Switching a host that already ran in circular mode¶
Nothing to do by hand. A host that previously wrote connections.N.ndjson (or
connections.ndjson, with rotation off) still holds connection records under names the
collection glob below does not match. On the first start in sealed mode the sniffer seals
those files into the connections-*.ndjson* naming — gzipped when seal_gzip: true —
so the next collection cycle ships them with everything else. Their sealed names carry
the time each file was last written, not the restart time.
/var/log/pqc-sniffer/pqc-sniffer.log records each file as it is sealed, followed by one
Sealed N NDJSON file(s) written under circular rotation warning. If some could not be
sealed, a warning says so; those files keep their old names and are retried on the next
start.
The sealed leftovers count towards max_files. Keep max_files at least as high as it
was in circular mode for the first start, or the oldest of them are pruned before a
collection cycle can take them.
The collection loop (BigFix task/property, run on a schedule)¶
- Match the sealed glob
connections-*.ndjson*in the output directory. This covers both.ndjsonand.ndjson.gz, and never matches the.activefile or the dot-prefixed intermediates. Do not collectconnections.ndjson.active— it is still being written. - Ship each matched file upstream over the BigFix relay chain to the collection point at (or near) the relay root.
- Delete each file only after its upload is confirmed. Because sealed filenames
are unique and never reused, a failed upload is safe to retry, and re-shipping a file
that was already delivered is harmless to dedup on
event.id(see below).
Pseudo-flow:
for f in connections-*.ndjson*: # excludes .active and dot-intermediates
if upload(f) == success:
rm f
# else: leave it; the retention cap bounds the backlog, retry next cycle
Cadence — poll faster than the seal rate¶
The exposure window is how fast sealed files accumulate to the retention cap
(max_files). Size the BigFix poll interval below the time it takes to fill and prune
max_files × max_size_mb. On a busy segment that can be minutes:
| Segment load (completed conns/s) | Data rate (~3.5 KB/record) | Time to fill max_files × max_size_mb (10 × 100 MB) |
|---|---|---|
| ~10/s (small) | ~35 KB/s | ~8 hours |
| ~100/s (moderate) | ~350 KB/s | ~50 minutes |
| ~1,000/s (busy) | ~3.5 MB/s | ~5 minutes |
| ~5,000/s (max, single sniffer) | ~17.5 MB/s | ~1.5 minutes |
If the collector falls behind the cap, the sniffer prunes the oldest sealed files
first (whole files, never a partial record) and logs a warning — so a rising prune-warning
count is the signal that the poll interval or link is too slow for that segment.
seal_gzip: true cuts the transferred volume ~10:1; seal_interval_seconds ensures even
idle segments hand off on a predictable cadence.
Last mile — one ingest path¶
At the BigFix collection point, land the files and ingest them into Elastic once,
centrally (Elastic Agent filestream, Logstash, or the Elastic ingest pipeline). Gunzip
.ndjson.gz on the way in if your ingest does not do it natively. This is the whole win:
one managed path from the relay root to Elastic instead of a path per segment.
Idempotency and dedup¶
Sealed filenames are unique and never reused, so at-least-once delivery (a retry after an
unconfirmed upload) is safe. If the same file is delivered twice, deduplicate downstream
on event.id — the connection key — as already documented for the Sniffer dashboards.
Do not rely on filename or offset tracking for dedup.
Latency caveat¶
BigFix collection is store-and-forward on a polling cadence (minutes or more). It suits the posture / inventory / compliance views (30-day windows). It is not near-real-time, so the Active Threat "live downgrade feed" (60-second refresh) is not served over a BigFix-ferried segment — that dashboard's expectation assumes streaming ingest. State this explicitly for any segment collected this way.