summaryrefslogtreecommitdiff
path: root/src/fanotify.c
diff options
context:
space:
mode:
authordaniel <daniel@planethacker.net>2025-05-06 16:57:32 -0700
committerdaniel <daniel@planethacker.net>2025-05-06 16:57:32 -0700
commit2278df1493e064c197913e49b5d1935942d83448 (patch)
tree42f06ab2f76e2ddf228bafbb03f79621975a4534 /src/fanotify.c
initial import
Diffstat (limited to 'src/fanotify.c')
-rw-r--r--src/fanotify.c210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/fanotify.c b/src/fanotify.c
new file mode 100644
index 0000000..869cc49
--- /dev/null
+++ b/src/fanotify.c
@@ -0,0 +1,210 @@
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <sys/fanotify.h>
+
+#include "json.h"
+#include "error.h"
+#include "output.h"
+#include "fanotify.h"
+#include "hash_ledger.h"
+#include "time_common.h"
+
+// set up fanotify descriptor. returns initialized descriptor on
+// success, -1 on error
+int setup_fanotify(void) {
+ int fan_fd = fanotify_init(FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK,
+ O_RDONLY | O_CLOEXEC | O_LARGEFILE);
+
+ if (fan_fd < 0) {
+ error("fanotify_init failed: %s", strerror(errno));
+ return -1;
+ }
+
+ // TODO mark all mount points instead of /
+ if (fanotify_mark(fan_fd,
+ FAN_MARK_ADD | FAN_MARK_MOUNT,
+ FAN_OPEN_EXEC | FAN_OPEN_EXEC_PERM,
+ AT_FDCWD,
+ "/") < 0) {
+ error("fanotify_mark failed: %s", strerror(errno));
+ close(fan_fd);
+ return -1;
+ }
+
+ // set non-blocking
+ int flags = fcntl(fan_fd, F_GETFL, 0);
+ fcntl(fan_fd, F_SETFL, flags | O_NONBLOCK);
+
+ return fan_fd;
+}
+
+typedef struct {
+ struct hash_entry *entry;
+ pattern_table_t *patterns;
+ size_t match_count;
+} match_context_t;
+
+static void match_callback(const struct ac_match *m, void *user_data) {
+ match_context_t *ctx = user_data;
+
+ //printf("fanotify matched: %s at offset %zu\n", m->id, m->offset);
+
+ if (ctx->entry->matched_pattern_count < MAX_MATCHED_PATTERNS) {
+ ctx->entry->matched_patterns[ctx->entry->matched_pattern_count++] = m->id;
+ }
+
+ pattern_t *p = pattern_table_find(ctx->patterns, m->id);
+ if (p && p->match_count < MAX_MATCHED_PATTERNS) {
+ p->match_offsets[p->match_count++] = m->offset;
+ }
+}
+
+// TODO handle old style FID-based events as well as old style FD
+// TODO hash-based allow and blocklists
+void select_fanotify(int fan_fd, agent_context_t *ctx) {
+ struct fanotify_event_metadata buf[4096];
+ ssize_t len = read(fan_fd, buf, sizeof(buf));
+ if (len < 0) {
+ if (errno != EAGAIN) {
+ error("fanotify read: %s", strerror(errno));
+ }
+
+ return;
+ }
+
+ struct fanotify_event_metadata *metadata;
+ for (metadata = buf; FAN_EVENT_OK(metadata, len); metadata = FAN_EVENT_NEXT(metadata, len)) {
+ if (metadata->mask & FAN_OPEN_EXEC_PERM) {
+ struct fanotify_response response;
+ response.fd = metadata->fd;
+ response.response = FAN_ALLOW;
+
+ char path[PATH_MAX];
+ snprintf(path, sizeof(path), "/proc/self/fd/%d", metadata->fd);
+
+ char target[PATH_MAX];
+ ssize_t len = readlink(path, target, sizeof(target) - 1);
+ if (len >= 0) {
+ target[len] = '\0';
+ } else {
+ error("readlink: %s", strerror(errno));
+ }
+
+ // hash_ledger_find(ctx->hash_ledger, target)
+ // - if not in ledger, create and scan.
+ // - if in ledger
+ // - scan if too much time passed, scan again
+ // - if verdict is block, send FAN_DENY and emit log w/ rule matches
+ // - if verdict is quarantine, block and quarantine
+ // = if verdict is allow, allow it.
+
+ struct hash_entry *he = hash_ledger_find(ctx->hash_ledger, target);
+ if (!he) {
+ struct stat sb;
+ stat(target, &sb);
+ he = hash_ledger_add_or_update(ctx->hash_ledger, target, &sb);
+ }
+
+ scan_verdict_t verdict = VERDICT_INFORMATIONAL;
+ if (he->last_scanned == 0 || time(NULL) - he->last_scanned > 600) {
+ match_context_t mctx = {
+ .entry = he,
+ .patterns = &ctx->rules.patterns,
+ .match_count = 0
+ };
+ for (size_t i = 0; i < ctx->rules.patterns.count; i++) {
+ ctx->rules.patterns.patterns[i].match_count = 0;
+ }
+ pattern_table_clear_matches(&ctx->rules.patterns);
+
+ ac_match_path(ctx->ac, target, match_callback, &mctx);
+ //ac_match_fd(ctx->ac, metadata->fd, match_callback, &mctx);
+
+ he->matched_rule_count = 0;
+ he->matched_pattern_count = 0;
+
+ for (size_t i = 0; i < ctx->rules.rule_count; i++) {
+ const rule_t *r = &ctx->rules.rules[i];
+
+ if (evaluate_rule(r, &ctx->rules.patterns)) {
+ //printf("fanotify match: %s\n", r->id);
+ if (he->matched_rule_count < MAX_MATCHED_RULES) {
+ he->matched_rules[he->matched_rule_count++] = r->id;
+ }
+
+ if (r->action == RULE_BLOCK) {
+ verdict = VERDICT_BLOCK;
+ break;
+ } else if (r->action == RULE_QUARANTINE && verdict < VERDICT_QUARANTINE) {
+ verdict = VERDICT_QUARANTINE;
+ } else if (r->action == RULE_ALLOW && verdict < VERDICT_ALLOW) {
+ verdict = VERDICT_ALLOW;
+ }
+ }
+ }
+
+ he->verdict = verdict;
+ he->last_scanned = time(NULL);
+ he->scan_count++;
+ }
+
+ switch (he->verdict) {
+ case VERDICT_BLOCK:
+ case VERDICT_QUARANTINE:
+ response.response = FAN_DENY; // deny blocked/quarantined files.
+ break;
+ default:
+ response.response = FAN_ALLOW; // fail open
+ break;
+ }
+
+ // TODO emit log for rule matches w/ allow action, not only block/quarantine
+ if (response.response == FAN_DENY) {
+ json_t buf = {0};
+ json_init(&buf);
+ json_start_object(&buf);
+
+ json_add_double(&buf, "timestamp", timestamp());
+ json_add_string(&buf, "hostname", ctx->hostname);
+ json_add_string(&buf, "event_type", "fanotify_block");
+ json_add_string(&buf, "verdict",
+ he->verdict == VERDICT_BLOCK ? "block" :
+ he->verdict == VERDICT_QUARANTINE ? "quarantine" :
+ he->verdict == VERDICT_ALLOW ? "allow" : "informational");
+
+ // TODO filesize
+ json_add_string(&buf, "md5", he->md5);
+ json_add_string(&buf, "sha256", he->sha256);
+ json_add_double(&buf, "entropy", he->entropy);
+
+ json_add_array_start(&buf, "matched_rules");
+ for (size_t i = 0; i < he->matched_rule_count; i++) {
+ json_array_add_string(&buf, he->matched_rules[i]);
+ }
+ json_end_array(&buf);
+
+ json_end_object(&buf);
+ output(json_get(&buf));
+ json_free(&buf);
+ }
+
+ //printf("\n\n\nfanotify: fd: %d, pid: %d, mask: %lld, target: %s (%s) verdict: %d\n\n\n", metadata->fd, metadata->pid, metadata->mask, target, he->md5, he->verdict);
+
+ if (write(fan_fd, &response, sizeof(response)) < 0) {
+ error("fanotify write response: %s", strerror(errno));
+ }
+ } else if (metadata->mask & FAN_OPEN_EXEC) {
+ // TODO separate branch for FAN_OPEN_EXEC if PERM isn't
+ // available--prefer this over proc_connector EXEC events
+ }
+
+ if (metadata->fd >= 0) {
+ close(metadata->fd);
+ }
+ }
+}