new(app): add append_output configuration option with fields and format

Signed-off-by: Luca Guerra <luca@guerra.sh>
This commit is contained in:
Luca Guerra 2024-08-26 15:15:42 +00:00 committed by poiana
parent 00ff9d82ea
commit d210ed2e4f
18 changed files with 627 additions and 102 deletions

View File

@ -36,6 +36,7 @@ add_executable(falco_unit_tests
engine/test_add_source.cpp engine/test_add_source.cpp
engine/test_alt_rule_loader.cpp engine/test_alt_rule_loader.cpp
engine/test_enable_rule.cpp engine/test_enable_rule.cpp
engine/test_extra_output.cpp
engine/test_falco_utils.cpp engine/test_falco_utils.cpp
engine/test_filter_details_resolver.cpp engine/test_filter_details_resolver.cpp
engine/test_filter_macro_resolver.cpp engine/test_filter_macro_resolver.cpp
@ -47,6 +48,7 @@ add_executable(falco_unit_tests
falco/test_configuration_rule_selection.cpp falco/test_configuration_rule_selection.cpp
falco/test_configuration_config_files.cpp falco/test_configuration_config_files.cpp
falco/test_configuration_env_vars.cpp falco/test_configuration_env_vars.cpp
falco/test_configuration_output_options.cpp
falco/test_configuration_schema.cpp falco/test_configuration_schema.cpp
falco/app/actions/test_select_event_sources.cpp falco/app/actions/test_select_event_sources.cpp
falco/app/actions/test_load_config.cpp falco/app/actions/test_load_config.cpp

View File

@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2024 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 <gtest/gtest.h>
#include "../test_falco_engine.h"
TEST_F(test_falco_engine, extra_format_all)
{
std::string rules_content = R"END(
- rule: legit_rule
desc: legit rule description
condition: evt.type=open
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
)END";
m_engine->add_extra_output_format("evt.type=%evt.type", "", "", "", false);
ASSERT_TRUE(load_rules(rules_content, "legit_rules.yaml")) << m_load_result_string;
EXPECT_EQ(get_compiled_rule_output("legit_rule"),"user=%user.name command=%proc.cmdline file=%fd.name evt.type=%evt.type");
}
TEST_F(test_falco_engine, extra_format_by_rule)
{
std::string rules_content = R"END(
- rule: legit_rule
desc: legit rule description
condition: evt.type=open
output: out 1
priority: INFO
- rule: another_rule
desc: legit rule description
condition: evt.type=open
output: out 2
priority: INFO
)END";
m_engine->add_extra_output_format("evt.type=%evt.type", "", "", "legit_rule", false);
ASSERT_TRUE(load_rules(rules_content, "legit_rules.yaml")) << m_load_result_string;
EXPECT_EQ(get_compiled_rule_output("legit_rule"),"out 1 evt.type=%evt.type");
EXPECT_EQ(get_compiled_rule_output("another_rule"),"out 2");
}
TEST_F(test_falco_engine, extra_format_by_tag_rule)
{
std::string rules_content = R"END(
- rule: legit_rule
desc: legit rule description
condition: evt.type=open
output: out 1
priority: INFO
tags: [tag1]
- rule: another_rule
desc: legit rule description
condition: evt.type=open
output: out 2
priority: INFO
tags: [tag1]
)END";
m_engine->add_extra_output_format("extra 1", "", "tag1", "", false);
m_engine->add_extra_output_format("extra 2", "", "", "another_rule", false);
ASSERT_TRUE(load_rules(rules_content, "legit_rules.yaml")) << m_load_result_string;
EXPECT_EQ(get_compiled_rule_output("legit_rule"),"out 1 extra 1");
EXPECT_EQ(get_compiled_rule_output("another_rule"),"out 2 extra 1 extra 2");
}
TEST_F(test_falco_engine, extra_format_replace_container_info)
{
std::string rules_content = R"END(
- rule: legit_rule
desc: legit rule description
condition: evt.type=open
output: out 1 (%container.info)
priority: INFO
tags: [tag1]
- rule: another_rule
desc: legit rule description
condition: evt.type=open
output: out 2
priority: INFO
tags: [tag1]
)END";
m_engine->add_extra_output_format("extra 1", "", "", "", true);
ASSERT_TRUE(load_rules(rules_content, "legit_rules.yaml")) << m_load_result_string;
EXPECT_EQ(get_compiled_rule_output("legit_rule"), "out 1 (extra 1)");
EXPECT_EQ(get_compiled_rule_output("another_rule"), "out 2 extra 1");
}
TEST_F(test_falco_engine, extra_format_do_not_replace_container_info)
{
std::string rules_content = R"END(
- rule: legit_rule
desc: legit rule description
condition: evt.type=open
output: out 1 (%container.info)
priority: INFO
tags: [tag1]
)END";
ASSERT_TRUE(load_rules(rules_content, "legit_rules.yaml")) << m_load_result_string;
auto output = get_compiled_rule_output("legit_rule");
EXPECT_TRUE(output.find("%container.info") == output.npos);
}
TEST_F(test_falco_engine, extra_fields_all)
{
std::string rules_content = R"END(
- rule: legit_rule
desc: legit rule description
condition: evt.type=open
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
)END";
std::unordered_map<std::string, std::string> extra_formatted_fields = {{"my_field", "hello %evt.num"}};
for (auto const& f : extra_formatted_fields)
{
m_engine->add_extra_output_formatted_field(f.first, f.second, "", "", "");
}
ASSERT_TRUE(load_rules(rules_content, "legit_rules.yaml")) << m_load_result_string;
EXPECT_EQ(get_compiled_rule_formatted_fields("legit_rule"), extra_formatted_fields);
}

View File

@ -0,0 +1,68 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2024 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 <gtest/gtest.h>
#include <falco/configuration.h>
TEST(ConfigurationRuleOutputOptions, parse_yaml)
{
falco_configuration falco_config;
ASSERT_NO_THROW(falco_config.init_from_content(R"(
append_output:
- source: syscall
tag: persistence
rule: some rule name
format: "gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4]"
- tag: persistence
fields:
- proc.aname[2]: "%proc.aname[2]"
- proc.aname[3]: "%proc.aname[3]"
- proc.aname[4]: "%proc.aname[4]"
format: "gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4]"
- source: k8s_audit
fields:
- ka.verb
- static_field: "static content"
)", {}));
EXPECT_EQ(falco_config.m_append_output.size(), 3);
EXPECT_EQ(falco_config.m_append_output[0].m_source, "syscall");
EXPECT_EQ(falco_config.m_append_output[0].m_tag, "persistence");
EXPECT_EQ(falco_config.m_append_output[0].m_rule, "some rule name");
EXPECT_EQ(falco_config.m_append_output[0].m_formatted_fields.size(), 0);
EXPECT_EQ(falco_config.m_append_output[0].m_format, "gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4]");
EXPECT_EQ(falco_config.m_append_output[1].m_tag, "persistence");
EXPECT_EQ(falco_config.m_append_output[1].m_format, "gparent=%proc.aname[2] ggparent=%proc.aname[3] gggparent=%proc.aname[4]");
EXPECT_EQ(falco_config.m_append_output[1].m_formatted_fields.size(), 3);
EXPECT_EQ(falco_config.m_append_output[1].m_formatted_fields["proc.aname[2]"], "%proc.aname[2]");
EXPECT_EQ(falco_config.m_append_output[1].m_formatted_fields["proc.aname[3]"], "%proc.aname[3]");
EXPECT_EQ(falco_config.m_append_output[1].m_formatted_fields["proc.aname[4]"], "%proc.aname[4]");
EXPECT_EQ(falco_config.m_append_output[2].m_source, "k8s_audit");
EXPECT_EQ(falco_config.m_append_output[2].m_formatted_fields.size(), 1);
EXPECT_EQ(falco_config.m_append_output[2].m_formatted_fields["static_field"], "static content");
EXPECT_EQ(falco_config.m_append_output[2].m_raw_fields.size(), 1);
EXPECT_EQ(falco_config.m_append_output[2].m_raw_fields.count("ka.verb"), 1);
}

View File

@ -85,3 +85,15 @@ std::string test_falco_engine::get_compiled_rule_condition(std::string rule_name
auto rule_description = m_engine->describe_rule(&rule_name, {}); auto rule_description = m_engine->describe_rule(&rule_name, {});
return rule_description["rules"][0]["details"]["condition_compiled"].template get<std::string>(); return rule_description["rules"][0]["details"]["condition_compiled"].template get<std::string>();
} }
std::string test_falco_engine::get_compiled_rule_output(std::string rule_name) const
{
auto rule_description = m_engine->describe_rule(&rule_name, {});
return rule_description["rules"][0]["details"]["output_compiled"].template get<std::string>();
}
std::unordered_map<std::string, std::string> test_falco_engine::get_compiled_rule_formatted_fields(std::string rule_name) const
{
auto rule_description = m_engine->describe_rule(&rule_name, {});
return rule_description["rules"][0]["details"]["extra_output_formatted_fields"].template get<std::unordered_map<std::string, std::string>>();
}

View File

@ -6,6 +6,7 @@
#include "rule_loading_messages.h" #include "rule_loading_messages.h"
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <unordered_map>
class test_falco_engine : public testing::Test class test_falco_engine : public testing::Test
{ {
@ -19,6 +20,8 @@ protected:
bool check_warning_message(const std::string& warning_msg) const; bool check_warning_message(const std::string& warning_msg) const;
bool check_error_message(const std::string& error_msg) const; bool check_error_message(const std::string& error_msg) const;
std::string get_compiled_rule_condition(std::string rule_name = "") const; std::string get_compiled_rule_condition(std::string rule_name = "") const;
std::string get_compiled_rule_output(std::string rule_name = "") const;
std::unordered_map<std::string, std::string> get_compiled_rule_formatted_fields(std::string rule_name) const;
std::string m_sample_ruleset = "sample-ruleset"; std::string m_sample_ruleset = "sample-ruleset";
std::string m_sample_source = falco_common::syscall_source; std::string m_sample_source = falco_common::syscall_source;

View File

@ -57,8 +57,7 @@ falco_engine::falco_engine(bool seed_rng)
m_rule_compiler(std::make_shared<rule_loader::compiler>()), m_rule_compiler(std::make_shared<rule_loader::compiler>()),
m_next_ruleset_id(0), m_next_ruleset_id(0),
m_min_priority(falco_common::PRIORITY_DEBUG), m_min_priority(falco_common::PRIORITY_DEBUG),
m_sampling_ratio(1), m_sampling_multiplier(0), m_sampling_ratio(1), m_sampling_multiplier(0)
m_replace_container_info(false)
{ {
if(seed_rng) if(seed_rng)
{ {
@ -76,6 +75,7 @@ falco_engine::~falco_engine()
m_rule_collector->clear(); m_rule_collector->clear();
m_rule_stats_manager.clear(); m_rule_stats_manager.clear();
m_sources.clear(); m_sources.clear();
m_extra_output_format.clear();
} }
sinsp_version falco_engine::engine_version() sinsp_version falco_engine::engine_version()
@ -194,8 +194,8 @@ void falco_engine::list_fields(const std::string &source, bool verbose, bool nam
std::unique_ptr<load_result> falco_engine::load_rules(const std::string &rules_content, const std::string &name) std::unique_ptr<load_result> falco_engine::load_rules(const std::string &rules_content, const std::string &name)
{ {
rule_loader::configuration cfg(rules_content, m_sources, name); rule_loader::configuration cfg(rules_content, m_sources, name);
cfg.output_extra = m_extra; cfg.extra_output_format = m_extra_output_format;
cfg.replace_output_container_info = m_replace_container_info; cfg.extra_output_fields = m_extra_output_fields;
// read rules YAML file and collect its definitions // read rules YAML file and collect its definitions
if(m_rule_reader->read(cfg, *m_rule_collector)) if(m_rule_reader->read(cfg, *m_rule_collector))
@ -455,6 +455,7 @@ std::unique_ptr<std::vector<falco_engine::rule_result>> falco_engine::process_ev
rule_result.priority_num = rule.priority; rule_result.priority_num = rule.priority;
rule_result.tags = rule.tags; rule_result.tags = rule.tags;
rule_result.exception_fields = rule.exception_fields; rule_result.exception_fields = rule.exception_fields;
rule_result.extra_output_fields = rule.extra_output_fields;
m_rule_stats_manager.on_event(rule); m_rule_stats_manager.on_event(rule);
res->push_back(rule_result); res->push_back(rule_result);
} }
@ -646,9 +647,22 @@ void falco_engine::get_json_details(
out["details"]["condition_operators"] = sequence_to_json_array(compiled_details.operators); out["details"]["condition_operators"] = sequence_to_json_array(compiled_details.operators);
out["details"]["condition_fields"] = sequence_to_json_array(compiled_details.fields); out["details"]["condition_fields"] = sequence_to_json_array(compiled_details.fields);
// Get extra requested fields
std::vector<std::string> out_fields;
for(auto const& f : r.extra_output_fields)
{
// add all the field keys
out_fields.emplace_back(f.second.first);
if (!f.second.second) // formatted field
{
out["details"]["extra_output_formatted_fields"][f.first] = f.second.first;
}
}
// Get fields from output string // Get fields from output string
auto fmt = create_formatter(r.source, r.output); auto fmt = create_formatter(r.source, r.output);
std::vector<std::string> out_fields;
fmt->get_field_names(out_fields); fmt->get_field_names(out_fields);
out["details"]["output_fields"] = sequence_to_json_array(out_fields); out["details"]["output_fields"] = sequence_to_json_array(out_fields);
@ -1082,10 +1096,37 @@ void falco_engine::set_sampling_multiplier(double sampling_multiplier)
m_sampling_multiplier = sampling_multiplier; m_sampling_multiplier = sampling_multiplier;
} }
void falco_engine::set_extra(const std::string &extra, bool replace_container_info) void falco_engine::add_extra_output_format(
const std::string &format,
const std::string &source,
const std::string &tag,
const std::string &rule,
bool replace_container_info
)
{ {
m_extra = extra; m_extra_output_format.push_back({format, source, tag, rule, replace_container_info});
m_replace_container_info = replace_container_info; }
void falco_engine::add_extra_output_formatted_field(
const std::string &key,
const std::string &format,
const std::string &source,
const std::string &tag,
const std::string &rule
)
{
m_extra_output_fields.push_back({key, format, source, tag, rule, false});
}
void falco_engine::add_extra_output_raw_field(
const std::string &key,
const std::string &source,
const std::string &tag,
const std::string &rule
)
{
std::string format = "%" + key;
m_extra_output_fields.push_back({key, format, source, tag, rule, true});
} }
inline bool falco_engine::should_drop_evt() const inline bool falco_engine::should_drop_evt() const

View File

@ -176,15 +176,40 @@ public:
// //
void set_sampling_multiplier(double sampling_multiplier); void set_sampling_multiplier(double sampling_multiplier);
// // You can optionally add "extra" output to the end
// You can optionally add "extra" formatting fields to the end
// of all output expressions. You can also choose to replace // of all output expressions. You can also choose to replace
// %container.info with the extra information or add it to the // %container.info with the extra information or add it to the
// end of the expression. This is used in open source falco to // end of the expression. This is used in open source falco to
// add k8s/container information to outputs when // add k8s/container information to outputs when
// available. // available.
// //
void set_extra(const std::string &extra, bool replace_container_info); void add_extra_output_format(
const std::string &format,
const std::string &source,
const std::string &tag,
const std::string &rule,
bool replace_container_info
);
// You can optionally add fields that will only show up in the object
// output (e.g. json, gRPC) alongside other output_fields
// and not in the text message output.
// You can add two types of fields: formatted which will act like
// an additional output format that appears in the output field
void add_extra_output_formatted_field(
const std::string &key,
const std::string &format,
const std::string &source,
const std::string &tag,
const std::string &rule
);
void add_extra_output_raw_field(
const std::string &key,
const std::string &source,
const std::string &tag,
const std::string &rule
);
// Represents the result of matching an event against a set of // Represents the result of matching an event against a set of
// rules. // rules.
@ -196,6 +221,7 @@ public:
std::string format; std::string format;
std::set<std::string> exception_fields; std::set<std::string> exception_fields;
std::set<std::string> tags; std::set<std::string> tags;
std::unordered_map<std::string, std::pair<std::string, bool>> extra_output_fields;
}; };
// //
@ -461,6 +487,6 @@ private:
static const std::string s_default_ruleset; static const std::string s_default_ruleset;
uint32_t m_default_ruleset_id; uint32_t m_default_ruleset_id;
std::string m_extra; std::vector<rule_loader::extra_output_format_conf> m_extra_output_format;
bool m_replace_container_info; std::vector<rule_loader::extra_output_field_conf> m_extra_output_fields;
}; };

View File

@ -79,6 +79,7 @@ struct falco_rule
std::string name; std::string name;
std::string description; std::string description;
std::string output; std::string output;
std::unordered_map<std::string, std::pair<std::string, bool>> extra_output_fields;
std::set<std::string> tags; std::set<std::string> tags;
std::set<std::string> exception_fields; std::set<std::string> exception_fields;
falco_common::priority_type priority; falco_common::priority_type priority;

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
#include <json/json.h> #include <nlohmann/json.hpp>
#include "formats.h" #include "formats.h"
#include "falco_engine.h" #include "falco_engine.h"
@ -35,7 +35,7 @@ falco_formats::~falco_formats()
std::string falco_formats::format_event(sinsp_evt *evt, const std::string &rule, const std::string &source, std::string falco_formats::format_event(sinsp_evt *evt, const std::string &rule, const std::string &source,
const std::string &level, const std::string &format, const std::set<std::string> &tags, const std::string &level, const std::string &format, const std::set<std::string> &tags,
const std::string &hostname) const const std::string &hostname, const std::unordered_map<std::string, std::pair<std::string, bool>> &extra_fields) const
{ {
std::string line; std::string line;
@ -48,27 +48,17 @@ std::string falco_formats::format_event(sinsp_evt *evt, const std::string &rule,
if(formatter->get_output_format() == sinsp_evt_formatter::OF_JSON) if(formatter->get_output_format() == sinsp_evt_formatter::OF_JSON)
{ {
std::string json_line; std::string json_fields;
// Format the event into a json object with all fields resolved // Format the event into a json object with all fields resolved
formatter->tostring(evt, json_line); formatter->tostring(evt, json_fields);
// The formatted string might have a leading newline. If it does, remove it.
if(json_line[0] == '\n')
{
json_line.erase(0, 1);
}
// For JSON output, the formatter returned a json-as-text // For JSON output, the formatter returned a json-as-text
// object containing all the fields in the original format // object containing all the fields in the original format
// message as well as the event time in ns. Use this to build // message as well as the event time in ns. Use this to build
// a more detailed object containing the event time, rule, // a more detailed object containing the event time, rule,
// severity, full output, and fields. // severity, full output, and fields.
Json::Value event; nlohmann::json event;
Json::Value rule_tags;
Json::FastWriter writer;
std::string full_line;
unsigned int rule_tags_idx = 0;
// Convert the time-as-nanoseconds to a more json-friendly ISO8601. // Convert the time-as-nanoseconds to a more json-friendly ISO8601.
time_t evttime = evt->get_ts() / 1000000000; time_t evttime = evt->get_ts() / 1000000000;
@ -94,43 +84,54 @@ std::string falco_formats::format_event(sinsp_evt *evt, const std::string &rule,
if(m_json_include_tags_property) if(m_json_include_tags_property)
{ {
if (tags.size() == 0) event["tags"] = tags;
{
// This sets an empty array
rule_tags = Json::arrayValue;
}
else
{
for (const auto &tag : tags)
{
rule_tags[rule_tags_idx++] = tag;
}
}
event["tags"] = rule_tags;
} }
full_line = writer.write(event); event["output_fields"] = nlohmann::json::parse(json_fields);
// Json::FastWriter may add a trailing newline. If it for (auto const& ef : extra_fields)
// does, remove it.
if(full_line[full_line.length() - 1] == '\n')
{ {
full_line.resize(full_line.length() - 1); std::string fformat = ef.second.first;
if (fformat.size() == 0)
{
continue;
}
if (!(fformat[0] == '*'))
{
fformat = "*" + fformat;
}
if(ef.second.second) // raw field
{
std::string json_field_map;
formatter = m_falco_engine->create_formatter(source, fformat);
formatter->tostring_withformat(evt, json_field_map, sinsp_evt_formatter::OF_JSON);
auto json_obj = nlohmann::json::parse(json_field_map);
event["output_fields"][ef.first] = json_obj[ef.first];
} else
{
event["output_fields"][ef.first] = format_string(evt, fformat, source);
}
} }
// Cheat-graft the output from the formatter into this line = event.dump();
// string. Avoids an unnecessary json parse just to
// merge the formatted fields at the object level.
full_line.pop_back();
full_line.append(", \"output_fields\": ");
full_line.append(json_line);
full_line.append("}");
line = full_line;
} }
return line; return line;
} }
std::string falco_formats::format_string(sinsp_evt *evt, const std::string &format, const std::string &source) const
{
std::string line;
std::shared_ptr<sinsp_evt_formatter> formatter;
formatter = m_falco_engine->create_formatter(source, format);
formatter->tostring_withformat(evt, line, sinsp_evt_formatter::OF_NORMAL);
return line;
}
std::map<std::string, std::string> falco_formats::get_field_values(sinsp_evt *evt, const std::string &source, std::map<std::string, std::string> falco_formats::get_field_values(sinsp_evt *evt, const std::string &source,
const std::string &format) const const std::string &format) const
{ {

View File

@ -31,7 +31,9 @@ public:
std::string format_event(sinsp_evt *evt, const std::string &rule, const std::string &source, std::string format_event(sinsp_evt *evt, const std::string &rule, const std::string &source,
const std::string &level, const std::string &format, const std::set<std::string> &tags, const std::string &level, const std::string &format, const std::set<std::string> &tags,
const std::string &hostname) const; const std::string &hostname, const std::unordered_map<std::string, std::pair<std::string, bool>> &extra_fields) const;
std::string format_string(sinsp_evt *evt, const std::string &format, const std::string &source) const;
std::map<std::string, std::string> get_field_values(sinsp_evt *evt, const std::string &source, std::map<std::string, std::string> get_field_values(sinsp_evt *evt, const std::string &source,
const std::string &format) const ; const std::string &format) const ;

View File

@ -20,6 +20,7 @@ limitations under the License.
#include <string> #include <string>
#include <vector> #include <vector>
#include <optional> #include <optional>
#include <unordered_map>
#include <yaml-cpp/yaml.h> #include <yaml-cpp/yaml.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "falco_source.h" #include "falco_source.h"
@ -261,6 +262,25 @@ namespace rule_loader
nlohmann::json res_json; nlohmann::json res_json;
}; };
struct extra_output_format_conf
{
std::string m_format;
std::string m_source;
std::string m_tag;
std::string m_rule;
bool m_replace_container_info;
};
struct extra_output_field_conf
{
std::string m_key;
std::string m_format;
std::string m_source;
std::string m_tag;
std::string m_rule;
bool m_raw;
};
/*! /*!
\brief Contains the info required to load rule definitions \brief Contains the info required to load rule definitions
*/ */
@ -278,8 +298,9 @@ namespace rule_loader
const std::string& content; const std::string& content;
const indexed_vector<falco_source>& sources; const indexed_vector<falco_source>& sources;
std::string name; std::string name;
std::string output_extra;
bool replace_output_container_info = false; std::vector<extra_output_format_conf> extra_output_format;
std::vector<extra_output_field_conf> extra_output_fields;
// outputs // outputs
std::unique_ptr<result> res; std::unique_ptr<result> res;

View File

@ -322,22 +322,6 @@ static std::shared_ptr<ast::expr> parse_condition(
} }
} }
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( void rule_loader::compiler::compile_list_infos(
configuration& cfg, configuration& cfg,
const collector& col, const collector& col,
@ -510,13 +494,64 @@ void rule_loader::compiler::compile_rule_infos(
// build rule output message // build rule output message
rule.output = r.output; rule.output = r.output;
// plugins sources do not have any container info and so we won't apply -pk, -pc, etc. for (auto& extra : cfg.extra_output_format)
// on the other hand, when using plugins you might want to append custom output based on the plugin
// TODO: this is not flexible enough (esp. if you mix plugin with syscalls),
// it would be better to add configuration options to control the output.
if (!cfg.replace_output_container_info || r.source == falco_common::syscall_source)
{ {
apply_output_substitutions(cfg, rule.output); if (extra.m_source != "" && r.source != extra.m_source)
{
continue;
}
if (extra.m_tag != "" && r.tags.count(extra.m_tag) == 0)
{
continue;
}
if (extra.m_rule != "" && r.name != extra.m_rule)
{
continue;
}
if (extra.m_replace_container_info)
{
if (rule.output.find(s_container_info_fmt) != std::string::npos)
{
rule.output = replace(rule.output, s_container_info_fmt, extra.m_format);
}
else
{
rule.output = rule.output + " " + extra.m_format;
}
} else
{
rule.output = rule.output + " " + extra.m_format;
}
}
if (rule.output.find(s_container_info_fmt) != std::string::npos)
{
rule.output = replace(rule.output, s_container_info_fmt, s_default_extra_fmt);
}
// build extra output fields if required
for (auto const& extra : cfg.extra_output_fields)
{
if (extra.m_source != "" && r.source != extra.m_source)
{
continue;
}
if (extra.m_tag != "" && r.tags.count(extra.m_tag) == 0)
{
continue;
}
if (extra.m_rule != "" && r.name != extra.m_rule)
{
continue;
}
rule.extra_output_fields[extra.m_key] = {extra.m_format, extra.m_raw};
} }
// validate the rule's output // validate the rule's output
@ -538,6 +573,18 @@ void rule_loader::compiler::compile_rule_infos(
r.output_ctx); r.output_ctx);
} }
// validate the rule's extra fields if any
for (auto const& ef : rule.extra_output_fields)
{
if(!is_format_valid(*cfg.sources.at(r.source), ef.second.first, err))
{
throw rule_load_exception(
falco::load_result::load_result::LOAD_ERR_COMPILE_OUTPUT,
err,
r.output_ctx);
}
}
if (!compile_condition(cfg, if (!compile_condition(cfg,
macro_resolver, macro_resolver,
lists, lists,

View File

@ -17,49 +17,55 @@ limitations under the License.
#include "actions.h" #include "actions.h"
#include <libsinsp/plugin_manager.h> #include <libsinsp/plugin_manager.h>
#include <falco_common.h>
using namespace falco::app; using namespace falco::app;
using namespace falco::app::actions; using namespace falco::app::actions;
void configure_output_format(falco::app::state& s) void configure_output_format(falco::app::state& s)
{ {
for (auto& eo : s.config->m_append_output)
{
if (eo.m_format != "")
{
s.engine->add_extra_output_format(eo.m_format, eo.m_source, eo.m_tag, eo.m_rule, false);
}
for (auto const& ff : eo.m_formatted_fields)
{
s.engine->add_extra_output_formatted_field(ff.first, ff.second, eo.m_source, eo.m_tag, eo.m_rule);
}
for (auto const& rf : eo.m_raw_fields)
{
s.engine->add_extra_output_raw_field(rf, eo.m_source, eo.m_tag, eo.m_rule);
}
}
// See https://falco.org/docs/rules/style-guide/ // See https://falco.org/docs/rules/style-guide/
const std::string container_info = "container_id=%container.id container_image=%container.image.repository container_image_tag=%container.image.tag container_name=%container.name"; const std::string container_info = "container_id=%container.id container_image=%container.image.repository container_image_tag=%container.image.tag container_name=%container.name";
const std::string k8s_info = "k8s_ns=%k8s.ns.name k8s_pod_name=%k8s.pod.name"; const std::string k8s_info = "k8s_ns=%k8s.ns.name k8s_pod_name=%k8s.pod.name";
const std::string gvisor_info = "vpid=%proc.vpid vtid=%thread.vtid"; const std::string gvisor_info = "vpid=%proc.vpid vtid=%thread.vtid";
std::string output_format;
bool replace_container_info = false;
if(s.options.print_additional == "c" || s.options.print_additional == "container") if(s.options.print_additional == "c" || s.options.print_additional == "container")
{ {
output_format = container_info; s.engine->add_extra_output_format(container_info, falco_common::syscall_source, "", "", true);
replace_container_info = true;
} }
else if(s.options.print_additional == "cg" || s.options.print_additional == "container-gvisor") else if(s.options.print_additional == "cg" || s.options.print_additional == "container-gvisor")
{ {
output_format = gvisor_info + " " + container_info; s.engine->add_extra_output_format(gvisor_info + " " + container_info, falco_common::syscall_source, "", "", true);
replace_container_info = true;
} }
else if(s.options.print_additional == "k" || s.options.print_additional == "kubernetes") else if(s.options.print_additional == "k" || s.options.print_additional == "kubernetes")
{ {
output_format = container_info + " " + k8s_info; s.engine->add_extra_output_format(container_info + " " + k8s_info, falco_common::syscall_source, "", "", true);
replace_container_info = true;
} }
else if(s.options.print_additional == "kg" || s.options.print_additional == "kubernetes-gvisor") else if(s.options.print_additional == "kg" || s.options.print_additional == "kubernetes-gvisor")
{ {
output_format = gvisor_info + " " + container_info + " " + k8s_info; s.engine->add_extra_output_format(gvisor_info + " " + container_info + " " + k8s_info, falco_common::syscall_source, "", "", true);
replace_container_info = true;
} }
else if(!s.options.print_additional.empty()) else if(!s.options.print_additional.empty())
{ {
output_format = s.options.print_additional; s.engine->add_extra_output_format(s.options.print_additional, "", "", "", false);
replace_container_info = false;
}
if(!output_format.empty())
{
s.engine->set_extra(output_format, replace_container_info);
} }
} }

View File

@ -312,7 +312,9 @@ static falco::app::run_result do_inspect(
{ {
for(auto& rule_res : *res) for(auto& rule_res : *res)
{ {
s.outputs->handle_event(rule_res.evt, rule_res.rule, rule_res.source, rule_res.priority_num, rule_res.format, rule_res.tags); s.outputs->handle_event(
rule_res.evt, rule_res.rule, rule_res.source, rule_res.priority_num,
rule_res.format, rule_res.tags, rule_res.extra_output_fields);
} }
} }

View File

@ -588,6 +588,7 @@ void falco_configuration::load_yaml(const std::string& config_name)
m_metrics_include_empty_values = m_config.get_scalar<bool>("metrics.include_empty_values", false); m_metrics_include_empty_values = m_config.get_scalar<bool>("metrics.include_empty_values", false);
m_config.get_sequence<std::vector<rule_selection_config>>(m_rules_selection, "rules"); m_config.get_sequence<std::vector<rule_selection_config>>(m_rules_selection, "rules");
m_config.get_sequence<std::vector<append_output_config>>(m_append_output, "append_output");
std::vector<std::string> load_plugins; std::vector<std::string> load_plugins;

View File

@ -107,6 +107,15 @@ public:
std::string m_rule; std::string m_rule;
}; };
struct append_output_config {
std::string m_source;
std::string m_tag;
std::string m_rule;
std::string m_format;
std::unordered_map<std::string, std::string> m_formatted_fields;
std::set<std::string> m_raw_fields;
};
falco_configuration(); falco_configuration();
virtual ~falco_configuration() = default; virtual ~falco_configuration() = default;
@ -134,6 +143,8 @@ public:
std::list<std::string> m_loaded_rules_folders; std::list<std::string> m_loaded_rules_folders;
// Rule selection options passed by the user // Rule selection options passed by the user
std::vector<rule_selection_config> m_rules_selection; std::vector<rule_selection_config> m_rules_selection;
// Append output configuration passed by the user
std::vector<append_output_config> m_append_output;
bool m_json_output; bool m_json_output;
bool m_json_include_output_property; bool m_json_include_output_property;
@ -219,6 +230,114 @@ private:
}; };
namespace YAML { namespace YAML {
template<>
struct convert<falco_configuration::append_output_config> {
static Node encode(const falco_configuration::append_output_config & rhs) {
Node node;
if(rhs.m_source != "")
{
node["source"] = rhs.m_source;
}
if(rhs.m_rule != "")
{
node["rule"] = rhs.m_rule;
}
if(rhs.m_tag != "")
{
node["tag"] = rhs.m_tag;
}
if(rhs.m_format != "")
{
node["format"] = rhs.m_format;
}
for(auto const& field : rhs.m_formatted_fields)
{
YAML::Node field_node;
field_node[field.first] = field.second;
node["fields"].push_back(field_node);
}
for(auto const& field : rhs.m_raw_fields)
{
node["fields"].push_back(field);
}
return node;
}
static bool decode(const Node& node, falco_configuration::append_output_config & rhs) {
if(!node.IsMap())
{
return false;
}
if(node["source"])
{
rhs.m_source = node["source"].as<std::string>();
}
if(node["tag"])
{
rhs.m_tag = node["tag"].as<std::string>();
}
if(node["rule"])
{
rhs.m_rule = node["rule"].as<std::string>();
}
if(node["format"])
{
rhs.m_format = node["format"].as<std::string>();
}
if(node["fields"])
{
if(!node["fields"].IsSequence())
{
return false;
}
for(auto& field_definition : node["fields"])
{
if(field_definition.IsMap() && field_definition.size() == 1)
{
YAML::const_iterator def = field_definition.begin();
std::string key = def->first.as<std::string>();
// it is an error to redefine an existing key
if (rhs.m_formatted_fields.count(key) != 0 || rhs.m_raw_fields.count(key) != 0)
{
return false;
}
rhs.m_formatted_fields[key] = def->second.as<std::string>();
} else if (field_definition.IsScalar())
{
std::string key = field_definition.as<std::string>();
// it is an error to redefine an existing key
if (rhs.m_formatted_fields.count(key) != 0)
{
return false;
}
rhs.m_raw_fields.insert(key);
} else {
return false;
}
}
}
return true;
}
};
template<> template<>
struct convert<falco_configuration::rule_selection_config> { struct convert<falco_configuration::rule_selection_config> {
static Node encode(const falco_configuration::rule_selection_config & rhs) { static Node encode(const falco_configuration::rule_selection_config & rhs) {

View File

@ -127,7 +127,8 @@ void falco_outputs::add_output(const falco::outputs::config &oc)
} }
void falco_outputs::handle_event(sinsp_evt *evt, const std::string &rule, const std::string &source, void falco_outputs::handle_event(sinsp_evt *evt, const std::string &rule, const std::string &source,
falco_common::priority_type priority, const std::string &format, std::set<std::string> &tags) falco_common::priority_type priority, const std::string &format, std::set<std::string> &tags,
std::unordered_map<std::string, std::pair<std::string, bool>> &extra_fields)
{ {
falco_outputs::ctrl_msg cmsg = {}; falco_outputs::ctrl_msg cmsg = {};
cmsg.ts = evt->get_ts(); cmsg.ts = evt->get_ts();
@ -157,9 +158,30 @@ void falco_outputs::handle_event(sinsp_evt *evt, const std::string &rule, const
} }
cmsg.msg = m_formats->format_event( cmsg.msg = m_formats->format_event(
evt, rule, source, falco_common::format_priority(priority), sformat, tags, m_hostname evt, rule, source, falco_common::format_priority(priority), sformat, tags, m_hostname, extra_fields
); );
cmsg.fields = m_formats->get_field_values(evt, source, sformat);
auto fields = m_formats->get_field_values(evt, source, sformat);
for (auto const& ef : extra_fields)
{
// when formatting for the control message we always want strings,
// so we can simply format raw fields as string
std::string fformat = ef.second.first;
if (fformat.size() == 0)
{
continue;
}
if (!(fformat[0] == '*'))
{
fformat = "*" + fformat;
}
fields[ef.first] = m_formats->format_string(evt, fformat, source);
}
cmsg.fields = fields;
cmsg.tags.insert(tags.begin(), tags.end()); cmsg.tags.insert(tags.begin(), tags.end());
cmsg.type = ctrl_msg_type::CTRL_MSG_OUTPUT; cmsg.type = ctrl_msg_type::CTRL_MSG_OUTPUT;

View File

@ -59,7 +59,8 @@ public:
is an event that has matched some rule). is an event that has matched some rule).
*/ */
void handle_event(sinsp_evt *evt, const std::string &rule, const std::string &source, void handle_event(sinsp_evt *evt, const std::string &rule, const std::string &source,
falco_common::priority_type priority, const std::string &format, std::set<std::string> &tags); falco_common::priority_type priority, const std::string &format, std::set<std::string> &tags,
std::unordered_map<std::string, std::pair<std::string, bool>> &extra_fields);
/*! /*!
\brief Format then send a generic message to all outputs. \brief Format then send a generic message to all outputs.