diff --git a/unit_tests/falco/app/actions/test_configure_interesting_sets.cpp b/unit_tests/falco/app/actions/test_configure_interesting_sets.cpp index 75cb4d42..849bbb85 100644 --- a/unit_tests/falco/app/actions/test_configure_interesting_sets.cpp +++ b/unit_tests/falco/app/actions/test_configure_interesting_sets.cpp @@ -15,137 +15,337 @@ limitations under the License. */ -#include "falco_utils.h" -#include "evttype_index_ruleset.h" -#include +#include + +#include +#include + #include -using namespace std; -using namespace libsinsp::filter; -using namespace falco::utils; - -// todo(jasondellaluce): these tests do not test the actual -// `configure_interesting_sets` action, but instead reproduces its logic -// and asserts the pre and post conditions. For now, this is the only thing -// we can do due to the falco_engine class lacking adequate accessor methods. -// In the future, we need to refactor this. - -static std::shared_ptr create_factory() -{ - std::shared_ptr ret(new sinsp_filter_factory(NULL)); - return ret; -} - -static std::shared_ptr create_ast( - std::shared_ptr f, std::string fltstr) -{ - libsinsp::filter::parser parser(fltstr); - std::shared_ptr ret(parser.parse()); - return ret; -} - -static std::shared_ptr create_filter( - std::shared_ptr f, - std::shared_ptr ast) -{ - sinsp_filter_compiler compiler(f, ast.get()); - std::shared_ptr filter(compiler.compile()); - return filter; -} - -static std::shared_ptr create_ruleset( - std::shared_ptr f) -{ - std::shared_ptr ret(new evttype_index_ruleset(f)); - return ret; -} - -static std::shared_ptr get_test_rulesets(const std::unordered_set& fltstrs) -{ - auto f = create_factory(); - auto r = create_ruleset(f); - - for (const auto &fltstr : fltstrs) - { - auto rule_ast = create_ast(f, fltstr); - auto rule_filter = create_filter(f, rule_ast); - falco_rule rule; - rule.name = fltstr; - rule.source = falco_common::syscall_source; - r->add(rule, rule_filter, rule_ast); - r->enable(fltstr, true, 0); - } - return r; -} - -static libsinsp::events::set extract_rules_event_set(std::shared_ptr& r) -{ - std::set tmp; - libsinsp::events::set events; - auto source = falco_common::syscall_source; - r->enabled_evttypes(tmp, 0); - for (const auto &ev : tmp) - { - events.insert((ppm_event_code) ev); - } - return events; -} - #define ASSERT_NAMES_EQ(a, b) { \ - ASSERT_EQ(std::set(a.begin(), a.end()), std::set(b.begin(), b.end())); \ + ASSERT_EQ(_order(a).size(), _order(b).size()); \ + ASSERT_EQ(_order(a), _order(b)); \ } -TEST(ConfigureInterestingSets, configure_interesting_sets) +#define ASSERT_NAMES_CONTAIN(a, b) { \ + ASSERT_NAMES_EQ(unordered_set_intersection(a, b), b); \ +} + +#define ASSERT_NAMES_NOCONTAIN(a, b) { \ + ASSERT_NAMES_EQ(unordered_set_intersection(a, b), strset_t({})); \ +} + +using strset_t = std::unordered_set; + +static std::set _order(const strset_t& s) { - /* Test scenario: - * Include one I/O syscall - * Include one non syscall event type - * Include one exclusionary syscall definition test ruleset - * Check sinsp enforced syscalls dependencies for: - * - spawned processes - * - network related syscalls - * Check that non syscalls events are enforced */ - std::unordered_set fltstrs = { - "(evt.type=connect or evt.type=accept)", - "evt.type in (open, ptrace, mmap, execve, read, container)", - "evt.type in (open, execve, mprotect) and not evt.type=mprotect"}; - std::unordered_set expected_syscalls_names = { - "connect", "accept", "open", "ptrace", "mmap", "execve", "read", "container"}; - std::unordered_set base_syscalls_sinsp_state_spawned_process = {"clone", "clone3", "fork", "vfork"}; - std::unordered_set base_syscalls_sinsp_state_network = {"socket", "bind", "close"}; - std::unordered_set base_events = {"procexit", "container"}; - - auto r = get_test_rulesets(fltstrs); - ASSERT_EQ(r->enabled_count(0), fltstrs.size()); - - /* Test if event types names were extracted from each rule in test ruleset. */ - auto rules_event_set = extract_rules_event_set(r); - auto rules_names = libsinsp::events::event_set_to_names(rules_event_set); - ASSERT_NAMES_EQ(rules_names, expected_syscalls_names); - - /* Enforce sinsp state syscalls and test if ruleset syscalls are in final set of syscalls. */ - auto base_event_set = libsinsp::events::sinsp_state_event_set(); - auto selected_event_set = base_event_set.merge(rules_event_set); - auto selected_names = libsinsp::events::event_set_to_names(selected_event_set); - auto intersection = unordered_set_intersection(selected_names, expected_syscalls_names); - ASSERT_NAMES_EQ(intersection, expected_syscalls_names); - - /* Test if sinsp state enforcement activated required syscalls for test ruleset. */ - intersection = unordered_set_intersection(selected_names, base_syscalls_sinsp_state_spawned_process); - ASSERT_NAMES_EQ(intersection, base_syscalls_sinsp_state_spawned_process); - intersection = unordered_set_intersection(selected_names, base_syscalls_sinsp_state_network); - ASSERT_NAMES_EQ(intersection, base_syscalls_sinsp_state_network); - - /* Test that no I/O syscalls are in the final set. */ - auto io_event_set = libsinsp::events::sc_set_to_event_set(libsinsp::events::io_sc_set()); - auto erased_event_set = selected_event_set.intersect(io_event_set); - selected_event_set = selected_event_set.diff(io_event_set); - selected_names = libsinsp::events::event_set_to_names(selected_event_set); - intersection = unordered_set_intersection(selected_names, libsinsp::events::event_set_to_names(erased_event_set)); - ASSERT_EQ(intersection.size(), 0); - - /* Test that enforced non syscalls events are in final events set. */ - intersection = unordered_set_intersection(selected_names, base_events); - ASSERT_NAMES_EQ(intersection, base_events); - + return std::set(s.begin(), s.end()); +} + +static std::string s_sample_ruleset = "sample-ruleset"; + +static std::string s_sample_source = falco_common::syscall_source; + +static strset_t s_sample_filters = { + "evt.type=connect or evt.type=accept", + "evt.type in (open, ptrace, mmap, execve, read, container)", + "evt.type in (open, execve, mprotect) and not evt.type=mprotect"}; + +static strset_t s_sample_generic_filters = { + "evt.type=syncfs or evt.type=fanotify_init"}; + +static strset_t s_sample_nonsyscall_filters = { + "evt.type in (procexit, switch, pluginevent, container)"}; + + +// todo(jasondellaluce): once we have deeper and more modular +// control on the falco engine, make this a little nicer +static std::shared_ptr mock_engine_from_filters(const strset_t& filters) +{ + // craft a fake ruleset with the given filters + int n_rules = 0; + std::string dummy_rules; + falco::load_result::rules_contents_t content = {{"dummy_rules.yaml", dummy_rules}}; + for (const auto& f : filters) + { + n_rules++; + dummy_rules += + "- rule: Dummy Rule " + std::to_string(n_rules) + "\n" + + " output: Dummy Output " + std::to_string(n_rules) + "\n" + + " condition: " + f + "\n" + + " desc: Dummy Desc " + std::to_string(n_rules) + "\n" + + " priority: CRITICAL\n\n"; + } + + // create a falco engine and load the ruleset + std::shared_ptr res(new falco_engine()); + auto filter_factory = std::shared_ptr( + new sinsp_filter_factory(nullptr)); + auto formatter_factory = std::shared_ptr( + new sinsp_evt_formatter_factory(nullptr)); + res->add_source(s_sample_source, filter_factory, formatter_factory); + res->load_rules(dummy_rules, "dummy_rules.yaml"); + res->enable_rule("", true, s_sample_ruleset); + return res; +} + +TEST(ConfigureInterestingSets, engine_codes_syscalls_set) +{ + auto engine = mock_engine_from_filters(s_sample_filters); + auto enabled_count = engine->num_rules_for_ruleset(s_sample_ruleset); + ASSERT_EQ(enabled_count, s_sample_filters.size()); + + // test if event code names were extracted from each rule in test ruleset. + auto rules_event_set = engine->event_codes_for_ruleset(s_sample_source); + auto rules_event_names = libsinsp::events::event_set_to_names(rules_event_set); + ASSERT_NAMES_EQ(rules_event_names, strset_t({ + "connect", "accept", "open", "ptrace", "mmap", "execve", "read", "container"})); + + // test if sc code names were extracted from each rule in test ruleset. + // note, this is not supposed to contain "container", as that's an event + // not mapped through the ppm_sc_code enumerative. + auto rules_sc_set = engine->sc_codes_for_ruleset(s_sample_source); + auto rules_sc_names = libsinsp::events::sc_set_to_names(rules_sc_set); + ASSERT_NAMES_EQ(rules_sc_names, strset_t({ + "connect", "accept", "open", "ptrace", "mmap", "execve", "read"})); +} + +TEST(ConfigureInterestingSets, preconditions_postconditions) +{ + falco::app::state s; + auto mock_engine = mock_engine_from_filters(s_sample_filters); + + s.engine = mock_engine; + s.config = nullptr; + auto result = falco::app::actions::configure_interesting_sets(s); + ASSERT_FALSE(result.success); + ASSERT_NE(result.errstr, ""); + + s.engine = nullptr; + s.config = std::make_shared(); + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_FALSE(result.success); + ASSERT_NE(result.errstr, ""); + + s.engine = mock_engine; + s.config = std::make_shared(); + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + + auto prev_selection_size = s.selected_sc_set.size(); + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + ASSERT_EQ(prev_selection_size, s.selected_sc_set.size()); +} + +TEST(ConfigureInterestingSets, engine_codes_nonsyscalls_set) +{ + auto filters = s_sample_filters; + filters.insert(s_sample_generic_filters.begin(), s_sample_generic_filters.end()); + filters.insert(s_sample_nonsyscall_filters.begin(), s_sample_nonsyscall_filters.end()); + + auto engine = mock_engine_from_filters(filters); + auto enabled_count = engine->num_rules_for_ruleset(s_sample_ruleset); + ASSERT_EQ(enabled_count, filters.size()); + + auto rules_event_set = engine->event_codes_for_ruleset(s_sample_source); + auto rules_event_names = libsinsp::events::event_set_to_names(rules_event_set); + // note: including even one generic event will cause PPME_GENERIC_E to be + // inluded in the ruleset's event codes. As such, when translating to names, + // PPME_GENERIC_E will cause all names of generic events to be added! + // This is a good example of information loss from ppm_event_code <-> ppm_sc_code. + auto generic_names = libsinsp::events::event_set_to_names({ppm_event_code::PPME_GENERIC_E}); + auto expected_names = strset_t({ + "connect", "accept", "open", "ptrace", "mmap", "execve", "read", "container", // ruleset + "procexit", "switch", "pluginevent"}); // from non-syscall event filters + expected_names.insert(generic_names.begin(), generic_names.end()); + ASSERT_NAMES_EQ(rules_event_names, expected_names); + + auto rules_sc_set = engine->sc_codes_for_ruleset(s_sample_source); + auto rules_sc_names = libsinsp::events::sc_set_to_names(rules_sc_set); + ASSERT_NAMES_EQ(rules_sc_names, strset_t({ + "connect", "accept", "open", "ptrace", "mmap", "execve", "read", + "syncfs", "fanotify_init", // from generic event filters + })); +} + +TEST(ConfigureInterestingSets, selection_not_allevents) +{ + // run app action with fake engine and without the `-A` option + falco::app::state s; + s.engine = mock_engine_from_filters(s_sample_filters); + s.options.all_events = false; + auto result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + + // todo(jasondellaluce): once we have deeper control on falco's outputs, + // also check if a warning has been printed in stderr + + // check that the final selected set is the one expected + ASSERT_NE(s.selected_sc_set.size(), 0); + auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + auto expected_sc_names = strset_t({ + // note: we expect the "read" syscall to have been erased + "connect", "accept", "open", "ptrace", "mmap", "execve", // from ruleset + "clone", "clone3", "fork", "vfork", // from sinsp state set (spawned_process) + "socket", "bind", "close" // from sinsp state set (network, files) + }); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); + + // check that all IO syscalls have been erased from the selection + auto io_set = libsinsp::events::io_sc_set(); + auto erased_sc_names = libsinsp::events::sc_set_to_names(io_set); + ASSERT_NAMES_NOCONTAIN(selected_sc_names, erased_sc_names); + + // check that final selected set is exactly sinsp state + ruleset + auto rule_set = s.engine->sc_codes_for_ruleset(s_sample_source, s_sample_ruleset); + auto state_set = libsinsp::events::sinsp_state_sc_set(); + for (const auto &erased : io_set) + { + rule_set.remove(erased); + state_set.remove(erased); + } + auto union_set = state_set.merge(rule_set); + auto inter_set = state_set.intersect(rule_set); + ASSERT_EQ(s.selected_sc_set.size(), state_set.size() + rule_set.size() - inter_set.size()); + ASSERT_EQ(s.selected_sc_set, union_set); +} + +TEST(ConfigureInterestingSets, selection_allevents) +{ + // run app action with fake engine and with the `-A` option + falco::app::state s; + s.engine = mock_engine_from_filters(s_sample_filters); + s.options.all_events = true; + auto result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + + // todo(jasondellaluce): once we have deeper control on falco's outputs, + // also check if a warning has not been printed in stderr + + // check that the final selected set is the one expected + ASSERT_NE(s.selected_sc_set.size(), 0); + auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + auto expected_sc_names = strset_t({ + // note: we expect the "read" syscall to not be erased + "connect", "accept", "open", "ptrace", "mmap", "execve", "read", // from ruleset + "clone", "clone3", "fork", "vfork", // from sinsp state set (spawned_process) + "socket", "bind", "close" // from sinsp state set (network, files) + }); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); + + // check that final selected set is exactly sinsp state + ruleset + auto rule_set = s.engine->sc_codes_for_ruleset(s_sample_source, s_sample_ruleset); + auto state_set = libsinsp::events::sinsp_state_sc_set(); + auto union_set = state_set.merge(rule_set); + auto inter_set = state_set.intersect(rule_set); + ASSERT_EQ(s.selected_sc_set.size(), state_set.size() + rule_set.size() - inter_set.size()); + ASSERT_EQ(s.selected_sc_set, union_set); +} + +TEST(ConfigureInterestingSets, selection_generic_evts) +{ + // run app action with fake engine and without the `-A` option + falco::app::state s; + auto filters = s_sample_filters; + filters.insert(s_sample_generic_filters.begin(), s_sample_generic_filters.end()); + s.engine = mock_engine_from_filters(filters); + auto result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + + // check that the final selected set is the one expected + ASSERT_NE(s.selected_sc_set.size(), 0); + auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + auto expected_sc_names = strset_t({ + // note: we expect the "read" syscall to not be erased + "connect", "accept", "open", "ptrace", "mmap", "execve", // from ruleset + "syncfs", "fanotify_init", // from ruleset (generic events) + "clone", "clone3", "fork", "vfork", // from sinsp state set (spawned_process) + "socket", "bind", "close" // from sinsp state set (network, files) + }); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); +} + +// expected combinations precedence: +// - final selected set is the union of rules events and base events +// (either default or custom positive set) +// - events in the custom negative set are removed from the selected set +// - if `-A` is not set, events from the IO set are removed from the selected set +TEST(ConfigureInterestingSets, selection_custom_base_set) +{ + // run app action with fake engine and without the `-A` option + falco::app::state s; + s.options.all_events = true; + s.engine = mock_engine_from_filters(s_sample_filters); + auto default_base_set = libsinsp::events::sinsp_state_sc_set(); + + // non-empty custom base set (both positive and negative) + s.config->m_base_syscalls = {"syncfs", "!accept"}; + auto result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + auto selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + auto expected_sc_names = strset_t({ + // note: `syncfs` has been added due to the custom base set, and `accept` + // has been remove due to the negative base set. + // note: `read` is not ignored due to the "-A" option being set. + // note: `accept` is not included even though it is matched by the rules, + // which means that the custom negation base set has precedence over the + // final selection set as a whole + "connect", "open", "ptrace", "mmap", "execve", "read", "syncfs" + }); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); + + // non-empty custom base set (both positive and negative with collision) + s.config->m_base_syscalls = {"syncfs", "accept", "!accept"}; + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + // note: in case of collision, negation has priority, so the expected + // names are the same as the case above + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); + + // non-empty custom base set (only positive) + s.config->m_base_syscalls = {"syncfs"}; + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + expected_sc_names = strset_t({ + // note: accept is not negated anymore + "connect", "accept", "open", "ptrace", "mmap", "execve", "read", "syncfs" + }); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); + + // non-empty custom base set (only negative) + s.config->m_base_syscalls = {"!accept"}; + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + expected_sc_names = unordered_set_union( + libsinsp::events::sc_set_to_names(default_base_set), + strset_t({ "connect", "open", "ptrace", "mmap", "execve", "read"})); + expected_sc_names.erase("accept"); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); + + // non-empty custom base set (positive, without -A) + s.options.all_events = false; + s.config->m_base_syscalls = {"read"}; + result = falco::app::actions::configure_interesting_sets(s); + ASSERT_TRUE(result.success); + ASSERT_EQ(result.errstr, ""); + selected_sc_names = libsinsp::events::sc_set_to_names(s.selected_sc_set); + expected_sc_names = strset_t({ + // note: read is both part of the custom base set and the rules set, + // but we expect the unset -A option to take precence + "connect", "accept", "open", "ptrace", "mmap", "execve", + }); + ASSERT_NAMES_CONTAIN(selected_sc_names, expected_sc_names); }