diff --git a/falco.yaml b/falco.yaml index 45562644..8325fa36 100644 --- a/falco.yaml +++ b/falco.yaml @@ -144,22 +144,30 @@ # # Like for 'rules_files', specifying a folder will load all the configs files present in it in a lexicographical order. # -# 3 merge-strategies are available: `append`, `override` and `add-only`. -# The default merge-strategy is `append`: +# 3 merge-strategies are available: +# `append` (default): # * existing sequence keys will be appended # * existing scalar keys will be overridden # * non-existing keys will be added -# To enable the `override` merge-strategy, `@` must be prepended to the entry, eg: "@/$HOME/override.yaml". +# `override`: # * existing keys will be overridden # * non-existing keys will be added -# Please note that since `@` is a reserved character by yaml spec, you need to quote the whole string. -# To enable the `add-only` merge-strategy, `+` must be prepended to the entry, eg: "+/$HOME/append.yaml". +# `add-only`: # * existing keys will be ignored # * non-existing keys will be added # -# When a merge-strategy is enabled for a folder entry, all of the included config files will use that merge-strategy. +# Each item on the list can be either a yaml map or a simple string. +# The simple string will be interpreted as the config file path, and the `append` merge-strategy will be enforced. +# When the item is a yaml map instead, it will be of the form: ` path: foo\n strategy: X`. +# When `strategy` is omitted, once again `append` is used. +# +# When a merge-strategy is enabled for a folder entry, all the included config files will use that merge-strategy. config_files: - /etc/falco/config.d + # Example of config file specified as yaml map with strategy made explicit. + #- path: $HOME/falco_local_configs/ + # strategy: add-only + # [Stable] `watch_config_files` # diff --git a/unit_tests/falco/test_configuration_config_files.cpp b/unit_tests/falco/test_configuration_config_files.cpp index 9d14cf14..025a89b3 100644 --- a/unit_tests/falco/test_configuration_config_files.cpp +++ b/unit_tests/falco/test_configuration_config_files.cpp @@ -245,8 +245,7 @@ TEST(Configuration, configuration_config_files_override) { std::filesystem::remove("conf_3.yaml"); } -TEST(Configuration, configuration_config_files_sequence) { - /* Test that included config files are able to override configs from main file */ +TEST(Configuration, configuration_config_files_sequence_strategy_default) { const std::string main_conf_yaml = yaml_helper::configs_key + ":\n" " - conf_2.yaml\n" // default merge-strategy: append @@ -306,11 +305,72 @@ TEST(Configuration, configuration_config_files_sequence) { std::filesystem::remove("conf_3.yaml"); } -TEST(Configuration, configuration_config_files_sequence_override) { - /* Test that included config files are able to override configs from main file */ +TEST(Configuration, configuration_config_files_sequence_strategy_append) { const std::string main_conf_yaml = yaml_helper::configs_key + ":\n" - " - '@conf_2.yaml'\n" // merge-strategy: override + " - path: conf_2.yaml\n" + " strategy: append\n" + " - conf_3.yaml\n" + "foo: [ bar ]\n" + "base_value:\n" + " id: 1\n" + " name: foo\n"; + const std::string conf_yaml_2 = + "foo: [ bar2 ]\n" // append to foo sequence + "base_value_2:\n" + " id: 2\n"; + const std::string conf_yaml_3 = + "base_value:\n" // override base_value + " id: 3\n"; + + std::ofstream outfile("main.yaml"); + outfile << main_conf_yaml; + outfile.close(); + + outfile.open("conf_2.yaml"); + outfile << conf_yaml_2; + outfile.close(); + + outfile.open("conf_3.yaml"); + outfile << conf_yaml_3; + outfile.close(); + + std::vector cmdline_config_options; + falco_configuration falco_config; + config_loaded_res res; + ASSERT_NO_THROW(res = falco_config.init_from_file("main.yaml", cmdline_config_options)); + + // main + conf_2 + conf_3 + ASSERT_EQ(res.size(), 3); + + ASSERT_TRUE(falco_config.m_config.is_defined("foo")); + std::vector foos; + auto expected_foos = std::vector{"bar", "bar2"}; + ASSERT_NO_THROW(falco_config.m_config.get_sequence>(foos, "foo")); + ASSERT_EQ(foos.size(), 2); // 2 elements in `foo` sequence because we appended to it + for(size_t i = 0; i < foos.size(); ++i) { + EXPECT_EQ(foos[i], expected_foos[i]) + << "Vectors foo's and expected_foo's differ at index " << i; + } + + ASSERT_TRUE(falco_config.m_config.is_defined("base_value.id")); + ASSERT_EQ(falco_config.m_config.get_scalar("base_value.id", 0), 3); // overridden! + ASSERT_FALSE(falco_config.m_config.is_defined( + "base_value.name")); // no more present since entire `base_value` block was overridden + ASSERT_TRUE(falco_config.m_config.is_defined("base_value_2.id")); + ASSERT_EQ(falco_config.m_config.get_scalar("base_value_2.id", 0), 2); + ASSERT_FALSE(falco_config.m_config.is_defined("base_value_3.id")); // not defined + + std::filesystem::remove("main.yaml"); + std::filesystem::remove("conf_2.yaml"); + std::filesystem::remove("conf_3.yaml"); +} + +TEST(Configuration, configuration_config_files_sequence_strategy_override) { + const std::string main_conf_yaml = yaml_helper::configs_key + + ":\n" + " - path: conf_2.yaml\n" + " strategy: override\n" " - conf_3.yaml\n" "foo: [ bar ]\n" "base_value:\n" @@ -367,11 +427,12 @@ TEST(Configuration, configuration_config_files_sequence_override) { std::filesystem::remove("conf_3.yaml"); } -TEST(Configuration, configuration_config_files_sequence_addonly) { +TEST(Configuration, configuration_config_files_sequence_strategy_addonly) { /* Test that included config files are able to override configs from main file */ const std::string main_conf_yaml = yaml_helper::configs_key + ":\n" - " - +conf_2.yaml\n" // merge-strategy: add-only + " - path: conf_2.yaml\n" + " strategy: add-only\n" " - conf_3.yaml\n" "foo: [ bar ]\n" "base_value:\n" @@ -429,6 +490,71 @@ TEST(Configuration, configuration_config_files_sequence_addonly) { std::filesystem::remove("conf_3.yaml"); } +TEST(Configuration, configuration_config_files_sequence_wrong_strategy) { + const std::string main_conf_yaml = yaml_helper::configs_key + + ":\n" + " - path: conf_2.yaml\n" + " strategy: wrong\n" + " - conf_3.yaml\n" + "foo: [ bar ]\n" + "base_value:\n" + " id: 1\n" + " name: foo\n"; + const std::string conf_yaml_2 = + "foo: [ bar2 ]\n" // append to foo sequence + "base_value_2:\n" + " id: 2\n"; + const std::string conf_yaml_3 = + "base_value:\n" // override base_value + " id: 3\n"; + + std::ofstream outfile("main.yaml"); + outfile << main_conf_yaml; + outfile.close(); + + outfile.open("conf_2.yaml"); + outfile << conf_yaml_2; + outfile.close(); + + outfile.open("conf_3.yaml"); + outfile << conf_yaml_3; + outfile.close(); + + std::vector cmdline_config_options; + falco_configuration falco_config; + config_loaded_res res; + ASSERT_NO_THROW(res = falco_config.init_from_file("main.yaml", cmdline_config_options)); + + // main + ASSERT_EQ(res.size(), 3); + auto validation = res["main.yaml"]; + // Since we are using a wrong strategy, the validation should fail + // but the enforced strategy should be "append" + ASSERT_NE(validation, yaml_helper::validation_ok); + + ASSERT_TRUE(falco_config.m_config.is_defined("foo")); + std::vector foos; + auto expected_foos = std::vector{"bar", "bar2"}; + ASSERT_NO_THROW(falco_config.m_config.get_sequence>(foos, "foo")); + ASSERT_EQ(foos.size(), 2); // 2 elements in `foo` sequence because we appended to it + for(size_t i = 0; i < foos.size(); ++i) { + EXPECT_EQ(foos[i], expected_foos[i]) + << "Vectors foo's and expected_foo's differ at index " << i; + } + + ASSERT_TRUE(falco_config.m_config.is_defined("base_value.id")); + ASSERT_EQ(falco_config.m_config.get_scalar("base_value.id", 0), 3); // overridden! + ASSERT_FALSE(falco_config.m_config.is_defined( + "base_value.name")); // no more present since entire `base_value` block was overridden + ASSERT_TRUE(falco_config.m_config.is_defined("base_value_2.id")); + ASSERT_EQ(falco_config.m_config.get_scalar("base_value_2.id", 0), 2); + ASSERT_FALSE(falco_config.m_config.is_defined("base_value_3.id")); // not defined + + std::filesystem::remove("main.yaml"); + std::filesystem::remove("conf_2.yaml"); + std::filesystem::remove("conf_3.yaml"); +} + TEST(Configuration, configuration_config_files_unexistent) { /* Test that including an unexistent file just skips it */ const std::string main_conf_yaml = yaml_helper::configs_key + diff --git a/userspace/engine/yaml_helper.h b/userspace/engine/yaml_helper.h index aeb9a6fb..f37a7630 100644 --- a/userspace/engine/yaml_helper.h +++ b/userspace/engine/yaml_helper.h @@ -85,25 +85,31 @@ public: inline static const std::string validation_failed = "failed"; inline static const std::string validation_none = "none"; - enum include_files_strategy { + enum config_files_strategy { STRATEGY_APPEND, // append to existing sequence keys, override scalar keys and add new ones STRATEGY_OVERRIDE, // override existing keys (sequences too) and add new ones STRATEGY_ADDONLY // only add new keys and ignore existing ones }; - static inline enum include_files_strategy get_include_file_strategy( - std::string& include_file_name) { - if(include_file_name.length() > 0) { - if(include_file_name[0] == '+') { - include_file_name.erase(0, 1); - return STRATEGY_ADDONLY; - } - if(include_file_name[0] == '@') { - include_file_name.erase(0, 1); - return STRATEGY_OVERRIDE; - } + static enum config_files_strategy strategy_from_string(const std::string& strategy) { + if(strategy == "override") { + return yaml_helper::STRATEGY_OVERRIDE; + } + if(strategy == "add-only") { + return yaml_helper::STRATEGY_ADDONLY; + } + return yaml_helper::STRATEGY_APPEND; + } + + static std::string strategy_to_string(const enum config_files_strategy strategy) { + switch(strategy) { + case yaml_helper::STRATEGY_OVERRIDE: + return "override"; + case yaml_helper::STRATEGY_ADDONLY: + return "add-only"; + default: + return "append"; } - return STRATEGY_APPEND; } /** @@ -158,7 +164,7 @@ public: } void include_config_file(const std::string& include_file_path, - enum include_files_strategy strategy = STRATEGY_APPEND, + enum config_files_strategy strategy = STRATEGY_APPEND, const nlohmann::json& schema = {}, std::vector* schema_warnings = nullptr) { auto loaded_nodes = load_from_file_int(include_file_path, schema, schema_warnings); diff --git a/userspace/falco/config_json_schema.h b/userspace/falco/config_json_schema.h index 6048c472..3a58faac 100644 --- a/userspace/falco/config_json_schema.h +++ b/userspace/falco/config_json_schema.h @@ -38,7 +38,30 @@ const char config_schema_string[] = LONG_STRING_CONST( "config_files": { "type": "array", "items": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "strategy": { + "type": "string", + "enum": [ + "append", + "override", + "add-only" + ] + } + }, + "required": [ + "path" + ] + } + ] } }, "watch_config_files": { diff --git a/userspace/falco/configuration.cpp b/userspace/falco/configuration.cpp index 06376125..fb20c6c6 100644 --- a/userspace/falco/configuration.cpp +++ b/userspace/falco/configuration.cpp @@ -162,13 +162,12 @@ void falco_configuration::merge_config_files(const std::string &config_name, m_loaded_configs_filenames.push_back(config_name); const auto ppath = std::filesystem::path(config_name); // Parse files to be included - std::vector include_files; - m_config.get_sequence>(include_files, yaml_helper::configs_key); - for(auto include_file : include_files) { - // This can modify include_file by dropping the '+/@' prefixes. - const auto merge_strategy = yaml_helper::get_include_file_strategy(include_file); - - auto include_file_path = std::filesystem::path(include_file); + std::list include_files; + m_config.get_sequence>( + include_files, + yaml_helper::configs_key); + for(const auto &include_file : include_files) { + auto include_file_path = std::filesystem::path(include_file.m_path); if(include_file_path == ppath) { throw std::logic_error("Config error: '" + yaml_helper::configs_key + "' directive tried to recursively include main config file: " + @@ -179,15 +178,15 @@ void falco_configuration::merge_config_files(const std::string &config_name, continue; } if(std::filesystem::is_regular_file(include_file_path)) { - m_loaded_configs_filenames.push_back(include_file); - m_config.include_config_file(include_file, - merge_strategy, + m_loaded_configs_filenames.push_back(include_file.m_path); + m_config.include_config_file(include_file.m_path, + include_file.m_strategy, m_config_schema, &validation_status); // Only report top most schema validation status - res[include_file] = validation_status[0]; + res[include_file.m_path] = validation_status[0]; } else if(std::filesystem::is_directory(include_file_path)) { - m_loaded_configs_folders.push_back(include_file); + m_loaded_configs_folders.push_back(include_file.m_path); std::vector v; const auto it_options = std::filesystem::directory_options::follow_directory_symlink | std::filesystem::directory_options::skip_permission_denied; @@ -200,7 +199,7 @@ void falco_configuration::merge_config_files(const std::string &config_name, std::sort(v.begin(), v.end()); for(const auto &f : v) { m_config.include_config_file(f, - merge_strategy, + include_file.m_strategy, m_config_schema, &validation_status); // Only report top most schema validation status diff --git a/userspace/falco/configuration.h b/userspace/falco/configuration.h index 54516cd5..0f4d4ab9 100644 --- a/userspace/falco/configuration.h +++ b/userspace/falco/configuration.h @@ -54,6 +54,11 @@ public: std::string m_open_params; }; + struct config_files_config { + std::string m_path; + yaml_helper::config_files_strategy m_strategy; + }; + struct kmod_config { int16_t m_buf_size_preset; bool m_drop_failed_exit; @@ -425,4 +430,39 @@ struct convert { return true; } }; + +template<> +struct convert { + static Node encode(const falco_configuration::config_files_config& rhs) { + Node node; + node["path"] = rhs.m_path; + node["strategy"] = yaml_helper::strategy_to_string(rhs.m_strategy); + return node; + } + + static bool decode(const Node& node, falco_configuration::config_files_config& rhs) { + if(!node.IsMap()) { + // Single string mode defaults to append strategy + rhs.m_path = node.as(); + rhs.m_strategy = yaml_helper::STRATEGY_APPEND; + return true; + } + + // Path is required + if(!node["path"]) { + return false; + } + rhs.m_path = node["path"].as(); + + // Strategy is not required + if(!node["strategy"]) { + rhs.m_strategy = yaml_helper::STRATEGY_APPEND; + } else { + std::string strategy = node["strategy"].as(); + rhs.m_strategy = yaml_helper::strategy_from_string(strategy); + } + return true; + } +}; + } // namespace YAML