358 lines
9.2 KiB
D
358 lines
9.2 KiB
D
module app;
|
||
|
||
import dfanotify; // твой модуль сверху
|
||
import depoll; // новый модуль для epoll
|
||
import event_actor;
|
||
|
||
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/<pid>/comm → имя ссылки /proc/<pid>/exe → первый токен из cmdline.
|
||
/// Если всё сломалось — "pid:<pid>".
|
||
string eventProcessName(int pid)
|
||
{
|
||
// 1) /proc/<pid>/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/<pid>/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/<pid>/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));
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
}
|