29namespace fs = std::filesystem;
35constexpr const char*
kQuarantineDir =
"/var/lib/bastionguard/rootguard/quarantine";
41 std::optional<std::uint32_t>
mode;
42 std::optional<std::uint32_t>
uid;
43 std::optional<std::uint32_t>
gid;
45 std::optional<std::uint64_t>
inode;
52[[noreturn]]
void fail(
const std::string& message)
54 throw std::runtime_error(message);
60 std::size_t consumed = 0;
61 unsigned long long parsed = 0;
63 parsed = std::stoull(value, &consumed, base);
65 fail(std::string(
"Invalid ") + label +
": " + value);
67 if (consumed != value.size())
68 fail(std::string(
"Invalid ") + label +
": " + value);
69 return static_cast<std::uint64_t
>(parsed);
75 fail(
"Usage: bastionguard-rootguard-action <set-protection|set-application-rules|restore|remove|block-service|unblock-service> [options]");
79 for (
int index = 2; index < argc; ++index) {
80 const std::string key = argv[index];
81 if (index + 1 >= argc)
82 fail(
"Missing value for " + key);
83 const std::string value = argv[++index];
84 if (key ==
"--policy") options.
policy = value;
85 else if (key ==
"--path") options.
path = value;
86 else if (key ==
"--mode") options.
mode =
static_cast<std::uint32_t
>(
parseUnsigned(value,
"mode", 8));
87 else if (key ==
"--uid") options.
uid =
static_cast<std::uint32_t
>(
parseUnsigned(value,
"uid"));
88 else if (key ==
"--gid") options.
gid =
static_cast<std::uint32_t
>(
parseUnsigned(value,
"gid"));
91 else if (key ==
"--state") options.
protection = value;
93 else if (key ==
"--trusted-executable") options.
trustedExecutables.emplace_back(value);
94 else if (key ==
"--denied-executable") options.
deniedExecutables.emplace_back(value);
95 else fail(
"Unknown option: " + key);
102 const auto normalizedPath = path.lexically_normal();
103 const auto normalizedPrefix = prefix.lexically_normal();
104 auto pathIt = normalizedPath.begin();
105 for (
auto prefixIt = normalizedPrefix.begin(); prefixIt != normalizedPrefix.end();
106 ++prefixIt, ++pathIt) {
107 if (pathIt == normalizedPath.end() || *pathIt != *prefixIt)
115 if (path.empty() || !path.is_absolute())
116 fail(
"An absolute path is required.");
117 const auto normalized = path.lexically_normal();
118 for (
const auto& component : normalized) {
119 if (component ==
"..")
120 fail(
"Parent traversal is not allowed.");
129 const auto ignoredPath = rule.path.lexically_normal();
131 : path == ignoredPath)
144 const auto protectedPath = rule.path.lexically_normal();
146 : path == protectedPath)
155 fail(
"The requested path is not covered by the RootGuard protected-path policy.");
160 const auto now = std::chrono::system_clock::now();
161 const auto time = std::chrono::system_clock::to_time_t(now);
163 localtime_r(&time, &tm);
164 std::ostringstream output;
165 output << std::put_time(&tm,
"%Y%m%d-%H%M%S");
170 const std::string& source)
174 const auto count = std::min(size - 1, source.size());
175 std::memcpy(destination, source.data(), count);
176 destination[count] =
'\0';
180 const std::uint32_t eventType,
181 const std::uint32_t verdict,
182 const struct stat* before =
nullptr,
183 const struct stat* after =
nullptr)
188 if (
const auto parent = policy.
eventLog.parent_path(); !parent.empty())
189 fs::create_directories(parent);
192 event.
pid =
static_cast<__u32
>(::getpid());
193 event.tgid =
event.pid;
194 event.ppid =
static_cast<__u32
>(::getppid());
195 event.event_type = eventType;
196 event.verdict = verdict;
199 event.file.inode =
static_cast<__u64
>(before->st_ino);
200 event.old_mode =
static_cast<__u32
>(before->st_mode & 07777U);
201 event.old_uid =
static_cast<__u32
>(before->st_uid);
202 event.old_gid =
static_cast<__u32
>(before->st_gid);
205 event.new_mode =
static_cast<__u32
>(after->st_mode & 07777U);
206 event.new_uid =
static_cast<__u32
>(after->st_uid);
207 event.new_gid =
static_cast<__u32
>(after->st_gid);
210 copyString(event.comm,
sizeof(event.comm),
"rootguard-action");
211 std::error_code actorError;
212 const fs::path actor = fs::read_symlink(
"/proc/self/exe", actorError);
215 sizeof(event.actor_executable), actor.string());
216 copyString(event.filename,
sizeof(event.filename), path.string());
228std::string
trim(std::string value)
230 const auto first = value.find_first_not_of(
" \t\r\n");
231 if (first == std::string::npos)
233 const auto last = value.find_last_not_of(
" \t\r\n");
234 return value.substr(first, last - first + 1);
239 struct stat metadata{};
240 if (::lstat(policyPath.c_str(), &metadata) != 0)
241 fail(std::string(
"Cannot stat policy: ") + std::strerror(errno));
242 if (!S_ISREG(metadata.st_mode) || S_ISLNK(metadata.st_mode))
243 fail(
"The RootGuard policy must be a regular non-symlink file.");
245 std::ifstream input(policyPath);
247 fail(
"Cannot read the RootGuard policy.");
248 std::vector<std::string> lines;
250 while (std::getline(input, line))
251 lines.push_back(line);
253 const std::map<std::string, std::string> replacements{
254 {
"mode", enabled ?
"enforce" :
"audit"},
255 {
"metadata_action", enabled ?
"block" :
"audit"},
256 {
"global_metadata_action",
"audit"},
257 {
"block_user_home",
"false"},
259 std::map<std::string, bool> found;
260 bool inEngine =
false;
261 std::size_t insertion = lines.size();
262 for (std::size_t index = 0; index < lines.size(); ++index) {
263 const std::string stripped =
trim(lines[index]);
264 if (!stripped.empty() && stripped.front() ==
'[' && stripped.back() ==
']') {
265 if (inEngine && stripped !=
"[engine]") {
269 inEngine = stripped ==
"[engine]";
273 if (!inEngine || stripped.empty() || stripped.front() ==
'#' || stripped.front() ==
';')
275 const auto separator = stripped.find(
'=');
276 if (separator == std::string::npos)
278 const std::string key =
trim(stripped.substr(0, separator));
279 const auto replacement = replacements.find(key);
280 if (replacement != replacements.end()) {
281 lines[index] = key +
" = " + replacement->second;
285 if (insertion == lines.size() && inEngine)
286 insertion = lines.size();
287 for (
auto iterator = replacements.rbegin(); iterator != replacements.rend(); ++iterator) {
288 if (!found[iterator->first])
289 lines.insert(lines.begin() +
static_cast<std::ptrdiff_t
>(insertion),
290 iterator->first +
" = " + iterator->second);
293 const fs::path temporary = policyPath.string() +
".rootguard.tmp." +
294 std::to_string(::getpid());
296 std::ofstream output(temporary, std::ios::trunc);
298 fail(
"Cannot create the temporary policy file.");
299 for (
const auto& item : lines)
300 output << item <<
'\n';
303 fail(
"Cannot write the temporary policy file.");
305 if (::chmod(temporary.c_str(), metadata.st_mode & 07777U) != 0 ||
306 ::chown(temporary.c_str(), metadata.st_uid, metadata.st_gid) != 0) {
307 fs::remove(temporary);
308 fail(std::string(
"Cannot preserve policy ownership/mode: ") + std::strerror(errno));
310 if (::rename(temporary.c_str(), policyPath.c_str()) != 0) {
311 fs::remove(temporary);
312 fail(std::string(
"Cannot atomically replace the policy: ") + std::strerror(errno));
317 const std::vector<fs::path>& entries,
320 std::vector<fs::path> normalized;
321 std::set<std::string> seen;
322 for (
const auto& entry : entries) {
324 struct stat metadata {};
325 const bool exists = ::stat(path.c_str(), &metadata) == 0;
326 if (!exists && errno != ENOENT)
327 fail(std::string(
"Cannot inspect ") + label +
": " + path.string() +
328 ": " + std::strerror(errno));
329 if (exists && !S_ISREG(metadata.st_mode))
330 fail(std::string(label) +
" is not a regular file: " + path.string());
331 if (exists && ::access(path.c_str(), X_OK) != 0)
332 fail(std::string(label) +
" is not executable: " + path.string());
340 std::error_code error;
341 const fs::path canonical = fs::weakly_canonical(path, error);
342 const fs::path finalPath = error ? path : canonical;
343 if (seen.insert(finalPath.string()).second)
344 normalized.push_back(finalPath);
346 std::sort(normalized.begin(), normalized.end());
351 const std::vector<std::string>& lines,
352 const struct stat& metadata)
354 const fs::path temporary = policyPath.string() +
".rootguard.tmp." +
355 std::to_string(::getpid());
357 std::ofstream output(temporary, std::ios::trunc);
359 fail(
"Cannot create the temporary policy file.");
360 for (
const auto& item : lines)
361 output << item <<
'\n';
364 fail(
"Cannot write the temporary policy file.");
367 std::ifstream validationInput(temporary);
368 std::ostringstream validationText;
369 validationText << validationInput.rdbuf();
372 }
catch (
const std::exception& error) {
373 fs::remove(temporary);
374 fail(std::string(
"Refusing to install an invalid RootGuard policy: ") +
378 if (::chmod(temporary.c_str(), metadata.st_mode & 07777U) != 0 ||
379 ::chown(temporary.c_str(), metadata.st_uid, metadata.st_gid) != 0) {
380 fs::remove(temporary);
381 fail(std::string(
"Cannot preserve policy ownership/mode: ") +
382 std::strerror(errno));
384 if (::rename(temporary.c_str(), policyPath.c_str()) != 0) {
385 fs::remove(temporary);
386 fail(std::string(
"Cannot atomically replace the policy: ") +
387 std::strerror(errno));
393 struct stat metadata {};
394 if (::lstat(options.
policy.c_str(), &metadata) != 0)
395 fail(std::string(
"Cannot stat policy: ") + std::strerror(errno));
396 if (!S_ISREG(metadata.st_mode) || S_ISLNK(metadata.st_mode))
397 fail(
"The RootGuard policy must be a regular non-symlink file.");
406 std::set<std::string> trustedKeys;
407 for (
const auto& path : trustedApplications)
408 trustedKeys.insert(path.string());
409 for (
const auto& path : trustedExecutables)
410 trustedKeys.insert(path.string());
411 for (
const auto& path : deniedExecutables) {
412 if (trustedKeys.count(path.string()) != 0)
413 fail(
"An executable cannot be both trusted and denied: " + path.string());
416 std::ifstream input(options.
policy);
418 fail(
"Cannot read the RootGuard policy.");
419 std::vector<std::string> sourceLines;
421 while (std::getline(input, line))
422 sourceLines.push_back(line);
424 const std::map<std::string, std::vector<fs::path>> replacements {
425 {
"trusted-applications", trustedApplications},
426 {
"trusted-executables", trustedExecutables},
427 {
"denied-executables", deniedExecutables},
429 std::set<std::string> emitted;
430 std::vector<std::string> output;
431 std::string skippedSection;
433 const auto emitSection = [&](
const std::string& section,
434 std::vector<std::string>& destination) {
435 destination.push_back(
"[" + section +
"]");
436 if (section ==
"trusted-applications") {
437 destination.push_back(
438 "# Routine user-space metadata activity only. This never grants privilege trust.");
439 }
else if (section ==
"trusted-executables") {
440 destination.push_back(
441 "# Executables trusted specifically for privilege transitions.");
443 destination.push_back(
444 "# Executables denied during RootGuard privilege-transition checks.");
446 for (
const auto& path : replacements.at(section))
447 destination.push_back(
"path = " + path.string());
448 destination.push_back(
"");
451 for (
const auto& sourceLine : sourceLines) {
452 const std::string stripped =
trim(sourceLine);
453 if (!stripped.empty() && stripped.front() ==
'[' && stripped.back() ==
']') {
454 const std::string section = stripped.substr(1, stripped.size() - 2);
455 skippedSection.clear();
456 if (replacements.count(section) != 0) {
457 skippedSection = section;
458 if (emitted.insert(section).second)
459 emitSection(section, output);
463 if (!skippedSection.empty())
465 output.push_back(sourceLine);
468 for (
const auto& [section, entries] : replacements) {
470 if (emitted.insert(section).second) {
471 if (!output.empty() && !output.back().empty())
472 output.push_back(
"");
473 emitSection(section, output);
478 std::cout <<
"RootGuard application rules saved. A service restart is required.\n";
483 if (!options.
mode || !options.
uid || !options.
gid)
484 fail(
"restore requires --mode, --uid and --gid.");
488 struct stat before{};
489 if (::lstat(path.c_str(), &before) != 0)
490 fail(std::string(
"Cannot restore missing path: ") + std::strerror(errno));
491 if (S_ISLNK(before.st_mode))
492 fail(
"Refusing to restore metadata through a symbolic link.");
495 fail(
"The file device no longer matches the protected baseline.");
497 static_cast<std::uint64_t
>(before.st_ino) != *options.
inode)
498 fail(
"The file inode no longer matches the protected baseline. Quarantine the replacement instead.");
500 const int openFlags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW |
501 (S_ISDIR(before.st_mode) ? O_DIRECTORY : O_NONBLOCK);
502 const int descriptor = ::open(path.c_str(), openFlags);
504 fail(std::string(
"Secure open failed: ") + std::strerror(errno));
505 struct stat verified{};
506 if (::fstat(descriptor, &verified) != 0 ||
507 verified.st_dev != before.st_dev || verified.st_ino != before.st_ino) {
509 fail(
"The protected path changed during the restore operation.");
511 if (::fchown(descriptor,
static_cast<uid_t
>(*options.
uid),
512 static_cast<gid_t
>(*options.
gid)) != 0) {
513 const std::string error = std::strerror(errno);
515 fail(
"fchown failed: " + error);
517 if (::fchmod(descriptor,
static_cast<mode_t
>(*options.
mode & 07777U)) != 0) {
518 const std::string error = std::strerror(errno);
520 fail(
"fchmod failed: " + error);
527 ::lstat(path.c_str(), &after);
530 std::cout <<
"Protected metadata restored. A system service, when applicable, was unmasked but not restarted.\n";
537 struct stat before{};
538 if (::lstat(path.c_str(), &before) != 0)
539 fail(std::string(
"Cannot remove path: ") + std::strerror(errno));
540 if (S_ISDIR(before.st_mode))
541 fail(
"Directory removal is not supported by the RootGuard incident action.");
543 std::string serviceWarning;
545 const bool identityMatches = options.
device && options.
inode &&
548 static_cast<std::uint64_t
>(before.st_ino) == *options.
inode;
551 if (scriptBased && !identityMatches) {
553 "The affected OpenRC/SysV definition may have been replaced; "
554 "RootGuard did not execute it as root. The service process may require manual containment.";
558 serviceWarning =
"Service blocking was not confirmed: " + result.message;
565 (
timestamp() +
"-" + std::to_string(::getpid()) +
"-" + path.filename().string());
566 if (::rename(path.c_str(), destination.c_str()) != 0)
567 fail(std::string(
"Quarantine rename failed: ") + std::strerror(errno));
569 std::ofstream manifest(destination.string() +
".manifest", std::ios::trunc);
570 manifest <<
"original_path=" << path.string() <<
'\n'
571 <<
"mode=" << std::oct << (before.st_mode & 07777U) << std::dec <<
'\n'
572 <<
"uid=" << before.st_uid <<
'\n'
573 <<
"gid=" << before.st_gid <<
'\n'
575 <<
"inode=" << before.st_ino <<
'\n';
577 ::chmod(destination.c_str(), 0000);
580 std::cout <<
"The file was removed from its original location and quarantined at "
581 << destination <<
".\n";
586int main(
const int argc,
char* argv[])
589 if (::geteuid() != 0)
590 fail(
"Administrative privileges are required.");
594 if (options.command ==
"set-protection") {
595 if (options.protection !=
"on" && options.protection !=
"off")
596 fail(
"set-protection requires --state on|off.");
597 const bool enabled = options.protection ==
"on";
602 std::cout << (enabled
603 ?
"Immediate blocking enabled for protected system paths; user home remains audit-only."
604 :
"Immediate metadata blocking disabled.") <<
'\n';
605 }
else if (options.command ==
"set-application-rules") {
607 }
else if (options.command ==
"restore") {
609 }
else if (options.command ==
"remove") {
611 }
else if (options.command ==
"block-service") {
616 struct stat current{};
617 if (!options.device || !options.inode ||
618 ::lstat(path.c_str(), ¤t) != 0 ||
620 static_cast<std::uint64_t
>(current.st_ino) != *options.inode) {
621 fail(
"Refusing to execute an OpenRC/SysV service definition without a matching protected identity.");
626 if (!result.success)
fail(result.message);
628 std::cout << result.message <<
'\n';
629 }
else if (options.command ==
"unblock-service") {
633 if (!result.success)
fail(result.message);
634 std::cout << result.message <<
'\n';
636 fail(
"Unknown action: " + options.command);
639 }
catch (
const std::exception& error) {
640 std::cerr <<
"RootGuard action: " << error.what() <<
'\n';
int main(int argc, char *argv[])
static __u64 kernelDeviceNumber(dev_t encodedDevice)
void onEvent(const rg_event &event) noexcept override
static Policy loadFromText(const std::string &content, const std::filesystem::path &source="<memory>")
static std::string read(const std::filesystem::path &path, bool requireSecureOwnership)
std::uint64_t parseUnsigned(const std::string &value, const char *label, const int base=10)
Policy loadPolicy(const fs::path &path)
std::string trim(std::string value)
std::vector< fs::path > normalizeApplicationEntries(const std::vector< fs::path > &entries, const char *label)
void fail(const std::string &message)
Options parseOptions(const int argc, char *argv[])
constexpr const char * kQuarantineDir
void requireProtectedPath(const Policy &policy, const fs::path &path)
fs::path normalizedAbsolute(const fs::path &path)
void quarantineFile(const Options &options, const Policy &policy)
void rewriteApplicationRules(const Options &options)
void logAction(const Policy &policy, const fs::path &path, const std::uint32_t eventType, const std::uint32_t verdict, const struct stat *before=nullptr, const struct stat *after=nullptr)
bool isProtectedPath(const Policy &policy, const fs::path &requested)
constexpr const char * kDefaultPolicy
bool isIgnoredPath(const Policy &policy, const fs::path &requested)
void rewriteProtectionPolicy(const fs::path &policyPath, const bool enabled)
void copyString(char *destination, const std::size_t size, const std::string &source)
bool pathStartsWith(const fs::path &path, const fs::path &prefix)
void restoreFile(const Options &options, const Policy &policy)
void writePolicyAtomically(const fs::path &policyPath, const std::vector< std::string > &lines, const struct stat &metadata)
std::optional< ServiceTarget > identifySystemService(const std::filesystem::path &path) noexcept
ServiceActionResult blockSystemService(const std::filesystem::path &path) noexcept
ServiceActionResult unblockSystemService(const std::filesystem::path &path) noexcept
@ RG_EVENT_FILE_QUARANTINED
@ RG_EVENT_SERVICE_BLOCKED
@ RG_EVENT_PROTECTION_MODE_CHANGED
@ RG_PATH_PROTECTED_BASELINE
std::vector< fs::path > trustedApplications
std::optional< std::uint32_t > mode
std::optional< std::uint32_t > uid
std::optional< std::uint64_t > inode
std::vector< fs::path > deniedExecutables
std::vector< fs::path > trustedExecutables
std::optional< std::uint32_t > gid
std::optional< std::uint64_t > device
std::vector< PolicyPath > ignoredPaths
std::filesystem::path eventLog
std::vector< PolicyPath > protectedPaths