Механизм отслеживания записи в файл (без фиксации изменений в журнал)
This commit is contained in:
		
							parent
							
								
									377d5c1f94
								
							
						
					
					
						commit
						aa214dcf96
					
				
					 5 changed files with 383 additions and 446 deletions
				
			
		
							
								
								
									
										2
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -20,7 +20,7 @@
 | 
			
		|||
			"type": "cppdbg",
 | 
			
		||||
			"request": "launch",
 | 
			
		||||
			"program": "${workspaceFolder}/bin/dwatch",
 | 
			
		||||
			"args": [], // Аргументы командной строки для программы, если нужны
 | 
			
		||||
			"args": ["-d", "/tmp/scripts"], // Аргументы командной строки для программы, если нужны
 | 
			
		||||
			"stopAtEntry": false, // Остановить на входе в main
 | 
			
		||||
			"cwd": "${workspaceFolder}",
 | 
			
		||||
			"environment": [],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										74
									
								
								scheme.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								scheme.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
```mermaid
 | 
			
		||||
sequenceDiagram
 | 
			
		||||
%%{init: { 'theme': 'default', 'themeVariables': {
 | 
			
		||||
    'primaryColor': '#ff0000',
 | 
			
		||||
    'nodeTextColor': '#ffffff',
 | 
			
		||||
    'edgeLabelBackground': '#f0f0f0'
 | 
			
		||||
}}}%%
 | 
			
		||||
    autonumber
 | 
			
		||||
    participant Watcher as Демон-наблюдатель
 | 
			
		||||
    participant Kernel as Ядро (fanotify)
 | 
			
		||||
    participant Editor as Процесс-редактор (PID X)
 | 
			
		||||
    participant FS as Файловая система (целевой каталог/файл)
 | 
			
		||||
    participant Ep as epoll
 | 
			
		||||
    participant Pfd as pidfd(PID X)
 | 
			
		||||
    participant Map as Карта соответствий (в памяти)
 | 
			
		||||
    participant ChLog as Журнал изменений
 | 
			
		||||
 | 
			
		||||
    Note over Watcher: Инициализация
 | 
			
		||||
    Watcher->>Kernel: fanotify_init(PRE_CONTENT|NONBLOCK|CLOEXEC, O_RDONLY|O_CLOEXEC|O_LARGEFILE)
 | 
			
		||||
    Watcher->>Kernel: fanotify_mark(ADD|ONLYDIR, EVENT_ON_CHILD | OPEN[_EXEC]_PERM | OPEN | CLOSE_WRITE, watchDir)
 | 
			
		||||
    Watcher->>Ep: add(fanotify_fd, tag=FAN)
 | 
			
		||||
 | 
			
		||||
    Note over Editor: Запрос доступа к файлу
 | 
			
		||||
    Editor->>FS: open(target, O_RDONLY/O_RDWR/EXEC)
 | 
			
		||||
    FS-->>Kernel: системный вызов доступа
 | 
			
		||||
    Kernel-->>Watcher: PERM-событие (+fd, mask, pid)
 | 
			
		||||
 | 
			
		||||
    Note over Watcher: Сбор контекста
 | 
			
		||||
    Watcher->>FS: fstat(fd) → (inode, dev)
 | 
			
		||||
    FS-->>Watcher: inode, dev
 | 
			
		||||
    Watcher->>Watcher: readlink(/proc/self/fd/FD) → file_path_open
 | 
			
		||||
    Watcher->>Watcher: read /proc/PID/comm → proc_name
 | 
			
		||||
    Watcher->>Watcher: read /proc/PID/status(Uid:) → RUID
 | 
			
		||||
    Watcher->>Watcher: read /proc/PID/loginuid → UID(loginuid)
 | 
			
		||||
 | 
			
		||||
    %% Кладём связку
 | 
			
		||||
    Watcher->>Map: PUT key=(PID,inode,dev), val={proc_name, RUID, UID, file_path_open, changed=false}
 | 
			
		||||
    Note over Map: Добавлена связка key=(PID,inode,dev)
 | 
			
		||||
 | 
			
		||||
    %% pidfd и epoll
 | 
			
		||||
    Watcher->>Pfd: pidfd_open(PID, flags=0)
 | 
			
		||||
    Pfd-->>Watcher: pidfd
 | 
			
		||||
    Watcher->>Ep: add(pidfd, tag=PID, events=IN|HUP|RDHUP|ERR)
 | 
			
		||||
 | 
			
		||||
    %% Разрешаем доступ
 | 
			
		||||
    Watcher->>Kernel: fanotify_response{fd, FAN_ALLOW}
 | 
			
		||||
    Watcher->>FS: close(fd)
 | 
			
		||||
 | 
			
		||||
    Note over Editor: Работа с файлом
 | 
			
		||||
    Editor->>FS: write(...)
 | 
			
		||||
    Editor->>FS: close()
 | 
			
		||||
    Kernel-->>Watcher: CLOSE_WRITE (+fd)
 | 
			
		||||
 | 
			
		||||
    Note over Watcher: Фиксация изменения
 | 
			
		||||
    Watcher->>FS: fstat(fd) → (inode, dev)
 | 
			
		||||
    FS-->>Watcher: inode, dev
 | 
			
		||||
    Watcher->>Map: GET key=(PID,inode,dev)
 | 
			
		||||
    Map-->>Watcher: {…}
 | 
			
		||||
    Watcher->>Map: SET changed=true
 | 
			
		||||
    Watcher->>FS: close(fd)
 | 
			
		||||
 | 
			
		||||
    Note over Editor: Завершение процесса
 | 
			
		||||
    Editor->>Editor: exit()
 | 
			
		||||
    Pfd-->>Ep: EPOLLIN/HUP (процесс завершён)
 | 
			
		||||
    Ep-->>Watcher: событие(tag=PID)
 | 
			
		||||
 | 
			
		||||
    %% Пишем журнал и чистим
 | 
			
		||||
    Watcher->>Map: GET_ALL where pid=PID
 | 
			
		||||
    Map-->>Watcher: список записей
 | 
			
		||||
    Watcher->>ChLog: APPEND для changed=true → {proc=proc_name, file=file_path_open, uid=UID(loginuid), ruid=RUID}
 | 
			
		||||
    Watcher->>Ep: remove(pidfd); Watcher->>Pfd: close(pidfd)
 | 
			
		||||
    Watcher->>Map: DELETE_ALL where pid=PID
 | 
			
		||||
    Note over Map: Удалены все связки для PID
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										603
									
								
								source/app.d
									
										
									
									
									
								
							
							
						
						
									
										603
									
								
								source/app.d
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -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 — полный путь к исполняемому файлу
 | 
			
		||||
	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)
 | 
			
		||||
ProcIds readProcIds(int pid)
 | 
			
		||||
{
 | 
			
		||||
	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";
 | 
			
		||||
	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));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void main(string[] args)
 | 
			
		||||
/// ---- ключ карты соответствий: (pid, dev, ino) ----
 | 
			
		||||
struct DevIno
 | 
			
		||||
{
 | 
			
		||||
	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
 | 
			
		||||
	ulong dev;
 | 
			
		||||
	ulong ino;
 | 
			
		||||
	int pid;
 | 
			
		||||
	bool opEquals(const DevIno rhs) const @safe nothrow
 | 
			
		||||
	{
 | 
			
		||||
		pre = new Fanotify(preInitFlags, O_RDONLY | O_LARGEFILE);
 | 
			
		||||
	}
 | 
			
		||||
	catch (Exception e)
 | 
			
		||||
	{
 | 
			
		||||
		writeln("pre: ", e.msg);
 | 
			
		||||
		return dev == rhs.dev && ino == rhs.ino && pid == rhs.pid;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// NOTIF: обычные уведомления (после факта), CLOSE_WRITE и т.п.
 | 
			
		||||
	uint notifInitFlags =
 | 
			
		||||
		FAN_CLASS_NOTIF
 | 
			
		||||
		| FAN_CLOEXEC
 | 
			
		||||
		| FAN_NONBLOCK
 | 
			
		||||
		| FAN_REPORT_FID
 | 
			
		||||
		| FAN_REPORT_DIR_FID
 | 
			
		||||
		| FAN_REPORT_NAME;
 | 
			
		||||
 | 
			
		||||
	try
 | 
			
		||||
	size_t toHash() const @safe nothrow
 | 
			
		||||
	{
 | 
			
		||||
		notif = new Fanotify(notifInitFlags, O_RDONLY | O_LARGEFILE);
 | 
			
		||||
		return (dev * 1_315_423_911UL) ^ (ino * 2_654_435_761UL) ^ (cast(size_t) pid);
 | 
			
		||||
	}
 | 
			
		||||
	catch (Exception e)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
	{
 | 
			
		||||
		writeln("notif", e.msg);
 | 
			
		||||
		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;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ---------------------------
 | 
			
		||||
	// 2) Ставим метки
 | 
			
		||||
	// ---------------------------
 | 
			
		||||
	auto path = readlinkFdPath(ev.eventFd);
 | 
			
		||||
 | 
			
		||||
	// Что именно хотим ловить
 | 
			
		||||
	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)
 | 
			
		||||
	if (auto ps = key in gMap)
 | 
			
		||||
	{
 | 
			
		||||
		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
 | 
			
		||||
	{
 | 
			
		||||
		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)
 | 
			
		||||
			{
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 watchDir;
 | 
			
		||||
 | 
			
		||||
	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)");
 | 
			
		||||
 | 
			
		||||
	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;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
			foreach (ev; events)
 | 
			
		||||
					if (fev.isOpenPerm || fev.isAccessPerm || fev.isOpenExecPerm)
 | 
			
		||||
					{
 | 
			
		||||
				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);
 | 
			
		||||
						onOpenPerm(fev, ep);
 | 
			
		||||
						continue;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				// Лог
 | 
			
		||||
				// 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 (fev.isCloseWrite)
 | 
			
		||||
					{
 | 
			
		||||
					// Реакции в реальном времени
 | 
			
		||||
					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);
 | 
			
		||||
						onCloseWrite(fev);
 | 
			
		||||
						continue;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// Кастомный postProcess если надо
 | 
			
		||||
				ev.postProcess();
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				auto pid = cast(int) e.tag;
 | 
			
		||||
				flushAndClearForPid(pid, ep);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ module depoll;
 | 
			
		|||
 | 
			
		||||
import core.sys.linux.epoll;
 | 
			
		||||
import core.sys.posix.unistd : close;
 | 
			
		||||
import core.stdc.errno : errno, EINTR;
 | 
			
		||||
import core.stdc.errno : errno, EINTR, EEXIST;
 | 
			
		||||
import core.stdc.string : strerror;
 | 
			
		||||
import std.string : fromStringz;
 | 
			
		||||
import std.exception : enforce;
 | 
			
		||||
| 
						 | 
				
			
			@ -26,39 +26,52 @@ class Epoll
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Добавление дескриптора с произвольным tag (например, tag=0 для fanotify, tag=pid для pidfd)
 | 
			
		||||
	void add(int fd, uint tag, uint events = EPOLLIN | EPOLLERR | EPOLLHUP)
 | 
			
		||||
	{
 | 
			
		||||
		epoll_event ev;
 | 
			
		||||
		ev.events = events;
 | 
			
		||||
		ev.data.u32 = tag;
 | 
			
		||||
		enforce(epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev) == 0,
 | 
			
		||||
			"epoll_ctl ADD: " ~ strerror(errno).fromStringz.idup);
 | 
			
		||||
		auto rc = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
 | 
			
		||||
		if (rc != 0 && errno == EEXIST)
 | 
			
		||||
		{
 | 
			
		||||
			// Если уже добавлен — делаем MOD
 | 
			
		||||
			rc = epoll_ctl(epfd_, EPOLL_CTL_MOD, fd, &ev);
 | 
			
		||||
		}
 | 
			
		||||
		enforce(rc == 0, "epoll_ctl ADD/MOD: " ~ strerror(errno).fromStringz.idup);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Опционально: метод для удаления fd
 | 
			
		||||
	/// Опционально: явное удаление
 | 
			
		||||
	void remove(int fd)
 | 
			
		||||
	{
 | 
			
		||||
		enforce(epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null) == 0,
 | 
			
		||||
			"epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup);
 | 
			
		||||
		auto rc = epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, null);
 | 
			
		||||
		enforce(rc == 0, "epoll_ctl DEL: " ~ strerror(errno).fromStringz.idup);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	struct Event
 | 
			
		||||
	{
 | 
			
		||||
		uint tag;
 | 
			
		||||
		uint events;
 | 
			
		||||
		uint tag; // то самое, что передавали в add()
 | 
			
		||||
		uint events; // EPOLL* биты
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Возвращает события; пустой массив — если таймаут/прерывание
 | 
			
		||||
	Event[] wait(int maxevents = 16, int timeout = -1)
 | 
			
		||||
	{
 | 
			
		||||
		epoll_event[] evs = new epoll_event[maxevents];
 | 
			
		||||
		int n = epoll_wait(epfd_, evs.ptr, maxevents, timeout);
 | 
			
		||||
		if (n <= 0)
 | 
			
		||||
		int n;
 | 
			
		||||
		// Терпим EINTR и повторяем
 | 
			
		||||
		while (true)
 | 
			
		||||
		{
 | 
			
		||||
			// Игнорируем EINTR и другие (как в оригинале: continue)
 | 
			
		||||
			return [];
 | 
			
		||||
			n = epoll_wait(epfd_, evs.ptr, maxevents, timeout);
 | 
			
		||||
			if (n < 0 && errno == EINTR)
 | 
			
		||||
				continue;
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
		if (n <= 0)
 | 
			
		||||
			return [];
 | 
			
		||||
 | 
			
		||||
		Event[] res;
 | 
			
		||||
		res.reserve(n);
 | 
			
		||||
		foreach (i; 0 .. n)
 | 
			
		||||
		{
 | 
			
		||||
			res ~= Event(evs[i].data.u32, evs[i].events);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,139 +0,0 @@
 | 
			
		|||
module event_actor;
 | 
			
		||||
 | 
			
		||||
import core.sys.posix.pwd : passwd, getpwuid_r;
 | 
			
		||||
import core.sys.posix.sys.types : uid_t;
 | 
			
		||||
import core.stdc.stdlib : malloc, free;
 | 
			
		||||
import core.stdc.string : strlen;
 | 
			
		||||
import std.file : readText, exists;
 | 
			
		||||
import std.string : splitLines, strip;
 | 
			
		||||
import std.conv : to;
 | 
			
		||||
 | 
			
		||||
// для токенизации по пробельным
 | 
			
		||||
import std.ascii : isWhite;
 | 
			
		||||
import std.algorithm.iteration : splitter;
 | 
			
		||||
import std.array : array;
 | 
			
		||||
 | 
			
		||||
struct EventActor
 | 
			
		||||
{
 | 
			
		||||
	uid_t uid; // выбранный UID (loginuid если есть, иначе ruid)
 | 
			
		||||
	string user; // имя пользователя для uid (может быть пустым)
 | 
			
		||||
	uid_t ruid, euid, suid, fsuid;
 | 
			
		||||
	string ruser, euser, suser, fsuser;
 | 
			
		||||
	bool hasLoginuid;
 | 
			
		||||
	uid_t loginuid;
 | 
			
		||||
	string loginuser;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Парсит строку вида: "Uid:\t<ruid>\t<euid>\t<suid>\t<fsuid>"
 | 
			
		||||
private bool parseUidLine(string line, out uid_t r, out uid_t e, out uid_t s, out uid_t f)
 | 
			
		||||
{
 | 
			
		||||
	// разобьём по любым пробельным символам
 | 
			
		||||
	auto toks = line.splitter!isWhite.array; // ["Uid:", "1000", "1000", "1000", "1000"]
 | 
			
		||||
	if (toks.length >= 5 && toks[0] == "Uid:")
 | 
			
		||||
	{
 | 
			
		||||
		try
 | 
			
		||||
		{
 | 
			
		||||
			r = toks[1].to!uid_t;
 | 
			
		||||
			e = toks[2].to!uid_t;
 | 
			
		||||
			s = toks[3].to!uid_t;
 | 
			
		||||
			f = toks[4].to!uid_t;
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception e)
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Главная функция: кто инициировал событие (по PID из fanotify_event_metadata)
 | 
			
		||||
EventActor eventActorFromPid(int pid)
 | 
			
		||||
{
 | 
			
		||||
	EventActor a;
 | 
			
		||||
	if (pid <= 0)
 | 
			
		||||
		return a;
 | 
			
		||||
 | 
			
		||||
	const base = "/proc/" ~ pid.to!string ~ "/";
 | 
			
		||||
	const statusPath = base ~ "status";
 | 
			
		||||
 | 
			
		||||
	if (exists(statusPath))
 | 
			
		||||
	{
 | 
			
		||||
		try
 | 
			
		||||
		{
 | 
			
		||||
			auto txt = readText(statusPath);
 | 
			
		||||
			uid_t r = 0, e = 0, s = 0, f = 0;
 | 
			
		||||
			foreach (line; txt.splitLines())
 | 
			
		||||
			{
 | 
			
		||||
				// ищем строку "Uid:"
 | 
			
		||||
				if (line.length >= 4 && line[0 .. 4] == "Uid:")
 | 
			
		||||
				{
 | 
			
		||||
					if (parseUidLine(line, r, e, s, f))
 | 
			
		||||
					{
 | 
			
		||||
						a.ruid = r;
 | 
			
		||||
						a.euid = e;
 | 
			
		||||
						a.suid = s;
 | 
			
		||||
						a.fsuid = f;
 | 
			
		||||
						a.ruser = userName(r);
 | 
			
		||||
						a.euser = userName(e);
 | 
			
		||||
						a.suser = userName(s);
 | 
			
		||||
						a.fsuser = userName(f);
 | 
			
		||||
					}
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception e)
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// loginuid — владелец сессии (если audit включён)
 | 
			
		||||
	const loginPath = base ~ "loginuid";
 | 
			
		||||
	if (exists(loginPath))
 | 
			
		||||
	{
 | 
			
		||||
		try
 | 
			
		||||
		{
 | 
			
		||||
			auto s = readText(loginPath).strip;
 | 
			
		||||
			if (s.length)
 | 
			
		||||
			{
 | 
			
		||||
				a.loginuid = s.to!uid_t;
 | 
			
		||||
				a.loginuser = userName(a.loginuid);
 | 
			
		||||
				a.hasLoginuid = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception e)
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// выбираем «итогового» пользователя
 | 
			
		||||
	if (a.hasLoginuid)
 | 
			
		||||
	{
 | 
			
		||||
		a.uid = a.loginuid;
 | 
			
		||||
		a.user = a.loginuser.length ? a.loginuser : ("uid:" ~ a.loginuid.to!string);
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		a.uid = a.ruid;
 | 
			
		||||
		a.user = a.ruser.length ? a.ruser : ("uid:" ~ a.ruid.to!string);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return a;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue