diff --git a/userspace/engine/rule_loader.cpp b/userspace/engine/rule_loader.cpp index 2fed30a2..a523df3f 100644 --- a/userspace/engine/rule_loader.cpp +++ b/userspace/engine/rule_loader.cpp @@ -27,14 +27,6 @@ static string s_default_extra_fmt = "%container.name (id=%container.id)"; using namespace std; using namespace libsinsp::filter; -static string ctxerr(std::string ctx, std::string e) -{ - e += "\n---\n"; - e += trim(ctx); - e += "\n---"; - return e; -} - // todo(jasondellaluce): this breaks string escaping in lists and exceptions static void quote_item(string& e) { @@ -83,7 +75,7 @@ static bool is_operator_for_list(std::string op) } static bool is_format_valid( - falco_engine* e, string src, string fmt, string& err) + falco_engine* e, const string& src, const string& fmt, string& err) { try { @@ -98,81 +90,177 @@ static bool is_format_valid( } } -static string yaml_format_object( - const string& content, - const vector& docs, - vector::iterator doc, - YAML::iterator node) + +template +static inline void define_info(indexed_vector& infos, T& info, uint32_t id) { - YAML::Node item = *node++; - YAML::Node cur_doc = *doc++; - // include the "- " sequence mark - size_t from = item.Mark().pos - 2; - size_t to = 0; - if (node != cur_doc.end()) + auto prev = infos.at(info.name); + if (prev) { - // end of item is beginning of next item - to = node->Mark().pos - 2; - } - else if (doc != docs.end()) - { - // end of item is beginning of next doc - to = doc->Mark().pos - 4; + info.index = prev->index; + info.visibility = id; + *prev = info; } else { - // end of item is end of file contents - to = content.length(); - } - string obj = content.substr(from, to - from); - return trim(obj); -} - -template -static bool yaml_is_type(const YAML::Node& v) -{ - T t; - return v.IsDefined() && v.IsScalar() && YAML::convert::decode(v, t); -} - -template -static void yaml_decode_seq(const YAML::Node& item, vector& out) -{ - T value; - for(const YAML::Node& v : item) - { - THROW(!v.IsScalar() || !YAML::convert::decode(v, value), - "Can't decode YAML sequence value: " + YAML::Dump(v)); - out.push_back(value); + info.index = id; + info.visibility = id; + infos.insert(info, info.name); } } template -static void yaml_decode_seq(const YAML::Node& item, set& out) +static inline void append_info(T* prev, T& info, uint32_t id) { - T value; - for(const YAML::Node& v : item) + prev->visibility = id; + prev->context.append(info.context); +} + +static void validate_exception_info( + rule_loader::context& ctx, + rule_loader::rule_exception_info &ex, + const string& source) +{ + if (ex.fields.is_list) { - THROW(!v.IsScalar() || !YAML::convert::decode(v, value), - "Can't decode YAML sequence value: " + YAML::Dump(v)); - out.insert(value); + if (!ex.comps.is_valid()) + { + ex.comps.is_list = true; + for (size_t i = 0; i < ex.fields.items.size(); i++) + { + ex.comps.items.push_back({false, "="}); + } + } + THROW(ex.fields.items.size() != ex.comps.items.size(), + "Rule exception item " + ex.name + + ": fields and comps lists must have equal length"); + for (auto &v : ex.comps.items) + { + THROW(!is_operator_defined(v.item), + "Rule exception item " + ex.name + ": comparison operator " + + v.item + " is not a supported comparison operator"); + } + for (auto &v : ex.fields.items) + { + THROW(!is_field_defined(ctx.engine, source, v.item), + "Rule exception item " + ex.name + ": field name " + + v.item + " is not a supported filter field"); + } + } + else + { + if (!ex.comps.is_valid()) + { + ex.comps.is_list = false; + ex.comps.items.push_back({false, "in"}); + } + THROW(ex.comps.is_list, "Rule exception item " + + ex.name + ": fields and comps must both be strings"); + THROW(!is_operator_defined(ex.comps.item), + "Rule exception item " + ex.name + ": comparison operator " + + ex.comps.item + " is not a supported comparison operator"); + THROW(!is_field_defined(ctx.engine, source, ex.fields.item), + "Rule exception item " + ex.name + ": field name " + + ex.fields.item + " is not a supported filter field"); + } +} + +static void build_rule_exception_infos( + vector exceptions, + set& exception_fields, + string& condition) +{ + string tmp; + for (auto &ex : exceptions) + { + string icond; + if(!ex.fields.is_list) + { + for (auto &val : ex.values) + { + THROW(val.is_list, "Expected values array for item " + + ex.name + " to contain a list of strings"); + icond += icond.empty() + ? ("(" + ex.fields.item + " " + + ex.comps.item + " (") + : ", "; + exception_fields.insert(ex.fields.item); + tmp = val.item; + quote_item(tmp); + icond += tmp; + } + icond += icond.empty() ? "" : "))"; + } + else + { + icond = "("; + for (auto &values : ex.values) + { + THROW(ex.fields.items.size() != values.items.size(), + "Exception item " + ex.name + + ": fields and values lists must have equal length"); + icond += icond == "(" ? "" : " or "; + icond += "("; + uint32_t k = 0; + string istr; + for (auto &field : ex.fields.items) + { + icond += k == 0 ? "" : " and "; + if (values.items[k].is_list) + { + istr = "("; + for (auto &v : values.items[k].items) + { + tmp = v.item; + quote_item(tmp); + istr += istr == "(" ? "" : ", "; + istr += tmp; + } + istr += ")"; + } + else + { + istr = values.items[k].item; + if(is_operator_for_list(ex.comps.items[k].item)) + { + paren_item(istr); + } + else + { + quote_item(istr); + } + } + icond += " " + field.item; + icond += " " + ex.comps.items[k].item + " " + istr; + exception_fields.insert(field.item); + k++; + } + icond += ")"; + } + icond += ")"; + if (icond == "()") + { + icond = ""; + } + } + condition += icond.empty() ? "" : " and not " + icond; } } // todo(jasondellaluce): this breaks string escaping in lists -static bool resolve_list(string& cnd, const YAML::Node& list) +static bool resolve_list(string& cnd, const rule_loader::list_info& list) { static string blanks = " \t\n\r"; static string delims = blanks + "(),="; string new_cnd; size_t start, end; bool used = false; - start = cnd.find(list["list"].as()); + start = cnd.find(list.name); while (start != string::npos) { // the characters surrounding the name must // be delims of beginning/end of string - end = start + list["list"].as().length(); + end = start + list.name.length(); if ((start == 0 || delims.find(cnd[start - 1]) != string::npos) && (end >= cnd.length() || delims.find(cnd[end]) != string::npos)) { @@ -189,13 +277,13 @@ static bool resolve_list(string& cnd, const YAML::Node& list) } // create substitution string by concatenating all values string sub = ""; - for (auto v : list["items"]) + for (auto &v : list.items) { if (!sub.empty()) { sub += ", "; } - sub += v.as(); + sub += v; } // if substituted list is empty, we need to // remove a comma from the left or the right @@ -225,24 +313,23 @@ static bool resolve_list(string& cnd, const YAML::Node& list) start += sub.length() + 1; used = true; } - start = cnd.find(list["list"].as(), start + 1); + start = cnd.find(list.name, start + 1); } return used; } static void resolve_macros( - const indexed_vector>>& macros, - map& used_macros, + indexed_vector& macros, shared_ptr& ast, - uint32_t index_visibility, - string on_unknown_err_prefix) + uint32_t visibility, + const string& on_unknown_err_prefix) { filter_macro_resolver macro_resolver; - for (auto &ref : macros) + for (auto &m : macros) { - if (ref.first["index"].as() < index_visibility) + if (m.index < visibility) { - macro_resolver.set_macro(ref.first["macro"].as(), ref.second); + macro_resolver.set_macro(m.name, m.cond_ast); } } macro_resolver.run(ast); @@ -250,24 +337,25 @@ static void resolve_macros( on_unknown_err_prefix + "Undefined macro '" + *macro_resolver.get_unknown_macros().begin() + "' used in filter."); - for (auto &resolved : macro_resolver.get_resolved_macros()) + for (auto &m : macro_resolver.get_resolved_macros()) { - used_macros[resolved] = true; + macros.at(m)->used = true; } } // note: there is no visibility order between filter conditions and lists -static shared_ptr parse_condition(string cnd, - std::map& used_lists, const indexed_vector& lists) +static shared_ptr parse_condition( + string condition, + indexed_vector& lists) { for (auto &l : lists) { - if (resolve_list(cnd, l)) + if (resolve_list(condition, l)) { - used_lists[l["list"].as()] = true; + l.used = true; } } - libsinsp::filter::parser p(cnd); + libsinsp::filter::parser p(condition); p.set_max_depth(1000); try { @@ -277,7 +365,7 @@ static shared_ptr parse_condition(string cnd, catch (const sinsp_exception& e) { throw falco_exception("Compilation error when compiling \"" - + cnd + "\": " + to_string(p.get_pos().col) + ": " + e.what()); + + condition + "\": " + to_string(p.get_pos().col) + ": " + e.what()); } } @@ -304,208 +392,29 @@ static shared_ptr compile_condition( return nullptr; } -static void define_info(indexed_vector& infos, - YAML::Node& item, string name, uint32_t id) +void apply_output_substitutions(rule_loader::context ctx, string& out) { - auto prev = infos.at(name); - if (prev) + if (out.find(s_container_info_fmt) != string::npos) { - item["index"] = (*prev)["index"]; - item["index_visibility"] = id; - (*prev) = item; - } - else - { - item["index"] = id; - item["index_visibility"] = id; - infos.insert(item, name); - } -} - -static void append_infos(YAML::Node& item, YAML::Node& append, uint32_t id) -{ - item["index_visibility"] = id; - item["context"] = item["context"].as() - + "\n\n" + append["context"].as(); -} - -static void validate_rule_exception( - falco_engine* engine, YAML::Node& ex, string source) -{ - switch (ex["fields"].Type()) - { - case YAML::NodeType::Scalar: - if (!ex["comps"].IsDefined()) - { - ex["comps"] = "in"; - } - else - { - THROW(!yaml_is_type(ex["fields"]) - || !yaml_is_type(ex["comps"]), - "Rule exception item " + ex["name"].as() - + ": fields and comps must both be strings"); - } - THROW(!is_field_defined( - engine, source, ex["fields"].as()), - "Rule exception item " + ex["name"].as() - + ": field name " + ex["fields"].as() - + " is not a supported filter field"); - THROW(!is_operator_defined(ex["comps"].as()), - "Rule exception item " + ex["name"].as() - + ": comparison operator " + ex["comps"].as() - + " is not a supported comparison operator"); - break; - case YAML::NodeType::Sequence: - if (!ex["comps"].IsDefined()) - { - ex["comps"] = vector(); - for (size_t i = 0; i < ex["fields"].size(); i++) - { - ex["comps"].push_back("="); - } - } - else - { - THROW(ex["fields"].size() != ex["comps"].size(), - "Rule exception item " + ex["name"].as() - + ": fields and comps lists must have equal length"); - } - for (auto field : ex["fields"]) - { - THROW(!yaml_is_type(field), - "Rule exception item " + ex["name"].as() - + ": fields must strings "); - THROW(!is_field_defined(engine, source, field.as()), - "Rule exception item " + ex["name"].as() - + ": field name " + field.as() - + " is not a supported filter field"); - } - for (auto comp : ex["comps"]) - { - THROW(!yaml_is_type(comp), - "Rule exception item " + ex["name"].as() - + ": comps must strings "); - THROW(!is_operator_defined(comp.as()), - "Rule exception item " + ex["name"].as() - + ": comparison operator " + comp.as() - + " is not a supported comparison operator"); - } - break; - default: - throw falco_exception( - "Rule exception fields must be a sequence or a string"); - } -} - -static void build_rule_exception_infos( - YAML::Node exceptions, set& exception_fields, string& condition) -{ - for (auto ex : exceptions) - { - string icond; - string value; - string exname = ex["name"].as(); - if(ex["fields"].IsScalar()) + if (ctx.replace_output_container_info) { - for (auto val : ex["values"]) - { - THROW(!yaml_is_type(val), - "Expected values array for item " - + exname + " to contain a list of strings"); - icond += icond.empty() - ? ("(" + ex["fields"].as() + " " - + ex["comps"].as() + " (") - : ", "; - exception_fields.insert(ex["fields"].as()); - value = val.as(); - quote_item(value); - icond += value; - } - icond += icond.empty() ? "" : "))"; + out = replace(out, s_container_info_fmt, ctx.output_extra); + return; } - else - { - icond = "("; - for (auto values : ex["values"]) - { - THROW(ex["fields"].size() != values.size(), - "Exception item " + exname - + ": fields and values lists must have equal length"); - icond += icond == "(" ? "" : " or "; - icond += "("; - uint32_t k = 0; - string istr; - for (auto field : ex["fields"]) - { - icond += k == 0 ? "" : " and "; - if (values[k].IsSequence()) - { - istr = "("; - for (auto v : values[k]) - { - value = v.as(); - quote_item(value); - istr += istr == "(" ? "" : ", "; - istr += value; - } - istr += ")"; - } - else - { - istr = values[k].as(); - if(is_operator_for_list(ex["comps"][k].as())) - { - paren_item(istr); - } - else - { - quote_item(istr); - } - } - icond += " " + ex["fields"][k].as() - + " " + ex["comps"][k].as() - + " " + istr; - exception_fields.insert(ex["fields"][k].as()); - k++; - } - icond += ")"; - } - icond += ")"; - if (icond == "()") - { - icond = ""; - } - } - condition += icond.empty() ? "" : " and not " + icond; + out = replace(out, s_container_info_fmt, s_default_extra_fmt); } + out += ctx.output_extra.empty() ? "" : " " + ctx.output_extra; } void rule_loader::clear() { m_cur_index = 0; - m_rules.clear(); m_rule_infos.clear(); m_list_infos.clear(); m_macro_infos.clear(); m_required_plugin_versions.clear(); } -const indexed_vector& rule_loader::rules() -{ - return m_rules; -} - -void rule_loader::configure( - falco_common::priority_type min_priority, - bool replace_container_info, - const string& extra) -{ - m_extra = extra; - m_min_priority = min_priority; - m_replace_container_info = replace_container_info; -} - bool rule_loader::is_plugin_compatible( const string &name, const string &version, @@ -521,7 +430,7 @@ bool rule_loader::is_plugin_compatible( auto it = m_required_plugin_versions.find(name); if (it != m_required_plugin_versions.end()) { - for(auto &rversion : it->second) + for (auto &rversion : it->second) { sinsp_plugin::version req_version(rversion); if (!plugin_version.check(req_version)) @@ -534,541 +443,251 @@ bool rule_loader::is_plugin_compatible( return true; } -void rule_loader::apply_output_substitutions(std::string& out) +void rule_loader::define(context& ctx, engine_version_info& info) { - if (out.find(s_container_info_fmt) != string::npos) - { - if (m_replace_container_info) - { - out = replace(out, s_container_info_fmt, m_extra); - return; - } - out = replace(out, s_container_info_fmt, s_default_extra_fmt); - } - out += m_extra.empty() ? "" : " " + m_extra; + auto v = falco_engine::engine_version(); + THROW(v < info.version, "Rules require engine version " + + to_string(info.version) + ", but engine version is " + to_string(v)); } -bool rule_loader::load(const string &content, falco_engine* engine, - vector& warnings, vector& errors) +void rule_loader::define(context& ctx, plugin_version_info& info) { - if (read(content, engine, warnings, errors)) - { - m_rules.clear(); - engine->clear_filters(); - return expand(engine, warnings, errors); - } - return false; + m_required_plugin_versions[info.name].insert(info.version); } -bool rule_loader::read(const string &content, falco_engine* engine, - vector& warnings, vector& errors) +void rule_loader::define(context& ctx, list_info& info) { - std::vector docs; - try - { - docs = YAML::LoadAll(content); - } - catch(const exception& e) - { - errors.push_back("Could not load YAML file: " + string(e.what())); - return false; - } - - for (auto doc = docs.begin(); doc != docs.end(); doc++) - { - if (doc->IsDefined() && !doc->IsNull()) - { - if(!doc->IsMap() && !doc->IsSequence()) - { - errors.push_back("Rules content is not yaml"); - return false; - } - if(!doc->IsSequence()) - { - errors.push_back("Rules content is not yaml array of objects"); - return false; - } - for(auto it = doc->begin(); it != doc->end(); it++) - { - if (!it->IsNull()) - { - string ctx = yaml_format_object(content, docs, doc, it); - YAML::Node item = *it; - try - { - THROW(!item.IsMap(), "Unexpected element type. " - "Each element should be a yaml associative array."); - item["context"] = ctx; - read_item(engine, item, warnings); - } - catch(const exception& e) - { - errors.push_back(ctxerr(ctx, e.what())); - return false; - } - } - } - } - } - return true; + define_info(m_list_infos, info, m_cur_index++); } -void rule_loader::read_item( - falco_engine* engine, YAML::Node& item, vector& warnings) +void rule_loader::append(context& ctx, list_info& info) { - if (item["required_engine_version"].IsDefined()) - { - read_required_engine_version(engine, item, warnings); - } - else if(item["required_plugin_versions"].IsDefined()) - { - read_required_plugin_versions(engine, item, warnings); - } - else if(item["macro"].IsDefined()) - { - read_macro(engine, item, warnings); - } - else if(item["list"].IsDefined()) - { - read_list(engine, item, warnings); - } - else if(item["rule"].IsDefined()) - { - read_rule(engine, item, warnings); - } - else - { - warnings.push_back("Unknown top level object"); - } + auto prev = m_list_infos.at(info.name); + THROW(!prev, "List " + info.name + + " has 'append' key but no list by that name already exists"); + prev->items.insert(prev->items.end(), info.items.begin(), info.items.end()); + append_info(prev, info, m_cur_index++); } -void rule_loader::read_required_engine_version( - falco_engine* engine, YAML::Node& item, vector& warnings) +void rule_loader::define(context& ctx, macro_info& info) { - uint32_t v = 0; - THROW(!YAML::convert::decode(item["required_engine_version"], v), - "Value of required_engine_version must be a number"); - auto engine_ver = falco_engine::engine_version(); - THROW(engine_ver < v, "Rules require engine version " + to_string(v) - + ", but engine version is " + to_string(engine_ver)); -} - -void rule_loader::read_required_plugin_versions( - falco_engine* engine, YAML::Node& item, vector& warnings) -{ - string name, ver; - THROW(!item["required_plugin_versions"].IsSequence(), - "Value of required_plugin_versions must be a sequence"); - for(const YAML::Node& plugin : item["required_plugin_versions"]) + if (!ctx.engine->is_source_valid(info.source)) { - THROW(!plugin["name"].IsDefined() - || !YAML::convert::decode(plugin["name"], name) - || name.empty(), - "required_plugin_versions item must have name property"); - THROW(!plugin["version"].IsDefined() - || !YAML::convert::decode(plugin["version"], ver) - || ver.empty(), - "required_plugin_versions item must have version property"); - m_required_plugin_versions[name].insert(ver); - } -} - -void rule_loader::read_list( - falco_engine* engine, YAML::Node& item, vector& warnings) -{ - string name; - THROW(!YAML::convert::decode(item["list"], name) || name.empty(), - "List name is empty"); - - THROW(!item["items"].IsDefined() || !item["items"].IsSequence(), - "List must have property items"); - - if(item["append"].IsDefined() && item["append"].as()) - { - auto prev = m_list_infos.at(name); - THROW(!prev, "List " + name + - " has 'append' key but no list by that name already exists"); - for (auto val : item["items"]) - { - (*prev)["items"].push_back(val.as()); - } - append_infos(*prev, item, m_cur_index++); - return; - } - define_info(m_list_infos, item, name, m_cur_index++); -} - -void rule_loader::read_macro( - falco_engine* engine, YAML::Node& item, vector& warnings) -{ - string name, cnd; - THROW(!YAML::convert::decode(item["macro"], name) || name.empty(), - "Macro name is empty"); - - THROW(!item["condition"].IsDefined() - || !YAML::convert::decode(item["condition"], cnd) - || cnd.empty(), - "Macro must have property condition"); - - if (!yaml_is_type(item["source"]) - || item["source"].as().empty()) - { - item["source"] = falco_common::syscall_source; - } - if (!engine->is_source_valid(item["source"].as())) - { - warnings.push_back("Macro " + name + ctx.warnings.push_back("Macro " + info.name + ": warning (unknown-source): unknown source " - + item["source"].as() + ", skipping"); + + info.source + ", skipping"); return; } - - if(item["append"].IsDefined() && item["append"].as()) - { - auto prev = m_macro_infos.at(name); - THROW(!prev, "Macro " + name - + " has 'append' key but no macro by that name already exists"); - (*prev)["condition"] = (*prev)["condition"].as() + " " + cnd; - append_infos(*prev, item, m_cur_index++); - return; - } - define_info(m_macro_infos, item, name, m_cur_index++); + define_info(m_macro_infos, info, m_cur_index++); } -void rule_loader::read_rule( - falco_engine* engine, YAML::Node& item, vector& warnings) +void rule_loader::append(context& ctx, macro_info& info) { - string name; - falco_common::priority_type priority; - THROW(!YAML::convert::decode(item["rule"], name) || name.empty(), - "Rule name is empty"); + auto prev = m_macro_infos.at(info.name); + THROW(!prev, "Macro " + info.name + + " has 'append' key but no macro by that name already exists"); + prev->cond += " "; + prev->cond += info.cond; + append_info(prev, info, m_cur_index++); +} - auto prev = m_rule_infos.at(name); - - if (!yaml_is_type(item["skip-if-unknown-filter"])) +void rule_loader::define(context& ctx, rule_info& info) +{ + if (!ctx.engine->is_source_valid(info.source)) { - item["skip-if-unknown-filter"] = false; - } - if (!yaml_is_type(item["warn_evttypes"])) - { - item["warn_evttypes"] = true; - } - if (!yaml_is_type(item["append"])) - { - item["append"] = false; - } - - if (!yaml_is_type(item["source"]) - || item["source"].as().empty()) - { - item["source"] = falco_common::syscall_source; - } - if (!engine->is_source_valid(item["source"].as())) - { - warnings.push_back("Rule " + name + ctx.warnings.push_back("Rule " + info.name + ": warning (unknown-source): unknown source " - + item["source"].as() + ", skipping"); - return; - } - THROW(prev && (*prev)["source"].as() != item["source"].as(), - "Rule " + name + " has been re-defined with a different source"); - - if (item["append"].as()) - { - THROW(!prev, "Rule " + name - + " has 'append' key but no rule by that name already exists"); - THROW(!item["condition"].IsDefined() && !item["exceptions"].IsDefined(), - "Appended rule must have exceptions or condition property"); - - if (item["exceptions"].IsDefined()) - { - read_rule_exceptions(engine, item, true); - } - - if (item["condition"].IsDefined()) - { - (*prev)["condition"] = (*prev)["condition"].as() - + " " + item["condition"].as(); - } - append_infos(*prev, item, m_cur_index++); + + info.source + ", skipping"); return; } - if (!item["condition"].IsDefined() || !item["output"].IsDefined() - || !item["desc"].IsDefined() || !item["priority"].IsDefined()) + auto prev = m_macro_infos.at(info.name); + THROW(prev && prev->source != info.source, + "Rule " + info.name + " has been re-defined with a different source"); + + for (auto &ex : info.exceptions) { - // we support enabled-only rules - THROW(!yaml_is_type(item["enabled"]), - "Rule must have properties 'condition', 'output', 'desc', and 'priority'"); - auto prev = m_rule_infos.at(name); - THROW(!prev, "Rule " + name - + " has 'enabled' key but no rule by that name already exists"); - (*prev)["enabled"] = item["enabled"].as(); - return; + THROW(!ex.fields.is_valid(), "Rule exception item " + + ex.name + ": must have fields property with a list of fields"); + validate_exception_info(ctx, ex, info.source); } - if (!yaml_is_type(item["enabled"])) - { - item["enabled"] = true; - } - - THROW(!yaml_is_type(item["priority"]) - || !falco_common::parse_priority( - item["priority"].as(), priority), - "Invalid priority"); - item["priority_num"] = (uint32_t) priority; - - string output = item["output"].as(); - item["output"] = trim(output); - - if (item["exceptions"].IsDefined()) - { - read_rule_exceptions(engine, item, false); - } - - define_info(m_rule_infos, item, name, m_cur_index++); + define_info(m_rule_infos, info, m_cur_index++); } -void rule_loader::read_rule_exceptions( - falco_engine* engine, YAML::Node& item, bool append) +void rule_loader::append(context& ctx, rule_info& info) { - string exname; - string rule = item["rule"].as(); - THROW(!item["exceptions"].IsSequence(), "Rule exceptions must be a sequence"); - for (auto ex : item["exceptions"]) + auto prev = m_rule_infos.at(info.name); + THROW(!prev, "Rule " + info.name + + " has 'append' key but no rule by that name already exists"); + THROW(info.cond.empty() && info.exceptions.empty(), + "Appended rule must have exceptions or condition property"); + + if (!info.cond.empty()) { - THROW(!YAML::convert::decode(ex["name"], exname) - || exname.empty(), "Rule exception item must have name property"); + prev->cond += " "; + prev->cond += info.cond; + } - if(!ex["values"].IsDefined()) + for (auto &ex : info.exceptions) + { + auto prev_ex = find_if(prev->exceptions.begin(), prev->exceptions.end(), + [&ex](const rule_loader::rule_exception_info& i) + { return i.name == ex.name; }); + if (prev_ex == prev->exceptions.end()) { - ex["values"] = vector({}); - } - - if (append) - { - bool is_new = true; - auto prev = m_rule_infos.at(rule); - YAML::Node prev_ex; - for (YAML::Node e : (*prev)["exceptions"]) - { - if (is_new && e["name"].as() == exname) - { - prev_ex = e; - is_new = false; - } - } - if (is_new) - { - THROW(!ex["fields"].IsDefined(), - "Rule exception new item " + exname - + ": must have fields property with a list of fields"); - THROW(!ex["values"].IsDefined(), - "Rule exception new item " + exname - + ": must have fields property with a list of values"); - validate_rule_exception(engine, ex, item["source"].as()); - (*prev)["exceptions"].push_back(ex); - } - else - { - THROW(ex["fields"].IsDefined(), - "Can not append exception fields to existing rule, only values"); - THROW(ex["comps"].IsDefined(), - "Can not append exception comps to existing rule, only values"); - for (auto vals : ex["values"]) - { - prev_ex["values"].push_back(vals); - } - } + THROW(!ex.fields.is_valid(), "Rule exception new item " + + ex.name + ": must have fields property with a list of fields"); + THROW(ex.values.empty(), "Rule exception new item " + + ex.name + ": must have fields property with a list of values"); + validate_exception_info(ctx, ex, prev->source); + prev->exceptions.push_back(ex); } else { - THROW(!ex["fields"].IsDefined(), - "Rule exception item " + exname - + ": must have fields property with a list of fields"); - validate_rule_exception(engine, ex, item["source"].as()); + THROW(ex.fields.is_valid(), + "Can not append exception fields to existing rule, only values"); + THROW(ex.comps.is_valid(), + "Can not append exception comps to existing rule, only values"); + prev_ex->values.insert( + prev_ex->values.end(), ex.values.begin(), ex.values.end()); } } + append_info(prev, info, m_cur_index++); } -bool rule_loader::expand(falco_engine* engine, - vector& warnings, vector& errors) +void rule_loader::enable(context& ctx, rule_info& info) { - indexed_vector lists; - indexed_vector macros; - map used_lists; - map used_macros; - - // expand all lists, macros, and rules - try - { - expand_list_infos(used_lists, lists); - expand_macro_infos(lists, used_lists, used_macros, macros); - expand_rule_infos(engine, lists, macros, used_lists, used_macros, warnings); - } - catch (exception& e) - { - errors.push_back(e.what()); - return false; - } - - // print info on any dangling lists or macros that were not used anywhere - for (auto &m : macros) - { - if (!used_macros[m.first["macro"].as()]) - { - warnings.push_back("macro " + m.first["macro"].as() - + " not referred to by any rule/macro"); - } - } - for (auto &l : lists) - { - if (!used_lists[l["list"].as()]) - { - warnings.push_back("list " + l["list"].as() - + " not referred to by any rule/macro/list"); - } - } - return true; + auto prev = m_rule_infos.at(info.name); + THROW(!prev, "Rule " + info.name + + " has 'enabled' key but no rule by that name already exists"); + prev->enabled = info.enabled; } -// note: there is a visibility ordering between lists -void rule_loader::expand_list_infos( - map& used, indexed_vector& out) +void rule_loader::compile_list_infos(context& ctx, indexed_vector& out) { - string value; - vector values; - for (auto l : m_list_infos) + string tmp; + vector used; + for (auto &list : m_list_infos) { try { - values.clear(); - for (auto item : l["items"]) + list_info v = list; + v.items.clear(); + for (auto &item : list.items) { - value = item.as(); - auto ref = m_list_infos.at(value); - if (ref && (*ref)["index"].as() < l["index_visibility"].as()) + auto ref = m_list_infos.at(item); + if (ref && ref->index < list.visibility) { - used[value] = true; - for (auto val : (*ref)["items"]) + used.push_back(ref->name); + for (auto val : ref->items) { - value = val.as(); - quote_item(value); - values.push_back(value); + quote_item(val); + v.items.push_back(val); } } else { - quote_item(value); - values.push_back(value); + tmp = item; + quote_item(tmp); + v.items.push_back(tmp); } } - auto new_list = YAML::Clone(l); - new_list["items"] = values; - out.insert(new_list, new_list["list"].as()); + v.used = false; + out.insert(v, v.name); } catch (exception& e) { - throw falco_exception(ctxerr(l["context"].as(), e.what())); + throw falco_exception(list.context.error(e.what())); } } + for (auto &v : used) + { + out.at(v)->used = true; + } } // note: there is a visibility ordering between macros -void rule_loader::expand_macro_infos( - const indexed_vector& lists, - map& used_lists, - map& used_macros, - indexed_vector& out) +void rule_loader::compile_macros_infos( + context& ctx, + indexed_vector& lists, + indexed_vector& out) { - for (auto m : m_macro_infos) + set used; + mark* info_ctx = NULL; + try { - try + for (auto &m : m_macro_infos) { - auto ast = parse_condition(m["condition"].as(), used_lists, lists); - auto pair = make_pair(m, ast); - out.insert(pair, m["macro"].as()); + info_ctx = &m.context; + macro_info entry = m; + entry.cond_ast = parse_condition(m.cond, lists); + entry.used = false; + out.insert(entry, m.name); } - catch (exception& e) + for (auto &m : out) { - throw falco_exception(ctxerr(m["context"].as(), e.what())); + info_ctx = &m.context; + resolve_macros(out, m.cond_ast, m.visibility, + "Compilation error when compiling \"" + m.cond + "\": "); } } - for (auto &m : out) + catch (exception& e) { - try - { - resolve_macros(out, used_macros, m.second, - m.first["index_visibility"].as(), - "Compilation error when compiling \"" - + m.first["condition"].as() + "\": "); - } - catch (exception& e) - { - throw falco_exception( - ctxerr(m.first["context"].as(), e.what())); - } + throw falco_exception(info_ctx->error(e.what())); } } -void rule_loader::expand_rule_infos( - falco_engine* engine, - const indexed_vector& lists, - const indexed_vector& macros, - map& used_lists, - map& used_macros, - vector& warnings) + +void rule_loader::compile_rule_infos( + context& ctx, + indexed_vector& lists, + indexed_vector& macros, + indexed_vector& out) { - string err; - for (auto r : m_rule_infos) + string err, condition; + for (auto &r : m_rule_infos) { try { - uint32_t priority = r["priority_num"].as(); - if ((falco_common::priority_type) priority > m_min_priority) + if (r.priority > ctx.min_priority) { continue; } - - set exception_fields; - string condition = r["condition"].as(); - if (r["exceptions"].IsDefined()) + + falco_rule rule; + condition = r.cond; + if (!r.exceptions.empty()) { build_rule_exception_infos( - r["exceptions"], exception_fields, condition); + r.exceptions, rule.exception_fields, condition); } - - auto ast = parse_condition(condition, used_lists, lists); - - resolve_macros(macros, used_macros, ast, MAX_VISIBILITY, ""); - - string output = r["output"].as(); - if (r["source"].as() == falco_common::syscall_source) - { - apply_output_substitutions(output); - } - - THROW(!is_format_valid(engine, r["source"].as(), output, err), - "Invalid output format '" + output + "': '" + err + "'"); + auto ast = parse_condition(condition, lists); + resolve_macros(macros, ast, MAX_VISIBILITY, ""); - falco_rule rule; - rule.name = r["rule"].as(); - rule.source = r["source"].as(); - rule.description = r["desc"].as(); - rule.output = output; - rule.priority = (falco_common::priority_type) priority; - rule.exception_fields = exception_fields; - yaml_decode_seq(r["tags"], rule.tags); - + rule.output = r.output; + if (r.source == falco_common::syscall_source) + { + apply_output_substitutions(ctx, rule.output); + } + THROW(!is_format_valid(ctx.engine, r.source, rule.output, err), + "Invalid output format '" + rule.output + "': '" + err + "'"); + + + rule.name = r.name; + rule.source = r.source; + rule.description = r.desc; + rule.priority = r.priority; + rule.tags = r.tags; // note: indexes are 0-based, but 0 is not an acceptable rule_id - auto rule_id = m_rules.insert(rule, rule.name) + 1; - auto filter = compile_condition(engine, rule_id, ast, rule.source, err); + auto id = out.insert(rule, rule.name) + 1; + auto filter = compile_condition(ctx.engine, id, ast, rule.source, err); if (!filter) { - if (r["skip-if-unknown-filter"].as() + if (r.skip_if_unknown_filter && err.find("nonexistent field") != string::npos) { - warnings.push_back( + ctx.warnings.push_back( "Rule " + rule.name + ": warning (unknown-field):"); continue; } @@ -1077,24 +696,61 @@ void rule_loader::expand_rule_infos( throw falco_exception("Rule " + rule.name + ": error " + err); } } - engine->add_filter(filter, rule.name, rule.source, rule.tags); - if (rule.source == falco_common::syscall_source - && r["warn_evttypes"].as()) + ctx.engine->add_filter(filter, rule.name, rule.source, rule.tags); + if (rule.source == falco_common::syscall_source && r.warn_evttypes) { auto evttypes = filter->evttypes(); if (evttypes.size() == 0 || evttypes.size() > 100) { - warnings.push_back( + ctx.warnings.push_back( "Rule " + rule.name + ": warning (no-evttype):\n" + + " matches too many evt.type values.\n" + " This has a significant performance penalty."); } } - engine->enable_rule(rule.name, r["enabled"].as()); + ctx.engine->enable_rule(rule.name, r.enabled); } catch (exception& e) { - throw falco_exception(ctxerr(r["context"].as(), e.what())); + throw falco_exception(r.context.error(e.what())); } } +} + +bool rule_loader::compile(context& ctx, indexed_vector& out) +{ + indexed_vector lists; + indexed_vector macros; + + // expand all lists, macros, and rules + try + { + compile_list_infos(ctx, lists); + compile_macros_infos(ctx, lists, macros); + compile_rule_infos(ctx, lists, macros, out); + } + catch (exception& e) + { + ctx.errors.push_back(e.what()); + return false; + } + + // print info on any dangling lists or macros that were not used anywhere + for (auto &m : macros) + { + if (!m.used) + { + ctx.warnings.push_back("macro " + m.name + + " not referred to by any rule/macro"); + } + } + for (auto &l : lists) + { + if (!l.used) + { + ctx.warnings.push_back("list " + l.name + + " not referred to by any rule/macro/list"); + } + } + return true; } \ No newline at end of file diff --git a/userspace/engine/rule_loader.h b/userspace/engine/rule_loader.h index 3c2ec54a..204099ce 100644 --- a/userspace/engine/rule_loader.h +++ b/userspace/engine/rule_loader.h @@ -33,110 +33,207 @@ class falco_engine; class rule_loader { public: + /*! + \brief Represents a section of text from which a certain info + struct has been decoded + */ + struct mark + { + std::string content; + + /*! + \brief Wraps an error by adding info about the text section + */ + inline std::string error(std::string err) + { + err += "\n---\n"; + err += trim(content); + err += "\n---"; + return err; + } + + /*! + \brief Appends another text section info to this one + */ + inline void append(mark& m) + { + content += "\n\n"; + content += m.content; + } + }; /*! - \brief Erases the internal states and all the loaded rules + \brief Contains the info required to load rule definitions + */ + struct context + { + context(const std::string& cont): content(cont) {} + const std::string& content; + std::string output_extra; + bool replace_output_container_info; + falco_common::priority_type min_priority; + std::vector warnings; + std::vector errors; + falco_engine* engine; + }; + + /*! + \brief Represents infos about an engine version requirement + */ + struct engine_version_info + { + uint32_t version; + }; + + /*! + \brief Represents infos about a plugin version requirement + */ + struct plugin_version_info + { + std::string name; + std::string version; + }; + + /*! + \brief Represents infos about a list + */ + struct list_info + { + mark context; + bool used; + size_t index; + size_t visibility; + std::string name; + std::vector items; + }; + + /*! + \brief Represents infos about a macro + */ + struct macro_info + { + mark context; + bool used; + size_t index; + size_t visibility; + std::string name; + std::string cond; + std::string source; + std::shared_ptr cond_ast; + }; + + /*! + \brief Represents infos about a single rule exception + */ + struct rule_exception_info + { + /*! + \brief This is necessary due to the dynamic-typed nature of + exceptions. Each of fields, comps, and values, can either be a + single value or a list of values. This is a simple hack to make + this easier to implement in C++, that is not non-dynamic-typed. + */ + struct entry { + bool is_list; + std::string item; + std::vector items; + + inline bool is_valid() const + { + return (is_list && !items.empty()) + || (!is_list && !item.empty()); + } + }; + + std::string name; + entry fields; + entry comps; + std::vector values; + }; + + /*! + \brief Represents infos about a rule + */ + struct rule_info + { + mark context; + size_t index; + size_t visibility; + std::string name; + std::string cond; + std::string source; + std::string desc; + std::string output; + std::set tags; + std::vector exceptions; + falco_common::priority_type priority; + bool enabled; + bool warn_evttypes; + bool skip_if_unknown_filter; + }; + + /*! + \brief Erases all the internal state and definitions */ virtual void clear(); - /*! - \brief Returns the rules loaded after the last invocation of load() - */ - virtual const indexed_vector& rules(); - - /*! - \brief Configures the loader. The changes will influence the next - invocation of load(). - \param min_priority The minimum priority below which rules are skipped - by the loader - \param extra Text to be appended/substituted in the output of all rules - \param replace_container_info If true, the extra string is used to - replace the "%container.info" token in rules outputs. If false, the - "%container.info" token is substituted with a default text and the - extra string is appended at the end of the rule output. If a rule - output does not contain "%container.info", then this flag has no effect - and the extra string is appended at the end of the rule output anyways. - */ - virtual void configure( - falco_common::priority_type min_priority, - bool replace_container_info, - const std::string& extra); - /*! \brief Returns true if the given plugin name and version are compatible - with the loaded rulesets. If false is returned, required_version is + with the internal definitions. If false is returned, required_version is filled with the required plugin version that didn't match. */ virtual bool is_plugin_compatible( const std::string& name, const std::string& version, std::string& required_version); + + /*! + \brief Uses the internal state to compile a list of falco_rules + */ + bool compile(context& ctx, indexed_vector& out); /*! - \brief Parses the content of a ruleset. This should be called multiple - times to load different rulesets. The internal state (e.g. loaded - rules, plugin version requirements, etc...) gets updated at each - invocation of the load() method. - \param content The contents of the ruleset - \param engine The instance of falco_engine used to add rule filters - \param warnings Filled-out with warnings - \param warnings Filled-out with errors - \return true if the ruleset content is loaded successfully + \brief Defines an info block. If a similar info block is found + in the internal state (e.g. another rule with same name), then + the previous definition gets overwritten */ - virtual bool load( - const std::string& content, - falco_engine* engine, - std::vector& warnings, - std::vector& errors); + virtual void define(context& ctx, engine_version_info& info); + virtual void define(context& ctx, plugin_version_info& info); + virtual void define(context& ctx, list_info& info); + virtual void define(context& ctx, macro_info& info); + virtual void define(context& ctx, rule_info& info); + + /*! + \brief Appends an info block to an existing one. An exception + is thrown if no existing definition can be matched with the appended + one + */ + virtual void append(context& ctx, list_info& info); + virtual void append(context& ctx, macro_info& info); + virtual void append(context& ctx, rule_info& info); + + /*! + \brief Updates the 'enabled' flag of an existing definition + */ + virtual void enable(context& ctx, rule_info& info); private: - typedef pair< - YAML::Node, - shared_ptr - > macro_node; - - bool read( - const std::string& content, falco_engine* engine, - std::vector& warnings, std::vector& errors); - void read_item( - falco_engine* engine, YAML::Node& item, vector& warnings); - void read_required_engine_version( - falco_engine* engine, YAML::Node& item, vector& warnings); - void read_required_plugin_versions( - falco_engine* engine, YAML::Node& item, vector& warnings); - void read_macro( - falco_engine* engine, YAML::Node& item, vector& warnings); - void read_list( - falco_engine* engine, YAML::Node& item, vector& warnings); - void read_rule( - falco_engine* engine, YAML::Node& item, vector& warnings); - void read_rule_exceptions( - falco_engine* engine, YAML::Node& item, bool append); - bool expand(falco_engine* engine, - std::vector& warnings, std::vector& errors); - void expand_list_infos( - std::map& used, indexed_vector& out); - void expand_macro_infos( - const indexed_vector& lists, - std::map& used_lists, - std::map& used_macros, - indexed_vector& out); - void expand_rule_infos( - falco_engine* engine, - const indexed_vector& lists, - const indexed_vector& macros, - std::map& used_lists, - std::map& used_macros, - vector& warnings); - void apply_output_substitutions(std::string& output); + void compile_list_infos( + context& ctx, + indexed_vector& out); + void compile_macros_infos( + context& ctx, + indexed_vector& lists, + indexed_vector& out); + void compile_rule_infos( + context& ctx, + indexed_vector& lists, + indexed_vector& macros, + indexed_vector& out); uint32_t m_cur_index; - std::string m_extra; - bool m_replace_container_info; - falco_common::priority_type m_min_priority; - indexed_vector m_rules; - indexed_vector m_rule_infos; - indexed_vector m_macro_infos; - indexed_vector m_list_infos; + indexed_vector m_rule_infos; + indexed_vector m_macro_infos; + indexed_vector m_list_infos; std::map> m_required_plugin_versions; };