Building a Small Rust eBPF EDR 4: File Telemetry and Persistence Detection

This post covers file telemetry in rand-guard. File events are important EDR signals for understanding persistence, credential access, and configuration tampering. But collecting every file access in the same way creates a lot of noise, and path handling becomes complicated quickly.

rand-guard starts from a small scope: watching persistence-sensitive paths instead of trying to cover all file activity broadly.

File Events Collected Today

The default config looks like this.

[file]
enabled = true
hooks = ["openat", "openat2", "write", "rename", "renameat2", "unlink", "unlinkat"]
watch_paths = ["/etc", "/usr/bin", "/bin"]
watch_patterns = ["*.service"]
exclude_paths = ["/var/log", "/tmp", "/proc", "/sys"]

The eBPF code separates open, write, rename, and unlink event families.

  • crates/ebpf/src/file_open.rs
  • crates/ebpf/src/file_write.rs
  • crates/ebpf/src/file_rename.rs
  • crates/ebpf/src/file_unlink.rs

In userspace, crates/user/src/normalize/file.rs converts raw file events into normalized events. Those normalized events include context from the process table, such as ppid, comm, and exe_path.

Why Watch and Exclude Lists Are Needed

File event volume can be high. A system constantly opens, writes, and removes files. If every event is printed unchanged, the output is hard to read and demos or tests become unstable.

So rand-guard splits the config into watched and excluded paths.

  • watch_paths are prefixes of interest.
  • watch_patterns are simple patterns such as *.service.
  • exclude_paths are noisy or special paths such as /tmp, /proc, and /sys.

The eBPF side also has a FILE_FILTER map. When userspace writes prefix filters into this map, eBPF can reduce some events on the kernel side. However, if patterns are configured or too many prefixes are present, the kernel filter is disabled and userspace filtering handles the decision. Complex pattern matching belongs in userspace, not in verifier-constrained eBPF code.

Path Reconstruction Is Not Perfect

Path handling is one of the hardest parts of file telemetry. A filename passed to a syscall is not always an absolute path. It can be relative, and its real meaning can depend on dirfd and mount namespaces.

The current rand-guard implementation does not try to solve this completely. eBPF performs bounded string reads over syscall arguments or limited path data, and userspace filters and prints what it can safely interpret.

This is an intentional MVP choice. Perfect path reconstruction would require more kernel/userspace state, namespace handling, and fd tracking. At this stage, it is more important to create visibility into persistence-sensitive paths and document the limits clearly.

Persistence-Sensitive Detection

config.example.toml contains built-in persistence detection settings.

[[detections.persistence]]
name = "systemd_service_modified"
paths = ["/etc/systemd/system/", "/usr/lib/systemd/system/", "/run/systemd/system/"]
patterns = ["*.service"]
operations = ["file_open", "file_write", "file_rename", "file_unlink"]

[[detections.persistence]]
name = "cron_modified"
paths = ["/etc/cron.d/", "/etc/cron.daily/", "/etc/crontab"]
operations = ["file_open", "file_write", "file_rename", "file_unlink"]

This detection is evaluated by check_persistence in crates/user/src/detections.rs. The rule compares operation, path prefix, and pattern. If a systemd service file or cron-related path is modified, the source file event can receive detection metadata.

{"event_type":"file_write","resolved_path":"/etc/systemd/system/demo.service","alert":true,"detection_type":"systemd_service_modified"}

The important point is that this detection does not prove an attack. A system administrator can legitimately modify a service file and generate the same event. The log marks persistence-sensitive file activity that is worth investigating.

Source Event Annotation and Alert Records

rand-guard has two ways to represent detection results.

The first is source event annotation. Built-in persistence detections can add alert and detection_type to the source event itself.

The second is a separate alert record created by generic [[rules]] matches.

{"event_type":"alert","rule_id":"FILE-001","rule_type":"file","source_event_type":"file_write","path":"/etc/shadow"}

For downstream consumers, separate event_type = "alert" records can provide a more stable detection stream. Source event annotations are useful when looking directly at raw telemetry with detection context attached.

Demo Scenario

The docs include safe local demos. When the agent is running, create and immediately remove a temporary service file.

sudo sh -c 'printf "# rand-guard demo\n" > /etc/rand-guard-demo.service'
sudo rm -f /etc/rand-guard-demo.service

To demonstrate the systemd persistence detection, create a temporary file under a systemd service directory.

sudo sh -c 'printf "# rand-guard persistence demo\n" > /etc/systemd/system/rand-guard-demo.service'
sudo rm -f /etc/systemd/system/rand-guard-demo.service

This demo writes to a real persistence-sensitive directory and then removes the file. It should only be run on a lab or development machine, not on a production host or sensitive system.

False Positive Perspective

File persistence detections naturally have false positives. Package installation, service deployment, administrator maintenance, and configuration management tools can all modify systemd or cron paths.

For that reason, this detection should not be connected to response actions such as blocking or quarantine. rand-guard does not implement automatic response actions. The output is an investigation signal.

Part 4 Summary

This post covered file telemetry.

  • The project collects open, write, rename, and unlink syscall event families.
  • Watch and exclude settings reduce the paths of interest.
  • eBPF prefix filtering and userspace filtering are used together.
  • Systemd service and cron paths are treated as persistence-sensitive detections.
  • Path reconstruction and success judgment are not complete today.

The next post covers network telemetry. It explains why network collection is disabled by default and what information can currently be collected from connect, bind, and listen.