Building a Small Rust eBPF EDR 6: MVP Rule Engine and Alert Records
Table of Contents
This post covers the userspace rule engine in rand-guard. Earlier posts described how process, file, and network telemetry is collected. Telemetry alone does not complete an EDR pipeline. The runtime also needs a step that marks which events are worth investigating.
rand-guard handles that step in userspace. The eBPF programs focus on event collection, while rule evaluation happens in crates/user.
Built-In Detections and Generic Rules
The current project has two detection flows.
The first flow is built-in detection. Persistence-sensitive file detection and suspicious network port detection belong here. They are configured through [[detections.persistence]] and [[detections.network]], and they can annotate source events with alert and detection_type.
The second flow is generic [[rules]]. These rules evaluate single normalized process, file, or network events. When a rule matches, the runtime emits a separate alert record.
The two flows have different purposes. Built-in detections provide small default security signals. Generic rules are the user-configurable MVP rule engine.
RuleEngine Flow
RuleEngine in crates/user/src/rules/engine.rs combines built-in rules and config rules.
pub fn new(config_rules: &[RuleConfig]) -> Self {
let mut rules = crate::rules::builtins::builtin_rules();
rules.extend(config_rules.iter().cloned());
Self { rules }
}
After an event is normalized, the main loop first writes the normal event and then evaluates the rule engine. Matching rules are converted into alerts and written as separate JSON records.
raw event
-> normalize
-> write source event
-> evaluate rules
-> write alert records
This ordering preserves both source telemetry and the detection stream. If only alerts are written, context can be lost. If only source events are written, downstream consumers have to extract detections from many different event shapes.
Process Rule
A process rule checks process names and parent names. The sample config includes a rule that looks for a web server spawning a shell.
[[rules]]
id = "PROC-001"
name = "Shell spawned by web server"
enabled = true
type = "process"
severity = "high"
action = "alert"
parent_names = ["nginx", "apache2", "httpd"]
process_names = ["sh", "bash", "dash"]
This rule compares parent and child comm values on a ProcessRelationship event. It models a small web-shell-like signal, but it does not prove an attack. Admin scripts or test environments can produce similar relationships.
File Rule
A file rule checks path, pattern, and operation.
[[rules]]
id = "FILE-001"
name = "Sensitive file touched"
enabled = false
type = "file"
severity = "high"
action = "alert"
paths = ["/etc/passwd", "/etc/shadow", "/etc/sudoers"]
operations = ["file_open", "file_write", "file_unlink", "file_rename"]
patterns = []
crates/user/src/rules/matchers.rs handles operation and path/pattern matching. Because there are several file event types, the engine converts FileOpen, FileWrite, FileRename, and FileUnlink families into common FileFields before evaluation.
That is simple but useful. Multiple file syscall families can be evaluated with one rule model.
Network Rule
A network rule checks direction, port, and optional process name.
[[rules]]
id = "NET-001"
name = "Suspicious outbound port"
enabled = false
type = "network"
severity = "medium"
action = "alert"
direction = "outbound"
ports = [4444, 1337, 31337]
If the process name filter is empty, direction and port are enough to match. If a process name list is provided, only those processes match on the selected ports. This is a simple but important way to reduce false positives.
Stable Alert Record
A rule match creates a separate event_type = "alert" record.
{"event_type":"alert","rule_id":"FILE-001","rule_name":"Sensitive file touched","rule_type":"file","severity":"high","action":"alert","source_event_type":"file_write","pid":100,"comm":"bash","path":"/etc/shadow","operation":"file_write"}
The separate alert record exists so downstream consumers can subscribe to detections easily. Source event shapes differ across process, file, and network telemetry. Alert records provide common fields such as rule id, severity, action, and source event type.
The current output target is stdout NDJSON. Even so, a stable alert shape will help if the project later connects to journald, files, or SIEM-style pipelines.
Why Not Build a DSL Yet
When building a rule engine, it is tempting to add regex, an expression DSL, boolean expressions, time windows, and multi-event correlation. That is not the current goal of rand-guard.
The MVP rule engine stays within this scope.
- process single-event match
- file single-event match
- network single-event match
- simple field equality/list matching
- separate alert record output
This limit is intentional. A complex DSL increases expressiveness, but also increases the burden for validation, error messages, tests, false positive handling, and documentation. For now, stabilizing the event model and alert output matters more.
Part 6 Summary
This post covered the MVP rule engine.
- Built-in detections and generic
[[rules]]have different roles. - The rule engine evaluates process, file, and network matchers over normalized events.
- Matches are written as separate stable alert records.
- The current rule engine is a single-event MVP, not a DSL or correlation engine.
The next post summarizes running, packaging, health records, benchmarks, and the roadmap. It explains how the project can be built and verified in practice, and where it can expand next.