module app; import dfanotify; import fanotify; import depoll; import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE, O_CLOEXEC, AT_FDCWD; import core.sys.posix.unistd : read, close; import core.sys.posix.sys.stat : fstat, stat_t; import core.stdc.errno : errno, EINTR; import core.stdc.string : strerror; import std.stdio : writeln, writefln, stderr; import std.file : isDir, readText, exists, read; import std.string : strip, splitLines, startsWith, toStringz, fromStringz, split; import std.conv : to; import std.exception : enforce; import std.getopt : getopt, defaultGetoptPrinter; import std.format : format; import core.sys.linux.epoll : EPOLLIN, EPOLLERR, EPOLLHUP, EPOLLRDHUP, EPOLL_CLOEXEC; // Переписать import core.sys.posix.sys.types : uid_t; import core.sys.posix.pwd : passwd, getpwuid_r; import core.stdc.stdlib : malloc, free; import core.stdc.string : strlen; /// ---- syscall + pidfd_open ---- extern (C) long syscall(long number, ...); version (X86_64) enum __NR_pidfd_open = 434; extern (C) int pidfd_open(int pid, uint flags) { return cast(int) syscall(__NR_pidfd_open, pid, flags); } /// ---- readlink по fd ---- extern (C) long readlink(const char* path, char* buf, size_t bufsiz); string readlinkFdPath(int fd) { char[4_096] buf; auto link = format("/proc/self/fd/%d", fd); auto n = readlink(link.toStringz, buf.ptr, buf.length.to!int - 1); if (n <= 0) return ""; buf[n] = 0; return buf.ptr.fromStringz.idup.strip; } /// ---- имя процесса и UIDы (ruid из status, uid из loginuid) ---- struct ProcIds { uint ruid; uint uid; } // uid = loginuid string readProcComm(int pid) { auto p = format("/proc/%s/comm", pid); string s; try { s = readText(p); } catch (Exception e) { return ""; } return s.strip; } uint readLoginUid(int pid) { auto p = format("/proc/%s/loginuid", pid); try { auto s = readText(p).strip; // loginuid может быть "-1" (unset). В этом случае вернём 4294967295 или 0 — выбери политику. if (s.length && s[0] == '-') return uint.max; // помечаем как "нет" return s.to!uint; } catch (Exception e) { return uint.max; // нет файла или отказ — считаем не установленным } } ProcIds readProcIds(int pid) { uint ruid = 0; auto p = format("/proc/%s/status", pid); string s; try { s = readText(p); } catch (Exception e) { return ProcIds(0, readLoginUid(pid)); } foreach (line; s.splitLines) { if (line.startsWith("Uid:")) { // Uid: RUID EUID SUID FSUID auto parts = line.split; if (parts.length >= 5) { ruid = parts[1].to!uint; } break; } } return ProcIds(ruid, readLoginUid(pid)); } /// ---- ключ карты соответствий: (pid, dev, ino) ---- struct DevIno { ulong dev; ulong ino; int pid; bool opEquals(const DevIno rhs) const @safe nothrow { return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid; } size_t toHash() const @safe nothrow { return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid); } } bool getDevIno(int fd, out DevIno di, int pid) { stat_t st; if (fstat(fd, &st) != 0) return false; di = DevIno(cast(ulong) st.st_dev, cast(ulong) st.st_ino, pid); return true; } /// ---- сессия файла для конкретного PID ---- struct Session { string procName; uint ruid; // из /proc/PID/status (Uid:) uint uid; // LOGINUID из /proc/PID/loginuid string filePathOpen; // путь, снятый на PERM bool changed; // был CLOSE_WRITE const(ubyte)[] fileBytesSource; const(ubyte)[] fileBytesDiff; } __gshared Session[DevIno] gMap; // (pid,dev,ino) -> Session __gshared int[int] gPidfdByPid; // pid -> pidfd private string userName(uid_t uid) { enum BUF = 4096; auto buf = cast(char*) malloc(BUF); scope (exit) if (buf) free(buf); passwd pwd; passwd* outp; auto rc = getpwuid_r(uid, &pwd, buf, BUF, &outp); if (rc == 0 && outp !is null) { return pwd.pw_name[0 .. strlen(pwd.pw_name)].idup; } return ""; } /// ---- журнал: proc, file, uid(loginuid), ruid ---- void logChange(string procName, string filePath, uint uid, uint ruid) { writefln(`proc="%s" file="%s" user=%s(%s) realUser=%s(%s)`, procName, filePath, userName(uid), uid, userName(ruid), ruid); } /// ---- маска на КАТАЛОГ ---- ulong dirMask() { return FAN_EVENT_ON_CHILD | FAN_OPEN_PERM | FAN_OPEN_EXEC_PERM | FAN_ACCESS_PERM | FAN_OPEN | FAN_CLOSE_WRITE; } ubyte[] readAllFromFd(int fd) { import std.array : appender; ubyte[16_384] buf; // 16 KiB буфер auto output = appender!(ubyte[]); for (;;) { auto n = read(fd, buf.ptr, buf.length); if (n <= 0) break; output.put(buf[0 .. n]); } return output.data; } /// ---- обработчики ---- void onOpenPerm(FanotifyEvent ev, Epoll ep) { const pid = ev.pid; auto procName = readProcComm(pid); auto ids = readProcIds(pid); DevIno key; if (!getDevIno(ev.eventFd, key, pid)) { ev.respond(FAN_ALLOW); return; } auto path = readlinkFdPath(ev.eventFd); const(ubyte)[] content; try { if (path.length) { content = readAllFromFd(ev.eventFd); } } catch (Exception e) { content = []; } if (auto ps = key in gMap) { // уже есть } else { gMap[key] = Session(procName, ids.ruid, ids.uid, path, false, content); } if (!(pid in gPidfdByPid)) { int pfd = -1; try { pfd = pidfd_open(pid, 0); if (pfd < 0) writeln("pidfd_open failed: ", strerror(errno).fromStringz); } catch (Exception e) { pfd = -1; } if (pfd >= 0) { try { ep.add(pfd, cast(uint) pid, EPOLLIN | EPOLLHUP | EPOLLRDHUP | EPOLLERR); gPidfdByPid[pid] = pfd; } catch (Exception e) { } } } ev.respond(FAN_ALLOW); } void onCloseWrite(FanotifyEvent ev) { DevIno key; if (!getDevIno(ev.eventFd, key, ev.pid)) return; if (auto ps = key in gMap) { ps.changed = true; import sdiff; const(ubyte)[] content; try { if (ps.filePathOpen.length) content = readAllFromFd(ev.eventFd); } catch (Exception e) { content = []; } auto dataSource = new MMFile(ps.fileBytesSource); auto dataChange = new MMFile(content); ps.fileBytesDiff = dataSource.diff(dataChange).slice(); } } void flushAndClearForPid(int pid, Epoll ep) { auto keys = gMap.keys.dup; foreach (k; keys) { if (k.pid != pid) continue; auto s = gMap[k]; if (s.changed) { logChange(s.procName, s.filePathOpen, s.uid, s.ruid); writeln(cast(string)s.fileBytesDiff); } gMap.remove(k); } if (auto p = pid in gPidfdByPid) { try { ep.remove(*p); } catch (Exception e) { } close(*p); gPidfdByPid.remove(pid); } } /// ---- main ---- enum uint FAN_TAG = 0; void main(string[] args) { string watchDir; auto help = getopt(args, "dir|d", &watchDir); if (help.helpWanted || watchDir.length == 0) { defaultGetoptPrinter("Usage: app -d ", help.options); return; } enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)"); enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK; auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC); try { fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir); } catch (Exception e) { stderr.writefln("fanotify_mark failed: %s", e.msg); return; } auto ep = new Epoll(EPOLL_CLOEXEC); ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP); writeln("watching dir: ", watchDir); for (;;) { auto evs = ep.wait(256, -1); if (evs.length == 0) continue; foreach (e; evs) { if (e.tag == FAN_TAG) { auto list = fan.readEvents(8_192); foreach (fev; list) { if (fev.isOverflow) { writeln( "FAN_Q_OVERFLOW — возможна потеря корреляции"); continue; } if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm) { onOpenPerm(fev, ep); continue; } if (fev.isCloseWrite) { onCloseWrite(fev); continue; } } } else { auto pid = cast(int) e.tag; flushAndClearForPid(pid, ep); } } } }