Skip to content
API Documentation
BastionGuard™
RootGuardClient.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™ RootGuard GTK integration
3 * Copyright (C) 2025–2026 Calogero Scarnà
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6#include "RootGuardClient.hpp"
7
8#include <algorithm>
9#include <array>
10#include <cerrno>
11#include <deque>
12#include <filesystem>
13#include <fstream>
14#include <sstream>
15#include <sys/types.h>
16#include <sys/wait.h>
17#include <unistd.h>
18
19#ifndef BASTIONGUARD_ROOTGUARD_SERVICE_HELPER
20#define BASTIONGUARD_ROOTGUARD_SERVICE_HELPER "/usr/libexec/bastionguard/bastionguard-rootguard-service"
21#endif
22
23#ifndef BASTIONGUARD_ROOTGUARD_ACTION_HELPER
24#define BASTIONGUARD_ROOTGUARD_ACTION_HELPER "/usr/libexec/bastionguard/bastionguard-rootguard-action"
25#endif
26
27#ifndef BASTIONGUARD_ROOTGUARD_EVENT_LOG
28#define BASTIONGUARD_ROOTGUARD_EVENT_LOG "/var/log/bastionguard/rootguard-events.jsonl"
29#endif
30
31#ifndef BASTIONGUARD_ROOTGUARD_POLICY
32#define BASTIONGUARD_ROOTGUARD_POLICY "/etc/bastionguard/rootguard.conf"
33#endif
34
35#ifndef BASTIONGUARD_ROOTGUARD_POLICY_VIEW
36#define BASTIONGUARD_ROOTGUARD_POLICY_VIEW "/run/bastionguard-rootguard.policy-view"
37#endif
38
39#ifndef BASTIONGUARD_PKEXEC
40#define BASTIONGUARD_PKEXEC "/usr/bin/pkexec"
41#endif
42
44namespace {
45
47 int exit_code{127};
48 std::string output;
49};
50
51CommandResult run_command(const std::vector<std::string>& arguments)
52{
53 CommandResult result;
54 if (arguments.empty())
55 return result;
56
57 int descriptors[2]{};
58 if (::pipe(descriptors) != 0)
59 return result;
60
61 const pid_t child = ::fork();
62 if (child < 0) {
63 ::close(descriptors[0]);
64 ::close(descriptors[1]);
65 return result;
66 }
67
68 if (child == 0) {
69 ::close(descriptors[0]);
70 ::dup2(descriptors[1], STDOUT_FILENO);
71 ::dup2(descriptors[1], STDERR_FILENO);
72 ::close(descriptors[1]);
73
74 std::vector<char*> argv;
75 argv.reserve(arguments.size() + 1);
76 for (const auto& argument : arguments)
77 argv.push_back(const_cast<char*>(argument.c_str()));
78 argv.push_back(nullptr);
79
80 ::execv(argv.front(), argv.data());
81 _exit(127);
82 }
83
84 ::close(descriptors[1]);
85 std::array<char, 4096> buffer{};
86 for (;;) {
87 const ssize_t count = ::read(descriptors[0], buffer.data(), buffer.size());
88 if (count > 0) {
89 result.output.append(buffer.data(), static_cast<std::size_t>(count));
90 continue;
91 }
92 if (count < 0 && errno == EINTR)
93 continue;
94 break;
95 }
96 ::close(descriptors[0]);
97
98 int status = 0;
99 while (::waitpid(child, &status, 0) < 0 && errno == EINTR) {
100 }
101 if (WIFEXITED(status))
102 result.exit_code = WEXITSTATUS(status);
103 else if (WIFSIGNALED(status))
104 result.exit_code = 128 + WTERMSIG(status);
105 return result;
106}
107
108std::string trim(std::string value)
109{
110 const auto first = value.find_first_not_of(" \t\r\n");
111 if (first == std::string::npos)
112 return {};
113 const auto last = value.find_last_not_of(" \t\r\n");
114 return value.substr(first, last - first + 1);
115}
116
117bool parse_bool(const std::string& value)
118{
119 return value == "true" || value == "1" || value == "yes" || value == "on";
120}
121
122ServiceStatus parse_status(const std::string& text)
123{
124 ServiceStatus status;
125 std::istringstream input(text);
126 std::string line;
127 while (std::getline(input, line)) {
128 const auto separator = line.find('=');
129 if (separator == std::string::npos)
130 continue;
131 const std::string key = trim(line.substr(0, separator));
132 const std::string value = trim(line.substr(separator + 1));
133 if (key == "active") status.active = parse_bool(value);
134 else if (key == "init") status.init_system = value;
135 else if (key == "state") status.state = value;
136 else if (key == "mode") status.mode = value;
137 else if (key == "metadata_action") status.metadata_action = value;
138 else if (key == "global_metadata_action") status.global_metadata_action = value;
139 else if (key == "auto_block_services") status.autoBlockServices = parse_bool(value);
140 else if (key == "block_user_home") status.blockUserHome = parse_bool(value);
141 else if (key == "pid") status.pid = value;
142 }
143 status.immediateProtection = status.mode == "enforce" &&
144 status.metadata_action == "block";
145 return status;
146}
147
148
150 std::filesystem::path path;
151 bool recursive{};
152};
153
154bool path_starts_with(const std::filesystem::path& path,
155 const std::filesystem::path& prefix)
156{
157 const auto normalized_path = path.lexically_normal();
158 const auto normalized_prefix = prefix.lexically_normal();
159 auto path_iterator = normalized_path.begin();
160 for (auto prefix_iterator = normalized_prefix.begin();
161 prefix_iterator != normalized_prefix.end();
162 ++prefix_iterator, ++path_iterator) {
163 if (path_iterator == normalized_path.end() ||
164 *path_iterator != *prefix_iterator)
165 return false;
166 }
167 return true;
168}
169
170std::vector<IgnoredPathRule> read_ignored_paths()
171{
172 std::ifstream input(BASTIONGUARD_ROOTGUARD_POLICY_VIEW);
173 if (!input)
174 return {};
175
176 std::vector<IgnoredPathRule> rules;
177 std::string line;
178 while (std::getline(input, line)) {
179 const auto separator = line.find('=');
180 if (separator == std::string::npos)
181 continue;
182 const std::string key = trim(line.substr(0, separator));
183 const std::string value = trim(line.substr(separator + 1));
184 if (value.empty() || value.front() != '/')
185 continue;
186 if (key == "ignored_path") {
187 rules.push_back(IgnoredPathRule{
188 std::filesystem::path(value).lexically_normal(), false});
189 } else if (key == "ignored_recursive") {
190 rules.push_back(IgnoredPathRule{
191 std::filesystem::path(value).lexically_normal(), true});
192 }
193 }
194 return rules;
195}
196
197bool is_ignored_event_path(const std::string& filename,
198 const std::vector<IgnoredPathRule>& rules)
199{
200 if (filename.empty() || filename == "--" || filename.front() != '/')
201 return false;
202 const std::filesystem::path path(filename);
203 for (const auto& rule : rules) {
204 if (rule.recursive ? path_starts_with(path, rule.path)
205 : path.lexically_normal() == rule.path)
206 return true;
207 }
208 return false;
209}
210
212{
213 std::ifstream input(BASTIONGUARD_ROOTGUARD_POLICY_VIEW);
214 ApplicationRules rules;
215 if (!input)
216 return rules;
217 std::string line;
218 while (std::getline(input, line)) {
219 const auto separator = line.find('=');
220 if (separator == std::string::npos)
221 continue;
222 const std::string key = trim(line.substr(0, separator));
223 const std::string value = trim(line.substr(separator + 1));
224 if (value.empty())
225 continue;
226 if (key == "trusted_application")
227 rules.trustedApplications.push_back(value);
228 else if (key == "trusted_executable")
229 rules.trustedExecutables.push_back(value);
230 else if (key == "denied_executable")
231 rules.deniedExecutables.push_back(value);
232 }
233 return rules;
234}
235
236std::vector<Event> read_recent_events(const std::size_t maximum)
237{
238 const auto ignored_paths = read_ignored_paths();
239 std::ifstream stream(BASTIONGUARD_ROOTGUARD_EVENT_LOG, std::ios::binary);
240 if (!stream)
241 return {};
242
243 constexpr std::streamoff kMaximumTailBytes = 2 * 1024 * 1024;
244 stream.seekg(0, std::ios::end);
245 const std::streamoff end = stream.tellg();
246 if (end < 0)
247 return {};
248 const std::streamoff start = end > kMaximumTailBytes
249 ? end - kMaximumTailBytes : 0;
250 stream.seekg(start, std::ios::beg);
251 std::string line;
252 if (start > 0)
253 std::getline(stream, line); // discard a potentially partial JSON line
254
255 std::deque<std::string> lines;
256 while (std::getline(stream, line)) {
257 lines.push_back(std::move(line));
258 if (lines.size() > maximum)
259 lines.pop_front();
260 }
261
262 std::vector<Event> events;
263 events.reserve(lines.size());
264 for (const auto& item : lines) {
265 if (auto event = parse_event_json(item)) {
266 if (!is_ignored_event_path(event->filename, ignored_paths))
267 events.push_back(std::move(*event));
268 }
269 }
270 return events;
271}
272
273std::string octal_argument(const std::uint32_t value)
274{
275 std::ostringstream output;
276 output << std::oct << (value & 07777U);
277 return output.str();
278}
279
280bool command_success(const CommandResult& result, const std::string& successFallback,
281 std::string& message)
282{
283 message = trim(result.output);
284 const bool success = result.exit_code == 0;
285 if (message.empty()) {
286 if (success) {
287 message = successFallback;
288 } else {
289 message = "RootGuard command failed with exit code " +
290 std::to_string(result.exit_code) + ".";
291 }
292 }
293 return success;
294}
295
296} // namespace
297
299{
300 dispatcher_.connect(sigc::mem_fun(*this, &Client::on_dispatch));
301 worker_ = std::thread(&Client::worker_loop, this);
302}
303
305{
306 {
307 std::lock_guard lock(mutex_);
308 stopping_ = true;
309 requests_.clear();
310 requests_.push_front(Request{Action::Quit, false, std::nullopt, {}});
311 }
312 condition_.notify_one();
313 if (worker_.joinable())
314 worker_.join();
315}
316
317sigc::signal<void(const Snapshot&)>& Client::signal_updated() noexcept
318{
319 return signal_updated_;
320}
321
322void Client::refresh() { enqueue(Request{Action::Refresh, false, std::nullopt, {}}); }
323void Client::start_service() { enqueue(Request{Action::Start, false, std::nullopt, {}}); }
324void Client::stop_service() { enqueue(Request{Action::Stop, false, std::nullopt, {}}); }
325void Client::reload_policy() { enqueue(Request{Action::Reload, false, std::nullopt, {}}); }
326void Client::restart_service() { enqueue(Request{Action::Restart, false, std::nullopt, {}}); }
327void Client::set_immediate_protection(const bool enabled)
328{
329 enqueue(Request{Action::SetProtection, enabled, std::nullopt, {}});
330}
331void Client::restore_event(const Event& event)
332{
333 enqueue(Request{Action::Restore, false, event, {}});
334}
335void Client::remove_event(const Event& event)
336{
337 enqueue(Request{Action::Remove, false, event, {}});
338}
340{
341 enqueue(Request{Action::SaveApplicationRules, false, std::nullopt,
342 std::move(rules)});
343}
344
346{
347 {
348 std::lock_guard lock(mutex_);
349 if (stopping_)
350 return;
351 if (request.action == Action::Refresh) {
352 const bool refreshAlreadyQueued = std::any_of(
353 requests_.begin(), requests_.end(),
354 [](const Request& queued) { return queued.action == Action::Refresh; });
355 if (refreshAlreadyQueued)
356 return;
357 }
358 requests_.push_back(std::move(request));
359 }
360 condition_.notify_one();
361}
362
363Snapshot Client::collect_snapshot(const std::string& operation_message,
364 const bool operation_success) const
365{
366 const CommandResult status_result = run_command({
368 });
369
370 Snapshot snapshot;
371 snapshot.service = parse_status(status_result.output);
372 snapshot.events = read_recent_events(400);
373 snapshot.application_rules = read_application_rules();
374 snapshot.operation_message = operation_message;
375 snapshot.operation_success = operation_success;
376 if (snapshot.service.state == "unknown" && status_result.exit_code != 0)
377 snapshot.service.state = "unavailable";
378 return snapshot;
379}
380
381bool Client::run_request(const Request& request, std::string& message) const
382{
383 if (request.action == Action::Refresh)
384 return true;
385
386 if (request.action == Action::Start || request.action == Action::Stop ||
387 request.action == Action::Reload || request.action == Action::Restart) {
388 const char* action = request.action == Action::Start ? "start" :
389 request.action == Action::Stop ? "stop" :
390 request.action == Action::Reload ? "reload" : "restart";
391 return command_success(run_command({
393 }), std::string("RootGuard action completed: ") + action, message);
394 }
395
396 if (request.action == Action::SetProtection) {
397 const auto result = run_command({
399 "set-protection", "--state", request.enabled ? "on" : "off"
400 });
401 if (!command_success(result, "RootGuard protection mode updated.", message))
402 return false;
403 const auto reload = run_command({
405 });
406 std::string reloadMessage;
407 const bool reloadSuccess = command_success(
408 reload, "RootGuard policy reloaded.", reloadMessage);
409 if (!reloadMessage.empty())
410 message += " " + reloadMessage;
411 if (!reloadSuccess && request.enabled) {
412 const auto rollback = run_command({
414 "set-protection", "--state", "off"
415 });
416 std::string rollbackMessage;
417 const bool rollbackSuccess = command_success(
418 rollback, "Unsafe future enablement was rolled back to audit mode.",
419 rollbackMessage);
420 message += rollbackSuccess
421 ? " " + rollbackMessage
422 : " WARNING: the policy rollback to audit mode failed: " + rollbackMessage;
423 }
424 return reloadSuccess;
425 }
426
427 if (request.action == Action::SaveApplicationRules) {
428 std::vector<std::string> command {
431 "set-application-rules"
432 };
433 const auto appendRules = [&command](const char* option,
434 const std::vector<std::string>& entries) {
435 for (const auto& entry : entries) {
436 command.emplace_back(option);
437 command.push_back(entry);
438 }
439 };
440 appendRules("--trusted-application",
442 appendRules("--trusted-executable",
444 appendRules("--denied-executable",
446
447 const auto saveResult = run_command(command);
448 if (!command_success(saveResult,
449 "RootGuard application rules saved.", message))
450 return false;
451
452 const auto restartResult = run_command({
455 "restart"
456 });
457 std::string restartMessage;
458 const bool restartSuccess = command_success(
459 restartResult, "RootGuard restarted with the new rules.",
460 restartMessage);
461 if (!restartMessage.empty())
462 message += " " + restartMessage;
463 return restartSuccess;
464 }
465
466 if (!request.event)
467 return false;
468 const Event& event = *request.event;
469 if (event.filename.empty()) {
470 message = "The incident has no actionable path.";
471 return false;
472 }
473
474 if (request.action == Action::Restore) {
475 const bool metadataAvailable = event.old_mode != 0 || event.new_mode != 0 ||
476 event.old_uid != 0 || event.new_uid != 0 ||
477 event.old_gid != 0 || event.new_gid != 0;
478 if (!metadataAvailable) {
479 if (is_system_service_path(event.filename)) {
480 return command_success(run_command({
482 "unblock-service", "--path", event.filename
483 }), "The blocked service was unmasked. It was not restarted.", message);
484 }
485 message = "The change was denied before it was committed; no metadata restoration was required.";
486 return true;
487 }
488 return command_success(run_command({
490 "restore", "--path", event.filename,
491 "--mode", octal_argument(event.old_mode),
492 "--uid", std::to_string(event.old_uid),
493 "--gid", std::to_string(event.old_gid),
494 "--device", std::to_string(event.device),
495 "--inode", std::to_string(event.inode)
496 }), "Protected metadata restored.", message);
497 }
498
499 if (request.action == Action::Remove) {
500 return command_success(run_command({
502 "remove", "--path", event.filename,
503 "--device", std::to_string(event.device),
504 "--inode", std::to_string(event.inode)
505 }), "The file was removed and quarantined.", message);
506 }
507
508 return false;
509}
510
512{
513 for (;;) {
514 Request request;
515 {
516 std::unique_lock lock(mutex_);
517 condition_.wait(lock, [this] { return !requests_.empty(); });
518 request = std::move(requests_.front());
519 requests_.pop_front();
520 }
521
522 if (request.action == Action::Quit)
523 break;
524
525 std::string message;
526 const bool success = run_request(request, message);
527 Snapshot snapshot = collect_snapshot(message, success);
528 {
529 std::lock_guard lock(mutex_);
530 pending_snapshot_ = std::move(snapshot);
531 snapshot_ready_ = true;
532 }
533 dispatcher_.emit();
534 }
535}
536
538{
539 Snapshot snapshot;
540 {
541 std::lock_guard lock(mutex_);
542 if (!snapshot_ready_)
543 return;
544 snapshot = std::move(pending_snapshot_);
545 snapshot_ready_ = false;
546 }
547 signal_updated_.emit(snapshot);
548}
549
550} // namespace BastionGuard::RootGuard
#define BASTIONGUARD_ROOTGUARD_ACTION_HELPER
#define BASTIONGUARD_ROOTGUARD_SERVICE_HELPER
#define BASTIONGUARD_ROOTGUARD_POLICY_VIEW
#define BASTIONGUARD_PKEXEC
#define BASTIONGUARD_ROOTGUARD_EVENT_LOG
sigc::signal< void(const Snapshot &)> & signal_updated() noexcept
Snapshot collect_snapshot(const std::string &operation_message, bool operation_success) const
sigc::signal< void(const Snapshot &)> signal_updated_
bool run_request(const Request &request, std::string &message) const
void save_application_rules(ApplicationRules rules)
void remove_event(const Event &event)
void restore_event(const Event &event)
std::condition_variable condition_
void set_immediate_protection(bool enabled)
CommandResult run_command(const std::vector< std::string > &arguments)
bool command_success(const CommandResult &result, const std::string &successFallback, std::string &message)
bool path_starts_with(const std::filesystem::path &path, const std::filesystem::path &prefix)
std::vector< Event > read_recent_events(const std::size_t maximum)
bool is_ignored_event_path(const std::string &filename, const std::vector< IgnoredPathRule > &rules)
std::optional< Event > parse_event_json(const std::string &line)
bool is_system_service_path(const std::string &path) noexcept
std::vector< std::string > deniedExecutables
std::vector< std::string > trustedExecutables
std::vector< std::string > trustedApplications