Механизм отслеживания записи в файл (без фиксации изменений в журнал)

This commit is contained in:
Alexander Zhirov 2025-08-30 20:32:44 +03:00
parent 377d5c1f94
commit 3bf9250ea2
Signed by: alexander
GPG key ID: C8D8BE544A27C511
5 changed files with 382 additions and 446 deletions

View file

@ -1,357 +1,346 @@
module app;
import dfanotify; // твой модуль сверху
import depoll; // новый модуль для epoll
import event_actor;
import dfanotify;
import fanotify;
import depoll;
import core.sys.posix.fcntl : O_RDONLY, O_LARGEFILE;
import core.stdc.errno : errno;
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.string : fromStringz, toStringz, join, strip;
import std.stdio : writeln, writefln, stderr;
import std.file : isDir, readText, exists;
import std.string : strip, splitLines, startsWith, toStringz, fromStringz, split;
import std.conv : to;
import std.stdio : writeln, writefln;
import std.getopt;
import std.algorithm : canFind;
import core.sys.posix.unistd : readlink;
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.stdc.stdint;
import std.exception;
// Переписать
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;
import std.path : baseName;
import std.file : readText, exists, read;
/// Вернуть путь для fd из события PRE_CONTENT.
/// Может вернуть "(deleted)" или пустую строку, если объект без имени.
string pathFromEventFd(int fd)
/// ---- 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)
{
// Конструируем путь в /proc/self/fd
auto linkPath = "/proc/self/fd/" ~ fd.to!string;
char[4096] buf;
return cast(int) syscall(__NR_pidfd_open, pid, flags);
}
// readlink не добавляет '\0'
auto n = readlink(linkPath.toStringz, buf.ptr, buf.length);
if (n < 0)
/// ---- 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 cast(string) buf[0 .. n].idup;
return s.strip;
}
/// Возвращает "имя процесса" для события fanotify.
/// Приоритет: /proc/<pid>/comm → имя ссылки /proc/<pid>/exe → первый токен из cmdline.
/// Если всё сломалось — "pid:<pid>".
string eventProcessName(int pid)
uint readLoginUid(int pid)
{
// 1) /proc/<pid>/comm — самое точное короткое имя
const commPath = "/proc/" ~ pid.to!string ~ "/comm";
auto p = format("/proc/%s/loginuid", pid);
try
{
if (exists(commPath))
{
auto s = readText(commPath).strip; // там обычно одна строка с \n
if (s.length)
return s;
}
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; // нет файла или отказ — считаем не установленным
}
}
// 2) /proc/<pid>/exe — полный путь к исполняемому файлу
ProcIds readProcIds(int pid)
{
uint ruid = 0;
auto p = format("/proc/%s/status", pid);
string s;
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;
}
s = readText(p);
}
catch (Exception e)
{
return ProcIds(0, readLoginUid(pid));
}
// 3) /proc/<pid>/cmdline — первый NUL-разделённый аргумент
try
foreach (line; s.splitLines)
{
const cmdPath = "/proc/" ~ pid.to!string ~ "/cmdline";
if (exists(cmdPath))
if (line.startsWith("Uid:"))
{
auto bytes = cast(const(ubyte)[]) read(cmdPath);
if (bytes.length)
// 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
}
__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;
}
/// ---- обработчики ----
void onOpenPerm(FanotifyEvent ev, Epoll ep)
{
const pid = ev.pid;
auto procName = readProcComm(pid);
auto ids = readProcIds(pid); // <-- ruid + loginuid
DevIno key;
if (!getDevIno(ev.eventFd, key, pid))
{
ev.respond(FAN_ALLOW);
return;
}
auto path = readlinkFdPath(ev.eventFd);
if (auto ps = key in gMap)
{
// уже есть
}
else
{
gMap[key] = Session(procName, ids.ruid, ids.uid, path, false);
}
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)
{
// до первого \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;
ev.respond(FAN_ALLOW);
}
// Удобная печать маски (для живых логов)
string maskToStr(uint64_t m)
void onCloseWrite(FanotifyEvent ev)
{
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";
DevIno key;
if (!getDevIno(ev.eventFd, key, ev.pid))
return;
if (auto ps = key in gMap)
ps.changed = true;
}
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);
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 watchPath = "/tmp/scripts";
bool markMount = false;
string watchDir;
getopt(args,
"path|p", &watchPath,
"mount", &markMount,
);
auto help = getopt(args, "dir|d", &watchDir);
if (help.helpWanted || watchDir.length == 0)
{
defaultGetoptPrinter("Usage: app -d <dir-to-watch>", help.options);
return;
}
enforce(isDir(watchDir), "Ожидается путь к КАТАЛОГУ (-d)");
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;
enum initFlags = FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK;
auto fan = new Fanotify(initFlags, O_RDONLY | O_LARGEFILE | O_CLOEXEC);
try
{
pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE);
fan.mark(FAN_MARK_ADD | FAN_MARK_ONLYDIR, dirMask(), AT_FDCWD, watchDir);
}
catch (Exception e)
{
writeln("pre: ", e.msg);
stderr.writefln("fanotify_mark failed: %s", e.msg);
return;
}
// NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п.
uint notifInitFlags =
FAN_CLASS_NOTIF
| FAN_CLOEXEC
| FAN_NONBLOCK
| FAN_REPORT_FID
| FAN_REPORT_DIR_FID
| FAN_REPORT_NAME;
auto ep = new Epoll(EPOLL_CLOEXEC);
ep.add(fan.handle, FAN_TAG, EPOLLIN | EPOLLERR | EPOLLHUP);
try
writeln("watching dir: ", watchDir);
for (;;)
{
notif = new Fanotify(notifInitFlags, O_RDONLY | O_LARGEFILE);
}
catch (Exception e)
{
writeln("notif", e.msg);
}
auto evs = ep.wait(256, -1);
if (evs.length == 0)
continue;
// ---------------------------
// 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)
foreach (e; evs)
{
auto tag = epEv.tag;
// Считываем пачку событий с соответствующего fd
FanotifyEvent[] events;
if (tag == TAG_PRE)
if (e.tag == FAN_TAG)
{
events = pre.readEvents(64 * 1024);
}
else if (tag == TAG_NOTIF)
{
events = notif.readEvents(64 * 1024);
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
{
continue;
}
foreach (ev; events)
{
auto pname = eventProcessName(ev.pid);
// writeln("pid = ", ev.pid, "\tproc = ", pname, "\tmask = ", maskToStr(ev.mask));
writefln("[%s] pid = %s proc = %s mask = %s name = %s",
(tag == TAG_PRE) ? "PRE" : "NOTIF",
ev.pid, pname,
maskToStr(ev.mask),
ev.name.length ? ev.name : "<неизвестно>"
);
auto actor = eventActorFromPid(ev.pid);
writeln("user=", actor.user, " uid=", actor.uid,
" ruid=", actor.ruid, "(", actor.ruser, ")",
" euid=", actor.euid, "(", actor.euser, ")");
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 - <name>" ...
writefln(" -> HANDLE CLOSE_WRITE for: %s", ev.name);
}
else if (ev.isMoved)
{
writefln(" -> HANDLE MOVED for: %s", ev.name);
}
else if (ev.isModify)
{
writefln(" -> HANDLE MODIFY for: %s", ev.name);
}
}
// Кастомный postProcess если надо
ev.postProcess();
auto pid = cast(int) e.tag;
flushAndClearForPid(pid, ep);
}
}
}