Skip to content
API Documentation
BastionGuard™
BastionGuard-ransomware-realtime-alert.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 *
17 * BastionGuard™ is a trademark of Calogero Scarnà.
18 * The BastionGuard™ name and branding are not licensed under the GPL.
19 */
20
22#include "Resource.hpp"
23
24#include <gtkmm.h>
25#include <glib/gi18n.h>
26#include <clocale>
27
28#include <queue>
29#include <thread>
30#include <atomic>
31#include <memory>
32#include <string>
33#include <fstream>
34#include <filesystem>
35#include <system_error>
36
37#include <sys/socket.h>
38#include <arpa/inet.h>
39#include <unistd.h>
40#include <pwd.h>
41#include <cerrno>
42#include <cstring>
43#include <sys/stat.h>
44
45#ifndef LOCALEDIR
46#error "LOCALEDIR non definita per questo target"
47#endif
48
49
50
51static std::string resolve_theme_css() {
52 const std::string conf =
53 Glib::get_home_dir() + "/.config/BastionGuard/theme.conf";
54 std::ifstream tf(conf);
55 std::string theme;
56 if (tf >> theme && theme == "dark")
57 return "BastionGuard-dark.css";
58 return "BastionGuard.css";
59}
60namespace fs = std::filesystem;
61
62class AlertApp {
63public:
65 : app(Gtk::Application::create("org.BastionGuard.realtime.alert"))
66 {
67 app->hold();
68
71 load_token();
72
73 app->signal_shutdown().connect(
74 sigc::mem_fun(*this, &AlertApp::stop_listener)
75 );
76 }
77
79 {
81 }
82
83 int run(int argc, char* argv[])
84 {
85 load_css();
87 return app->run(argc, argv);
88 }
89
90private:
91 Glib::RefPtr<Gtk::Application> app;
92
93 std::queue<std::pair<std::string, std::string>> alertQueue;
94 std::unique_ptr<AlertWindowRealtime> current_window;
95
96 std::string SECURITY_TOKEN;
97 std::string TOKEN_PATH;
98 std::string LOG_PATH;
99
100 std::atomic<bool> running{false};
101 std::thread listener_thread;
102 int server_fd{-1};
103
105 {
106 const char* home = std::getenv("HOME");
107 if (!home) {
108 if (passwd* pw = getpwuid(getuid())) {
109 home = pw->pw_dir;
110 }
111 }
112
113 if (!home) {
114 home = "/tmp";
115 }
116
117 const std::string base = std::string(home) + "/.local/share/BastionGuard";
118 std::error_code ec;
119 fs::create_directories(base, ec);
120
121 TOKEN_PATH = base + "/ransomware.token";
122 LOG_PATH = base + "/alert-gui.log";
123 }
124
125 void log_user(const std::string& msg)
126 {
127 std::ofstream f(LOG_PATH, std::ios::app);
128 if (f.is_open()) {
129 f << msg << '\n';
130 }
131 }
132
134 {
135 const std::string system_token = "/etc/BastionGuard/ransomware.token";
136 const std::string user_token = TOKEN_PATH;
137
138 if (!fs::exists(system_token)) {
139 log_user(_("[ERRORE] Token root NON trovato: ") + system_token);
140 return;
141 }
142
143 if (fs::exists(user_token)) {
144 std::error_code ec;
145 fs::remove(user_token, ec);
146 if (!ec) {
147 log_user(_("[INFO] Vecchio token utente rimosso"));
148 } else {
149 log_user(_("[ERRORE] Non posso rimuovere vecchio token utente"));
150 }
151 }
152
153 std::ifstream src(system_token, std::ios::binary);
154 if (!src.is_open()) {
155 log_user(_("[ERRORE] Non posso aprire token root"));
156 return;
157 }
158
159 std::ofstream dst(user_token, std::ios::binary | std::ios::trunc);
160 if (!dst.is_open()) {
161 log_user(_("[ERRORE] Non posso creare token utente"));
162 return;
163 }
164
165 dst << src.rdbuf();
166 dst.close();
167
168 ::chmod(user_token.c_str(), 0600);
169 ::chown(user_token.c_str(), getuid(), getgid());
170
171 log_user(_("[OK] Token copiato nella home: ") + user_token);
172 }
173
175 {
176 std::ifstream f(TOKEN_PATH);
177 if (!f.is_open()) {
178 log_user(_("[ERRORE] Non posso aprire token utente"));
179 return;
180 }
181
182 std::getline(f, SECURITY_TOKEN);
183 if (SECURITY_TOKEN.empty()) {
184 log_user(_("[ERRORE] Token utente vuoto"));
185 } else {
186 log_user(_("[OK] Token caricato"));
187 }
188 }
189
190 void load_css()
191 {
192 try {
193 auto css = Gtk::CssProvider::create();
194 css->load_from_path(resource(resolve_theme_css()));
195
196 auto display = Gdk::Display::get_default();
197 if (display) {
198 Gtk::StyleContext::add_provider_for_display(
199 display,
200 css,
201 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
202 );
203 }
204 } catch (const std::exception& e) {
205 log_user(std::string("[WARN] load_css exception: ") + e.what());
206 } catch (...) {
207 log_user("[WARN] load_css exception sconosciuta");
208 }
209 }
210
212 {
213 if (running.load()) {
214 return;
215 }
216
217 running = true;
218
219 listener_thread = std::thread([this]() {
220 log_user(_("[INFO] Avvio listener TCP..."));
221
222 server_fd = ::socket(AF_INET, SOCK_STREAM, 0);
223 if (server_fd < 0) {
224 log_user(_("[ERRORE] socket fallita"));
225 running = false;
226 return;
227 }
228
229 int opt = 1;
230 ::setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
231
232 sockaddr_in addr{};
233 addr.sin_family = AF_INET;
234 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
235 addr.sin_port = htons(1025);
236
237 if (::bind(server_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
238 log_user(_("[ERRORE] bind fallita: ") + std::string(std::strerror(errno)));
239 ::close(server_fd);
240 server_fd = -1;
241 running = false;
242 return;
243 }
244
245 log_user(_("[OK] Bind su 127.0.0.1:1025"));
246
247 if (::listen(server_fd, 5) < 0) {
248 log_user(_("[ERRORE] listen fallita"));
249 ::close(server_fd);
250 server_fd = -1;
251 running = false;
252 return;
253 }
254
255 log_user(_("[OK] Listener avviato"));
256
257 while (running.load()) {
258 int client = ::accept(server_fd, nullptr, nullptr);
259 if (client < 0) {
260 if (!running.load()) {
261 break;
262 }
263
264 if (errno == EINTR) {
265 continue;
266 }
267
268 log_user(_("[WARN] accept fallita: ") + std::string(std::strerror(errno)));
269 continue;
270 }
271
272 char buf[2048];
273 const ssize_t n = ::read(client, buf, sizeof(buf) - 1);
274 ::close(client);
275
276 if (n <= 0) {
277 continue;
278 }
279
280 buf[n] = '\0';
281 std::string msg(buf);
282
283 const auto p1 = msg.find('|');
284 const auto p2 = (p1 == std::string::npos) ? std::string::npos : msg.find('|', p1 + 1);
285
286 if (p1 == std::string::npos || p2 == std::string::npos) {
287 continue;
288 }
289
290 const std::string token = msg.substr(0, p1);
291 const std::string file = msg.substr(p1 + 1, p2 - p1 - 1);
292 const std::string family = msg.substr(p2 + 1);
293
294 if (file.find("antiransom_inotify.log") != std::string::npos) {
295 log_user(_("[IGNORATO] Alert proveniente dal log interno"));
296 continue;
297 }
298
299 if (token != SECURITY_TOKEN) {
300 log_user(_("[SCARTATO] Token non valido"));
301 continue;
302 }
303
304 // Il percorso ricevuto dal processo realtime deve indicare
305 // esclusivamente un file regolare assoluto. Non seguire
306 // symlink e non mostrare azioni di quarantena per directory.
307 const fs::path alert_path(file);
308 std::error_code path_ec;
309 const fs::file_status path_status =
310 fs::symlink_status(alert_path, path_ec);
311
312 if (!alert_path.is_absolute() || path_ec ||
313 !fs::exists(path_status) ||
314 !fs::is_regular_file(path_status)) {
315 log_user(_("[SCARTATO] Percorso alert non regolare: ") + file);
316 continue;
317 }
318
319 Glib::signal_idle().connect_once([this, file, family]() {
320 if (!running.load()) {
321 return;
322 }
323 enqueue_alert(file, family);
324 });
325 }
326
327 if (server_fd >= 0) {
328 ::close(server_fd);
329 server_fd = -1;
330 }
331
332 log_user(_("[INFO] Listener TCP terminato"));
333 });
334 }
335
337 {
338 const bool was_running = running.exchange(false);
339 if (!was_running) {
340 if (listener_thread.joinable()) {
341 listener_thread.join();
342 }
343 return;
344 }
345
346 if (server_fd >= 0) {
347 ::shutdown(server_fd, SHUT_RDWR);
348 ::close(server_fd);
349 server_fd = -1;
350 }
351
352 if (listener_thread.joinable()) {
353 listener_thread.join();
354 }
355 }
356
357 void enqueue_alert(const std::string& file, const std::string& family)
358 {
359 alertQueue.push({file, family});
360
361 if (!current_window) {
362 show_next();
363 }
364 }
365
367 {
368 if (current_window || alertQueue.empty()) {
369 return;
370 }
371
372 auto [file, family] = alertQueue.front();
373 alertQueue.pop();
374
375 current_window = std::make_unique<AlertWindowRealtime>(file, family, alertQueue.size());
376
377 current_window->signal_ignore_all.connect([this]() {
378 std::queue<std::pair<std::string, std::string>> empty;
379 std::swap(alertQueue, empty);
380
381 if (current_window) {
382 current_window->hide();
383 }
384 });
385
386 current_window->signal_hide().connect([this]() {
387 Glib::signal_idle().connect_once([this]() {
388 current_window.reset();
389 show_next();
390 });
391 });
392
393 current_window->present();
394 }
395};
396
397static bool load_lang_conf()
398{
399 const std::string lang_conf =
400 Glib::get_home_dir() + "/.config/BastionGuard/lang.conf";
401
402 std::ifstream f(lang_conf);
403 if (!f.is_open()) {
404 return false;
405 }
406
407 std::string line;
408 bool any = false;
409
410 while (std::getline(f, line)) {
411 if (line.empty() || line[0] == '#') {
412 continue;
413 }
414
415 const auto pos = line.find('=');
416 if (pos == std::string::npos) {
417 continue;
418 }
419
420 std::string key = line.substr(0, pos);
421 std::string val = line.substr(pos + 1);
422
423 auto ltrim = [](std::string& s) {
424 const auto p = s.find_first_not_of(" \t\r\n");
425 if (p == std::string::npos) {
426 s.clear();
427 } else {
428 s.erase(0, p);
429 }
430 };
431
432 auto rtrim = [](std::string& s) {
433 const auto p = s.find_last_not_of(" \t\r\n");
434 if (p == std::string::npos) {
435 s.clear();
436 } else {
437 s.erase(p + 1);
438 }
439 };
440
441 ltrim(key);
442 rtrim(key);
443 ltrim(val);
444 rtrim(val);
445
446 if (!key.empty() && !val.empty()) {
447 ::setenv(key.c_str(), val.c_str(), 1);
448 any = true;
449 }
450 }
451
452 return any;
453}
454
455int main(int argc, char* argv[])
456{
457 const bool loaded = load_lang_conf();
458
459 std::setlocale(LC_ALL, "");
460 bindtextdomain("BastionGuard", LOCALEDIR);
461 bind_textdomain_codeset("BastionGuard", "UTF-8");
462 textdomain("BastionGuard");
463
464 if (loaded) {
465 std::cerr << _("[Lang] Config caricata da ~/.config/BastionGuard/lang.conf") << '\n';
466 } else {
467 std::cerr << _("[Lang] Nessuna config trovata, uso locale di sistema") << '\n';
468 }
469
470 AlertApp app;
471 return app.run(argc, argv);
472}
static std::string resolve_theme_css()
#define _(s)
int main(int argc, char *argv[])
static bool load_lang_conf()
static std::string resolve_theme_css()
#define LOCALEDIR
#define AF_INET
Definition bsc_ebpf.c:56
std::queue< std::pair< std::string, std::string > > alertQueue
Glib::RefPtr< Gtk::Application > app
int run(int argc, char *argv[])
void log_user(const std::string &msg)
void enqueue_alert(const std::string &file, const std::string &family)
std::string resource(const std::string &rel)
Definition Resource.hpp:31