mirror of
https://github.com/falcosecurity/falco.git
synced 2025-06-08 14:24:54 +00:00
In order to support external rules loaders that may extend the falco rules format with new top level objects, move away from providing individual filter objects to the filter_ruleset via calls to add(). Instead, pass the entire compile output returned by the compiler to the ruleset using a new method add_compile_output(). Custom users can then cast back the compile output to the appropriate derived class for use in the ruleset. Move the declaration of the compile output to a standalone class so it can be used by rulesets without including the entire rules loader header files, and add a new factory method new_compile_output() to the compiler so it can create a derived class if necessary. This change is backwards-compatible with existing rulesets, as the default implementation of add_compile_output() simply iterates over rules and calls add() for each rule. This change also speeds up rule loading. Previously, each rule condition was compiled twice: 1. First, in the compiler, to see if it was valid. 2. Second, in the falco engine before providing each rule to the ruleset. Add the compiled filter to the falco_rule object instead of throwing it away in the compiler. Signed-off-by: Mark Stemm <mark.stemm@gmail.com>
577 lines
14 KiB
C++
577 lines
14 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
/*
|
|
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 <string>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <vector>
|
|
|
|
#include "rule_loader_compiler.h"
|
|
#include "filter_macro_resolver.h"
|
|
#include "filter_warning_resolver.h"
|
|
|
|
#define MAX_VISIBILITY ((uint32_t) -1)
|
|
|
|
#define THROW(cond, err, ctx) { if ((cond)) { throw rule_loader::rule_load_exception(falco::load_result::LOAD_ERR_VALIDATE, (err), (ctx)); } }
|
|
|
|
static std::string s_container_info_fmt = "%container.info";
|
|
static std::string s_default_extra_fmt = "container_id=%container.id container_name=%container.name";
|
|
|
|
using namespace libsinsp::filter;
|
|
|
|
// todo(jasondellaluce): this breaks string escaping in lists and exceptions
|
|
static void quote_item(std::string& e)
|
|
{
|
|
if (e.find(" ") != std::string::npos && e[0] != '"' && e[0] != '\'')
|
|
{
|
|
e = '"' + e + '"';
|
|
}
|
|
}
|
|
|
|
static void paren_item(std::string& e)
|
|
{
|
|
if(e[0] != '(')
|
|
{
|
|
e = '(' + e + ')';
|
|
}
|
|
}
|
|
|
|
static inline bool is_operator_for_list(const std::string& op)
|
|
{
|
|
auto ops = libsinsp::filter::parser::supported_operators(true);
|
|
return find(ops.begin(), ops.end(), op) != ops.end();
|
|
}
|
|
|
|
static bool is_format_valid(const falco_source& source, std::string fmt, std::string& err)
|
|
{
|
|
try
|
|
{
|
|
std::shared_ptr<gen_event_formatter> formatter;
|
|
formatter = source.formatter_factory->create_formatter(fmt);
|
|
return true;
|
|
}
|
|
catch(std::exception &e)
|
|
{
|
|
err = e.what();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void build_rule_exception_infos(
|
|
const std::vector<rule_loader::rule_exception_info>& exceptions,
|
|
std::set<std::string>& exception_fields,
|
|
std::string& condition)
|
|
{
|
|
std::string tmp;
|
|
for (const auto &ex : exceptions)
|
|
{
|
|
std::string icond;
|
|
if(!ex.fields.is_list)
|
|
{
|
|
for (const auto &val : ex.values)
|
|
{
|
|
THROW(val.is_list,
|
|
"Expected values array to contain a list of strings",
|
|
ex.ctx)
|
|
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 (const auto &values : ex.values)
|
|
{
|
|
THROW(ex.fields.items.size() != values.items.size(),
|
|
"Fields and values lists must have equal length",
|
|
ex.ctx);
|
|
icond += icond == "(" ? "" : " or ";
|
|
icond += "(";
|
|
uint32_t k = 0;
|
|
std::string istr;
|
|
for (const auto &field : ex.fields.items)
|
|
{
|
|
icond += k == 0 ? "" : " and ";
|
|
if (values.items[k].is_list)
|
|
{
|
|
istr = "(";
|
|
for (const 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;
|
|
}
|
|
}
|
|
|
|
static inline rule_loader::list_info* list_info_from_name(
|
|
const rule_loader::collector& c, const std::string& name)
|
|
{
|
|
auto ret = c.lists().at(name);
|
|
if (!ret)
|
|
{
|
|
throw falco_exception("can't find internal list info at name: " + name);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static inline rule_loader::macro_info* macro_info_from_name(
|
|
const rule_loader::collector& c, const std::string& name)
|
|
{
|
|
auto ret = c.macros().at(name);
|
|
if (!ret)
|
|
{
|
|
throw falco_exception("can't find internal macro info at name: " + name);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// todo(jasondellaluce): this breaks string escaping in lists
|
|
static bool resolve_list(std::string& cnd, const falco_list& list)
|
|
{
|
|
static std::string blanks = " \t\n\r";
|
|
static std::string delims = blanks + "(),=";
|
|
std::string tmp;
|
|
std::string new_cnd;
|
|
size_t start, end;
|
|
bool used = false;
|
|
start = cnd.find(list.name);
|
|
while (start != std::string::npos)
|
|
{
|
|
// the characters surrounding the name must
|
|
// be delims of beginning/end of string
|
|
end = start + list.name.length();
|
|
if ((start == 0 || delims.find(cnd[start - 1]) != std::string::npos)
|
|
&& (end >= cnd.length() || delims.find(cnd[end]) != std::string::npos))
|
|
{
|
|
// shift pointers to consume all whitespaces
|
|
while (start > 0
|
|
&& blanks.find(cnd[start - 1]) != std::string::npos)
|
|
{
|
|
start--;
|
|
}
|
|
while (end < cnd.length()
|
|
&& blanks.find(cnd[end]) != std::string::npos)
|
|
{
|
|
end++;
|
|
}
|
|
// create substitution string by concatenating all values
|
|
std::string sub = "";
|
|
for (const auto &v : list.items)
|
|
{
|
|
if (!sub.empty())
|
|
{
|
|
sub += ", ";
|
|
}
|
|
tmp = v;
|
|
quote_item(tmp);
|
|
sub += tmp;
|
|
}
|
|
// if substituted list is empty, we need to
|
|
// remove a comma from the left or the right
|
|
if (sub.empty())
|
|
{
|
|
if (start > 0 && cnd[start - 1] == ',')
|
|
{
|
|
start--;
|
|
}
|
|
else if (end < cnd.length() && cnd[end] == ',')
|
|
{
|
|
end++;
|
|
}
|
|
}
|
|
// compose new string with substitution
|
|
new_cnd = "";
|
|
if (start > 0)
|
|
{
|
|
new_cnd += cnd.substr(0, start) + " ";
|
|
}
|
|
new_cnd += sub + " ";
|
|
if (end <= cnd.length())
|
|
{
|
|
new_cnd += cnd.substr(end);
|
|
}
|
|
cnd = new_cnd;
|
|
start += sub.length() + 1;
|
|
used = true;
|
|
}
|
|
start = cnd.find(list.name, start + 1);
|
|
}
|
|
return used;
|
|
}
|
|
|
|
static void resolve_macros(
|
|
const indexed_vector<rule_loader::macro_info>& infos,
|
|
indexed_vector<falco_macro>& macros,
|
|
std::shared_ptr<ast::expr>& ast,
|
|
const std::string& condition,
|
|
uint32_t visibility,
|
|
const rule_loader::context &ctx)
|
|
{
|
|
filter_macro_resolver macro_resolver;
|
|
for (const auto &m : infos)
|
|
{
|
|
if (m.index < visibility)
|
|
{
|
|
auto macro = macros.at(m.name);
|
|
macro_resolver.set_macro(m.name, macro->condition);
|
|
}
|
|
}
|
|
macro_resolver.run(ast);
|
|
|
|
// Note: only complaining about the first error or unknown macro
|
|
const auto& errors_macros = macro_resolver.get_errors();
|
|
const auto& unresolved_macros = macro_resolver.get_unknown_macros();
|
|
if(!errors_macros.empty() || !unresolved_macros.empty())
|
|
{
|
|
auto errpos = !errors_macros.empty()
|
|
? errors_macros.begin()->second
|
|
: unresolved_macros.begin()->second;
|
|
std::string errmsg = !errors_macros.empty()
|
|
? errors_macros.begin()->first
|
|
: ("Undefined macro '" + unresolved_macros.begin()->first + "' used in filter.");
|
|
const rule_loader::context cond_ctx(errpos, condition, ctx);
|
|
THROW(true, errmsg, cond_ctx);
|
|
}
|
|
|
|
for (const auto &it : macro_resolver.get_resolved_macros())
|
|
{
|
|
macros.at(it.first)->used = true;
|
|
}
|
|
}
|
|
|
|
// note: there is no visibility order between filter conditions and lists
|
|
static std::shared_ptr<ast::expr> parse_condition(
|
|
std::string condition,
|
|
indexed_vector<falco_list>& lists,
|
|
const rule_loader::context &ctx)
|
|
{
|
|
for (auto &l : lists)
|
|
{
|
|
if (resolve_list(condition, l))
|
|
{
|
|
l.used = true;
|
|
}
|
|
}
|
|
libsinsp::filter::parser p(condition);
|
|
p.set_max_depth(1000);
|
|
try
|
|
{
|
|
std::shared_ptr<ast::expr> res_ptr(p.parse());
|
|
return res_ptr;
|
|
}
|
|
catch (const sinsp_exception& e)
|
|
{
|
|
rule_loader::context parsectx(p.get_pos(), condition, ctx);
|
|
|
|
throw rule_loader::rule_load_exception(
|
|
falco::load_result::LOAD_ERR_COMPILE_CONDITION,
|
|
e.what(),
|
|
parsectx);
|
|
}
|
|
}
|
|
|
|
static void apply_output_substitutions(
|
|
rule_loader::configuration& cfg,
|
|
std::string& out)
|
|
{
|
|
if (out.find(s_container_info_fmt) != std::string::npos)
|
|
{
|
|
if (cfg.replace_output_container_info)
|
|
{
|
|
out = replace(out, s_container_info_fmt, cfg.output_extra);
|
|
return;
|
|
}
|
|
out = replace(out, s_container_info_fmt, s_default_extra_fmt);
|
|
}
|
|
out += cfg.output_extra.empty() ? "" : " " + cfg.output_extra;
|
|
}
|
|
|
|
void rule_loader::compiler::compile_list_infos(
|
|
configuration& cfg,
|
|
const collector& col,
|
|
indexed_vector<falco_list>& out) const
|
|
{
|
|
std::list<std::string> used;
|
|
falco_list v;
|
|
for (const auto &list : col.lists())
|
|
{
|
|
v.name = list.name;
|
|
v.items.clear();
|
|
for (const auto &item : list.items)
|
|
{
|
|
const auto ref = col.lists().at(item);
|
|
if (ref && ref->index < list.visibility)
|
|
{
|
|
used.push_back(ref->name);
|
|
for (const auto &val : ref->items)
|
|
{
|
|
v.items.push_back(val);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
v.items.push_back(item);
|
|
}
|
|
}
|
|
v.used = false;
|
|
auto list_id = out.insert(v, v.name);
|
|
out.at(list_id)->id = list_id;
|
|
}
|
|
for (const auto &v : used)
|
|
{
|
|
out.at(v)->used = true;
|
|
}
|
|
}
|
|
|
|
// note: there is a visibility ordering between macros
|
|
void rule_loader::compiler::compile_macros_infos(
|
|
configuration& cfg,
|
|
const collector& col,
|
|
indexed_vector<falco_list>& lists,
|
|
indexed_vector<falco_macro>& out) const
|
|
{
|
|
for (const auto &m : col.macros())
|
|
{
|
|
falco_macro entry;
|
|
entry.name = m.name;
|
|
entry.condition = parse_condition(m.cond, lists, m.cond_ctx);
|
|
entry.used = false;
|
|
auto macro_id = out.insert(entry, m.name);
|
|
out.at(macro_id)->id = macro_id;
|
|
}
|
|
|
|
for (auto &m : out)
|
|
{
|
|
auto info = macro_info_from_name(col, m.name);
|
|
resolve_macros(col.macros(), out, m.condition, info->cond, info->visibility, info->ctx);
|
|
}
|
|
}
|
|
|
|
static bool err_is_unknown_type_or_field(const std::string& err)
|
|
{
|
|
return err.find("nonexistent field") != std::string::npos
|
|
|| err.find("invalid formatting token") != std::string::npos
|
|
|| err.find("unknown event type") != std::string::npos;
|
|
}
|
|
|
|
void rule_loader::compiler::compile_rule_infos(
|
|
configuration& cfg,
|
|
const collector& col,
|
|
indexed_vector<falco_list>& lists,
|
|
indexed_vector<falco_macro>& macros,
|
|
indexed_vector<falco_rule>& out) const
|
|
{
|
|
std::string err, condition;
|
|
std::set<falco::load_result::load_result::warning_code> warn_codes;
|
|
filter_warning_resolver warn_resolver;
|
|
for (const auto &r : col.rules())
|
|
{
|
|
// skip the rule if it has an unknown source
|
|
if (r.unknown_source)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// note: this should not be nullptr if the source is not unknown
|
|
auto source = cfg.sources.at(r.source);
|
|
THROW(!source,
|
|
std::string("Unknown source at compile-time") + r.source,
|
|
r.ctx);
|
|
|
|
// build filter AST by parsing the condition, building exceptions,
|
|
// and resolving lists and macros
|
|
falco_rule rule;
|
|
|
|
condition = r.cond;
|
|
if (!r.exceptions.empty())
|
|
{
|
|
build_rule_exception_infos(
|
|
r.exceptions, rule.exception_fields, condition);
|
|
}
|
|
rule.condition = parse_condition(condition, lists, r.cond_ctx);
|
|
resolve_macros(col.macros(), macros, rule.condition, condition, MAX_VISIBILITY, r.ctx);
|
|
|
|
// check for warnings in the filtering condition
|
|
warn_codes.clear();
|
|
if (warn_resolver.run(rule.condition.get(), warn_codes))
|
|
{
|
|
for (const auto &w : warn_codes)
|
|
{
|
|
cfg.res->add_warning(w, "", r.ctx);
|
|
}
|
|
}
|
|
|
|
// build rule output message
|
|
rule.output = r.output;
|
|
if (r.source == falco_common::syscall_source)
|
|
{
|
|
apply_output_substitutions(cfg, rule.output);
|
|
}
|
|
|
|
// validate the rule's output
|
|
if(!is_format_valid(*cfg.sources.at(r.source), rule.output, err))
|
|
{
|
|
// skip the rule silently if skip_if_unknown_filter is true and
|
|
// we encountered some specific kind of errors
|
|
if (err_is_unknown_type_or_field(err) && r.skip_if_unknown_filter)
|
|
{
|
|
cfg.res->add_warning(
|
|
falco::load_result::load_result::LOAD_UNKNOWN_FILTER,
|
|
err,
|
|
r.output_ctx);
|
|
continue;
|
|
}
|
|
throw rule_load_exception(
|
|
falco::load_result::load_result::LOAD_ERR_COMPILE_OUTPUT,
|
|
err,
|
|
r.output_ctx);
|
|
}
|
|
|
|
// validate the rule's condition: we compile it into a sinsp filter
|
|
// on-the-fly and we throw an exception with details on failure
|
|
sinsp_filter_compiler compiler(cfg.sources.at(r.source)->filter_factory, rule.condition.get());
|
|
try
|
|
{
|
|
std::shared_ptr<sinsp_filter> sfPtr(compiler.compile());
|
|
}
|
|
catch (const sinsp_exception& e)
|
|
{
|
|
// skip the rule silently if skip_if_unknown_filter is true and
|
|
// we encountered some specific kind of errors
|
|
std::string err = e.what();
|
|
if (err_is_unknown_type_or_field(err) && r.skip_if_unknown_filter)
|
|
{
|
|
cfg.res->add_warning(
|
|
falco::load_result::load_result::LOAD_UNKNOWN_FILTER,
|
|
err,
|
|
r.cond_ctx);
|
|
continue;
|
|
}
|
|
rule_loader::context ctx(compiler.get_pos(), condition, r.cond_ctx);
|
|
throw rule_loader::rule_load_exception(
|
|
falco::load_result::load_result::LOAD_ERR_COMPILE_CONDITION,
|
|
err,
|
|
ctx);
|
|
}
|
|
|
|
// populate set of event types and emit an special warning
|
|
if(r.source == falco_common::syscall_source)
|
|
{
|
|
auto evttypes = libsinsp::filter::ast::ppm_event_codes(rule.condition.get());
|
|
if ((evttypes.empty() || evttypes.size() > 100) && r.warn_evttypes)
|
|
{
|
|
cfg.res->add_warning(
|
|
falco::load_result::load_result::LOAD_NO_EVTTYPE,
|
|
"Rule matches too many evt.type values. This has a significant performance penalty.",
|
|
r.ctx);
|
|
}
|
|
}
|
|
|
|
// finalize the rule definition and add it to output
|
|
rule.name = r.name;
|
|
rule.source = r.source;
|
|
rule.description = r.desc;
|
|
rule.priority = r.priority;
|
|
rule.tags = r.tags;
|
|
auto rule_id = out.insert(rule, rule.name);
|
|
out.at(rule_id)->id = rule_id;
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<rule_loader::compile_output> rule_loader::compiler::new_compile_output()
|
|
{
|
|
return std::make_unique<compile_output>();
|
|
}
|
|
|
|
void rule_loader::compiler::compile(
|
|
configuration& cfg,
|
|
const collector& col,
|
|
compile_output& out) const
|
|
{
|
|
// expand all lists, macros, and rules
|
|
try
|
|
{
|
|
compile_list_infos(cfg, col, out.lists);
|
|
compile_macros_infos(cfg, col, out.lists, out.macros);
|
|
compile_rule_infos(cfg, col, out.lists, out.macros, out.rules);
|
|
}
|
|
catch(rule_load_exception &e)
|
|
{
|
|
cfg.res->add_error(e.ec, e.msg, e.ctx);
|
|
return;
|
|
}
|
|
|
|
// print info on any dangling lists or macros that were not used anywhere
|
|
for (const auto &m : out.macros)
|
|
{
|
|
if (!m.used)
|
|
{
|
|
cfg.res->add_warning(
|
|
falco::load_result::load_result::LOAD_UNUSED_MACRO,
|
|
"Macro not referred to by any other rule/macro",
|
|
macro_info_from_name(col, m.name)->ctx);
|
|
}
|
|
}
|
|
for (const auto &l : out.lists)
|
|
{
|
|
if (!l.used)
|
|
{
|
|
cfg.res->add_warning(
|
|
falco::load_result::LOAD_UNUSED_LIST,
|
|
"List not referred to by any other rule/macro",
|
|
list_info_from_name(col, l.name)->ctx);
|
|
}
|
|
}
|
|
}
|