diff --git a/unit_tests/engine/test_filter_details_resolver.cpp b/unit_tests/engine/test_filter_details_resolver.cpp new file mode 100644 index 00000000000..d83c100d44b --- /dev/null +++ b/unit_tests/engine/test_filter_details_resolver.cpp @@ -0,0 +1,49 @@ +/* +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 ASSERT_EQd 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 + + +TEST(DetailsResolver, resolve_ast) +{ + std::string cond = "(spawned_process or evt.type = open) and (proc.name icontains cat or proc.name in (known_procs, ps))"; + auto ast = libsinsp::filter::parser(cond).parse(); + filter_details details; + details.known_macros.insert("spawned_process"); + details.known_lists.insert("known_procs"); + filter_details_resolver resolver; + resolver.run(ast.get(), details); + + // Assert fields + ASSERT_EQ(details.fields.size(), 2); + ASSERT_NE(details.fields.find("evt.type"), details.fields.end()); + ASSERT_NE(details.fields.find("proc.name"), details.fields.end()); + + // Assert macros + ASSERT_EQ(details.macros.size(), 1); + ASSERT_NE(details.macros.find("spawned_process"), details.macros.end()); + + // Assert operators + ASSERT_EQ(details.operators.size(), 3); + ASSERT_NE(details.operators.find("="), details.operators.end()); + ASSERT_NE(details.operators.find("icontains"), details.operators.end()); + ASSERT_NE(details.operators.find("in"), details.operators.end()); + + // Assert lists + ASSERT_EQ(details.lists.size(), 1); + ASSERT_NE(details.lists.find("known_procs"), details.lists.end()); +} \ No newline at end of file diff --git a/userspace/engine/CMakeLists.txt b/userspace/engine/CMakeLists.txt index d6b2b1cf8d6..7ee9a1b906e 100644 --- a/userspace/engine/CMakeLists.txt +++ b/userspace/engine/CMakeLists.txt @@ -18,6 +18,7 @@ set(FALCO_ENGINE_SOURCE_FILES json_evt.cpp evttype_index_ruleset.cpp formats.cpp + filter_details_resolver.cpp filter_macro_resolver.cpp filter_warning_resolver.cpp stats_manager.cpp diff --git a/userspace/engine/falco_engine.cpp b/userspace/engine/falco_engine.cpp index 8ff591595d8..f8db92225f6 100644 --- a/userspace/engine/falco_engine.cpp +++ b/userspace/engine/falco_engine.cpp @@ -27,6 +27,9 @@ limitations under the License. #include #include #include +#include + +#include #include #include @@ -42,6 +45,7 @@ limitations under the License. #include "utils.h" #include "banned.h" // This raises a compilation error when certain functions are used #include "evttype_index_ruleset.h" +#include "filter_details_resolver.h" const std::string falco_engine::s_default_ruleset = "falco-default-ruleset"; @@ -427,27 +431,296 @@ std::size_t falco_engine::add_source(const std::string &source, return m_sources.insert(src, source); } -void falco_engine::describe_rule(std::string *rule) const +void falco_engine::describe_rule(std::string *rule, bool json) const { - static const char* rule_fmt = "%-50s %s\n"; - fprintf(stdout, rule_fmt, "Rule", "Description"); - fprintf(stdout, rule_fmt, "----", "-----------"); - if (!rule) + if(!json) { - for (auto &r : m_rules) + static const char *rule_fmt = "%-50s %s\n"; + fprintf(stdout, rule_fmt, "Rule", "Description"); + fprintf(stdout, rule_fmt, "----", "-----------"); + if(!rule) { - auto str = falco::utils::wrap_text(r.description, 51, 110) + "\n"; - fprintf(stdout, rule_fmt, r.name.c_str(), str.c_str()); + for(auto &r : m_rules) + { + auto str = falco::utils::wrap_text(r.description, 51, 110) + "\n"; + fprintf(stdout, rule_fmt, r.name.c_str(), str.c_str()); + } } + else + { + auto r = m_rules.at(*rule); + if(r == nullptr) + { + return; + } + auto str = falco::utils::wrap_text(r->description, 51, 110) + "\n"; + fprintf(stdout, rule_fmt, r->name.c_str(), str.c_str()); + } + + return; } + + std::unique_ptr insp(new sinsp()); + Json::FastWriter writer; + std::string json_str; + + if(!rule) + { + // In this case we build json information about + // all rules, macros and lists + Json::Value output; + + // Store information about rules + Json::Value rules_array = Json::arrayValue; + for(const auto& r : m_rules) + { + auto ri = m_rule_collector.rules().at(r.name); + Json::Value rule; + get_json_details(r, *ri, insp.get(), rule); + + // Append to rule array + rules_array.append(rule); + } + output["rules"] = rules_array; + + // Store information about macros + Json::Value macros_array; + for(const auto &m : m_rule_collector.macros()) + { + Json::Value macro; + get_json_details(m, macro); + macros_array.append(macro); + } + output["macros"] = macros_array; + + // Store information about lists + Json::Value lists_array = Json::arrayValue; + for(const auto &l : m_rule_collector.lists()) + { + Json::Value list; + get_json_details(l, list); + lists_array.append(list); + } + output["lists"] = lists_array; + + json_str = writer.write(output); + } else { - auto r = m_rules.at(*rule); - auto str = falco::utils::wrap_text(r->description, 51, 110) + "\n"; - fprintf(stdout, rule_fmt, r->name.c_str(), str.c_str()); + // build json information for just the specified rule + auto ri = m_rule_collector.rules().at(*rule); + if(ri == nullptr) + { + throw falco_exception("Rule \"" + *rule + "\" is not loaded"); + } + auto r = m_rules.at(ri->name); + Json::Value rule; + get_json_details(*r, *ri, insp.get(), rule); + json_str = writer.write(rule); } + + fprintf(stdout, "%s", json_str.c_str()); } +void falco_engine::get_json_details(const falco_rule &r, + const rule_loader::rule_info &ri, + sinsp *insp, + Json::Value &rule) const +{ + Json::Value rule_info; + + // Fill general rule information + rule_info["name"] = r.name; + rule_info["condition"] = ri.cond; + rule_info["priority"] = format_priority(r.priority, false); + rule_info["output"] = r.output; + rule_info["description"] = r.description; + rule_info["enabled"] = ri.enabled; + rule_info["source"] = r.source; + Json::Value tags = Json::arrayValue; + for(const auto &t : ri.tags) + { + tags.append(t); + } + rule_info["tags"] = tags; + rule["info"] = rule_info; + + // Parse rule condition and build the AST + // Assumption: no exception because rules have already been loaded. + auto ast = libsinsp::filter::parser(ri.cond).parse(); + Json::Value json_details; + get_json_details(ast.get(), json_details); + rule["details"] = json_details; + + // Get fields from output string + sinsp_evt_formatter fmt(insp, r.output); + std::vector out_fields; + fmt.get_field_names(out_fields); + Json::Value outputFields = Json::arrayValue; + for(const auto &of : out_fields) + { + outputFields.append(of); + } + rule["details"]["output_fields"] = outputFields; + + // Get fields from exceptions + Json::Value exception_fields = Json::arrayValue; + for(const auto &f : r.exception_fields) + { + exception_fields.append(f); + } + rule["details"]["exception_fields"] = exception_fields; + + // Get operators from exceptions + Json::Value exception_operators = Json::arrayValue; + for(const auto &e : ri.exceptions) + { + if(e.comps.is_list) + { + for(const auto& c : e.comps.items) + { + if(c.is_list) + { + // considering max two levels of lists + for(const auto& i : c.items) + { + exception_operators.append(i.item); + } + } + else + { + exception_operators.append(c.item); + } + } + } + else + { + exception_operators.append(e.comps.item); + } + } + rule["details"]["exception_operators"] = exception_operators; + + if(ri.source == falco_common::syscall_source) + { + // Store event types + Json::Value events; + get_json_evt_types(ast.get(), events); + rule["details"]["events"] = events; + } +} + +void falco_engine::get_json_details(const rule_loader::macro_info& m, + Json::Value& macro) const +{ + Json::Value macro_info; + + macro_info["name"] = m.name; + macro_info["condition"] = m.cond; + macro["info"] = macro_info; + + // Assumption: no exception because rules have already been loaded. + auto ast = libsinsp::filter::parser(m.cond).parse(); + + Json::Value json_details; + get_json_details(ast.get(), json_details); + macro["details"] = json_details; + + // Store event types + Json::Value events; + get_json_evt_types(ast.get(), events); + macro["details"]["events"] = events; +} + +void falco_engine::get_json_details(const rule_loader::list_info& l, + Json::Value& list) const +{ + Json::Value list_info; + list_info["name"] = l.name; + + Json::Value items = Json::arrayValue; + Json::Value lists = Json::arrayValue; + for(const auto &i : l.items) + { + if(m_rule_collector.lists().at(i) != nullptr) + { + lists.append(i); + continue; + } + items.append(i); + } + + list_info["items"] = items; + list["info"] = list_info; + list["details"]["lists"] = lists; +} + +void falco_engine::get_json_details(libsinsp::filter::ast::expr* ast, + Json::Value& output) const +{ + filter_details details; + for(const auto &m : m_rule_collector.macros()) + { + details.known_macros.insert(m.name); + } + + for(const auto &l : m_rule_collector.lists()) + { + details.known_lists.insert(l.name); + } + + // Resolve the AST details + filter_details_resolver resolver; + resolver.run(ast, details); + + Json::Value macros = Json::arrayValue; + for(const auto &m : details.macros) + { + macros.append(m); + } + output["macros"] = macros; + + Json::Value operators = Json::arrayValue; + for(const auto &o : details.operators) + { + operators.append(o); + } + output["operators"] = operators; + + Json::Value condition_fields = Json::arrayValue; + for(const auto &f : details.fields) + { + condition_fields.append(f); + } + output["condition_fields"] = condition_fields; + + Json::Value lists = Json::arrayValue; + for(const auto &l : details.lists) + { + lists.append(l); + } + output["lists"] = lists; + + details.reset(); +} + +void falco_engine::get_json_evt_types(libsinsp::filter::ast::expr* ast, + Json::Value& output) const +{ + output = Json::arrayValue; + auto evtcodes = libsinsp::filter::ast::ppm_event_codes(ast); + if(evtcodes.size() != libsinsp::events::all_event_set().size()) + { + auto syscodes = libsinsp::filter::ast::ppm_sc_codes(ast); + auto syscodes_to_evt_names = libsinsp::events::sc_set_to_event_names(syscodes); + auto evtcodes_to_evt_names = libsinsp::events::event_set_to_names(evtcodes, false); + for (const auto& n : unordered_set_union(syscodes_to_evt_names, evtcodes_to_evt_names)) + { + output.append(n); + } + } +} + + void falco_engine::print_stats() const { std::string out; diff --git a/userspace/engine/falco_engine.h b/userspace/engine/falco_engine.h index 78e99e1d918..7fee24b2443 100644 --- a/userspace/engine/falco_engine.h +++ b/userspace/engine/falco_engine.h @@ -37,6 +37,7 @@ limitations under the License. #include "falco_common.h" #include "falco_source.h" #include "falco_load_result.h" +#include "filter_details_resolver.h" // // This class acts as the primary interface between a program and the @@ -123,7 +124,7 @@ class falco_engine // Print details on the given rule. If rule is NULL, print // details on all rules. // - void describe_rule(std::string *rule) const; + void describe_rule(std::string *rule, bool json) const; // // Print statistics on how many events matched each rule. @@ -298,6 +299,20 @@ class falco_engine // inline bool should_drop_evt() const; + // Retrieve json details from rules, macros, lists + void get_json_details(const falco_rule& r, + const rule_loader::rule_info& ri, + sinsp* insp, + Json::Value& rule) const; + void get_json_details(const rule_loader::macro_info& m, + Json::Value& macro) const; + void get_json_details(const rule_loader::list_info& l, + Json::Value& list) const; + void get_json_details(libsinsp::filter::ast::expr* ast, + Json::Value& output) const; + void get_json_evt_types(libsinsp::filter::ast::expr* ast, + Json::Value& output) const; + rule_loader::collector m_rule_collector; indexed_vector m_rules; stats_manager m_rule_stats_manager; diff --git a/userspace/engine/filter_details_resolver.cpp b/userspace/engine/filter_details_resolver.cpp new file mode 100644 index 00000000000..e7c9757c5e8 --- /dev/null +++ b/userspace/engine/filter_details_resolver.cpp @@ -0,0 +1,103 @@ +/* +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 "filter_details_resolver.h" + +using namespace libsinsp::filter; + +void filter_details::reset() +{ + fields.clear(); + macros.clear(); + operators.clear(); + lists.clear(); +} + +void filter_details_resolver::run(ast::expr* filter, filter_details& details) +{ + visitor v(details); + filter->accept(&v); +} + +void filter_details_resolver::visitor::visit(ast::and_expr* e) +{ + for(size_t i = 0; i < e->children.size(); i++) + { + m_expect_macro = true; + e->children[i]->accept(this); + m_expect_macro = false; + } +} + +void filter_details_resolver::visitor::visit(ast::or_expr* e) +{ + for(size_t i = 0; i < e->children.size(); i++) + { + m_expect_macro = true; + e->children[i]->accept(this); + m_expect_macro = false; + } +} + +void filter_details_resolver::visitor::visit(ast::not_expr* e) +{ + e->child->accept(this); +} + +void filter_details_resolver::visitor::visit(ast::list_expr* e) +{ + if(m_expect_list) + { + for(const auto& item : e->values) + { + if(m_details.known_lists.find(item) != m_details.known_lists.end()) + { + m_details.lists.insert(item); + } + } + } +} + +void filter_details_resolver::visitor::visit(ast::binary_check_expr* e) +{ + m_expect_macro = false; + m_details.fields.insert(e->field); + m_details.operators.insert(e->op); + m_expect_list = true; + e->value->accept(this); + m_expect_list = false; +} + +void filter_details_resolver::visitor::visit(ast::unary_check_expr* e) +{ + m_expect_macro = false; + m_details.fields.insert(e->field); + m_details.operators.insert(e->op); +} + +void filter_details_resolver::visitor::visit(ast::value_expr* e) +{ + if(m_expect_macro) + { + auto it = m_details.known_macros.find(e->value); + if(it == m_details.known_macros.end()) + { + return; + } + + m_details.macros.insert(e->value); + } +} \ No newline at end of file diff --git a/userspace/engine/filter_details_resolver.h b/userspace/engine/filter_details_resolver.h new file mode 100644 index 00000000000..01a9b33c3e9 --- /dev/null +++ b/userspace/engine/filter_details_resolver.h @@ -0,0 +1,79 @@ +/* +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 +#include +#include +#include + +struct filter_details +{ + // input macros and lists + std::unordered_set known_macros; + std::unordered_set known_lists; + + // output details + std::unordered_set fields; + std::unordered_set macros; + std::unordered_set operators; + std::unordered_set lists; + + void reset(); +}; + +/*! + \brief Helper class for getting details about rules' filters. +*/ +class filter_details_resolver +{ +public: + /*! + \brief Visits a filter AST and stores details about macros, lists, + fields and operators used. + \param filter The filter AST to be processed. + \param details Helper structure used to state known macros and + lists on input, and to store all the retrieved details as output. + */ + void run(libsinsp::filter::ast::expr* filter, + filter_details& details); + +private: + struct visitor : public libsinsp::filter::ast::expr_visitor + { + visitor(filter_details& details) : + m_details(details), + m_expect_list(false), + m_expect_macro(false) {} + visitor(visitor&&) = default; + visitor& operator = (visitor&&) = default; + visitor(const visitor&) = delete; + visitor& operator = (const visitor&) = delete; + + void visit(libsinsp::filter::ast::and_expr* e) override; + void visit(libsinsp::filter::ast::or_expr* e) override; + void visit(libsinsp::filter::ast::not_expr* e) override; + void visit(libsinsp::filter::ast::value_expr* e) override; + void visit(libsinsp::filter::ast::list_expr* e) override; + void visit(libsinsp::filter::ast::unary_check_expr* e) override; + void visit(libsinsp::filter::ast::binary_check_expr* e) override; + + filter_details& m_details; + bool m_expect_list; + bool m_expect_macro; + }; +}; diff --git a/userspace/falco/app/actions/load_rules_files.cpp b/userspace/falco/app/actions/load_rules_files.cpp index e41ad517e7e..73874dbaaa0 100644 --- a/userspace/falco/app/actions/load_rules_files.cpp +++ b/userspace/falco/app/actions/load_rules_files.cpp @@ -118,13 +118,13 @@ falco::app::run_result falco::app::actions::load_rules_files(falco::app::state& if (s.options.describe_all_rules) { - s.engine->describe_rule(NULL); + s.engine->describe_rule(NULL, s.config->m_json_output); return run_result::exit(); } if (!s.options.describe_rule.empty()) { - s.engine->describe_rule(&(s.options.describe_rule)); + s.engine->describe_rule(&(s.options.describe_rule), s.config->m_json_output); return run_result::exit(); }