diff --git a/unit_tests/engine/engine_helper.h b/unit_tests/engine/engine_helper.h new file mode 100644 index 00000000000..74841400709 --- /dev/null +++ b/unit_tests/engine/engine_helper.h @@ -0,0 +1,53 @@ +/* +Copyright (C) 2023 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +#pragma once +#include + +// When updating unit_tests/falco_rules_test.yaml bump this +#define N_VALID_TEST_RULES_FALCO_RULES_TEST_YAML 6 + +#define ASSERT_CONTAINS(a, b) \ + { \ + auto a1 = a; \ + auto b1 = b; \ + uint32_t prev_size = a1.size(); \ + for(const auto& val : b1) \ + { \ + a1.insert(val); \ + } \ + ASSERT_EQ(prev_size, a1.size()); \ + } + +#define ASSERT_NOT_CONTAINS(a, b) \ + { \ + auto a1 = a; \ + auto b1 = b; \ + uint32_t prev_size = a1.size(); \ + for(const auto& val : b1) \ + { \ + a1.insert(val); \ + } \ + ASSERT_EQ(prev_size + b1.size(), a1.size()); \ + } + +#define ASSERT_STRING_EQUAL(a, b) \ + { \ + auto a1 = a; \ + auto b1 = b; \ + ASSERT_EQ(a1.compare(b1), 0); \ + } diff --git a/unit_tests/engine/test_rule_loader_reader.cpp b/unit_tests/engine/test_rule_loader_reader.cpp new file mode 100644 index 00000000000..792d0ddc5f6 --- /dev/null +++ b/unit_tests/engine/test_rule_loader_reader.cpp @@ -0,0 +1,110 @@ +/* +Copyright (C) 2023 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +#include +#include +#include "engine_helper.h" +#include +#include "../falco/app/actions/app_action_helpers.h" + +static std::shared_ptr mock_engine() +{ + // Create a mock Falco engine + std::shared_ptr engine(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)); + engine->add_source("syscall", filter_factory, formatter_factory); + return engine; +} + +TEST(RuleLoaderReader, append_merge_override_enabled) +{ + falco::app::state s; + s.engine = mock_engine(); + s.options.rules_filenames.push_back("../unit_tests/falco_rules_test1.yaml"); + + auto result = falco::app::actions::load_rules_files(s); + ASSERT_TRUE(result.success); + + auto rules1 = s.engine->get_rules(); + std::unordered_set rules_names = {}; + std::unordered_set expected_rules_names = {"Dummy Rule 0", "Dummy Rule 1", \ + "Dummy Rule 2", "Dummy Rule 4 Disabled", "Dummy Rule 5", "Dummy Rule 6"}; + std::unordered_set not_expected_rules_names = {"Dummy Rule 3 Invalid"}; + ASSERT_EQ(rules1.size(), N_VALID_TEST_RULES_FALCO_RULES_TEST_YAML); + + for(const auto& r : rules1) + { + rules_names.insert(r.name); + if (r.name.compare(std::string("Dummy Rule 0")) == 0) + { + // Test condition where we append to tags, cond and output + ASSERT_TRUE(r.enabled); + std::set some_desired_tags = {"maturity_stable", "test1", "test2"}; + ASSERT_CONTAINS(r.tags, some_desired_tags); + ASSERT_STRING_EQUAL(r.cond, std::string("evt.type in (execve, execveat) and proc.name=cat and proc.cmdline contains test")); + ASSERT_STRING_EQUAL(r.output, std::string("%evt.type %evt.num %proc.aname[5] %proc.name %proc.tty %proc.exepath %fd.name proc_exeline=%proc.exeline proc_exepath=%proc.exepath")); + ASSERT_EQ(r.priority, falco_common::priority_type::PRIORITY_CRITICAL); + } + else if (r.name.compare(std::string("Dummy Rule 1")) == 0) + { + // Test rules merging aka override only re-defined keys, else keep old keys + std::set some_desired_tags = {"maturity_incubating", "host", "container"}; // ensure prev definition + ASSERT_STRING_EQUAL(r.desc, std::string("My test desc 1")); // ensure prev definition + ASSERT_EQ(r.priority, falco_common::priority_type::PRIORITY_CRITICAL); // ensure prev definition + ASSERT_CONTAINS(r.tags, some_desired_tags); + ASSERT_STRING_EQUAL(r.cond, std::string("evt.type in (ptrace)")); // ensure new definition + ASSERT_STRING_EQUAL(r.output, std::string("%evt.type %evt.num")); // ensure new definition + ASSERT_FALSE(r.enabled); // ensure new definition + } + else if (r.name.compare(std::string("Dummy Rule 2")) == 0) + { + // Test where we have overridden a rule to ONLY NOT be enabled + ASSERT_EQ(r.priority, falco_common::priority_type::PRIORITY_NOTICE); + ASSERT_FALSE(r.enabled); + } + else if (r.name.compare(std::string("Dummy Rule 4 Disabled")) == 0) + { + // Test if entire rule defined just once is disabled + ASSERT_FALSE(r.enabled); + } + else if (r.name.compare(std::string("Dummy Rule 5")) == 0) + { + // Test if we correctly support append mode with override for enabled and priority + std::set some_desired_tags = {"maturity_sandbox", "host", "container"}; + ASSERT_TRUE(r.enabled); // ensure new definition + ASSERT_EQ(r.priority, falco_common::priority_type::PRIORITY_CRITICAL); // ensure new definition + ASSERT_STRING_EQUAL(r.cond, std::string("evt.type in (open, openat, openat2) and proc.name=cat")); // ensure correct append + ASSERT_STRING_EQUAL(r.output, std::string("%evt.type %evt.num %proc.cmdline %container.ip")); // ensure correct append + ASSERT_CONTAINS(r.tags, some_desired_tags); // ensure correct append + } + else if (r.name.compare(std::string("Dummy Rule 6")) == 0) + { + // Test if we correctly support append mode when used only to override enabled and priority + std::set some_desired_tags = {"maturity_sandbox", "host"}; + ASSERT_STRING_EQUAL(r.cond, std::string("evt.type in (ptrace)")); // ensure prev definition + ASSERT_STRING_EQUAL(r.output, std::string("%evt.type %proc.cmdline")); // ensure prev definition + ASSERT_TRUE(r.enabled); // ensure new definition + ASSERT_EQ(r.priority, falco_common::priority_type::PRIORITY_CRITICAL); // ensure new definition + ASSERT_CONTAINS(r.tags, some_desired_tags); // ensure prev definition + } + } + ASSERT_CONTAINS(rules_names, expected_rules_names); + ASSERT_NOT_CONTAINS(rules_names, not_expected_rules_names); +} diff --git a/unit_tests/falco_rules_test1.yaml b/unit_tests/falco_rules_test1.yaml new file mode 100644 index 00000000000..bb3e5d43eae --- /dev/null +++ b/unit_tests/falco_rules_test1.yaml @@ -0,0 +1,111 @@ +# Test ruleset 1 + +- list: http_server_binaries + items: [nginx, httpd, httpd-foregroun, lighttpd, apache, apache2] + +- list: http_server_binaries + append: true + items: [test] + +- macro: interpreted_procs + condition: (proc.name in (test1, test2)) + +- macro: interpreted_procs + append: true + condition: and evt.num > 0 + +- rule: Dummy Rule 0 + desc: My test desc 0 + condition: evt.type in (execve, execveat) + enabled: true + output: '%evt.type %evt.num %proc.aname[5] %proc.name %proc.tty %proc.exepath %fd.name' + priority: CRITICAL + tags: [maturity_stable, host, container, network, mitre_lateral_movement, T1021.004] + +- rule: Dummy Rule 1 + desc: My test desc 1 + condition: evt.type in (open, openat, openat2) + enabled: true + output: '%evt.type %evt.num %proc.aname[5] %proc.name %proc.tty %proc.exepath %fd.name' + priority: CRITICAL + tags: [maturity_incubating, host, container] + +- rule: Dummy Rule 2 + desc: My test desc 2 + condition: evt.type in (mprotect, mmap) + enabled: true + output: '%evt.type %evt.num %proc.exepath' + priority: NOTICE + tags: [maturity_incubating, host, container] + +# Test invalid / incomplete rule +- rule: Dummy Rule 3 Invalid + condition: evt.type in (execve, execveat) and proc.name=cat and proc.cmdline contains invalid + +- rule: Dummy Rule 4 Disabled + desc: My test desc 4 + condition: evt.type in (mprotect, mmap) + enabled: false + output: '%evt.type %evt.num %proc.exepath' + priority: NOTICE + tags: [maturity_incubating, host, container] + +- rule: Dummy Rule 5 + desc: My test desc 5 + condition: evt.type in (open, openat, openat2) + enabled: false + output: '%evt.type %evt.num %proc.cmdline' + priority: INFORMATIONAL + tags: [maturity_sandbox] + +- rule: Dummy Rule 6 + desc: My test desc 6 + condition: evt.type in (ptrace) + enabled: false + output: '%evt.type %proc.cmdline' + priority: INFORMATIONAL + tags: [maturity_sandbox, host] + +# Test appending to rule +- rule: Dummy Rule 0 + append: true + condition: and proc.name=cat and proc.cmdline contains test + output: 'proc_exeline=%proc.exeline proc_exepath=%proc.exepath' + tags: [test1, test2] + +# Test merge / partial override +- rule: Dummy Rule 1 + condition: evt.type in (ptrace) + output: '%evt.type %evt.num' + enabled: false + +- rule: Dummy Rule 2 + enabled: false + +- rule: Dummy Rule 2 + enabled: true + +- rule: Dummy Rule 2 + enabled: false + +# Test append for "appendable" fields `condition`, `output`, `tags` +# + partial override for eligible fields `enabled` and `priority` +- rule: Dummy Rule 5 + append: true + desc: My test desc 5 + condition: and proc.name=cat + enabled: true + priority: CRITICAL + tags: [maturity_sandbox, host, container] + +- rule: Dummy Rule 5 + append: true + output: '%container.ip' + +- rule: Dummy Rule 6 + append: true + priority: CRITICAL + +- rule: Dummy Rule 6 + append: true + enabled: true diff --git a/userspace/engine/falco_common.h b/userspace/engine/falco_common.h index 5a1e822fea8..b2d70b4e4df 100644 --- a/userspace/engine/falco_common.h +++ b/userspace/engine/falco_common.h @@ -64,7 +64,8 @@ namespace falco_common PRIORITY_WARNING = 4, PRIORITY_NOTICE = 5, PRIORITY_INFORMATIONAL = 6, - PRIORITY_DEBUG = 7 + PRIORITY_DEBUG = 7, + PRIORITY_INVALID = 8, }; bool parse_priority(std::string v, priority_type& out); diff --git a/userspace/engine/falco_engine.h b/userspace/engine/falco_engine.h index 7fee24b2443..563555bbaec 100644 --- a/userspace/engine/falco_engine.h +++ b/userspace/engine/falco_engine.h @@ -231,6 +231,14 @@ class falco_engine std::set &evttypes, const std::string &ruleset = s_default_ruleset); + // + // Return all rules form the rule collector. + // + inline indexed_vector get_rules() const + { + return m_rule_collector.rules(); + } + // // Given an event source and ruleset, return the set of ppm_sc_codes // for which this ruleset can run and match events. diff --git a/userspace/engine/rule_loader_collector.cpp b/userspace/engine/rule_loader_collector.cpp index 3a87fcf7697..7840a32de2b 100644 --- a/userspace/engine/rule_loader_collector.cpp +++ b/userspace/engine/rule_loader_collector.cpp @@ -222,7 +222,41 @@ void rule_loader::collector::define(configuration& cfg, rule_info& info) validate_exception_info(*source, ex); } - define_info(m_rule_infos, info, m_cur_index++); + // Reconstruct prev info if no new info and only merge re-defined fields + if (prev) + { + if (info.desc.empty()) + { + info.desc = prev->desc; + } + if (info.cond.empty()) + { + info.cond = prev->cond; + } + if (info.output.empty()) + { + info.output = prev->output; + } + if (info.tags.empty()) + { + info.tags = prev->tags; + } + if (info.priority == falco_common::priority_type::PRIORITY_INVALID) + { + info.priority = prev->priority; + } + } + + // Only add a valid rule that at least has the rule name plus + // desc, condition, output, and priority + if (!info.desc.empty() && + !info.cond.empty() && + !info.output.empty() && + info.priority >= falco_common::priority_type::PRIORITY_EMERGENCY && info.priority < falco_common::priority_type::PRIORITY_INVALID + ) + { + define_info(m_rule_infos, info, m_cur_index++); + } } void rule_loader::collector::append(configuration& cfg, rule_info& info) @@ -232,9 +266,6 @@ void rule_loader::collector::append(configuration& cfg, rule_info& info) THROW(!prev, "Rule has 'append' key but no rule by that name already exists", info.ctx); - THROW(info.cond.empty() && info.exceptions.empty(), - "Appended rule must have exceptions or condition property", - info.ctx); auto source = cfg.sources.at(prev->source); // note: this is not supposed to happen @@ -242,12 +273,40 @@ void rule_loader::collector::append(configuration& cfg, rule_info& info) std::string("Unknown source ") + prev->source, info.ctx); + // enabled and priority are the cases where we allow override also when using append + // for better user experience given the introduction of the rules maturity framework + prev->enabled = info.enabled; + + if (info.priority < falco_common::priority_type::PRIORITY_INVALID) + { + prev->priority = info.priority; + } + + // Below fields are fields were we append items + if (!info.cond.empty()) { prev->cond += " "; prev->cond += info.cond; } + if (!info.output.empty()) + { + prev->output += " "; + prev->output += info.output; + } + + if (!info.tags.empty()) + { + for (auto itr : info.tags) + { + if (!itr.empty()) + { + prev->tags.insert(itr); + } + } + } + for (auto &ex : info.exceptions) { auto prev_ex = find_if(prev->exceptions.begin(), prev->exceptions.end(), diff --git a/userspace/engine/rule_loader_reader.cpp b/userspace/engine/rule_loader_reader.cpp index 71ebfde4d76..e4b084fcdb7 100644 --- a/userspace/engine/rule_loader_reader.cpp +++ b/userspace/engine/rule_loader_reader.cpp @@ -18,7 +18,6 @@ limitations under the License. #include #include "rule_loader_reader.h" - #define THROW(cond, err, ctx) { if ((cond)) { throw rule_loader::rule_load_exception(falco::load_result::LOAD_ERR_YAML_VALIDATE, (err), (ctx)); } } // Don't call this directly, call decode_val/decode_optional_val instead. @@ -318,6 +317,10 @@ static void read_item( if(append) { + if(!item["items"].IsDefined()) + { + throw falco_exception("Appended list must have items property"); + } collector.append(cfg, v); } else @@ -346,6 +349,10 @@ static void read_item( if(append) { + if(!item["condition"].IsDefined()) + { + throw falco_exception("Appended macro must have condition property"); + } collector.append(cfg, v); } else @@ -369,61 +376,97 @@ static void read_item( v.enabled = true; v.warn_evttypes = true; v.skip_if_unknown_filter = false; + v.priority = falco_common::priority_type::PRIORITY_INVALID; decode_optional_val(item, "append", append, ctx); if(append) { - decode_optional_val(item, "condition", v.cond, ctx); + if(!item["condition"].IsDefined() && !item["exceptions"].IsDefined() \ + && !item["output"].IsDefined() && !item["tags"].IsDefined() \ + && !item["enabled"].IsDefined() && !item["priority"].IsDefined()) + { + throw falco_exception("Appended rule must have exceptions or condition or output or tags or enabled or priority property"); + } + + // option to append to condition property if(item["condition"].IsDefined()) { + decode_optional_val(item, "condition", v.cond, ctx); v.cond_ctx = rule_loader::context(item["condition"], rule_loader::context::RULE_CONDITION, "", ctx); } + + // option to append to output property + if(item["output"].IsDefined()) + { + decode_optional_val(item, "output", v.output, ctx); + v.output_ctx = rule_loader::context(item["output"], rule_loader::context::RULE_OUTPUT, "", ctx); + v.output = trim(v.output); + } + + // option to append to tags property + if(item["tags"].IsDefined()) + { + decode_tags(item, v.tags, ctx); + } + + // option to override priority in append mode + if(item["priority"].IsDefined()) + { + std::string priority; + decode_val(item, "priority", priority, ctx); + rule_loader::context prictx(item["priority"], rule_loader::context::RULE_PRIORITY, "", ctx); + THROW(!falco_common::parse_priority(priority, v.priority), + "Invalid priority", prictx); + } + read_rule_exceptions(item, v, ctx, append); + // option to override enabled in append mode + decode_optional_val(item, "enabled", v.enabled, ctx); collector.append(cfg, v); } else { - // If the rule does *not* have any of - // condition/output/desc/priority, it *must* - // have an enabled property. Use the enabled - // property to set the enabled status of an - // earlier rule. - if (!item["condition"].IsDefined() && - !item["output"].IsDefined() && - !item["desc"].IsDefined() && - !item["priority"].IsDefined()) + std::string priority; + v.source = falco_common::syscall_source; + + if(item["source"].IsDefined()) { - decode_val(item, "enabled", v.enabled, ctx); - collector.enable(cfg, v); + decode_optional_val(item, "source", v.source, ctx); } - else + if(item["condition"].IsDefined()) { - std::string priority; - - // All of these are required decode_val(item, "condition", v.cond, ctx); v.cond_ctx = rule_loader::context(item["condition"], rule_loader::context::RULE_CONDITION, "", ctx); - + } + if(item["output"].IsDefined()) + { decode_val(item, "output", v.output, ctx); v.output_ctx = rule_loader::context(item["output"], rule_loader::context::RULE_OUTPUT, "", ctx); - + v.output = trim(v.output); + } + if(item["desc"].IsDefined()) + { decode_val(item, "desc", v.desc, ctx); + } + if(item["priority"].IsDefined()) + { decode_val(item, "priority", priority, ctx); - - v.output = trim(v.output); - v.source = falco_common::syscall_source; rule_loader::context prictx(item["priority"], rule_loader::context::RULE_PRIORITY, "", ctx); THROW(!falco_common::parse_priority(priority, v.priority), - "Invalid priority", prictx); - decode_optional_val(item, "source", v.source, ctx); - decode_optional_val(item, "enabled", v.enabled, ctx); - decode_optional_val(item, "warn_evttypes", v.warn_evttypes, ctx); - decode_optional_val(item, "skip-if-unknown-filter", v.skip_if_unknown_filter, ctx); + "Invalid priority", prictx); + } + if(item["tags"].IsDefined()) + { decode_tags(item, v.tags, ctx); - read_rule_exceptions(item, v, ctx, append); - collector.define(cfg, v); } + + decode_optional_val(item, "enabled", v.enabled, ctx); + decode_optional_val(item, "warn_evttypes", v.warn_evttypes, ctx); + decode_optional_val(item, "skip-if-unknown-filter", v.skip_if_unknown_filter, ctx); + read_rule_exceptions(item, v, ctx, append); + + collector.define(cfg, v); } } else