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.