Building a Small Rust eBPF EDR 6: MVP Rule Engine and Alert Records

이번 글에서는 rand-guard의 userspace rule engine을 다룬다. 앞선 글에서 process, file, network telemetry를 수집하는 방법을 봤다. 하지만 telemetry만으로는 EDR pipeline이 완성되지 않는다. 이벤트를 보고 어떤 것이 조사할 가치가 있는지 표시하는 단계가 필요하다.

rand-guard는 이 단계를 userspace에서 처리한다. eBPF program은 event collection에 집중하고, rule evaluation은 crates/user에서 한다.

built-in detection과 generic rules

현재 프로젝트에는 두 종류의 탐지 흐름이 있다.

첫 번째는 built-in detection이다. persistence-sensitive file detection과 suspicious network port detection이 여기에 해당한다. 이들은 config의 [[detections.persistence]], [[detections.network]]로 설정되고, source event에 alert, detection_type을 붙일 수 있다.

두 번째는 generic [[rules]]다. process, file, network normalized event에 대해 단일 이벤트 matcher를 평가하고, match되면 별도의 alert record를 출력한다.

두 흐름은 목적이 조금 다르다. built-in detection은 프로젝트가 기본적으로 제공하는 작은 보안 signal이다. generic rules는 사용자가 config로 조정할 수 있는 MVP rule engine이다.

RuleEngine의 흐름

crates/user/src/rules/engine.rsRuleEngine은 config rules와 built-in rules를 함께 가진다.

pub fn new(config_rules: &[RuleConfig]) -> Self {
    let mut rules = crate::rules::builtins::builtin_rules();
    rules.extend(config_rules.iter().cloned());
    Self { rules }
}

event가 normalized된 후 main loop는 먼저 normal event를 출력하고, 그 다음 rule engine을 평가한다. match된 rule은 alert로 변환되어 별도 JSON record로 출력된다.

raw event
  -> normalize
  -> write source event
  -> evaluate rules
  -> write alert records

이 순서는 source telemetry와 detection stream을 모두 남긴다는 의미다. alert만 남기면 context를 잃을 수 있고, source event만 남기면 downstream에서 detection만 모으기 어렵다.

process rule

process rule은 process name과 parent name을 본다. sample config에는 web server가 shell을 spawn하는 관계를 잡는 rule이 있다.

[[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"]

이 rule은 ProcessRelationship event에서 parent와 child comm을 비교한다. web shell behavior를 모델링하는 작은 signal이지만, 공격 확정은 아니다. admin script나 test 환경에서도 비슷한 process relationship이 생길 수 있다.

file rule

file rule은 path, pattern, 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는 operation과 path/pattern match를 처리한다. file event 종류가 여러 개이기 때문에 engine은 FileOpen, FileWrite, FileRename, FileUnlink 계열을 공통 FileFields 형태로 바꿔 평가한다.

이 방식은 간단하지만 유용하다. 여러 file syscall family를 하나의 rule model로 평가할 수 있기 때문이다.

network rule

network rule은 direction, port, 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]

process name filter가 비어 있으면 direction과 port만으로 match한다. process name list를 넣으면 특정 process가 해당 port를 사용할 때만 match하게 만들 수 있다. 이것은 false positive를 줄이는 단순하지만 중요한 방법이다.

stable alert record

rule match는 별도 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"}

alert record가 별도로 있는 이유는 downstream consumer가 detection만 쉽게 구독할 수 있게 하기 위해서다. source event의 shape는 process, file, network마다 다르다. 하지만 alert record는 rule id, severity, action, source event type 같은 공통 field를 가진다.

현재 output target은 stdout NDJSON이다. 그래도 alert record shape를 안정적으로 유지하면 나중에 journald, file, SIEM pipeline으로 연결할 때도 유리하다.

왜 DSL을 만들지 않았나

rule engine을 만들다 보면 regex, expression DSL, boolean expression, time window, multi-event correlation을 넣고 싶어진다. 하지만 rand-guard의 현재 목표는 그것이 아니다.

MVP rule engine은 다음 범위에 머문다.

  • process single-event match
  • file single-event match
  • network single-event match
  • simple field equality/list matching
  • separate alert record output

이 제한은 의도적이다. 복잡한 DSL은 rule expressiveness를 높이지만, validation, error message, test, false positive handling, documentation 부담도 함께 커진다. 현재는 event model과 alert output을 안정화하는 것이 더 중요하다.

6편 정리

이번 글에서는 MVP rule engine을 살펴봤다.

  • built-in detection과 generic [[rules]]는 역할이 다르다.
  • rule engine은 normalized event에 대해 process, file, network matcher를 평가한다.
  • match 결과는 별도 stable alert record로 출력된다.
  • 현재 rule engine은 single-event MVP이며 DSL이나 correlation engine이 아니다.

다음 글에서는 실행, 패키징, health record, benchmark, roadmap을 정리한다. 프로젝트를 실제로 어떻게 빌드하고 검증하고 어디까지 확장할 수 있는지 살펴본다.