Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(userspace/falco,userspace/engine): rule json schema validation #3313

Merged
merged 9 commits into from
Sep 11, 2024
80 changes: 68 additions & 12 deletions unit_tests/engine/test_rule_loader.cpp

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions unit_tests/falco/test_configuration_schema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ TEST(Configuration, schema_yaml_helper_validator)
EXPECT_NO_THROW(conf.load_from_string(sample_yaml));

// We pass a string variable but not a schema
std::string validation;
std::vector<std::string> validation;
EXPECT_NO_THROW(conf.load_from_string(sample_yaml, nlohmann::json{}, &validation));
EXPECT_EQ(validation, yaml_helper::validation_none);
EXPECT_EQ(validation[0], yaml_helper::validation_none);

// We pass a schema but not a string storage for the validation; no validation takes place
EXPECT_NO_THROW(conf.load_from_string(sample_yaml, falco_config.m_config_schema, nullptr));

// We pass everything
EXPECT_NO_THROW(conf.load_from_string(sample_yaml, falco_config.m_config_schema, &validation));
EXPECT_EQ(validation, yaml_helper::validation_ok);
EXPECT_EQ(validation[0], yaml_helper::validation_ok);
}
6 changes: 5 additions & 1 deletion userspace/engine/falco_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ limitations under the License.

const std::string falco_engine::s_default_ruleset = "falco-default-ruleset";

static const std::string rule_schema_string = R"({"$schema":"http://json-schema.org/draft-06/schema#","type":"array","items":{"$ref":"#/definitions/FalcoRule"},"definitions":{"FalcoRule":{"type":"object","additionalProperties":false,"properties":{"required_engine_version":{"type":"string"},"macro":{"type":"string"},"condition":{"type":"string"},"list":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/definitions/Item"}},"rule":{"type":"string"},"desc":{"type":"string"},"enabled":{"type":"boolean"},"output":{"type":"string"},"append":{"type":"boolean"},"priority":{"$ref":"#/definitions/Priority"},"exceptions":{"type":"array","items":{"$ref":"#/definitions/Exception"}},"override":{"$ref":"#/definitions/Override"},"tags":{"type":"array","items":{"type":"string"}}},"required":[],"title":"FalcoRule"},"Item":{"anyOf":[{"type":"integer"},{"type":"string"}],"title":"Item"},"Exception":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string"},"fields":{},"comps":{},"values":{}},"required":["name","values"],"title":"Exception"},"Priority":{"type":"string","enum":["EMERGENCY","ALERT","CRITICAL","ERROR","WARNING","NOTICE","INFO","INFORMATIONAL","DEBUG"],"title":"Priority"},"OverriddenItem":{"type":"string","enum":["append","replace"],"title":"Priority"},"Override":{"type":"object","additionalProperties":false,"properties":{"items":{"$ref":"#/definitions/OverriddenItem"},"desc":{"$ref":"#/definitions/OverriddenItem"},"condition":{"$ref":"#/definitions/OverriddenItem"},"output":{"$ref":"#/definitions/OverriddenItem"},"priority":{"$ref":"#/definitions/OverriddenItem"},"enabled":{"$ref":"#/definitions/OverriddenItem"},"exceptions":{"$ref":"#/definitions/OverriddenItem"}},"minProperties":1,"title":"Override"}}})";

using namespace falco;

falco_engine::falco_engine(bool seed_rng)
Expand All @@ -67,6 +69,8 @@ falco_engine::falco_engine(bool seed_rng)
m_default_ruleset_id = find_ruleset_id(s_default_ruleset);

fill_engine_state_funcs(m_engine_state);

m_rule_schema = nlohmann::json::parse(rule_schema_string);
}

falco_engine::~falco_engine()
Expand Down Expand Up @@ -198,7 +202,7 @@ std::unique_ptr<load_result> falco_engine::load_rules(const std::string &rules_c
cfg.extra_output_fields = m_extra_output_fields;

// 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, m_rule_schema))
{
// compile the definitions (resolve macro/list refs, exceptions, ...)
m_last_compile_output = m_rule_compiler->new_compile_output();
Expand Down
2 changes: 2 additions & 0 deletions userspace/engine/falco_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ class falco_engine
const std::vector<plugin_version_requirement>& plugins,
std::string& err) const;

nlohmann::json m_rule_schema;

private:
// Create a ruleset using the provided factory and set the
// engine state funcs for it.
Expand Down
3 changes: 3 additions & 0 deletions userspace/engine/falco_load_result.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ class load_result {
// has_warnings() can both be true if there were only warnings.
virtual bool has_warnings() = 0;

// Return json schema validation status.
virtual std::string schema_validation() = 0;

// This represents a set of rules contents as a mapping from
// rules content name (usually filename) to rules content. The
// rules content is actually a reference to the actual string
Expand Down
77 changes: 75 additions & 2 deletions userspace/engine/rule_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.
#include <string>

#include "rule_loader.h"
#include "yaml_helper.h"


static const std::string item_type_strings[] = {
Expand Down Expand Up @@ -296,6 +297,15 @@ bool rule_loader::result::has_warnings()
return (warnings.size() > 0);
}

std::string rule_loader::result::schema_validation()
{
if (schema_validation_status.empty())
{
return yaml_helper::validation_none;
}
return schema_validation_status[0];
}

void rule_loader::result::add_error(load_result::error_code ec, const std::string& msg, const context& ctx)
{
error err = {ec, msg, ctx};
Expand All @@ -311,6 +321,11 @@ void rule_loader::result::add_warning(load_result::warning_code wc, const std::s
warnings.push_back(warn);
}

void rule_loader::result::set_schema_validation_status(const std::vector<std::string>& status)
{
schema_validation_status = status;
}

const std::string& rule_loader::result::as_string(bool verbose, const rules_contents_t& contents)
{
if(verbose)
Expand Down Expand Up @@ -351,6 +366,31 @@ const std::string& rule_loader::result::as_summary_string()
os << "Invalid";
}

// Only print schema validation info if any validation was requested
if (!schema_validation_status.empty())
{
bool schema_valid = schema_validation() == yaml_helper::validation_ok;
// Only print info when there are validation warnings
if (!schema_valid)
{
os << std::endl;

os << " " << schema_validation_status.size() << " schema warnings: [";
bool first = true;
for(auto& status : schema_validation_status)
{
if(!first)
{
os << " ";
}
first = false;

os << status;
}
os << "]";
}
}

if(!errors.empty())
{
os << std::endl;
Expand Down Expand Up @@ -423,6 +463,26 @@ const std::string& rule_loader::result::as_verbose_string(const rules_contents_t
os << "Invalid";
}

// Only print schema validation info if any validation was requested
if (!schema_validation_status.empty())
{
bool schema_valid = schema_validation() == yaml_helper::validation_ok;
// Only print info when there are validation warnings
if (!schema_valid)
{
os << std::endl;

os << schema_validation_status.size()
<< " Schema warnings:" << std::endl;

for(auto& status : schema_validation_status)
{
os << "------" << std::endl;
os << status << std::endl;
}
os << "------" << std::endl;
}
}
if (!errors.empty())
{
os << std::endl;
Expand Down Expand Up @@ -482,8 +542,22 @@ const nlohmann::json& rule_loader::result::as_json(const rules_contents_t& conte
j["name"] = name;
j["successful"] = success;

j["errors"] = nlohmann::json::array();
// Only print schema validation info if any validation was requested
if (!schema_validation_status.empty())
{
bool schema_valid = schema_validation() == yaml_helper::validation_ok;
j["schema_valid"] = schema_valid;
j["schema_warnings"] = nlohmann::json::array();
if (!schema_valid)
{
for (const auto &schema_warning : schema_validation_status)
{
j["schema_warnings"].push_back(schema_warning);
}
}
}

j["errors"] = nlohmann::json::array();
for(auto &err : errors)
{
nlohmann::json jerr;
Expand All @@ -499,7 +573,6 @@ const nlohmann::json& rule_loader::result::as_json(const rules_contents_t& conte
}

j["warnings"] = nlohmann::json::array();

for(auto &warn : warnings)
{
nlohmann::json jwarn;
Expand Down
4 changes: 4 additions & 0 deletions userspace/engine/rule_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,16 @@ namespace rule_loader
void add_warning(falco::load_result::warning_code ec,
const std::string& msg,
const context& ctx);

void set_schema_validation_status(const std::vector<std::string>& status);
std::string schema_validation();
protected:

const std::string& as_summary_string();
const std::string& as_verbose_string(const falco::load_result::rules_contents_t& contents);
std::string name;
bool success;
std::vector<std::string> schema_validation_status;

std::vector<error> errors;
std::vector<warning> warnings;
Expand Down
9 changes: 6 additions & 3 deletions userspace/engine/rule_loader_reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ limitations under the License.
#include "rule_loader_reader.h"
#include "falco_engine_version.h"
#include "rule_loading_messages.h"
#include "yaml_helper.h"
#include <libsinsp/logger.h>

#include <re2/re2.h>
Expand Down Expand Up @@ -783,13 +784,15 @@ void rule_loader::reader::read_item(
}
}

bool rule_loader::reader::read(rule_loader::configuration& cfg, collector& collector)
bool rule_loader::reader::read(rule_loader::configuration& cfg, collector& collector, const nlohmann::json& schema)
{
std::vector<YAML::Node> docs;
yaml_helper reader;
std::vector<std::string> schema_warnings;
rule_loader::context ctx(cfg.name);
try
{
docs = YAML::LoadAll(cfg.content);
docs = reader.loadall_from_string(cfg.content, schema, &schema_warnings);
}
catch (YAML::ParserException& e)
{
Expand All @@ -807,7 +810,7 @@ bool rule_loader::reader::read(rule_loader::configuration& cfg, collector& colle
cfg.res->add_error(falco::load_result::LOAD_ERR_YAML_PARSE, "unknown YAML parsing error", ctx);
return false;
}

cfg.res->set_schema_validation_status(schema_warnings);
for (auto doc = docs.begin(); doc != docs.end(); doc++)
{
if (doc->IsDefined() && !doc->IsNull())
Expand Down
2 changes: 1 addition & 1 deletion userspace/engine/rule_loader_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class reader
\brief Reads the contents of a ruleset and uses a collector to store
thew new definitions
*/
virtual bool read(configuration& cfg, collector& loader);
virtual bool read(configuration& cfg, collector& loader, const nlohmann::json& schema={});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-breaking change.


/*!
\brief Engine version used to be represented as a simple progressive
Expand Down
73 changes: 49 additions & 24 deletions userspace/falco/yaml_helper.h → userspace/engine/yaml_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ limitations under the License.
#include <valijson/schema_parser.hpp>
#include <valijson/validator.hpp>

#include "config_falco.h"

#include "event_drops.h"
#include "falco_outputs.h"

class yaml_helper;

class yaml_visitor {
Expand Down Expand Up @@ -89,40 +84,67 @@ class yaml_helper
inline static const std::string configs_key = "config_files";
inline static const std::string validation_ok = "ok";
inline static const std::string validation_failed = "failed";
inline static const std::string validation_none = "schema not provided";
inline static const std::string validation_none = "none";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none better reflect the fact that no validation took place, without exposing more info than needed.


/**
* Load all the YAML document represented by the input string.
* Since this is used by rule loader, does not process env vars.
*/
std::vector<YAML::Node> loadall_from_string(const std::string& input, const nlohmann::json& schema={}, std::vector<std::string> *schema_warnings=nullptr)
{
auto nodes = YAML::LoadAll(input);
if (schema_warnings)
{
schema_warnings->clear();
if(!schema.empty())
{
// Validate each node.
for(const auto& node : nodes)
{
validate_node(node, schema, schema_warnings);
}
}
else
{
schema_warnings->push_back(validation_none);
}
}
return nodes;
}

/**
* Load the YAML document represented by the input string.
*/
void load_from_string(const std::string& input, const nlohmann::json& schema={}, std::string *validation=nullptr)
void load_from_string(const std::string& input, const nlohmann::json& schema={}, std::vector<std::string> *schema_warnings=nullptr)
{
m_root = YAML::Load(input);
pre_process_env_vars(m_root);

if (validation)
if (schema_warnings)
{
schema_warnings->clear();
if(!schema.empty())
{
*validation = validate_node(m_root, schema);
validate_node(m_root, schema, schema_warnings);
}
else
{
*validation = validation_none;
schema_warnings->push_back(validation_none);
}
}
}

/**
* Load the YAML document from the given file path.
*/
void load_from_file(const std::string& path, const nlohmann::json& schema={}, std::string *validation=nullptr)
void load_from_file(const std::string& path, const nlohmann::json& schema={}, std::vector<std::string> *schema_warnings=nullptr)
{
m_root = load_from_file_int(path, schema, validation);
m_root = load_from_file_int(path, schema, schema_warnings);
}

void include_config_file(const std::string& include_file_path, const nlohmann::json& schema={}, std::string *validation=nullptr)
void include_config_file(const std::string& include_file_path, const nlohmann::json& schema={}, std::vector<std::string> *schema_warnings=nullptr)
{
auto loaded_nodes = load_from_file_int(include_file_path, schema, validation);
auto loaded_nodes = load_from_file_int(include_file_path, schema, schema_warnings);
for(auto n : loaded_nodes)
{
/*
Expand Down Expand Up @@ -218,26 +240,27 @@ class yaml_helper
private:
YAML::Node m_root;

YAML::Node load_from_file_int(const std::string& path, const nlohmann::json& schema={}, std::string *validation=nullptr)
YAML::Node load_from_file_int(const std::string& path, const nlohmann::json& schema, std::vector<std::string> *schema_warnings)
{
auto root = YAML::LoadFile(path);
pre_process_env_vars(root);

if (validation)
if (schema_warnings)
{
schema_warnings->clear();
if(!schema.empty())
{
*validation = validate_node(root, schema);
validate_node(root, schema, schema_warnings);
}
else
{
*validation = validation_none;
schema_warnings->push_back(validation_none);
}
}
return root;
}

std::string validate_node(const YAML::Node &node, const nlohmann::json& schema={})
void validate_node(const YAML::Node &node, const nlohmann::json& schema, std::vector<std::string> *schema_warnings)
{
// Validate the yaml against our json schema
valijson::Schema schemaDef;
Expand All @@ -252,16 +275,18 @@ class yaml_helper
{
valijson::ValidationResults::Error error;
// report only the top-most error
if (validationResults.popError(error))
while (validationResults.popError(error))
{
return std::string(validation_failed + " for ")
schema_warnings->push_back(std::string(validation_failed + " for ")
+ std::accumulate(error.context.begin(), error.context.end(), std::string(""))
+ ": "
+ error.description;
+ error.description);
}
return validation_failed;
}
return validation_ok;
else
{
schema_warnings->push_back(validation_ok);
}
}

/*
Expand Down
Loading
Loading