Skip to content
API Documentation
BastionGuard™
RootGuardEvent.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 "RootGuardEvent.hpp"
8
9#include <functional>
10#include <iomanip>
11#include <limits>
12#include <sstream>
13
15namespace {
16
17std::optional<std::string> json_string(const std::string& json,
18 const std::string& key)
19{
20 const std::string marker = "\"" + key + "\":\"";
21 const auto begin = json.find(marker);
22 if (begin == std::string::npos)
23 return std::nullopt;
24
25 std::string result;
26 bool escaped = false;
27 for (std::size_t index = begin + marker.size(); index < json.size(); ++index) {
28 const char character = json[index];
29 if (escaped) {
30 switch (character) {
31 case 'n': result.push_back('\n'); break;
32 case 'r': result.push_back('\r'); break;
33 case 't': result.push_back('\t'); break;
34 case '\\': result.push_back('\\'); break;
35 case '"': result.push_back('"'); break;
36 default: result.push_back(character); break;
37 }
38 escaped = false;
39 } else if (character == '\\') {
40 escaped = true;
41 } else if (character == '"') {
42 return result;
43 } else {
44 result.push_back(character);
45 }
46 }
47 return std::nullopt;
48}
49
50std::optional<std::uint64_t> json_u64(const std::string& json,
51 const std::string& key)
52{
53 const std::string marker = "\"" + key + "\":";
54 const auto begin = json.find(marker);
55 if (begin == std::string::npos)
56 return std::nullopt;
57
58 std::size_t consumed = 0;
59 try {
60 const unsigned long long value =
61 std::stoull(json.substr(begin + marker.size()), &consumed, 10);
62 if (consumed == 0)
63 return std::nullopt;
64 return static_cast<std::uint64_t>(value);
65 } catch (...) {
66 return std::nullopt;
67 }
68}
69
70std::optional<std::int64_t> json_i64(const std::string& json,
71 const std::string& key)
72{
73 const std::string marker = "\"" + key + "\":";
74 const auto begin = json.find(marker);
75 if (begin == std::string::npos)
76 return std::nullopt;
77
78 std::size_t consumed = 0;
79 try {
80 const long long value =
81 std::stoll(json.substr(begin + marker.size()), &consumed, 10);
82 if (consumed == 0)
83 return std::nullopt;
84 return static_cast<std::int64_t>(value);
85 } catch (...) {
86 return std::nullopt;
87 }
88}
89
90std::uint32_t as_u32(const std::optional<std::uint64_t>& value)
91{
92 return value && *value <= 0xffffffffULL
93 ? static_cast<std::uint32_t>(*value) : 0U;
94}
95
96std::int32_t as_i32(const std::optional<std::int64_t>& value)
97{
98 if (!value ||
99 *value < std::numeric_limits<std::int32_t>::min() ||
100 *value > std::numeric_limits<std::int32_t>::max())
101 return 0;
102 return static_cast<std::int32_t>(*value);
103}
104
105std::string octal_mode(const std::uint32_t mode)
106{
107 std::ostringstream output;
108 output << std::oct << (mode & 07777U);
109 return output.str();
110}
111
112bool has_prefix(const std::string& path, const std::string& prefix)
113{
114 return path == prefix ||
115 (path.size() > prefix.size() && path.compare(0, prefix.size(), prefix) == 0 &&
116 path[prefix.size()] == '/');
117}
118
119
120} // namespace
121
122std::optional<Event> parse_event_json(const std::string& line)
123{
124 Event event;
125 event.raw_json = line;
126
127 const auto event_name = json_string(line, "event");
128 const auto verdict = json_string(line, "verdict");
129 if (!event_name || !verdict)
130 return std::nullopt;
131
132 event.received_at = json_string(line, "received_at").value_or("");
133 event.event = *event_name;
134 event.verdict = *verdict;
135 event.pid = as_u32(json_u64(line, "pid"));
136 event.tgid = as_u32(json_u64(line, "tgid"));
137 event.ppid = as_u32(json_u64(line, "ppid"));
138 event.old_euid = as_u32(json_u64(line, "old_euid"));
139 event.new_euid = as_u32(json_u64(line, "new_euid"));
140 event.rule_id = as_u32(json_u64(line, "rule_id"));
141 event.reason_flags = as_u32(json_u64(line, "reason_flags"));
142 event.lsm_flags = as_i32(json_i64(line, "lsm_flags"));
143 event.auxiliary = as_u32(json_u64(line, "auxiliary"));
144 event.device = json_u64(line, "device").value_or(0);
145 event.inode = json_u64(line, "inode").value_or(0);
146 event.actor_device = json_u64(line, "actor_device").value_or(0);
147 event.actor_inode = json_u64(line, "actor_inode").value_or(0);
148 event.parent_device = json_u64(line, "parent_device").value_or(0);
149 event.parent_inode = json_u64(line, "parent_inode").value_or(0);
150 event.actor_start_boottime_ns =
151 json_u64(line, "actor_start_boottime_ns").value_or(0);
152 event.old_mode = as_u32(json_u64(line, "old_mode"));
153 event.new_mode = as_u32(json_u64(line, "new_mode"));
154 event.old_uid = as_u32(json_u64(line, "old_uid"));
155 event.new_uid = as_u32(json_u64(line, "new_uid"));
156 event.old_gid = as_u32(json_u64(line, "old_gid"));
157 event.new_gid = as_u32(json_u64(line, "new_gid"));
158 event.comm = json_string(line, "comm").value_or("unknown");
159 event.parent_comm = json_string(line, "parent_comm").value_or("");
160 event.actor_executable = json_string(line, "actor_executable").value_or("");
161 event.parent_executable = json_string(line, "parent_executable").value_or("");
162 event.filename = json_string(line, "filename").value_or("");
163 event.path_resolution = json_string(line, "path_resolution").value_or("basename-only");
164 return event;
165}
166
167std::string format_event(const Event& event)
168{
169 std::ostringstream output;
170 output << '[' << (event.received_at.empty() ? "unknown time" : event.received_at)
171 << "] [" << event.verdict << "] " << event.event
172 << " process=" << event.comm
173 << " pid=" << event.pid
174 << " tgid=" << event.tgid
175 << " ppid=" << event.ppid;
176
177 if (event.old_euid != event.new_euid)
178 output << " euid=" << event.old_euid << "->" << event.new_euid;
179 if (event.old_mode != event.new_mode)
180 output << " mode=" << octal_mode(event.old_mode)
181 << "->" << octal_mode(event.new_mode);
182 if (event.old_uid != event.new_uid || event.old_gid != event.new_gid)
183 output << " owner=" << event.old_uid << ':' << event.old_gid
184 << "->" << event.new_uid << ':' << event.new_gid;
185 if (event.rule_id != 0)
186 output << " rule=" << event.rule_id;
187 if (!event.parent_comm.empty())
188 output << " parent=" << event.parent_comm;
189 if (!event.parent_executable.empty())
190 output << " parent-exe=" << event.parent_executable;
191 if (event.parent_device != 0 || event.parent_inode != 0)
192 output << " parent-identity=" << event.parent_device
193 << ':' << event.parent_inode;
194 if (!event.actor_executable.empty())
195 output << " actor=" << event.actor_executable;
196 if (event.actor_device != 0 || event.actor_inode != 0)
197 output << " actor-identity=" << event.actor_device
198 << ':' << event.actor_inode;
199 if (!event.filename.empty())
200 output << " file=" << event.filename
201 << " path-resolution=" << event.path_resolution;
202 if (event.device != 0 || event.inode != 0)
203 output << " identity=" << event.device << ':' << event.inode;
204
205 output << " reason=0x" << std::hex << event.reason_flags << std::dec;
206 return output.str();
207}
208
209std::string event_fingerprint(const Event& event)
210{
211 const auto hash = std::hash<std::string>{}(event.raw_json);
212 std::ostringstream output;
213 output << std::hex << hash;
214 return output.str();
215}
216
217EventSeverity classify_event(const Event& event) noexcept
218{
219 if (event.filename.empty() || event.filename == "--")
221 if (event.verdict != "blocked" && event.verdict != "alert" &&
222 event.verdict != "audit")
224
225 if (event.event == "service-blocked")
227
228 const bool metadataEvent =
229 event.event == "permission-change" ||
230 event.event == "owner-change" ||
231 event.event == "xattr-change" ||
232 event.event == "acl-change";
233 if (metadataEvent) {
234 /*
235 * Explicitly protected identities retain their existing incident
236 * semantics because they have a baseline and may have been blocked.
237 */
238 if (event.rule_id != 0)
240
241 /*
242 * Desktop policy: unprotected global privilege metadata is diagnostic
243 * telemetry only. Keep it visible as an observation when explicitly
244 * enabled, but never let backup/package metadata replay interrupt the
245 * user with a popup. Historical broad-surveillance records remain
246 * telemetry-only.
247 */
248 if ((event.reason_flags & RG_REASON_GLOBAL_METADATA) != 0)
249 return (event.reason_flags & RG_REASON_METADATA_BURST) != 0
252
254 }
255
256 if (event.event == "protected-unlink" ||
257 event.event == "protected-rename" ||
258 event.event == "protected-hardlink" ||
259 event.event == "integrity-drift")
260 return event.rule_id != 0
263
265}
266
267bool is_security_observation(const Event& event) noexcept
268{
270}
271
272bool is_actionable_incident(const Event& event) noexcept
273{
275}
276
277bool is_system_service_path(const std::string& path) noexcept
278{
279 if (path.empty())
280 return false;
281 const bool systemd = has_prefix(path, "/etc/systemd/system") ||
282 has_prefix(path, "/usr/lib/systemd/system") ||
283 has_prefix(path, "/lib/systemd/system");
284 const bool initScript = has_prefix(path, "/etc/init.d") ||
285 has_prefix(path, "/etc/conf.d");
286 const bool dinit = has_prefix(path, "/etc/dinit.d") ||
287 has_prefix(path, "/usr/lib/dinit.d");
288 return systemd || initScript || dinit;
289}
290
291} // namespace BastionGuard::RootGuard
nlohmann::json json
Definition Backend.cpp:2908
std::optional< std::int64_t > json_i64(const std::string &json, const std::string &key)
std::optional< std::string > json_string(const std::string &json, const std::string &key)
std::uint32_t as_u32(const std::optional< std::uint64_t > &value)
bool has_prefix(const std::string &path, const std::string &prefix)
std::int32_t as_i32(const std::optional< std::int64_t > &value)
std::optional< std::uint64_t > json_u64(const std::string &json, const std::string &key)
std::string event_fingerprint(const Event &event)
bool is_actionable_incident(const Event &event) noexcept
bool is_security_observation(const Event &event) noexcept
std::string format_event(const Event &event)
std::optional< Event > parse_event_json(const std::string &line)
EventSeverity classify_event(const Event &event) noexcept
bool is_system_service_path(const std::string &path) noexcept
@ RG_REASON_METADATA_BURST
@ RG_REASON_GLOBAL_METADATA