Continuous monitoring¶
--interval <seconds> turns the one-shot check into a long-running monitor
that re-checks on a timer and reports what changed between cycles — the time
dimension on top of the point-in-time checks.
delta --processes --ports --packages --interval 30
delta --cfg policy.json --processes --ports --interval 30 --json
delta --processes --interval 60 --state /var/lib/delta/state
Each cycle Delta collects the requested domains, runs any configured policy checks, reduces the observation to feature keys (the same keys as Fleet / cohort analysis), and diffs against the previous cycle:
+ <key>— something appeared (new process, newly opened port, installed package, changed file checksum, an interface going PROMISC, a new route…);- <key>— something disappeared.
The first cycle establishes the baseline silently. The loop runs until interrupted (SIGINT/SIGTERM) and exits after finishing the current cycle.
State persistence¶
Without --state drift is tracked only within the running process. With
--state <file> the observation is written each cycle and reloaded on
startup, so a periodic (cron) invocation detects drift since the last run —
not only within one resident daemon.
Output¶
Text (default), one block per cycle (+ added, - removed, ~ behavioral):
[2026-05-30T09:13:50] cycle 3: +0 -1 ~1; checks FAIL
- proc:auditd
~ conn_established=412 (z=+5.3)
With --json, one NDJSON object per cycle (see JSON output), carrying
added / removed / anomalies arrays. The policy result and the
temporal drift are independent: a cycle can pass policy yet still show drift.
Behavioral anomalies¶
Alongside exact-match drift, the monitor keeps a lightweight statistical model
(an exponentially-weighted moving mean/variance) of per-cycle volume
metrics — process, open-connection, neighbor and account counts, plus usable
memory (mem_available_pct) and unreclaimable slab (slab_unreclaim_kb),
where a kernel leak shows up as a trend well before any static threshold is
crossed. After a short
warm-up, an observation more than a few standard deviations from the learned
norm is reported as a ~ anomaly. This catches gradual/volume shifts (e.g. a
spike in outbound connections) that an exact keyset cannot, without shipping raw
history off the device. With --state the model persists in a <state>.bh
sidecar so it survives restarts.
Event-driven mode¶
On Linux, when a path worth watching exists (the --fingerprint directory and
sensitive files like /etc/passwd, /etc/ld.so.preload,
/etc/resolv.conf), the loop blocks on inotify and re-checks immediately
on a change — --interval then acts as the maximum latency / heartbeat rather
than a fixed poll. Where inotify is unavailable it falls back to interval
polling.
Exec events (short-lived processes)¶
A per-cycle snapshot cannot see a program that starts and exits between two
collections — precisely the shape of a downloader, a scanner sweep or a
dropper. On Linux the monitor therefore subscribes to the kernel’s proc
connector (netlink PROC_EVENT_EXEC): a background reader resolves each
executed program’s name while its /proc entry still exists and aggregates
counts per name between cycles.
The observations surface through the existing machinery:
each executed name joins the drift keyset as
exec:<name>— a program that ran between snapshots but never appeared in the process table shows up as+ exec:wget;the per-cycle exec volume feeds the behavioral model (metric
execs), so a spike in short-lived executions is flagged as a~anomaly even when every individual name looks benign;the JSON cycle object carries
"execs": { "total": n }(plus"dropped"when the name cap or a kernel-queue overflow lost events).
Subscribing needs CAP_NET_ADMIN and a kernel with CONFIG_PROC_EVENTS
(and the initial network namespace); when unavailable the monitor silently
stays snapshot-only. The tap is skipped under --root — exec events are a
live-host signal, meaningless for an offline image. Memory is bounded: at most
512 distinct names are held per cycle window; volume beyond that still counts
toward the total.
Note that a one-shot execution appears as + exec:<name> in one cycle and
- exec:<name> in the next — the removal is the keyset returning to rest,
not a second event.
Syscall behavior¶
With --trace the monitor also observes what the software does, and not
only what exists (see Syscall behavior tracing). Each cycle takes one bounded syscall
sample, which gives data to the two parts of the loop:
the behavior classes of each process join the drift keyset as
sc:<process>:<class>. A service that starts to load kernel modules thus gives+ sc:nginx:module. Syscall volume produces no key, because it changes between cycles;the syscall rate of the host goes to the behavioral model as
syscall_rate. The monitor reports a large change as a~anomaly.
The sample uses time inside the cycle (--trace-window, 1 second by
default). Keep the window much shorter than --interval.
Notes¶
Mounts are not part of the temporal keyset; processes, ports, packages, file fingerprints, network, kernel modules, resolvers, accounts, syscall behavior classes and device identity are.
Combine with Shipping reports to a server to stream drift to a collector.