module app; import dfanotify; // твой модуль сверху import depoll; // новый модуль для epoll import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE; import core.stdc.errno : errno; import core.stdc.string : strerror; import std.string : fromStringz, toStringz, join, strip; import std.conv : to; import std.stdio : writeln, writefln; import std.getopt; import std.algorithm : canFind; import core.sys.posix.unistd : readlink; import core.stdc.stdint; import std.exception; import std.path : baseName; import std.file : readText, exists, read; /// Вернуть путь для fd из события PRE_CONTENT. /// Может вернуть "(deleted)" или пустую строку, если объект без имени. string pathFromEventFd(int fd) { // Конструируем путь в /proc/self/fd auto linkPath = "/proc/self/fd/" ~ fd.to!string; char[4096] buf; // readlink не добавляет '\0' auto n = readlink(linkPath.toStringz, buf.ptr, buf.length); if (n < 0) { return ""; } return cast(string) buf[0 .. n].idup; } /// Возвращает "имя процесса" для события fanotify. /// Приоритет: /proc//comm → имя ссылки /proc//exe → первый токен из cmdline. /// Если всё сломалось — "pid:". string eventProcessName(int pid) { // 1) /proc//comm — самое точное короткое имя const commPath = "/proc/" ~ pid.to!string ~ "/comm"; try { if (exists(commPath)) { auto s = readText(commPath).strip; // там обычно одна строка с \n if (s.length) return s; } } catch (Exception e) { } // 2) /proc//exe — полный путь к исполняемому файлу try { char[4096] buf; auto link = "/proc/" ~ pid.to!string ~ "/exe"; auto n = readlink(link.toStringz, buf.ptr, buf.length); if (n > 0) { auto exePath = cast(string) buf[0 .. n].idup; auto name = baseName(exePath); if (name.length) return name; } } catch (Exception e) { } // 3) /proc//cmdline — первый NUL-разделённый аргумент try { const cmdPath = "/proc/" ~ pid.to!string ~ "/cmdline"; if (exists(cmdPath)) { auto bytes = cast(const(ubyte)[]) read(cmdPath); if (bytes.length) { // до первого \0 size_t i = 0; for (; i < bytes.length && bytes[i] != 0; ++i) { } auto first = cast(string) bytes[0 .. i].idup; if (first.length) { auto name = baseName(first); if (name.length) return name; } } } } catch (Exception e) { } // fallback return "pid:" ~ pid.to!string; } // Удобная печать маски (для живых логов) string maskToStr(uint64_t m) { string[] parts; if (m & FAN_OPEN) parts ~= "OPEN"; if (m & FAN_ACCESS) parts ~= "ACCESS"; if (m & FAN_MODIFY) parts ~= "MODIFY"; if (m & FAN_CLOSE_WRITE) parts ~= "CLOSE_WRITE"; if (m & FAN_CLOSE_NOWRITE) parts ~= "CLOSE_NOWRITE"; if (m & FAN_CREATE) parts ~= "CREATE"; if (m & FAN_DELETE) parts ~= "DELETE"; if (m & FAN_MOVED_FROM) parts ~= "MOVED_FROM"; if (m & FAN_MOVED_TO) parts ~= "MOVED_TO"; if (m & FAN_OPEN_PERM) parts ~= "OPEN_PERM"; if (m & FAN_ACCESS_PERM) parts ~= "ACCESS_PERM"; if (m & FAN_OPEN_EXEC) parts ~= "OPEN_EXEC"; if (m & FAN_OPEN_EXEC_PERM) parts ~= "OPEN_EXEC_PERM"; if (m & FAN_Q_OVERFLOW) parts ~= "Q_OVERFLOW"; if (m & FAN_FS_ERROR) parts ~= "FS_ERROR"; return parts.length ? parts.join("|") : "0"; } void main(string[] args) { string watchPath = "/tmp/scripts"; bool markMount = false; getopt(args, "path|p", &watchPath, "mount", &markMount, ); writefln("[*] Watch: %s (mode: %s)", watchPath, markMount ? "MOUNT" : "DIR"); Fanotify pre, notif; // --------------------------- // 1) Создаём две группы // --------------------------- // PRE_CONTENT: решаем "до доступа" // Важно: для permission-событий обязательно включить REPORT_* для имени // и DFID (иначе имя не извлечёшь). uint preInitFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK; try { pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE); } catch (Exception e) { writeln("pre: ", e.msg); } // NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п. uint notifInitFlags = FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_NONBLOCK | FAN_REPORT_FID | FAN_REPORT_DIR_FID | FAN_REPORT_NAME; try { notif = new Fanotify(notifInitFlags, O_RDONLY | O_LARGEFILE); } catch (Exception e) { writeln("notif", e.msg); } // --------------------------- // 2) Ставим метки // --------------------------- // Что именно хотим ловить enum uint64_t PERM_MASK = FAN_OPEN_PERM | FAN_ACCESS_PERM | FAN_OPEN_EXEC_PERM; enum uint64_t NOTIF_MASK = FAN_OPEN | FAN_OPEN_EXEC | FAN_MODIFY | FAN_CLOSE_WRITE | FAN_CLOSE_NOWRITE | FAN_CREATE | FAN_DELETE | FAN_MOVED_FROM | FAN_MOVED_TO; // Важно: для слежения за потомками каталога добавляем FAN_EVENT_ON_CHILD. auto preMask = PERM_MASK | FAN_EVENT_ON_CHILD; auto notifMask = NOTIF_MASK | FAN_EVENT_ON_CHILD; uint baseFlags = FAN_MARK_ADD /*| FAN_MARK_ONLYDIR*/ ; // Если нужно «видеть всё на файловой системе/монтировании», используй MOUNT: // (на многих сценариях это предпочтительно, иначе будет только каталог + его прямые дети) if (markMount) { baseFlags |= FAN_MARK_MOUNT; } // Метки на обе группы pre.mark(baseFlags, preMask, /*dirFd*/ 0, watchPath); notif.mark(baseFlags, notifMask, /*dirFd*/ 0, watchPath); // --------------------------- // 3) Готовим epoll (теперь через ООП-класс) // --------------------------- Epoll ep = new Epoll(); enum uint TAG_PRE = 1; enum uint TAG_NOTIF = 2; ep.add(pre.handle, TAG_PRE); ep.add(notif.handle, TAG_NOTIF); // --------------------------- // 4) Главный цикл // --------------------------- while (true) { auto epEvents = ep.wait(); foreach (epEv; epEvents) { auto tag = epEv.tag; // Считываем пачку событий с соответствующего fd FanotifyEvent[] events; if (tag == TAG_PRE) { events = pre.readEvents(64 * 1024); } else if (tag == TAG_NOTIF) { events = notif.readEvents(64 * 1024); } else { continue; } foreach (ev; events) { auto pname = eventProcessName(ev.pid); writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask)); if (tag == TAG_PRE && (ev.isOpenPerm || ev.isAccessPerm)) { string path = pathFromEventFd(ev.eventFd); writeln("Попытка доступа к: ", path.length ? path : "<неизвестно>"); // ev.respond(FAN_ALLOW); // или FAN_DENY по своей политике baseName(path) == "test" ? ev.respond(FAN_DENY) : ev.respond(FAN_ALLOW); } // Лог // writefln("[%s] pid=%s mask=%s name=%s", // (tag == TAG_PRE) ? "PRE" : "NOTIF", // ev.pid.to!string, // maskToStr(ev.mask), // ev.name.length ? ev.name : "(unknown)"); // --- ВЕТКА РЕШЕНИЯ ДЛЯ PERM-СОБЫТИЙ --- // if (tag == TAG_PRE && // (ev.isOpenPerm || ev.isAccessPerm || ev.isOpenExecPerm)) // { // // Пример «политики»: запрещать выполнение из временных директорий // // (поменяй на свои условия) // bool deny = false; // // простейший сэмпл-правил // auto nm = ev.name; // if (nm.length) // { // if (nm.canFind("/tmp/") && ev.isOpenExecPerm) // { // deny = true; // } // } // // Отправляем решение ядру // ev.respond(deny ? FAN_DENY : FAN_ALLOW); // writefln(" -> %s", deny ? "DENY" : "ALLOW"); // } // --- ПОСТ-ФАКТУМ УВЕДОМЛЕНИЯ --- if (tag == TAG_NOTIF) { // Реакции в реальном времени if (ev.isCloseWrite) { // Напр., дернуть диф/бэкап/индексацию // snag --config ... create --comment "CLOSE_WRITE - " ... writefln(" -> HANDLE CLOSE_WRITE for: %s", ev.name); } else if (ev.isModify) { writefln(" -> HANDLE MODIFY for: %s", ev.name); } } // Кастомный postProcess если надо ev.postProcess(); } } } }