Skip to content

Commit

Permalink
Reload rules while profiling (#990)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jw3 committed Jan 10, 2024
1 parent e775d0b commit 5004d49
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 15 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/pyo3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
26 changes: 25 additions & 1 deletion crates/pyo3/src/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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::<PyProfiler>()?;
m.add_class::<ProcHandle>()?;
m.add_class::<ExecHandle>()?;
m.add_function(wrap_pyfunction!(reload_profiler_rules, m)?)?;

Ok(())
}
2 changes: 1 addition & 1 deletion crates/pyo3/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
21 changes: 21 additions & 0 deletions crates/pyo3/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,10 +257,28 @@ fn checked_system(py: Python) -> PyResult<PySystem> {
})
}

/// 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<String> {
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::<PySystem>()?;
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(())
}
22 changes: 12 additions & 10 deletions examples/show_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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")


Expand Down
3 changes: 2 additions & 1 deletion fapolicy_analyzer/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
34 changes: 32 additions & 2 deletions fapolicy_analyzer/ui/rules/rules_admin_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,13 +31,15 @@
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
from fapolicy_analyzer.ui.store import (
dispatch,
get_notifications_feature,
get_system_feature,
get_profiling_feature,
)
from fapolicy_analyzer.ui.strings import (
APPLY_CHANGESETS_ERROR_MESSAGE,
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions news/990.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow rules to be loaded dynamically into a profiling session

0 comments on commit 5004d49

Please sign in to comment.