From 5004d49b2b8b81179ba875800d42cd89ce050c7d Mon Sep 17 00:00:00 2001 From: John Wass Date: Wed, 10 Jan 2024 16:20:01 -0500 Subject: [PATCH] Reload rules while profiling (#990) Allow rules to be loaded dynamically into a profiling session. This allows for a better test / update cycle while profiling as the profiler does not have to be shutdown to update rules. This also adds a new concept, the "Rule Identity", which provides a sha256 hash of the rule database to allow change monitoring across both profiler and systemd daemon execution. This hash is based on the compiled rules to provide a precise content based hash. Closes #985 Closes #989 --- Cargo.lock | 1 + crates/pyo3/Cargo.toml | 1 + crates/pyo3/src/profiler.rs | 26 +++++++++++++- crates/pyo3/src/rules.rs | 2 +- crates/pyo3/src/system.rs | 21 ++++++++++++ examples/show_rules.py | 22 ++++++------ fapolicy_analyzer/ui/main_window.py | 3 +- .../ui/rules/rules_admin_page.py | 34 +++++++++++++++++-- news/990.added.md | 1 + 9 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 news/990.added.md diff --git a/Cargo.lock b/Cargo.lock index 429e2c796..eff926ff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,6 +407,7 @@ dependencies = [ "fapolicy-daemon", "fapolicy-rules", "fapolicy-trust", + "fapolicy-util", "log", "pyo3", "pyo3-log", diff --git a/crates/pyo3/Cargo.toml b/crates/pyo3/Cargo.toml index ff55205b5..09da9f57b 100644 --- a/crates/pyo3/Cargo.toml +++ b/crates/pyo3/Cargo.toml @@ -23,6 +23,7 @@ fapolicy-app = { path = "../app" } fapolicy-daemon = { path = "../daemon" } fapolicy-rules = { path = "../rules" } fapolicy-trust = { path = "../trust" } +fapolicy-util = { path = "../util" } [features] default = [] diff --git a/crates/pyo3/src/profiler.rs b/crates/pyo3/src/profiler.rs index b93228c98..103994150 100644 --- a/crates/pyo3/src/profiler.rs +++ b/crates/pyo3/src/profiler.rs @@ -8,10 +8,12 @@ use chrono::Utc; use fapolicy_analyzer::users::read_users; +use fapolicy_app::sys::Error::WriteRulesFail; use fapolicy_daemon::fapolicyd::wait_until_ready; +use fapolicy_daemon::pipe; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use pyo3::{PyResult, Python}; +use pyo3::{exceptions, PyResult, Python}; use std::collections::HashMap; use std::fs::File; use std::io::Write; @@ -23,6 +25,7 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; use std::{io, thread}; +use crate::system::PySystem; use fapolicy_daemon::profiler::Profiler; use fapolicy_rules::read::load_rules_db; @@ -472,9 +475,30 @@ impl PyProfiler { } } +/// Update the compiled.rules in place and send a signal to the fapolicyd pipe to reload +/// Cleanup of the change here is handled in the normal shutdown flow by the profiler +#[pyfunction] +fn reload_profiler_rules(system: &PySystem) -> PyResult<()> { + println!("writing rules update"); + + let compiled_rules_path = PathBuf::from(&system.rs.config.system.rules_file_path) + .parent() + .expect("invalid toml: rules_file_path") + .join("compiled.rules"); + + fapolicy_rules::write::compiled_rules(&system.rs.rules_db, &compiled_rules_path) + .map_err(WriteRulesFail) + .map_err(|e| exceptions::PyRuntimeError::new_err(format!("{:?}", e)))?; + + pipe::reload_rules() + .map_err(|e| exceptions::PyRuntimeError::new_err(format!("Reload failed: {:?}", e))) +} + pub fn init_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_function(wrap_pyfunction!(reload_profiler_rules, m)?)?; + Ok(()) } diff --git a/crates/pyo3/src/rules.rs b/crates/pyo3/src/rules.rs index bfc9f4c7c..e359b97f8 100644 --- a/crates/pyo3/src/rules.rs +++ b/crates/pyo3/src/rules.rs @@ -215,7 +215,7 @@ pub(crate) fn to_text(db: &DB) -> String { .1 } -fn text_for_entry(e: &Entry) -> String { +pub(crate) fn text_for_entry(e: &Entry) -> String { match e { Invalid { text, .. } => text.clone(), InvalidSet { text, .. } => text.clone(), diff --git a/crates/pyo3/src/system.rs b/crates/pyo3/src/system.rs index 05cdeeb5d..2b29b0985 100644 --- a/crates/pyo3/src/system.rs +++ b/crates/pyo3/src/system.rs @@ -15,7 +15,10 @@ use fapolicy_analyzer::events::db::DB as EventDB; use fapolicy_app::app::State; use fapolicy_app::cfg; use fapolicy_app::sys::deploy_app_state; +use fapolicy_rules::db::Entry::Comment; use fapolicy_trust::stat::Status::*; +use fapolicy_util::sha::sha256_digest; +// use fapolicy_util::sha::sha256_digest; use crate::acl::{PyGroup, PyUser}; use crate::analysis::PyEventLog; @@ -254,10 +257,28 @@ fn checked_system(py: Python) -> PyResult { }) } +/// Generate a sha256 hash of the db text +/// The text hashed here is the same as what would be written to +/// compiled.rules by either fapolicyd or the analyzer +#[pyfunction] +pub fn rule_identity(system: &PySystem) -> PyResult { + let txt = system + .rs + .rules_db + .iter() + .fold(String::new(), |acc, (_, (_, x))| match x { + Comment(_) => acc, + e => format!("{}\n{}\n", acc, crate::rules::text_for_entry(e)), + }); + sha256_digest(txt.as_bytes()) + .map_err(|e| exceptions::PyRuntimeError::new_err(format!("{:?}", e))) +} + pub fn init_module(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(config_difference, m)?)?; m.add_function(wrap_pyfunction!(rules_difference, m)?)?; m.add_function(wrap_pyfunction!(checked_system, m)?)?; + m.add_function(wrap_pyfunction!(rule_identity, m)?)?; Ok(()) } diff --git a/examples/show_rules.py b/examples/show_rules.py index bedcec00e..bb81a765d 100644 --- a/examples/show_rules.py +++ b/examples/show_rules.py @@ -17,19 +17,21 @@ import argparse import sys -red = '\033[91m' -green = '\033[92m' -yellow = '\033[93m' -blue = '\033[96m' -gray = '\033[33m' +red = "\033[91m" +green = "\033[92m" +yellow = "\033[93m" +blue = "\033[96m" +gray = "\033[33m" def main(*argv): parser = argparse.ArgumentParser() - parser.add_argument("--plain", action='store_true', help="Plain text rules") + parser.add_argument("--plain", action="store_true", help="Plain text rules") args = parser.parse_args(argv) s1 = System() + print(f"Rule Identity: {rule_identity(s1)}") + if args.plain: print(s1.rules_text()) else: @@ -38,14 +40,14 @@ def main(*argv): if r.origin != origin: origin = r.origin print() - print(gray, end='') + print(gray, end="") print(f"🗎 [{origin}]\033[0m") - print(green if r.is_valid else red, end='') + print(green if r.is_valid else red, end="") print(f"{r.id} {r.text} \033[0m") for info in r.info: - marker = f'[{info.category}]' if info.category != 'e' else '' - print(yellow if info.category == 'w' else blue, end='') + marker = f"[{info.category}]" if info.category != "e" else "" + print(yellow if info.category == "w" else blue, end="") print(f"\t- {marker} {info.message} \033[0m") diff --git a/fapolicy_analyzer/ui/main_window.py b/fapolicy_analyzer/ui/main_window.py index b51ef6a32..b9421141d 100644 --- a/fapolicy_analyzer/ui/main_window.py +++ b/fapolicy_analyzer/ui/main_window.py @@ -197,7 +197,7 @@ def __pack_main_content(self, page: UIPage): page.dispose() return - if self.__page: + if self.__page is not None: self.__page.dispose() self.__page = page self.mainContent.pack_start(page.get_ref(), True, True, 0) @@ -482,6 +482,7 @@ def on_rulesAdminMenu_activate(self, *args, **kwargs): rulesPage = router(PAGE_SELECTION.RULES_ADMIN) if kwargs.get("rule_id", None) is not None: rulesPage.highlight_row_from_data(kwargs["rule_id"]) + rulesPage.refresh_toolbar += self._refresh_toolbar self.__pack_main_content(rulesPage) def on_profileExecMenu_activate(self, *args): diff --git a/fapolicy_analyzer/ui/rules/rules_admin_page.py b/fapolicy_analyzer/ui/rules/rules_admin_page.py index eabcb1232..683c04bb1 100644 --- a/fapolicy_analyzer/ui/rules/rules_admin_page.py +++ b/fapolicy_analyzer/ui/rules/rules_admin_page.py @@ -17,7 +17,9 @@ import logging from typing import Any, Optional, Sequence, Tuple -from fapolicy_analyzer import Rule, System +from events import Events + +from fapolicy_analyzer import Rule, System, reload_profiler_rules from fapolicy_analyzer.ui.actions import ( Notification, NotificationType, @@ -29,6 +31,7 @@ request_rules_text, ) from fapolicy_analyzer.ui.changeset_wrapper import Changeset, RuleChangeset +from fapolicy_analyzer.ui.reducers.profiler_reducer import ProfilerState from fapolicy_analyzer.ui.rules.rules_list_view import RulesListView from fapolicy_analyzer.ui.rules.rules_status_info import RulesStatusInfo from fapolicy_analyzer.ui.rules.rules_text_view import RulesTextView @@ -36,6 +39,7 @@ dispatch, get_notifications_feature, get_system_feature, + get_profiling_feature, ) from fapolicy_analyzer.ui.strings import ( APPLY_CHANGESETS_ERROR_MESSAGE, @@ -56,14 +60,20 @@ VALIDATION_NOTE_CATEGORY = "invalid rules" -class RulesAdminPage(UIConnectedWidget, UIPage): +class RulesAdminPage(UIConnectedWidget, UIPage, Events): def __init__(self): features = [ {get_system_feature(): {"on_next": self.on_next_system}}, {get_notifications_feature(): {"on_next": self.on_next_notifications}}, + {get_profiling_feature(): {"on_next": self.on_next_profiling_state}}, ] UIConnectedWidget.__init__(self, features=features) + self.__events__ = [ + "refresh_toolbar", + ] + Events.__init__(self) + actions = { "rules": [ UIAction( @@ -80,6 +90,13 @@ def __init__(self): signals={"clicked": self.on_save_clicked}, sensitivity_func=self.__rules_dirty, ), + UIAction( + name="Profile", + tooltip="Apply to Profiler", + icon="media-seek-forward", + signals={"clicked": self.on_load_in_profiler_clicked}, + sensitivity_func=self.__can_load_in_profiler, + ), ], } UIPage.__init__(self, actions) @@ -100,6 +117,7 @@ def __init__(self): self._first_pass = True self.__system: System = None self.__validation_notifications: Sequence[Notification] = [] + self.__profiling_active = False self._list_view.treeView.connect( "row-collapsed", self._list_view.on_row_collapsed @@ -310,3 +328,15 @@ def on_next_notifications(self, notifications: Sequence[Notification]): self.__validation_notifications = [ n for n in notifications if n.category == VALIDATION_NOTE_CATEGORY ] + + def on_next_profiling_state(self, state: ProfilerState): + if self.__profiling_active != state.running: + print(state) + self.__profiling_active = state.running + self.refresh_toolbar() + + def on_load_in_profiler_clicked(self, *args): + reload_profiler_rules(self.__system) + + def __can_load_in_profiler(self): + return self.__profiling_active and not self.__rules_dirty() diff --git a/news/990.added.md b/news/990.added.md new file mode 100644 index 000000000..823bc53ec --- /dev/null +++ b/news/990.added.md @@ -0,0 +1 @@ +Allow rules to be loaded dynamically into a profiling session