Monitor what is modifiying your files
Files | Log | Commits | Refs | README
Size: 13278 bytes
/* See LICENSE file for copyright and license details.
* filemon - monitors directory recursively and tracks which processes modify files
*/
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/inotify.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/select.h>
#include <time.h>
#include <unistd.h>
#include <dirent.h>
#include <libgen.h>
#include <limits.h>
#include <linux/cn_proc.h>
#include <linux/connector.h>
#include <linux/netlink.h>
#include <sys/fanotify.h>
#include "proc.h"
#include "uid.h"
#define MAX_EVENTS 10
#define MAX_PROCS 1024
#define MAX_PATH 4096
#define PROC_BUF_SIZE 1024
enum {
STATE_INIT,
STATE_MONITORING,
STATE_SHUTDOWN
};
typedef struct FileEvent {
char path[MAX_PATH];
time_t ts;
uint32_t mask;
} FileEvent;
/* global state */
static int state = STATE_INIT;
static int nlfd = -1;
static int ifd = -1;
static int efd = -1;
static Process procs[MAX_PROCS];
static int nprocs = 0;
static char dir[MAX_PATH];
static int usefan = -1, fanfd = -1;
struct fev {
char path[256];
pid_t pid;
time_t ts;
};
static struct fev fevs[32];
static int nfevs = 0;
/* function declarations */
static void die(const char *fmt, ...);
static void usage(void);
static int initnetlink(void);
static int initinotify(const char *dir);
static int addwatch(int fd, const char *path);
static void handleproc(void);
static void handlefile(void);
static Process *findproc(pid_t pid);
static void addproc(pid_t pid, pid_t ppid, const char *comm);
static void rmproc(pid_t pid);
static Process *correlate(const char *path, time_t timestamp);
static void logchange(const char *path, Process *proc, uint32_t mask);
static void cleanup(void);
static void sighandler(int sig);
static void scanprocs(void);
static int initfan(void);
static Process *corfan(const char *path, time_t ts);
static Process *corheur(const char *path, time_t ts);
static void
die(const char *fmt, ...)
{
va_list ap;
int saved_errno;
saved_errno = errno;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
if (fmt[0] && fmt[strlen(fmt)-1] == ':')
fprintf(stderr, " %s", strerror(saved_errno));
fputc('\n', stderr);
exit(1);
}
static void
usage(void)
{
die("usage: filemon directory\n");
}
static void
sighandler(int sig)
{
(void)sig;
state = STATE_SHUTDOWN;
}
static int
initnetlink(void)
{
struct sockaddr_nl sa_nl;
struct epoll_event ev;
int sock, bufsize = 65536;
sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
if (sock < 0)
die("socket:");
memset(&sa_nl, 0, sizeof(sa_nl));
sa_nl.nl_family = AF_NETLINK;
sa_nl.nl_groups = CN_IDX_PROC;
sa_nl.nl_pid = getpid();
if (bind(sock, (struct sockaddr *)&sa_nl, sizeof(sa_nl)) < 0)
die("bind:");
if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)) < 0)
die("setsockopt:");
struct {
struct nlmsghdr nl_hdr;
struct cn_msg cn_msg;
enum proc_cn_mcast_op cn_mcast;
} msg;
memset(&msg, 0, sizeof(msg));
msg.nl_hdr.nlmsg_len = sizeof(msg);
msg.nl_hdr.nlmsg_type = NLMSG_DONE;
msg.nl_hdr.nlmsg_flags = 0;
msg.nl_hdr.nlmsg_seq = 0;
msg.nl_hdr.nlmsg_pid = getpid();
msg.cn_msg.id.idx = CN_IDX_PROC;
msg.cn_msg.id.val = CN_VAL_PROC;
msg.cn_msg.len = sizeof(enum proc_cn_mcast_op);
msg.cn_mcast = PROC_CN_MCAST_LISTEN;
if (send(sock, &msg, sizeof(msg), 0) < 0)
die("send:");
ev.events = EPOLLIN;
ev.data.fd = sock;
if (epoll_ctl(efd, EPOLL_CTL_ADD, sock, &ev) < 0)
die("epoll_ctl:");
return sock;
}
static void
readfan(void)
{
char buf[4096], path[256], fdpath[64];
struct fanotify_event_metadata *meta;
ssize_t len, plen;
if ((len = read(fanfd, buf, sizeof(buf))) < (ssize_t)sizeof(*meta))
return;
for (meta = (struct fanotify_event_metadata *)buf;
FAN_EVENT_OK(meta, len);
meta = FAN_EVENT_NEXT(meta, len))
{
if (meta->fd >= 0)
close(meta->fd);
if (meta->vers != FANOTIFY_METADATA_VERSION || meta->fd < 0 || nfevs >= 32)
continue;
snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", meta->fd);
if ((plen = readlink(fdpath, path, sizeof(path) - 1)) <= 0)
continue;
path[plen] = 0;
strncpy(fevs[nfevs].path, path, sizeof(fevs[nfevs].path) - 1);
fevs[nfevs].path[sizeof(fevs[nfevs].path) - 1] = 0;
fevs[nfevs].pid = meta->pid;
fevs[nfevs].ts = time(NULL);
++nfevs;
}
}
static void
handleproc(void)
{
struct {
struct nlmsghdr nl_hdr;
struct cn_msg cn_msg;
struct proc_event proc_ev;
} msg;
ssize_t len;
struct proc_event *ev;
len = recv(nlfd, &msg, sizeof(msg), 0);
if (len < 0) {
if (errno == EINTR || errno == EAGAIN)
return;
die("recv:");
}
if (len < (ssize_t)(sizeof(msg.nl_hdr) + sizeof(msg.cn_msg)))
return;
ev = &msg.proc_ev;
switch (ev->what) {
case PROC_EVENT_EXEC:
addproc(ev->event_data.exec.process_pid,
ev->event_data.exec.process_tgid,
"unknown");
break;
case PROC_EVENT_EXIT:
rmproc(ev->event_data.exit.process_pid);
break;
case PROC_EVENT_FORK:
addproc(ev->event_data.fork.child_pid,
ev->event_data.fork.parent_pid,
"unknown");
break;
default:
break;
}
}
static Process *
findproc(pid_t pid)
{
Process *p = procs;
for (; p < procs + MAX_PROCS && (p->pid != pid || !p->active); ++p);
return p < procs + MAX_PROCS ? p : NULL;
}
static void
addproc(pid_t pid, pid_t ppid, const char *comm)
{
Process *proc = findproc(pid);
if (!proc && (proc = procs, 1))
for (; proc < procs + MAX_PROCS && proc->active; ++proc);
if (proc >= procs + MAX_PROCS)
return;
proc->active || nprocs++;
*proc = (Process){pid, ppid, 0, 0, "", "", time(NULL), 1};
strncpy(proc->comm, comm, sizeof(proc->comm) - 1);
proc->comm[sizeof(proc->comm) - 1] = '\0';
updateproc(proc);
}
static void
rmproc(pid_t pid)
{
Process *proc = findproc(pid);
(proc && (proc->active = 0, nprocs--, 0));
}
static int
initinotify(const char *dir)
{
struct epoll_event ev;
int fd;
fd = inotify_init1(IN_CLOEXEC);
if (fd < 0)
die("inotify_init1:");
if (addwatch(fd, dir) < 0) {
close(fd);
die("add_watch_recursive:");
}
ev.events = EPOLLIN;
ev.data.fd = fd;
if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev) < 0) {
close(fd);
die("epoll_ctl:");
}
return fd;
}
static int
addwatch(int fd, const char *path)
{
DIR *dir;
struct dirent *entry;
struct stat statbuf;
char fullpath[MAX_PATH];
int wd;
wd = inotify_add_watch(fd, path, IN_CREATE | IN_DELETE | IN_MODIFY |
IN_MOVED_FROM | IN_MOVED_TO | IN_CLOSE_WRITE);
if (wd < 0)
return -1;
dir = opendir(path);
if (!dir)
return -1;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.' && (!entry->d_name[1] ||
(entry->d_name[1] == '.' && !entry->d_name[2])))
continue;
if (snprintf(fullpath, sizeof(fullpath), "%s/%s",
path, entry->d_name) >= (int)sizeof(fullpath)) {
closedir(dir);
return -1;
}
if (stat(fullpath, &statbuf) < 0)
continue;
if (S_ISDIR(statbuf.st_mode)) {
if (addwatch(fd, fullpath) < 0) {
closedir(dir);
return -1;
}
}
}
closedir(dir);
return 0;
}
static void
handlefile(void)
{
char buffer[4096], path[MAX_PATH], *ptr;
ssize_t len;
struct inotify_event *event;
Process *proc;
len = read(ifd, buffer, sizeof(buffer));
if (len < 0) {
if (errno == EINTR || errno == EAGAIN)
return;
die("read:");
}
ptr = buffer;
while (ptr < buffer + len) {
event = (struct inotify_event *)ptr;
if (event->len > 0) {
if (dir[strlen(dir) - 1] == '/') {
snprintf(path, sizeof(path), "%s%s",
dir, event->name);
} else {
snprintf(path, sizeof(path), "%s/%s",
dir, event->name);
}
proc = correlate(path, time(NULL));
logchange(path, proc, event->mask);
if (event->mask & IN_CREATE) {
struct stat statbuf;
if (stat(path, &statbuf) == 0 &&
S_ISDIR(statbuf.st_mode)) {
addwatch(ifd, path);
}
}
}
ptr += sizeof(struct inotify_event) + event->len;
}
}
static int
hasaccess(Process *proc, const char *filepath)
{
char *fdir, *dpath;
int cwdlen, dpathlen, result;
if (!(fdir = strdup(filepath)))
return 0;
dpath = dirname(fdir);
if (!proc->cwd[0] || !strcmp(proc->cwd, "unknown")) {
free(fdir);
return 0;
}
cwdlen = strlen(proc->cwd);
dpathlen = strlen(dpath);
result = !strcmp(dpath, proc->cwd) ||
(!strncmp(dpath, proc->cwd, cwdlen) && dpath[cwdlen] == '/') ||
(!strncmp(proc->cwd, dpath, dpathlen) && proc->cwd[dpathlen] == '/');
free(fdir);
return result;
}
static Process *
corheur(const char *path, time_t ts)
{
Process *best, *proc;
time_t bestdiff, diff;
best = NULL;
bestdiff = LONG_MAX;
for (proc = procs; proc < procs + MAX_PROCS; ++proc) {
if (!proc->active || proc->start > ts ||
proc->pid < 100 || ts - proc->start > 86400 ||
strstr(proc->comm, "kworker") || strstr(proc->comm, "ksoftirqd") ||
strstr(proc->comm, "migration") || proc->comm[0] == '[')
continue;
if (ts - proc->start > 60)
updateproc(proc);
if (!hasaccess(proc, path))
continue;
diff = ts - proc->start;
if (diff < bestdiff) {
bestdiff = diff;
best = proc;
}
}
return best;
}
static Process *
corfan(const char *path, time_t ts)
{
int i;
for (i = 0; i < nfevs; ++i) {
if (ts - fevs[i].ts > 5 || strcmp(fevs[i].path, path)) {
continue;
}
Process *proc = findproc(fevs[i].pid);
if (proc) {
updateproc(proc);
return proc;
}
addproc(fevs[i].pid, 0, "fanotify");
if ((proc = findproc(fevs[i].pid))) {
updateproc(proc);
return proc;
}
return NULL;
}
return NULL;
}
static Process *
correlate(const char *path, time_t ts)
{
Process *proc;
fd_set rfds;
int result;
struct timeval tv = {0, 0};
if (usefan < 0) {
initfan();
}
if (usefan && fanfd >= 0) {
FD_ZERO(&rfds);
FD_SET(fanfd, &rfds);
result = select(fanfd + 1, &rfds, NULL, NULL, &tv);
if (result > 0) {
readfan();
}
}
if (usefan) {
proc = corfan(path, ts);
if (proc) {
return proc;
}
}
return corheur(path, ts);
}
static const char *
maskstr(uint32_t mask)
{
if (mask & IN_CREATE)
return "CREATE";
if (mask & IN_DELETE)
return "DELETE";
if (mask & IN_MODIFY)
return "MODIFY";
if (mask & IN_CLOSE_WRITE)
return "WRITE";
if (mask & IN_MOVED_FROM)
return "MOVE_FROM";
if (mask & IN_MOVED_TO)
return "MOVE_TO";
return "UNKNOWN";
}
static void
logchange(const char *path, Process *proc, uint32_t mask)
{
time_t now;
struct tm *tm;
char ts[32];
now = time(NULL);
tm = localtime(&now);
strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm);
if (proc) {
printf("%s %s %s pid=%d user=%s gid=%d comm=%s cwd=%s\n",
ts, maskstr(mask), path,
proc->pid, uidname(proc->uid), proc->gid, proc->comm, proc->cwd);
} else {
printf("%s %s %s pid=? user=? gid=? comm=? cwd=?\n",
ts, maskstr(mask), path);
}
fflush(stdout);
}
static void
cleanup(void)
{
if (nlfd != -1)
close(nlfd);
if (ifd != -1)
close(ifd);
if (efd != -1)
close(efd);
}
static void
scanprocs(void)
{
DIR *proc_dir;
struct dirent *entry;
pid_t pid;
char *endptr;
proc_dir = opendir("/proc");
if (!proc_dir)
return;
while ((entry = readdir(proc_dir))) {
pid = strtol(entry->d_name, &endptr, 10);
if (!(*endptr || pid <= 0))
addproc(pid, 0, "existing");
}
closedir(proc_dir);
}
static int
initfan(void)
{
struct epoll_event ev;
return usefan != -1 ? usefan :
(usefan = 0,
!getuid() &&
(fanfd = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY | O_LARGEFILE)) >= 0 &&
!fanotify_mark(fanfd, FAN_MARK_ADD | FAN_MARK_MOUNT,
FAN_MODIFY | FAN_CLOSE_WRITE | FAN_OPEN | FAN_ACCESS, AT_FDCWD, dir) &&
(ev.events = EPOLLIN, ev.data.fd = fanfd,
!epoll_ctl(efd, EPOLL_CTL_ADD, fanfd, &ev)) ?
(usefan = 1) : (fanfd >= 0 && close(fanfd), fanfd = -1, 0));
}
int
main(int argc, char **argv)
{
(void)argc;
int nfds;
struct epoll_event events[MAX_EVENTS];
if (!*++argv)
usage();
if (strlen(*argv) >= MAX_PATH)
die("directory path too long");
strcpy(dir, *argv);
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
efd = epoll_create1(EPOLL_CLOEXEC);
if (efd < 0)
die("epoll_create1:");
nlfd = initnetlink();
ifd = initinotify(dir);
uidinit();
scanprocs();
printf("filemon - started monitoring %s\n", dir);
printf("active processes: %d\n", nprocs);
fflush(stdout);
for (; state != STATE_SHUTDOWN;) {
nfds = epoll_wait(efd, events, MAX_EVENTS, -1);
if (nfds < 0) {
if (errno == EINTR)
continue;
die("epoll_wait:");
}
for (struct epoll_event *ev = events; ev < events + nfds; ++ev)
ev->data.fd == nlfd ? handleproc() :
ev->data.fd == fanfd ? readfan() : handlefile();
}
cleanup();
return 0;
}