diff --git a/CMakeLists.txt b/CMakeLists.txt index 53a7aaf13..6de0062e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ set(LIBDDWAF_SOURCE ${libddwaf_SOURCE_DIR}/src/object_store.cpp ${libddwaf_SOURCE_DIR}/src/collection.cpp ${libddwaf_SOURCE_DIR}/src/condition.cpp + ${libddwaf_SOURCE_DIR}/src/expression.cpp ${libddwaf_SOURCE_DIR}/src/rule.cpp ${libddwaf_SOURCE_DIR}/src/ruleset_info.cpp ${libddwaf_SOURCE_DIR}/src/ip_utils.cpp diff --git a/src/condition.cpp b/src/condition.cpp index c293a2847..2807ec02b 100644 --- a/src/condition.cpp +++ b/src/condition.cpp @@ -107,7 +107,7 @@ std::optional condition::match(const object_store &store, } std::optional optional_match; - if (source == data_source::keys) { + if (source == expression::data_source::keys) { object::key_iterator it(object, key_path, objects_excluded, limits_); optional_match = match_target(it, processor, transformers, deadline); } else { diff --git a/src/condition.hpp b/src/condition.hpp index 2d9a9950d..ded5fcc50 100644 --- a/src/condition.hpp +++ b/src/condition.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -28,14 +29,12 @@ class condition { public: using ptr = std::shared_ptr; - enum class data_source : uint8_t { values, keys }; - struct target_type { target_index root; std::string name; std::vector key_path{}; std::vector transformers{}; - data_source source{data_source::values}; + expression::data_source source{expression::data_source::values}; }; condition(std::vector targets, std::shared_ptr processor, diff --git a/src/expression.cpp b/src/expression.cpp new file mode 100644 index 000000000..0f93455ba --- /dev/null +++ b/src/expression.cpp @@ -0,0 +1,343 @@ +// Unless explicitly stated otherwise all files in this repository are +// dual-licensed under the Apache-2.0 License or BSD-3-Clause License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2021 Datadog, Inc. + +#include +#include + +#include "exception.hpp" +#include "expression.hpp" +#include "log.hpp" +#include "transformer/manager.hpp" + +namespace ddwaf { + +std::optional expression::evaluator::eval_object(const ddwaf_object *object, + const rule_processor::base::ptr &processor, + const std::vector &transformers) const +{ + const size_t length = + find_string_cutoff(object->stringValue, object->nbEntries, limits.max_string_length); + + if (!transformers.empty()) { + ddwaf_object src; + ddwaf_object dst; + ddwaf_object_stringl_nc(&src, object->stringValue, length); + ddwaf_object_invalid(&dst); + + auto transformed = transformer::manager::transform(src, dst, transformers); + scope_exit on_exit([&dst] { ddwaf_object_free(&dst); }); + if (transformed) { + return processor->match_object(&dst); + } + } + + return processor->match({object->stringValue, length}); +} + +template +std::optional expression::evaluator::eval_target(const condition &cond, T &it, + const rule_processor::base::ptr &processor, const std::vector &transformers) +{ + std::optional last_result = std::nullopt; + + for (; it; ++it) { + if (deadline.expired()) { + throw ddwaf::timeout_exception(); + } + + if (it.type() != DDWAF_OBJ_STRING) { + continue; + } + + DDWAF_TRACE("Value %s", (*it)->stringValue); + auto optional_match = eval_object(*it, processor, transformers); + if (!optional_match.has_value()) { + continue; + } + + last_result = std::move(optional_match); + last_result->key_path = std::move(it.get_current_path()); + + if (cond.children.scalar.empty()) { + break; + } + + cache.set_eval_highlight(&cond, last_result->matched); + cache.set_eval_scalar(&cond, *it); + + bool chain_result = true; + for (const auto *next_cond : cond.children.scalar) { + if (!eval_condition(*next_cond, eval_scope::local)) { + chain_result = false; + break; + } + } + + if (chain_result) { + break; + } + } + + return last_result; +} + +const rule_processor::base::ptr &expression::evaluator::get_processor(const condition &cond) const +{ + if (cond.processor || cond.data_id.empty()) { + return cond.processor; + } + + auto it = dynamic_processors.find(cond.data_id); + if (it == dynamic_processors.end()) { + return cond.processor; + } + + return it->second; +} + +// NOLINTNEXTLINE(misc-no-recursion) +bool expression::evaluator::eval_condition(const condition &cond, eval_scope scope) +{ + auto &cond_cache = cache.get_condition_cache(cond); + + if (cond_cache.result.has_value()) { + return true; + } + + const auto &processor = get_processor(cond); + if (!processor) { + DDWAF_DEBUG("Condition doesn't have a valid processor"); + return false; + } + + for (std::size_t ti = 0; ti < cond.targets.size(); ++ti) { + const auto &target = cond.targets[ti]; + + if (deadline.expired()) { + throw ddwaf::timeout_exception(); + } + + if (scope != target.scope || cond_cache.targets.find(ti) != cond_cache.targets.end()) { + continue; + } + + // TODO: iterators could be cached to avoid reinitialisation + const ddwaf_object *object = nullptr; + if (target.scope == eval_scope::global) { + object = store.get_target(target.root); + } else { + object = cache.get_eval_entity(target.parent, target.entity); + } + + if (object == nullptr) { + continue; + } + + DDWAF_TRACE("Evaluating target %s", target.name.c_str()); + + std::optional optional_match; + if (target.source == data_source::keys) { + object::key_iterator it(object, target.key_path, objects_excluded, limits); + optional_match = eval_target(cond, it, processor, target.transformers); + } else { + object::value_iterator it(object, target.key_path, objects_excluded, limits); + optional_match = eval_target(cond, it, processor, target.transformers); + } + + if (!optional_match.has_value()) { + continue; + } + + cond_cache.targets.emplace(ti); + + optional_match->address = target.name; + cond_cache.result = optional_match; + + if (!cond.children.object.empty()) { + cache.set_eval_object(&cond, object); + + bool chain_result = true; + for (const auto *next_cond : cond.children.object) { + if (!eval_condition(*next_cond, eval_scope::local)) { + chain_result = false; + break; + } + } + + if (!chain_result) { + continue; + } + } + + DDWAF_TRACE("Target %s matched parameter value %s", target.name.c_str(), + optional_match->resolved.c_str()); + + return true; + } + + return cond_cache.result.has_value(); +} + +bool expression::evaluator::eval() +{ + // NOLINTNEXTLINE(readability-use-anyofallof) + for (const auto &cond : conditions) { + if (!eval_condition(*cond, eval_scope::global)) { + return false; + } + } + return true; +} + +bool expression::eval(cache_type &cache, const object_store &store, + const std::unordered_set &objects_excluded, + const std::unordered_map &dynamic_processors, + ddwaf::timer &deadline) const +{ + if (cache.conditions.size() != conditions_.size()) { + cache.conditions.reserve(conditions_.size()); + cache.store.reserve(conditions_.size()); + } + + // TODO the cache result alone might be insufficient + if (!cache.result) { + evaluator runner{ + deadline, limits_, conditions_, store, objects_excluded, dynamic_processors, cache}; + cache.result = runner.eval(); + } + + return cache.result; +} + +memory::vector expression::get_matches(cache_type &cache) +{ + if (!cache.result) { + return {}; + } + + memory::vector matches; + + for (const auto &cond : conditions_) { + auto it = cache.conditions.find(cond.get()); + if (it == cache.conditions.end()) { + // Bug + return {}; + } + + auto &cond_cache = it->second; + + // clang-tidy has trouble with an optional after two levels of indirection + auto &result = cond_cache.result; + if (result.has_value()) { + matches.emplace_back(std::move(result.value())); + } else { + // Bug + return {}; + } + } + + return matches; +} + +namespace { +std::tuple explode_local_address(std::string_view str) +{ + constexpr std::string_view prefix = "match."; + auto pos = str.find(prefix); + if (pos == std::string_view::npos) { + return {false, 0, {}}; + } + str.remove_prefix(prefix.size()); + + // TODO everything below this point should throw instead of returning false + pos = str.find('.'); + if (pos == std::string_view::npos) { + return {false, 0, {}}; + } + + auto index_str = str.substr(0, pos); + std::size_t index = 0; + auto result = std::from_chars(index_str.data(), index_str.data() + index_str.size(), index); + if (result.ec == std::errc::invalid_argument) { + return {false, 0, {}}; + } + + expression::eval_entity entity; + auto entity_str = str.substr(pos + 1, str.size() - (pos + 1)); + if (entity_str == "object") { + entity = expression::eval_entity::object; + } else if (entity_str == "scalar") { + entity = expression::eval_entity::scalar; + } else if (entity_str == "highlight") { + entity = expression::eval_entity::highlight; + } else { + return {false, 0, {}}; + } + + return {true, index, entity}; +} + +} // namespace + // +void expression_builder::add_target(std::string name, std::vector key_path, + std::vector transformers, expression::data_source source) +{ + auto [res, index, entity] = explode_local_address(name); + if (res) { + add_local_target( + std::move(name), index, entity, std::move(key_path), std::move(transformers), source); + } else { + add_global_target(std::move(name), std::move(key_path), std::move(transformers), source); + } +} + +void expression_builder::add_global_target(std::string name, std::vector key_path, + std::vector transformers, expression::data_source source) +{ + expression::condition::target_type target; + target.scope = expression::eval_scope::global; + target.root = get_target_index(name); + target.key_path = std::move(key_path); + target.name = std::move(name); + target.transformers = std::move(transformers); + target.source = source; + + auto &cond = conditions_.back(); + cond->targets.emplace_back(std::move(target)); +} + +void expression_builder::add_local_target(std::string name, std::size_t cond_idx, + expression::eval_entity entity, std::vector key_path, + std::vector transformers, expression::data_source source) +{ + if (cond_idx >= (conditions_.size() - 1)) { + throw std::invalid_argument( + "local target references subsequent condition (or itself): current = " + + std::to_string(conditions_.size() - 1) + ", referenced = " + std::to_string(cond_idx)); + } + + auto &parent = conditions_[cond_idx]; + auto &cond = conditions_.back(); + + if (entity == expression::eval_entity::object) { + parent->children.object.emplace(cond.get()); + } else { + parent->children.scalar.emplace(cond.get()); + } + + expression::condition::target_type target; + target.scope = expression::eval_scope::local; + target.parent = parent.get(); + target.entity = entity; + target.key_path = std::move(key_path); + target.name = std::move(name); + target.transformers = std::move(transformers); + target.source = source; + + cond->targets.emplace_back(std::move(target)); +} + +} // namespace ddwaf diff --git a/src/expression.hpp b/src/expression.hpp new file mode 100644 index 000000000..a63a1f3ff --- /dev/null +++ b/src/expression.hpp @@ -0,0 +1,239 @@ +// Unless explicitly stated otherwise all files in this repository are +// dual-licensed under the Apache-2.0 License or BSD-3-Clause License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2021 Datadog, Inc. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "clock.hpp" +#include "context_allocator.hpp" +#include "event.hpp" +#include "iterator.hpp" +#include "log.hpp" +#include "object_store.hpp" +#include "rule_processor/base.hpp" +#include "transformer/manager.hpp" +#include "utils.hpp" + +namespace ddwaf { + +class expression { +public: + using ptr = std::shared_ptr; + + enum class data_source : uint8_t { values, keys }; + enum class eval_scope : uint8_t { global, local }; + enum class eval_entity : uint8_t { highlight, scalar, object }; + + struct condition { + using ptr = std::shared_ptr; + + struct cache_type { + std::unordered_set targets{}; + std::optional result{std::nullopt}; + }; + + struct target_type { + eval_scope scope{eval_scope::global}; + std::string name; + + // Global scope + target_index root{}; + + // Local scope + const condition *parent{nullptr}; + eval_entity entity{eval_entity::object}; + + // Applicable to either scope + std::vector key_path{}; + + // Transformers + std::vector transformers{}; + data_source source{data_source::values}; + }; + + std::vector targets; + std::shared_ptr processor; + std::string data_id; + struct { + std::unordered_set scalar{}; + std::unordered_set object{}; + } children; + }; + + struct eval_result { + ddwaf_object highlight{nullptr, 0, {nullptr}, 0, DDWAF_OBJ_INVALID}; + const ddwaf_object *scalar{nullptr}; + const ddwaf_object *object{nullptr}; + }; + + struct cache_type { + bool result{false}; + std::unordered_map conditions{}; + std::unordered_map store{}; + + condition::cache_type &get_condition_cache(const condition &cond) + { + return conditions[&cond]; + } + + void set_eval_highlight(const condition *cond, const memory::string &str) + { + auto &res = store[cond]; + ddwaf_object_stringl_nc(&res.highlight, str.c_str(), str.size()); + } + + void set_eval_scalar(const condition *cond, const ddwaf_object *obj) + { + store[cond].scalar = obj; + } + + void set_eval_object(const condition *cond, const ddwaf_object *obj) + { + store[cond].object = obj; + } + + const ddwaf_object *get_eval_entity(const condition *cond, eval_entity entity) + { + auto it = store.find(cond); + if (it != store.end()) { + switch (entity) { + case eval_entity::highlight: + return &it->second.highlight; + case eval_entity::scalar: + return it->second.scalar; + case eval_entity::object: + return it->second.object; + } + } + return nullptr; + } + }; + + struct evaluator { + bool eval(); + bool eval_condition(const condition &cond, eval_scope scope); + + template + std::optional eval_target(const condition &cond, T &it, + const rule_processor::base::ptr &processor, + const std::vector &transformers); + + std::optional eval_object(const ddwaf_object *object, + const rule_processor::base::ptr &processor, + const std::vector &transformers) const; + + [[nodiscard]] const rule_processor::base::ptr &get_processor(const condition &cond) const; + + ddwaf::timer &deadline; + const ddwaf::object_limits &limits; + const std::vector &conditions; + const object_store &store; + const std::unordered_set &objects_excluded; + const std::unordered_map &dynamic_processors; + cache_type &cache; + }; + + explicit expression(std::vector &&conditions, ddwaf::object_limits limits = {}) + : limits_(limits), conditions_(std::move(conditions)) + {} + + bool eval(cache_type &cache, const object_store &store, + const std::unordered_set &objects_excluded, + const std::unordered_map &dynamic_processors, + ddwaf::timer &deadline) const; + + memory::vector get_matches(cache_type &cache); + + void get_addresses(std::unordered_set &addresses) const + { + for (const auto &cond : conditions_) { + for (const auto &target : cond->targets) { + if (target.scope == eval_scope::global) { + addresses.emplace(target.name); + } + } + } + } + + // For testing + [[nodiscard]] std::size_t get_num_conditions() const { return conditions_.size(); } + +protected: + ddwaf::object_limits limits_; + std::vector conditions_; +}; + +class expression_builder { +public: + explicit expression_builder(std::size_t num_conditions, ddwaf::object_limits limits = {}) + : limits_(limits) + { + conditions_.reserve(num_conditions); + } + + void start_condition() { conditions_.emplace_back(std::make_shared()); } + template void start_condition(Args... args) + { + auto cond = std::make_shared(); + cond->processor = std::make_unique(std::forward(args)...); + conditions_.emplace_back(std::move(cond)); + } + + void start_condition(std::string data_id) + { + auto cond = std::make_shared(); + cond->data_id = std::move(data_id); + conditions_.emplace_back(std::move(cond)); + } + + void set_data_id(std::string data_id) + { + auto &cond = conditions_.back(); + cond->data_id = std::move(data_id); + } + + template void set_processor(Args... args) + { + auto &cond = conditions_.back(); + cond->processor = std::make_unique(args...); + } + + void set_processor(rule_processor::base::ptr &&processor) + { + auto &cond = conditions_.back(); + cond->processor = std::move(processor); + } + + void add_target(std::string name, std::vector key_path = {}, + std::vector transformers = {}, + expression::data_source source = expression::data_source::values); + + expression::ptr build() + { + return std::make_shared(std::move(conditions_), limits_); + } + +protected: + void add_global_target(std::string name, std::vector key_path = {}, + std::vector transformers = {}, + expression::data_source source = expression::data_source::values); + + void add_local_target(std::string name, std::size_t cond_idx, expression::eval_entity entity, + std::vector key_path = {}, std::vector transformers = {}, + expression::data_source source = expression::data_source::values); + + ddwaf::object_limits limits_; + std::vector conditions_; +}; + +} // namespace ddwaf diff --git a/src/iterator.hpp b/src/iterator.hpp index b693ce031..3e3bb46c3 100644 --- a/src/iterator.hpp +++ b/src/iterator.hpp @@ -38,6 +38,7 @@ template class iterator_base { [[nodiscard]] size_t depth() { return stack_.size() + path_.size(); } [[nodiscard]] memory::vector get_current_path() const; [[nodiscard]] const ddwaf_object *get_underlying_object() { return current_; } + [[nodiscard]] const ddwaf_object *get_root_object() { return stack_.front().first; } protected: bool should_exclude(const ddwaf_object *obj) const diff --git a/src/parser/parser_v1.cpp b/src/parser/parser_v1.cpp index f93e0bc65..ea71c5e0e 100644 --- a/src/parser/parser_v1.cpp +++ b/src/parser/parser_v1.cpp @@ -27,84 +27,82 @@ namespace ddwaf::parser::v1 { namespace { -condition::ptr parseCondition( - parameter::map &rule, std::vector transformers, ddwaf::object_limits limits) +expression::ptr parse_expression(parameter::vector &conditions_array, + const std::vector &transformers, ddwaf::object_limits limits) { - auto operation = at(rule, "operation"); - auto params = at(rule, "parameters"); + expression_builder builder(conditions_array.size(), limits); + for (const auto &cond_param : conditions_array) { + auto cond = static_cast(cond_param); - parameter::map options; - std::shared_ptr processor; - if (operation == "phrase_match") { - auto list = at(params, "list"); + builder.start_condition(); - std::vector patterns; - std::vector lengths; + auto operation = at(cond, "operation"); + auto params = at(cond, "parameters"); - patterns.reserve(list.size()); - lengths.reserve(list.size()); + parameter::map options; + std::shared_ptr processor; + if (operation == "phrase_match") { + auto list = at(params, "list"); - for (auto &pattern : list) { - if (pattern.type != DDWAF_OBJ_STRING) { - throw ddwaf::parsing_error("phrase_match list item not a string"); - } + std::vector patterns; + std::vector lengths; - patterns.push_back(pattern.stringValue); - lengths.push_back((uint32_t)pattern.nbEntries); - } + patterns.reserve(list.size()); + lengths.reserve(list.size()); - processor = std::make_shared(patterns, lengths); - } else if (operation == "match_regex") { - auto regex = at(params, "regex"); - options = at(params, "options", options); + for (auto &pattern : list) { + if (pattern.type != DDWAF_OBJ_STRING) { + throw ddwaf::parsing_error("phrase_match list item not a string"); + } - auto case_sensitive = at(options, "case_sensitive", false); - auto min_length = at(options, "min_length", 0); - if (min_length < 0) { - throw ddwaf::parsing_error("min_length is a negative number"); - } + patterns.push_back(pattern.stringValue); + lengths.push_back((uint32_t)pattern.nbEntries); + } - processor = - std::make_shared(regex, min_length, case_sensitive); - } else if (operation == "is_xss") { - processor = std::make_shared(); - } else if (operation == "is_sqli") { - processor = std::make_shared(); - } else { - throw ddwaf::parsing_error("unknown processor: " + std::string(operation)); - } + processor = std::make_shared(patterns, lengths); + } else if (operation == "match_regex") { + auto regex = at(params, "regex"); + options = at(params, "options", options); - std::vector targets; - auto inputs = at(params, "inputs"); - targets.reserve(inputs.size()); - for (const auto &input_param : inputs) { - auto input = static_cast(input_param); - if (input.empty()) { - throw ddwaf::parsing_error("empty address"); - } + auto case_sensitive = at(options, "case_sensitive", false); + auto min_length = at(options, "min_length", 0); + if (min_length < 0) { + throw ddwaf::parsing_error("min_length is a negative number"); + } - std::string root; - std::string key_path; - size_t pos = input.find(':', 0); - if (pos == std::string::npos || pos + 1 >= input.size()) { - root = input; + processor = + std::make_shared(regex, min_length, case_sensitive); + } else if (operation == "is_xss") { + processor = std::make_shared(); + } else if (operation == "is_sqli") { + processor = std::make_shared(); } else { - root = input.substr(0, pos); - key_path = input.substr(pos + 1, input.size()); + throw ddwaf::parsing_error("unknown processor: " + std::string(operation)); } + builder.set_processor(std::move(processor)); + + auto inputs = at(params, "inputs"); + for (const auto &input_param : inputs) { + auto input = static_cast(input_param); + if (input.empty()) { + throw ddwaf::parsing_error("empty address"); + } - condition::target_type target; - target.root = get_target_index(root); - target.name = std::move(root); - if (!key_path.empty()) { - target.key_path.emplace_back(key_path); + std::string root; + std::vector key_path; + size_t pos = input.find(':', 0); + if (pos == std::string::npos || pos + 1 >= input.size()) { + root = input; + } else { + root = input.substr(0, pos); + key_path.emplace_back(input.substr(pos + 1, input.size())); + } + + builder.add_target(std::move(root), std::move(key_path), transformers); } - target.transformers = transformers; - targets.emplace_back(std::move(target)); } - return std::make_shared( - std::move(targets), std::move(processor), std::string{}, limits); + return builder.build(); } void parseRule(parameter::map &rule, base_section_info &info, @@ -129,13 +127,8 @@ void parseRule(parameter::map &rule, base_section_info &info, rule_transformers.emplace_back(id.value()); } - std::vector conditions; auto conditions_array = at(rule, "conditions"); - conditions.reserve(conditions_array.size()); - for (const auto &cond_param : conditions_array) { - auto cond = static_cast(cond_param); - conditions.push_back(parseCondition(cond, rule_transformers, limits)); - } + auto expression = parse_expression(conditions_array, rule_transformers, limits); std::unordered_map tags; for (auto &[key, value] : at(rule, "tags")) { @@ -151,7 +144,7 @@ void parseRule(parameter::map &rule, base_section_info &info, } auto rule_ptr = std::make_shared( - std::string(id), at(rule, "name"), std::move(tags), std::move(conditions)); + std::string(id), at(rule, "name"), std::move(tags), std::move(expression)); rule_ids.emplace(rule_ptr->get_id()); rs.insert_rule(rule_ptr); diff --git a/src/parser/parser_v2.cpp b/src/parser/parser_v2.cpp index cdba5c317..387cb104b 100644 --- a/src/parser/parser_v2.cpp +++ b/src/parser/parser_v2.cpp @@ -99,7 +99,7 @@ std::pair parse_processor( } std::vector parse_transformers( - const parameter::vector &root, condition::data_source &source) + const parameter::vector &root, expression::data_source &source) { if (root.empty()) { return {}; @@ -114,9 +114,9 @@ std::vector parse_transformers( if (id.has_value()) { transformers.emplace_back(id.value()); } else if (transformer == "keys_only") { - source = ddwaf::condition::data_source::keys; + source = ddwaf::expression::data_source::keys; } else if (transformer == "values_only") { - source = ddwaf::condition::data_source::values; + source = ddwaf::expression::data_source::values; } else { throw ddwaf::parsing_error("invalid transformer " + std::string(transformer)); } @@ -124,57 +124,61 @@ std::vector parse_transformers( return transformers; } -condition::ptr parse_rule_condition(const parameter::map &root, - std::unordered_map &rule_data_ids, condition::data_source source, +expression::ptr parse_expression(const parameter::vector &conditions_array, + std::unordered_map &rule_data_ids, expression::data_source source, const std::vector &transformers, const object_limits &limits) { - auto operation = at(root, "operator"); - auto params = at(root, "parameters"); + expression_builder builder(conditions_array.size(), limits); - auto [rule_data_id, processor] = parse_processor(operation, params); - if (!processor && !rule_data_id.empty()) { - rule_data_ids.emplace(rule_data_id, operation); - } - std::vector targets; - auto inputs = at(params, "inputs"); - if (inputs.empty()) { - throw ddwaf::parsing_error("empty inputs"); - } + for (const auto &cond_param : conditions_array) { + auto root = static_cast(cond_param); - for (const auto &input_param : inputs) { - auto input = static_cast(input_param); - auto address = at(input, "address"); + builder.start_condition(); - if (address.empty()) { - throw ddwaf::parsing_error("empty address"); + auto operation = at(root, "operator"); + auto params = at(root, "parameters"); + + auto [rule_data_id, processor] = parse_processor(operation, params); + builder.set_data_id(rule_data_id); + builder.set_processor(std::move(processor)); + if (!processor && !rule_data_id.empty()) { + rule_data_ids.emplace(rule_data_id, operation); } - auto kp = at>(input, "key_path", {}); - for (const auto &path : kp) { - if (path.empty()) { - throw ddwaf::parsing_error("empty key_path"); - } + std::vector targets; + auto inputs = at(params, "inputs"); + if (inputs.empty()) { + throw ddwaf::parsing_error("empty inputs"); } - condition::target_type target; - target.root = get_target_index(address); - target.name = address; - target.key_path = std::move(kp); + for (const auto &input_param : inputs) { + auto input = static_cast(input_param); + auto address = at(input, "address"); - auto it = input.find("transformers"); - if (it == input.end()) { - target.source = source; - target.transformers = transformers; - } else { - auto input_transformers = static_cast(it->second); - target.source = condition::data_source::values; - target.transformers = parse_transformers(input_transformers, target.source); + if (address.empty()) { + throw ddwaf::parsing_error("empty address"); + } + + auto kp = at>(input, "key_path", {}); + for (const auto &path : kp) { + if (path.empty()) { + throw ddwaf::parsing_error("empty key_path"); + } + } + + auto it = input.find("transformers"); + if (it == input.end()) { + builder.add_target(address, std::move(kp), transformers, source); + } else { + auto input_transformers = static_cast(it->second); + source = expression::data_source::values; + auto new_transformers = parse_transformers(input_transformers, source); + builder.add_target(address, std::move(kp), std::move(new_transformers), source); + } } - targets.emplace_back(target); } - return std::make_shared( - std::move(targets), std::move(processor), std::move(rule_data_id), limits); + return builder.build(); } rule_spec parse_rule(parameter::map &rule, @@ -182,19 +186,13 @@ rule_spec parse_rule(parameter::map &rule, rule::source_type source) { std::vector rule_transformers; - auto data_source = ddwaf::condition::data_source::values; + auto data_source = ddwaf::expression::data_source::values; auto transformers = at(rule, "transformers", {}); rule_transformers = parse_transformers(transformers, data_source); - std::vector conditions; auto conditions_array = at(rule, "conditions"); - conditions.reserve(conditions_array.size()); - - for (const auto &cond_param : conditions_array) { - auto cond = static_cast(cond_param); - conditions.push_back( - parse_rule_condition(cond, rule_data_ids, data_source, rule_transformers, limits)); - } + auto expression = + parse_expression(conditions_array, rule_data_ids, data_source, rule_transformers, limits); std::unordered_map tags; for (auto &[key, value] : at(rule, "tags")) { @@ -210,7 +208,7 @@ rule_spec parse_rule(parameter::map &rule, } return {at(rule, "enabled", true), source, at(rule, "name"), std::move(tags), - std::move(conditions), at>(rule, "on_match", {})}; + std::move(expression), at>(rule, "on_match", {})}; } rule_target_spec parse_rules_target(const parameter::map &target) @@ -312,7 +310,7 @@ condition::ptr parse_filter_condition(const parameter::map &root, const object_l target.root = get_target_index(address); target.name = address; target.key_path = std::move(key_path); - target.source = condition::data_source::values; + target.source = expression::data_source::values; auto it = input.find("transformers"); if (it != input.end()) { diff --git a/src/parser/specification.hpp b/src/parser/specification.hpp index acc0c6470..5c8657d75 100644 --- a/src/parser/specification.hpp +++ b/src/parser/specification.hpp @@ -22,7 +22,7 @@ struct rule_spec { rule::source_type source; std::string name; std::unordered_map tags; - std::vector conditions; + expression::ptr expr; std::vector actions; }; diff --git a/src/rule.cpp b/src/rule.cpp index f8485f8d2..e8264f229 100644 --- a/src/rule.cpp +++ b/src/rule.cpp @@ -21,45 +21,14 @@ std::optional rule::match(const object_store &store, cache_type &cache, ddwaf::timer &deadline) const { // An event was already produced, so we skip the rule - if (cache.result) { + if (cache.result || !expression_->eval(cache.expr_cache, store, objects_excluded, + dynamic_processors, deadline)) { return std::nullopt; } - // On the first run, go through the conditions. Stop either at the first - // condition that didn't match and return no event or go through all - // and return an event. - // On subsequent runs, we can start at the first condition that did not - // match, because if the conditions matched with the data of the first - // run, then they having new data will make them match again. The condition - // that failed (and stopped the processing), we can run it again, but only - // on the new data. The subsequent conditions, we need to run with all data. - std::vector::const_iterator cond_iter; - bool run_on_new; - if (cache.last_cond.has_value()) { - cond_iter = *cache.last_cond; - run_on_new = true; - } else { - cond_iter = conditions_.cbegin(); - run_on_new = false; - } - - while (cond_iter != conditions_.cend()) { - auto &&cond = *cond_iter; - auto opt_match = - cond->match(store, objects_excluded, run_on_new, dynamic_processors, deadline); - if (!opt_match.has_value()) { - cache.last_cond = cond_iter; - return std::nullopt; - } - cache.matches.emplace_back(std::move(*opt_match)); - - run_on_new = false; - cond_iter++; - } - cache.result = true; - ddwaf::event evt{this, std::move(cache.matches)}; + ddwaf::event evt{this, expression_->get_matches(cache.expr_cache)}; return {std::move(evt)}; } diff --git a/src/rule.hpp b/src/rule.hpp index a236abf7f..0317c2156 100644 --- a/src/rule.hpp +++ b/src/rule.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -29,15 +30,14 @@ class rule { struct cache_type { bool result{false}; - memory::vector matches; - std::optional::const_iterator> last_cond{}; + expression::cache_type expr_cache; }; rule(std::string id, std::string name, std::unordered_map tags, - std::vector conditions, std::vector actions = {}, - bool enabled = true, source_type source = source_type::base) + expression::ptr expr, std::vector actions = {}, bool enabled = true, + source_type source = source_type::base) : enabled_(enabled), source_(source), id_(std::move(id)), name_(std::move(name)), - tags_(std::move(tags)), conditions_(std::move(conditions)), actions_(std::move(actions)) + tags_(std::move(tags)), expression_(std::move(expr)), actions_(std::move(actions)) {} rule(const rule &) = delete; @@ -46,7 +46,7 @@ class rule { rule(rule &&rhs) noexcept : enabled_(rhs.enabled_), source_(rhs.source_), id_(std::move(rhs.id_)), name_(std::move(rhs.name_)), tags_(std::move(rhs.tags_)), - conditions_(std::move(rhs.conditions_)), actions_(std::move(rhs.actions_)) + expression_(std::move(rhs.expression_)), actions_(std::move(rhs.actions_)) {} rule &operator=(rule &&rhs) noexcept @@ -56,7 +56,7 @@ class rule { id_ = std::move(rhs.id_); name_ = std::move(rhs.name_); tags_ = std::move(rhs.tags_); - conditions_ = std::move(rhs.conditions_); + expression_ = std::move(rhs.expression_); actions_ = std::move(rhs.actions_); return *this; } @@ -87,9 +87,7 @@ class rule { void get_addresses(std::unordered_set &addresses) const { - for (const auto &cond : conditions_) { - for (const auto &target : cond->get_targets()) { addresses.emplace(target.name); } - } + return expression_->get_addresses(addresses); } void set_actions(std::vector new_actions) { actions_ = std::move(new_actions); } @@ -100,7 +98,7 @@ class rule { std::string id_; std::string name_; std::unordered_map tags_; - std::vector conditions_; + expression::ptr expression_; std::vector actions_; }; diff --git a/src/ruleset_builder.cpp b/src/ruleset_builder.cpp index d5619d8a6..c7f7aecd2 100644 --- a/src/ruleset_builder.cpp +++ b/src/ruleset_builder.cpp @@ -82,7 +82,7 @@ std::shared_ptr ruleset_builder::build(parameter::map &root, base_rules // Initially, new rules are generated from their spec for (const auto &[id, spec] : base_rules_) { auto rule_ptr = std::make_shared( - id, spec.name, spec.tags, spec.conditions, spec.actions, spec.enabled, spec.source); + id, spec.name, spec.tags, spec.expr, spec.actions, spec.enabled, spec.source); // The string_view should be owned by the rule_ptr final_base_rules_.emplace(rule_ptr->get_id(), rule_ptr); @@ -125,7 +125,7 @@ std::shared_ptr ruleset_builder::build(parameter::map &root, base_rules // Initially, new rules are generated from their spec for (const auto &[id, spec] : user_rules_) { auto rule_ptr = std::make_shared( - id, spec.name, spec.tags, spec.conditions, spec.actions, spec.enabled, spec.source); + id, spec.name, spec.tags, spec.expr, spec.actions, spec.enabled, spec.source); // The string_view should be owned by the rule_ptr final_user_rules_.emplace(rule_ptr->get_id(), rule_ptr); diff --git a/tests/collection_test.cpp b/tests/collection_test.cpp index 4bee7ae66..11fa930ba 100644 --- a/tests/collection_test.cpp +++ b/tests/collection_test.cpp @@ -17,19 +17,13 @@ TYPED_TEST_SUITE(TestCollection, CollectionTypes); // Validate that a rule within the collection matches only once TYPED_TEST(TestCollection, SingleRuleMatch) { - std::vector targets; - - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); TypeParam rule_collection; rule_collection.insert(rule); @@ -72,39 +66,29 @@ TYPED_TEST(TestCollection, MultipleRuleCachedMatch) std::vector rules; TypeParam rule_collection; { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); rules.emplace_back(rule); rule_collection.insert(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); rules.emplace_back(rule); rule_collection.insert(rule); @@ -149,37 +133,29 @@ TYPED_TEST(TestCollection, MultipleRuleFailAndMatch) std::vector rules; TypeParam rule_collection; { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); rules.emplace_back(rule); rule_collection.insert(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); rules.emplace_back(rule); rule_collection.insert(rule); @@ -221,28 +197,16 @@ TYPED_TEST(TestCollection, MultipleRuleFailAndMatch) // Validate that the rule cache is acted on TYPED_TEST(TestCollection, SingleRuleMultipleCalls) { - std::vector conditions; - { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - conditions.emplace_back(std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"}))); - } - - { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); + expression_builder builder(2); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); - conditions.emplace_back(std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"}))); - } + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); TypeParam rule_collection; rule_collection.insert(rule); @@ -288,38 +252,30 @@ TEST(TestPriorityCollection, NoRegularMatchAfterPriorityMatch) collection regular; priority_collection priority; { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); rules.emplace_back(rule); regular.insert(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"redirect"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"redirect"}); rules.emplace_back(rule); priority.insert(rule); @@ -367,38 +323,30 @@ TEST(TestPriorityCollection, PriorityMatchAfterRegularMatch) collection regular; priority_collection priority; { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); rules.emplace_back(rule); regular.insert(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"redirect"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"redirect"}); rules.emplace_back(rule); priority.insert(rule); @@ -446,38 +394,31 @@ TEST(TestPriorityCollection, NoPriorityMatchAfterPriorityMatch) std::vector rules; priority_collection priority; { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared("id1", "name1", std::move(tags), - std::move(conditions), std::vector{"block"}); + + auto rule = std::make_shared( + "id1", "name1", std::move(tags), builder.build(), std::vector{"block"}); rules.emplace_back(rule); priority.insert(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"redirect"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"redirect"}); rules.emplace_back(rule); priority.insert(rule); diff --git a/tests/condition_test.cpp b/tests/condition_test.cpp index 2a338b9c6..83b925fed 100644 --- a/tests/condition_test.cpp +++ b/tests/condition_test.cpp @@ -140,7 +140,7 @@ TEST(TestCondition, MatchOnKeys) std::vector targets; targets.push_back({get_target_index("server.request.query"), "server.request.query", {}, {}, - condition::data_source::keys}); + expression::data_source::keys}); auto cond = std::make_shared( std::move(targets), std::make_unique("value", 0, true)); @@ -175,7 +175,7 @@ TEST(TestCondition, MatchOnKeysWithTransformer) std::vector targets; targets.push_back({get_target_index("server.request.query"), "server.request.query", {}, - {transformer_id::lowercase}, condition::data_source::keys}); + {transformer_id::lowercase}, expression::data_source::keys}); auto cond = std::make_shared( std::move(targets), std::make_unique("value", 0, true)); diff --git a/tests/context_test.cpp b/tests/context_test.cpp index a085592f7..c34e592de 100644 --- a/tests/context_test.cpp +++ b/tests/context_test.cpp @@ -22,19 +22,13 @@ class context : public ddwaf::context { TEST(TestContext, MatchTimeout) { - std::vector targets; - - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); auto ruleset = std::make_shared(); ruleset->insert_rule(rule); @@ -53,19 +47,13 @@ TEST(TestContext, MatchTimeout) TEST(TestContext, NoMatch) { - std::vector targets; - - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); auto ruleset = std::make_shared(); ruleset->insert_rule(rule); @@ -85,19 +73,13 @@ TEST(TestContext, NoMatch) TEST(TestContext, Match) { - std::vector targets; - - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); auto ruleset = std::make_shared(); ruleset->insert_rule(rule); @@ -119,38 +101,28 @@ TEST(TestContext, MatchMultipleRulesInCollectionSingleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -190,39 +162,29 @@ TEST(TestContext, MatchMultipleRulesWithPrioritySingleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - // This rule has actions, so it'll be have priority - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"block"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"block"}); ruleset->insert_rule(rule); } @@ -272,38 +234,28 @@ TEST(TestContext, MatchMultipleRulesInCollectionDoubleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -355,38 +307,29 @@ TEST(TestContext, MatchMultipleRulesWithPriorityDoubleRunPriorityLast) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"block"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"block"}); ruleset->insert_rule(rule); } @@ -458,38 +401,29 @@ TEST(TestContext, MatchMultipleRulesWithPriorityDoubleRunPriorityFirst) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared("id1", "name1", std::move(tags), - std::move(conditions), std::vector{"block"}); + auto rule = std::make_shared( + "id1", "name1", std::move(tags), builder.build(), std::vector{"block"}); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -543,38 +477,29 @@ TEST(TestContext, MatchMultipleRulesWithPriorityUntilAllActionsMet) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"redirect"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"redirect"}); ruleset->insert_rule(rule); } @@ -644,38 +569,28 @@ TEST(TestContext, MatchMultipleCollectionsSingleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type1"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type2"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -698,38 +613,30 @@ TEST(TestContext, MatchMultiplePriorityCollectionsSingleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type1"}, {"category", "category1"}}; - auto rule = std::make_shared("id1", "name1", std::move(tags), - std::move(conditions), std::vector{"block"}); + auto rule = std::make_shared( + "id1", "name1", std::move(tags), builder.build(), std::vector{"block"}); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type2"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"redirect"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"redirect"}); ruleset->insert_rule(rule); } @@ -752,38 +659,28 @@ TEST(TestContext, MatchMultipleCollectionsDoubleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type1"}, {"category", "category1"}}; - auto rule = std::make_shared( - "id1", "name1", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id1", "name1", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type2"}, {"category", "category2"}}; - auto rule = std::make_shared( - "id2", "name2", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id2", "name2", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -818,38 +715,30 @@ TEST(TestContext, MatchMultiplePriorityCollectionsDoubleRun) { auto ruleset = std::make_shared(); { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type1"}, {"category", "category1"}}; - auto rule = std::make_shared("id1", "name1", std::move(tags), - std::move(conditions), std::vector{"block"}); + auto rule = std::make_shared( + "id1", "name1", std::move(tags), builder.build(), std::vector{"block"}); ruleset->insert_rule(rule); } { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type2"}, {"category", "category2"}}; - auto rule = std::make_shared("id2", "name2", std::move(tags), - std::move(conditions), std::vector{"redirect"}); + auto rule = std::make_shared( + "id2", "name2", std::move(tags), builder.build(), std::vector{"redirect"}); ruleset->insert_rule(rule); } @@ -887,19 +776,14 @@ TEST(TestContext, RuleFilterWithCondition) // Generate rule ddwaf::rule::ptr rule; { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category"}}; - rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -945,19 +829,14 @@ TEST(TestContext, RuleFilterTimeout) // Generate rule ddwaf::rule::ptr rule; { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category"}}; - rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -998,19 +877,14 @@ TEST(TestContext, NoRuleFilterWithCondition) // Generate rule ddwaf::rule::ptr rule; { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{ {"type", "type"}, {"category", "category"}}; - rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -1062,7 +936,7 @@ TEST(TestContext, MultipleRuleFiltersNonOverlappingRules) {"type", "type"}, {"category", "category"}}; rules.emplace_back(std::make_shared("id" + std::to_string(i), "name", - std::move(tags), std::vector{}, std::vector{})); + std::move(tags), expression::ptr{}, std::vector{})); ruleset->insert_rule(rules.back()); } @@ -1136,7 +1010,7 @@ TEST(TestContext, MultipleRuleFiltersOverlappingRules) {"type", "type"}, {"category", "category"}}; rules.emplace_back(std::make_shared(std::string(id), "name", std::move(tags), - std::vector{}, std::vector{})); + expression::ptr{}, std::vector{})); ruleset->insert_rule(rules.back()); } @@ -1246,7 +1120,7 @@ TEST(TestContext, MultipleRuleFiltersNonOverlappingRulesWithConditions) {"type", "type"}, {"category", "category"}}; rules.emplace_back(std::make_shared(std::string(id), "name", std::move(tags), - std::vector{}, std::vector{})); + expression::ptr{}, std::vector{})); ruleset->insert_rule(rules.back()); } @@ -1338,7 +1212,7 @@ TEST(TestContext, MultipleRuleFiltersOverlappingRulesWithConditions) {"type", "type"}, {"category", "category"}}; rules.emplace_back(std::make_shared(std::string(id), "name", std::move(tags), - std::vector{}, std::vector{})); + expression::ptr{}, std::vector{})); ruleset->insert_rule(rules.back()); } @@ -1419,21 +1293,16 @@ TEST(TestContext, MultipleRuleFiltersOverlappingRulesWithConditions) TEST(TestContext, InputFilterExclude) { - condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; - - std::vector targets{client_ip}; - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); auto obj_filter = std::make_shared(); - obj_filter->insert(client_ip.root, client_ip.name); + obj_filter->insert(get_target_index("http.client_ip"), "http.client_ip"); std::vector filter_conditions; std::set filter_rules{rule.get()}; @@ -1461,21 +1330,16 @@ TEST(TestContext, InputFilterExclude) TEST(TestContext, InputFilterExcludeRule) { - condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; - - std::vector targets{client_ip}; - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); auto obj_filter = std::make_shared(); - obj_filter->insert(client_ip.root, client_ip.name); + obj_filter->insert(get_target_index("http.client_ip"), "http.client_ip"); std::vector filter_conditions; std::set filter_rules{rule.get()}; @@ -1506,28 +1370,25 @@ TEST(TestContext, InputFilterExcludeRule) TEST(TestContext, InputFilterWithCondition) { - condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; - condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; - auto ruleset = std::make_shared(); { - std::vector> conditions; - std::vector targets{client_ip}; - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "type"}, {"category", "category"}}; - auto rule = std::make_shared( - "id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { + condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; + condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; + auto obj_filter = std::make_shared(); obj_filter->insert(client_ip.root, client_ip.name); @@ -1600,44 +1461,40 @@ TEST(TestContext, InputFilterWithCondition) TEST(TestContext, InputFilterMultipleRules) { - condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; - condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; - auto ruleset = std::make_shared(); { - std::vector> conditions; - std::vector targets{client_ip}; - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "ip_type"}, {"category", "category"}}; - auto rule = std::make_shared( - "ip_id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = + std::make_shared("ip_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector> conditions; - std::vector targets{usr_id}; - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr_id"); std::unordered_map tags{ {"type", "usr_type"}, {"category", "category"}}; - auto rule = std::make_shared( - "usr_id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = + std::make_shared("usr_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { + condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; + condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; + auto obj_filter = std::make_shared(); obj_filter->insert(client_ip.root, client_ip.name); obj_filter->insert(usr_id.root, usr_id.name); @@ -1712,44 +1569,39 @@ TEST(TestContext, InputFilterMultipleRules) TEST(TestContext, InputFilterMultipleRulesMultipleFilters) { - condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; - condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; - auto ruleset = std::make_shared(); { - std::vector> conditions; - std::vector targets{client_ip}; - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "ip_type"}, {"category", "category"}}; - auto rule = std::make_shared( - "ip_id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = + std::make_shared("ip_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector> conditions; - std::vector targets{usr_id}; - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr_id"); std::unordered_map tags{ {"type", "usr_type"}, {"category", "category"}}; - auto rule = std::make_shared( - "usr_id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = + std::make_shared("usr_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { + condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; + auto obj_filter = std::make_shared(); obj_filter->insert(client_ip.root, client_ip.name); @@ -1762,6 +1614,8 @@ TEST(TestContext, InputFilterMultipleRulesMultipleFilters) } { + condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; + auto obj_filter = std::make_shared(); obj_filter->insert(usr_id.root, usr_id.name); @@ -1835,57 +1689,46 @@ TEST(TestContext, InputFilterMultipleRulesMultipleFilters) TEST(TestContext, InputFilterMultipleRulesMultipleFiltersMultipleObjects) { - condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; - condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; - condition::target_type cookie_header{ - get_target_index("server.request.headers"), "server.request.headers", {"cookie"}}; - auto ruleset = std::make_shared(); { - std::vector> conditions; - std::vector targets{client_ip}; - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition( + std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{ {"type", "ip_type"}, {"category", "category"}}; - auto rule = std::make_shared( - "ip_id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = + std::make_shared("ip_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector> conditions; - std::vector targets{usr_id}; - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr_id"); std::unordered_map tags{ {"type", "usr_type"}, {"category", "category"}}; - auto rule = std::make_shared( - "usr_id", "name", std::move(tags), std::move(conditions), std::vector{}); + auto rule = + std::make_shared("usr_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } { - std::vector> conditions; - std::vector targets{cookie_header}; - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"mycookie"})); - conditions.emplace_back(std::move(cond)); + expression_builder builder(1); + builder.start_condition(std::vector{"mycookie"}); + builder.add_target("server.request.headers", {"cookie"}); std::unordered_map tags{ {"type", "cookie_type"}, {"category", "category"}}; - auto rule = std::make_shared("cookie_id", "name", std::move(tags), - std::move(conditions), std::vector{}); + auto rule = + std::make_shared("cookie_id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); } @@ -1893,6 +1736,11 @@ TEST(TestContext, InputFilterMultipleRulesMultipleFiltersMultipleObjects) auto ip_rule = ruleset->rules[0]; auto usr_rule = ruleset->rules[1]; auto cookie_rule = ruleset->rules[2]; + condition::target_type client_ip{get_target_index("http.client_ip"), "http.client_ip", {}}; + condition::target_type usr_id{get_target_index("usr.id"), "usr.id", {}}; + condition::target_type cookie_header{ + get_target_index("server.request.headers"), "server.request.headers", {"cookie"}}; + { auto obj_filter = std::make_shared(); obj_filter->insert(client_ip.root, client_ip.name); diff --git a/tests/expression_test.cpp b/tests/expression_test.cpp new file mode 100644 index 000000000..efb58ad59 --- /dev/null +++ b/tests/expression_test.cpp @@ -0,0 +1,775 @@ +// Unless explicitly stated otherwise all files in this repository are +// dual-licensed under the Apache-2.0 License or BSD-3-Clause License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2021 Datadog, Inc. + +#include "expression.hpp" +#include "test.h" + +using namespace ddwaf; + +TEST(TestExpression, SimpleMatch) +{ + expression_builder builder(1); + builder.start_condition(".*", 0, true); + builder.add_target("server.request.query"); + + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = ".*", + .address = "server.request.query", + .path = {}, + .value = "value", + .highlight = "value"}); +} + +TEST(TestExpression, MultiInputMatchOnSecond) +{ + expression_builder builder(1); + builder.start_condition("^value$", 0, true); + builder.add_target("server.request.query"); + builder.add_target("server.request.body"); + + auto expr = builder.build(); + + ddwaf::object_store store; + expression::cache_type cache; + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "bad")); + + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.body", ddwaf_object_string(&tmp, "value")); + + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = "^value$", + .address = "server.request.body", + .path = {}, + .value = "value", + .highlight = "value"}); + } +} + +TEST(TestExpression, DuplicateInput) +{ + expression_builder builder(1); + builder.start_condition("^value$", 0, true); + builder.add_target("server.request.query"); + + auto expr = builder.build(); + + expression::cache_type cache; + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "bad")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + } +} + +TEST(TestExpression, MatchDuplicateInputNoCache) +{ + expression_builder builder(1); + builder.start_condition("^value$", 0, true); + builder.add_target("server.request.query"); + + auto expr = builder.build(); + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "bad")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = "^value$", + .address = "server.request.query", + .path = {}, + .value = "value", + .highlight = "value"}); + } +} + +TEST(TestExpression, TwoConditionsSingleInputNoMatch) +{ + expression_builder builder(2); + + builder.start_condition("value", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^value$", 0, true); + builder.add_target("server.request.query"); + + auto expr = builder.build(); + + expression::cache_type cache; + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "bad_value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + } +} + +TEST(TestExpression, TwoConditionsSingleInputMatch) +{ + expression_builder builder(2); + + builder.start_condition("value", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^value$", 0, true); + builder.add_target("server.request.query"); + + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); +} + +TEST(TestExpression, TwoConditionsMultiInputSingleEvalMatch) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("body", 0, true); + builder.add_target("server.request.body"); + + auto expr = builder.build(); + + ddwaf::object_store store; + expression::cache_type cache; + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "query")); + ddwaf_object_map_add(&root, "server.request.body", ddwaf_object_string(&tmp, "body")); + + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); +} + +TEST(TestExpression, TwoConditionsMultiInputMultiEvalMatch) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("body", 0, true); + builder.add_target("server.request.body"); + + auto expr = builder.build(); + + ddwaf::object_store store; + expression::cache_type cache; + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "query")); + + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.body", ddwaf_object_string(&tmp, "body")); + ddwaf_object_map_add( + &root, "server.request.query", ddwaf_object_string(&tmp, "red-herring")); + + store.insert(root); + + ddwaf::timer deadline{2s}; + + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + } +} + +TEST(TestExpression, SingleObjectChain) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^thermometer$", 0, true); + builder.add_target("match.0.object"); + + auto expr = builder.build(); + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add( + &root, "server.request.query", ddwaf_object_string(&tmp, "some query")); + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object query; + ddwaf_object_map(&query); + ddwaf_object_map_add(&query, "value1", ddwaf_object_string(&tmp, "some query")); + ddwaf_object_map_add(&query, "value2", ddwaf_object_string(&tmp, "thermometer")); + + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &query); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + } +} + +TEST(TestExpression, SingleScalarChain) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^query$", 0, true); + builder.add_target("match.0.scalar"); + + auto expr = builder.build(); + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object query; + + ddwaf_object_map(&query); + ddwaf_object_map_add(&query, "value1", ddwaf_object_string(&tmp, "some query")); + + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &query); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object query; + + ddwaf_object_map(&query); + ddwaf_object_map_add(&query, "value2", ddwaf_object_string(&tmp, "some query")); + ddwaf_object_map_add(&query, "value1", ddwaf_object_string(&tmp, "query")); + + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &query); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, + {.op = "match_regex", + .op_value = "query", + .address = "server.request.query", + .path = {"value1"}, + .value = "query", + .highlight = "query"}, + {.op = "match_regex", + .op_value = "^query$", + .address = "match.0.scalar", + .path = {}, + .value = "query", + .highlight = "query"}); + } +} + +TEST(TestExpression, SingleHighlightChain) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^query$", 0, true); + builder.add_target("match.0.highlight"); + + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "some query")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, + {.op = "match_regex", + .op_value = "query", + .address = "server.request.query", + .path = {}, + .value = "some query", + .highlight = "query"}, + {.op = "match_regex", + .op_value = "^query$", + .address = "match.0.highlight", + .path = {}, + .value = "query", + .highlight = "query"}); +} + +TEST(TestExpression, HybridScalarChain) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^query$", 0, true); + builder.add_target("match.0.scalar"); + builder.add_target("server.request.body"); + + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "some query")); + ddwaf_object_map_add(&root, "server.request.body", ddwaf_object_string(&tmp, "query")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, + {.op = "match_regex", + .op_value = "query", + .address = "server.request.query", + .path = {}, + .value = "some query", + .highlight = "query"}, + {.op = "match_regex", + .op_value = "^query$", + .address = "server.request.body", + .path = {}, + .value = "query", + .highlight = "query"}); +} + +TEST(TestExpression, HybridObjectChain) +{ + expression_builder builder(2); + + builder.start_condition("query", 0, true); + builder.add_target("server.request.query"); + + builder.start_condition("^thermometer$", 0, true); + builder.add_target("match.0.object"); + builder.add_target("server.request.body"); + + auto expr = builder.build(); + + { + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add( + &root, "server.request.query", ddwaf_object_string(&tmp, "some query")); + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_FALSE(expr->eval(cache, store, {}, {}, deadline)); + } + + { + ddwaf_object root; + ddwaf_object tmp; + + ddwaf_object_map(&root); + ddwaf_object_map_add( + &root, "server.request.query", ddwaf_object_string(&tmp, "some query")); + ddwaf_object_map_add( + &root, "server.request.body", ddwaf_object_string(&tmp, "thermometer")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, + {.op = "match_regex", + .op_value = "query", + .address = "server.request.query", + .path = {}, + .value = "some query", + .highlight = "query"}, + {.op = "match_regex", + .op_value = "^thermometer$", + .address = "server.request.body", + .path = {}, + .value = "thermometer", + .highlight = "thermometer"}); + } +} + +TEST(TestExpression, MatchWithKeyPath) +{ + expression_builder builder(1); + builder.start_condition(".*", 0, true); + builder.add_target("server.request.query", {"key"}); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object submap; + ddwaf_object tmp; + ddwaf_object_map(&submap); + ddwaf_object_map_add(&submap, "key", ddwaf_object_string(&tmp, "value")); + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &submap); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = ".*", + .address = "server.request.query", + .path = {"key"}, + .value = "value", + .highlight = "value"}); +} + +TEST(TestExpression, MatchWithTransformer) +{ + expression_builder builder(1); + builder.start_condition("value", 0, true); + builder.add_target("server.request.query", {}, {transformer_id::lowercase}); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "VALUE")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = "value", + .address = "server.request.query", + .path = {}, + .value = "value", + .highlight = "value"}); +} + +TEST(TestExpression, MatchWithMultipleTransformers) +{ + expression_builder builder(1); + builder.start_condition("^ value $", 0, true); + builder.add_target("server.request.query", {}, + {transformer_id::compress_whitespace, transformer_id::lowercase}); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, " VALUE ")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = "^ value $", + .address = "server.request.query", + .path = {}, + .value = " value ", + .highlight = " value "}); +} + +TEST(TestExpression, MatchOnKeys) +{ + expression_builder builder(1); + builder.start_condition("value", 0, true); + builder.add_target("server.request.query", {}, {}, expression::data_source::keys); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object value; + ddwaf_object_map(&value); + ddwaf_object_map_add(&value, "value", ddwaf_object_string(&tmp, "1729")); + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &value); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = "value", + .address = "server.request.query", + .path = {"value"}, + .value = "value", + .highlight = "value"}); +} + +TEST(TestExpression, MatchOnKeysWithTransformer) +{ + expression_builder builder(1); + builder.start_condition("value", 0, true); + builder.add_target( + "server.request.query", {}, {transformer_id::lowercase}, expression::data_source::keys); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object value; + ddwaf_object_map(&value); + ddwaf_object_map_add(&value, "VALUE", ddwaf_object_string(&tmp, "1729")); + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &value); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_TRUE(expr->eval(cache, store, {}, {}, deadline)); + auto matches = expr->get_matches(cache); + EXPECT_MATCHES(matches, {.op = "match_regex", + .op_value = "value", + .address = "server.request.query", + .path = {"VALUE"}, + .value = "value", + .highlight = "value"}); +} + +TEST(TestExpression, ExcludeInput) +{ + expression_builder builder(1); + builder.start_condition(".*", 0, true); + builder.add_target("server.request.query"); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object tmp; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_string(&tmp, "value")); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_FALSE(expr->eval(cache, store, {&root.array[0]}, {}, deadline)); +} + +TEST(TestExpression, ExcludeKeyPath) +{ + expression_builder builder(1); + builder.start_condition(".*", 0, true); + builder.add_target("server.request.query"); + auto expr = builder.build(); + + ddwaf_object root; + ddwaf_object map; + ddwaf_object tmp; + ddwaf_object_map(&map); + ddwaf_object_map_add(&map, "key", ddwaf_object_string(&tmp, "value")); + + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", &map); + + ddwaf::object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + + expression::cache_type cache; + EXPECT_FALSE(expr->eval(cache, store, {&map.array[0]}, {}, deadline)); +} diff --git a/tests/mkmap_test.cpp b/tests/mkmap_test.cpp index 7a8221712..b7b6b2468 100644 --- a/tests/mkmap_test.cpp +++ b/tests/mkmap_test.cpp @@ -33,7 +33,7 @@ TEST(TestMultiKeyMap, Find) tags.emplace("category", spec.category); auto rule_ptr = std::make_shared( - std::string(spec.id), "name", decltype(tags)(tags), std::vector{}); + std::string(spec.id), "name", decltype(tags)(tags), expression::ptr{}); rules.emplace_back(rule_ptr); ruledb.insert(rule_ptr->get_tags(), rule_ptr.get()); } @@ -98,7 +98,7 @@ TEST(TestMultiKeyMap, Multifind) tags.emplace("category", spec.category); auto rule_ptr = std::make_shared( - std::string(spec.id), "name", decltype(tags)(tags), std::vector{}); + std::string(spec.id), "name", decltype(tags)(tags), expression::ptr{}); rules.emplace_back(rule_ptr); ruledb.insert(rule_ptr->get_tags(), rule_ptr.get()); } diff --git a/tests/parser_v2_rules_test.cpp b/tests/parser_v2_rules_test.cpp index 5d01f8133..d195dfe9c 100644 --- a/tests/parser_v2_rules_test.cpp +++ b/tests/parser_v2_rules_test.cpp @@ -43,7 +43,7 @@ TEST(TestParserV2Rules, ParseRule) parser::rule_spec &rule = rules["1"]; EXPECT_TRUE(rule.enabled); - EXPECT_EQ(rule.conditions.size(), 3); + EXPECT_EQ(rule.expr->get_num_conditions(), 3); EXPECT_EQ(rule.actions.size(), 0); EXPECT_STR(rule.name, "rule1"); EXPECT_EQ(rule.tags.size(), 2); @@ -215,7 +215,7 @@ TEST(TestParserV2Rules, ParseMultipleRules) { parser::rule_spec &rule = rules["1"]; EXPECT_TRUE(rule.enabled); - EXPECT_EQ(rule.conditions.size(), 3); + EXPECT_EQ(rule.expr->get_num_conditions(), 3); EXPECT_EQ(rule.actions.size(), 0); EXPECT_STR(rule.name, "rule1"); EXPECT_EQ(rule.tags.size(), 2); @@ -226,7 +226,7 @@ TEST(TestParserV2Rules, ParseMultipleRules) { parser::rule_spec &rule = rules["secondrule"]; EXPECT_TRUE(rule.enabled); - EXPECT_EQ(rule.conditions.size(), 1); + EXPECT_EQ(rule.expr->get_num_conditions(), 1); EXPECT_EQ(rule.actions.size(), 1); EXPECT_STR(rule.actions[0], "block"); EXPECT_STR(rule.name, "rule2"); @@ -285,7 +285,7 @@ TEST(TestParserV2Rules, ParseMultipleRulesOneInvalid) { parser::rule_spec &rule = rules["1"]; EXPECT_TRUE(rule.enabled); - EXPECT_EQ(rule.conditions.size(), 3); + EXPECT_EQ(rule.expr->get_num_conditions(), 3); EXPECT_EQ(rule.actions.size(), 0); EXPECT_STR(rule.name, "rule1"); EXPECT_EQ(rule.tags.size(), 2); @@ -296,7 +296,7 @@ TEST(TestParserV2Rules, ParseMultipleRulesOneInvalid) { parser::rule_spec &rule = rules["secondrule"]; EXPECT_TRUE(rule.enabled); - EXPECT_EQ(rule.conditions.size(), 1); + EXPECT_EQ(rule.expr->get_num_conditions(), 1); EXPECT_EQ(rule.actions.size(), 1); EXPECT_STR(rule.actions[0], "block"); EXPECT_STR(rule.name, "rule2"); @@ -354,7 +354,7 @@ TEST(TestParserV2Rules, ParseMultipleRulesOneDuplicate) { parser::rule_spec &rule = rules["1"]; EXPECT_TRUE(rule.enabled); - EXPECT_EQ(rule.conditions.size(), 3); + EXPECT_EQ(rule.expr->get_num_conditions(), 3); EXPECT_EQ(rule.actions.size(), 0); EXPECT_STR(rule.name, "rule1"); EXPECT_EQ(rule.tags.size(), 2); diff --git a/tests/rule_test.cpp b/tests/rule_test.cpp index e92d922bd..1a786fe25 100644 --- a/tests/rule_test.cpp +++ b/tests/rule_test.cpp @@ -10,20 +10,16 @@ using namespace ddwaf; TEST(TestRule, Match) { - std::vector targets; - - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); - - std::vector> conditions{std::move(cond)}; + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; ddwaf::rule rule( - "id", "name", std::move(tags), std::move(conditions), {"update", "block", "passlist"}); + "id", "name", std::move(tags), builder.build(), {"update", "block", "passlist"}); - ddwaf_object root, tmp; + ddwaf_object root; + ddwaf_object tmp; ddwaf_object_map(&root); ddwaf_object_map_add(&root, "http.client_ip", ddwaf_object_string(&tmp, "192.168.0.1")); @@ -55,18 +51,15 @@ TEST(TestRule, Match) TEST(TestRule, NoMatch) { - std::vector targets; + expression_builder builder(1); + builder.start_condition(std::vector{}); + builder.add_target("http.client_ip"); - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{})); - - std::vector> conditions{std::move(cond)}; std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - ddwaf::rule rule("id", "name", std::move(tags), std::move(conditions)); + ddwaf::rule rule("id", "name", std::move(tags), builder.build()); - ddwaf_object root, tmp; + ddwaf_object root; + ddwaf_object tmp; ddwaf_object_map(&root); ddwaf_object_map_add(&root, "http.client_ip", ddwaf_object_string(&tmp, "192.168.0.1")); @@ -82,35 +75,24 @@ TEST(TestRule, NoMatch) TEST(TestRule, ValidateCachedMatch) { - std::vector> conditions; - - { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.push_back(std::move(cond)); - } + expression_builder builder(2); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); - { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.push_back(std::move(cond)); - } + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - ddwaf::rule rule("id", "name", std::move(tags), std::move(conditions)); + ddwaf::rule rule("id", "name", std::move(tags), builder.build()); ddwaf::rule::cache_type cache; // To validate that the cache works, we pass an object store containing // only the latest address. This ensures that the IP condition can't be // matched on the second run. { - ddwaf_object root, tmp; + ddwaf_object root; + ddwaf_object tmp; ddwaf_object_map(&root); ddwaf_object_map_add(&root, "http.client_ip", ddwaf_object_string(&tmp, "192.168.0.1")); @@ -123,7 +105,8 @@ TEST(TestRule, ValidateCachedMatch) } { - ddwaf_object root, tmp; + ddwaf_object root; + ddwaf_object tmp; ddwaf_object_map(&root); ddwaf_object_map_add(&root, "usr.id", ddwaf_object_string(&tmp, "admin")); @@ -163,28 +146,16 @@ TEST(TestRule, ValidateCachedMatch) TEST(TestRule, MatchWithoutCache) { - std::vector> conditions; + expression_builder builder(2); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); - { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.push_back(std::move(cond)); - } - - { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.push_back(std::move(cond)); - } + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - ddwaf::rule rule("id", "name", std::move(tags), std::move(conditions)); + ddwaf::rule rule("id", "name", std::move(tags), builder.build()); // In this instance we pass a complete store with both addresses but an // empty cache on every run to ensure that both conditions are matched on @@ -238,28 +209,16 @@ TEST(TestRule, MatchWithoutCache) TEST(TestRule, NoMatchWithoutCache) { - std::vector> conditions; - - { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.push_back(std::move(cond)); - } + expression_builder builder(2); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); - { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.push_back(std::move(cond)); - } + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - ddwaf::rule rule("id", "name", std::move(tags), std::move(conditions)); + ddwaf::rule rule("id", "name", std::move(tags), builder.build()); // In this test we validate that when the cache is empty and only one // address is passed, the filter doesn't match (as it should be). @@ -294,28 +253,16 @@ TEST(TestRule, NoMatchWithoutCache) TEST(TestRule, FullCachedMatchSecondRun) { - std::vector> conditions; + expression_builder builder(2); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); - { - std::vector targets; - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - auto cond = std::make_shared( - std::move(targets), std::make_unique( - std::vector{"192.168.0.1"})); - conditions.push_back(std::move(cond)); - } - - { - std::vector targets; - targets.push_back({get_target_index("usr.id"), "usr.id", {}, {}}); - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"admin"})); - conditions.push_back(std::move(cond)); - } + builder.start_condition(std::vector{"admin"}); + builder.add_target("usr.id"); std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - ddwaf::rule rule("id", "name", std::move(tags), std::move(conditions)); + ddwaf::rule rule("id", "name", std::move(tags), builder.build()); // In this test we validate that when a match has already occurred, the // second run for the same rule returns no events regardless of input. @@ -352,18 +299,14 @@ TEST(TestRule, FullCachedMatchSecondRun) TEST(TestRule, ExcludeObject) { - std::vector targets; - - targets.push_back({get_target_index("http.client_ip"), "http.client_ip", {}, {}}); - - auto cond = std::make_shared(std::move(targets), - std::make_unique(std::vector{"192.168.0.1"})); + expression_builder builder(1); + builder.start_condition(std::vector{"192.168.0.1"}); + builder.add_target("http.client_ip"); - std::vector> conditions{std::move(cond)}; std::unordered_map tags{{"type", "type"}, {"category", "category"}}; ddwaf::rule rule( - "id", "name", std::move(tags), std::move(conditions), {"update", "block", "passlist"}); + "id", "name", std::move(tags), builder.build(), {"update", "block", "passlist"}); ddwaf_object root, tmp; ddwaf_object_map(&root); diff --git a/tests/ruleset_test.cpp b/tests/ruleset_test.cpp index 8d81a0157..516038ad9 100644 --- a/tests/ruleset_test.cpp +++ b/tests/ruleset_test.cpp @@ -15,7 +15,7 @@ rule::ptr make_rule(std::string id, std::string name, rule::source_type source = rule::source_type::base) { return std::make_shared(std::move(id), std::move(name), std::move(tags), - std::vector{}, std::move(actions), true, source); + expression::ptr{}, std::move(actions), true, source); } } // namespace diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 4d06f2568..953247cdc 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -38,6 +38,32 @@ std::ostream &operator<<(std::ostream &os, const indent &offset) } // namespace +std::ostream &operator<<(std::ostream &os, const event::match &m) +{ + os << indent(4) << "{\n" + << indent(8) << "operator: " << m.op << ",\n" + << indent(8) << "operator_value: " << m.op_value << ",\n" + << indent(8) << "address: " << m.address << ",\n" + << indent(8) << "path: ["; + + bool start = true; + for (const auto &p : m.path) { + if (!start) { + os << ", "; + } else { + start = false; + } + os << p; + } + + os << "],\n" + << indent(8) << "value: " << m.value << ",\n" + << indent(8) << "highlight: " << m.highlight << "\n" + << indent(4) << "}\n"; + + return os; +} + std::ostream &operator<<(std::ostream &os, const event &e) { os << "{\n" @@ -73,7 +99,7 @@ std::ostream &operator<<(std::ostream &os, const event &e) os << indent(8) << "{\n"; os << indent(12) << "operator: " << m.op << ",\n" - << indent(12) << "operator_value:" << m.op_value << ",\n" + << indent(12) << "operator_value: " << m.op_value << ",\n" << indent(12) << "address: " << m.address << ",\n" << indent(12) << "path: ["; @@ -401,6 +427,11 @@ void PrintTo(const std::list &events, ::std::ostream *os) for (const auto &e : events) { *os << e; } } +void PrintTo(const std::list &matches, ::std::ostream *os) +{ + for (const auto &m : matches) { *os << m; } +} + WafResultActionMatcher::WafResultActionMatcher(std::vector &&values) : expected_(std::move(values)) { @@ -463,6 +494,51 @@ bool WafResultDataMatcher::MatchAndExplain( return events.empty(); } +bool MatchMatcher::MatchAndExplain( + std::list matches, ::testing::MatchResultListener * /*unused*/) const +{ + if (matches.size() != expected_matches_.size()) { + return false; + } + + for (const auto &expected : expected_matches_) { + bool found = false; + for (auto it = matches.begin(); it != matches.end(); ++it) { + auto &obtained = *it; + if (obtained == expected) { + matches.erase(it); + found = true; + break; + } + } + if (!found) { + return false; + } + } + + return matches.empty(); +} + +std::list from_matches( + const ddwaf::memory::vector &matches) +{ + std::list match_list; + + for (const auto &m : matches) { + ddwaf::test::event::match new_match; + new_match.value = m.resolved; + new_match.highlight = m.matched; + new_match.op = m.operator_name; + new_match.op_value = m.operator_value; + new_match.address = m.address; + for (const auto &k : m.key_path) { new_match.path.emplace_back(k); } + + match_list.emplace_back(std::move(new_match)); + } + + return match_list; +} + size_t getFileSize(const char *filename) { struct stat st; diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index 775ee782e..fa23e978e 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -33,6 +33,7 @@ bool operator==(const event::match &lhs, const event::match &rhs); bool operator==(const event &lhs, const event &rhs); std::ostream &operator<<(std::ostream &os, const event &e); +std::ostream &operator<<(std::ostream &os, const event::match &m); std::string object_to_json(const ddwaf_object &obj); @@ -88,6 +89,7 @@ ::testing::AssertionResult ValidateSchema(const std::string &result); // Required by gtest to pretty print relevant types void PrintTo(const ddwaf_object &actions, ::std::ostream *os); void PrintTo(const std::list &events, ::std::ostream *os); +void PrintTo(const std::list &matches, ::std::ostream *os); class WafResultActionMatcher { public: @@ -125,6 +127,29 @@ class WafResultDataMatcher { std::vector expected_events_; }; +class MatchMatcher { +public: + explicit MatchMatcher(std::vector expected_matches) + : expected_matches_(std::move(expected_matches)) + {} + + bool MatchAndExplain( + std::list, ::testing::MatchResultListener *) const; + + void DescribeTo(::std::ostream *os) const + { + for (const auto &expected : expected_matches_) { *os << expected; } + } + + void DescribeNegationTo(::std::ostream *os) const + { + for (const auto &expected : expected_matches_) { *os << expected; } + } + +protected: + std::vector expected_matches_; +}; + inline ::testing::PolymorphicMatcher WithActions( std::vector &&values) { @@ -137,6 +162,15 @@ inline ::testing::PolymorphicMatcher WithEvents( return ::testing::MakePolymorphicMatcher(WafResultDataMatcher(std::move(expected))); } +inline ::testing::PolymorphicMatcher WithMatches( + std::vector &&expected) +{ + return ::testing::MakePolymorphicMatcher(MatchMatcher(std::move(expected))); +} + +std::list from_matches( + const ddwaf::memory::vector &matches); + // NOLINTBEGIN(cppcoreguidelines-macro-usage) #define EXPECT_EVENTS(result, ...) \ { \ @@ -146,6 +180,8 @@ inline ::testing::PolymorphicMatcher WithEvents( auto events = doc.as>(); \ EXPECT_THAT(events, WithEvents({__VA_ARGS__})); \ } + +#define EXPECT_MATCHES(matches, ...) EXPECT_THAT(from_matches(matches), WithMatches({__VA_ARGS__})); // NOLINTEND(cppcoreguidelines-macro-usage) ddwaf_object readFile(std::string_view filename, std::string_view base = "./"); diff --git a/validator/tests/rules/chained_evaluation/001_scalar_chain.yaml b/validator/tests/rules/chained_evaluation/001_scalar_chain.yaml new file mode 100644 index 000000000..6482a2e45 --- /dev/null +++ b/validator/tests/rules/chained_evaluation/001_scalar_chain.yaml @@ -0,0 +1,31 @@ +{ + name: "Basic run with match", + runs: [ + { + input: { + server.request.query: processbuilder.base64data + }, + rules: [ + { + exp-000-001: [ + { + address: server.request.query, + value: processbuilder.base64data, + highlight: [ + processbuilder + ] + }, + { + address: match.0.scalar, + value: processbuilder.base64data, + highlight: [ + base64data + ] + }, + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/chained_evaluation/002_highlight_chain.yaml b/validator/tests/rules/chained_evaluation/002_highlight_chain.yaml new file mode 100644 index 000000000..03104e379 --- /dev/null +++ b/validator/tests/rules/chained_evaluation/002_highlight_chain.yaml @@ -0,0 +1,38 @@ +{ + name: "Basic run with match", + runs: [ + { + input: { + server.request.query: "evil_exploit:ZXhlYyh0YWtlX292ZXJfd29ybGQp" + }, + rules: [ + { + exp-000-002: [ + { + address: server.request.query, + value: "evil_exploit:ZXhlYyh0YWtlX292ZXJfd29ybGQp", + highlight: [ + "evil_exploit:ZXhlYyh0YWtlX292ZXJfd29ybGQp" + ] + }, + { + address: match.0.highlight, + value: "evil_exploit:ZXhlYyh0YWtlX292ZXJfd29ybGQp", + highlight: [ + ZXhlYyh0YWtlX292ZXJfd29ybGQp + ] + }, + { + address: match.1.highlight, + value: exec(take_over_world), + highlight: [ + exec + ] + } + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/chained_evaluation/003_object_chain.yaml b/validator/tests/rules/chained_evaluation/003_object_chain.yaml new file mode 100644 index 000000000..77349b9ff --- /dev/null +++ b/validator/tests/rules/chained_evaluation/003_object_chain.yaml @@ -0,0 +1,35 @@ +{ + name: "Basic run with match", + runs: [ + { + input: { + server.request.query: { + comment : { + text: "we're going to try to exec() something" + } + } + }, + rules: [ + { + exp-000-003: [ + { + address: server.request.query, + value: "we're going to try to exec() something", + highlight: [ + exec( + ] + }, + { + address: match.0.object, + value: comment, + highlight: [ + comment + ] + }, + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/chained_evaluation/ruleset.yaml b/validator/tests/rules/chained_evaluation/ruleset.yaml new file mode 100644 index 000000000..7fce8e6d7 --- /dev/null +++ b/validator/tests/rules/chained_evaluation/ruleset.yaml @@ -0,0 +1,94 @@ +version: '2.1' +rules: + - id: exp-000-001 + name: 'Chained scalar' + tags: + type: java_code_injection + crs_id: '944110' + category: attack_attempt + conditions: + - parameters: + inputs: + - address: server.request.query + - address: server.request.body + - address: server.request.path_params + - address: server.request.headers.no_cookies + - address: grpc.server.request.message + regex: (?:runtime|processbuilder) + options: + case_sensitive: true + min_length: 7 + operator: match_regex + - parameters: + inputs: + - address: match.0.scalar + regex: (?:unmarshaller|base64data|java\.) + options: + case_sensitive: true + min_length: 5 + operator: match_regex + transformers: + - lowercase + - id: exp-000-002 + name: 'Chained highlight' + tags: + type: java_code_injection + category: attack_attempt + conditions: + - parameters: + inputs: + - address: server.request.query + - address: server.request.body + - address: server.request.path_params + - address: server.request.headers.no_cookies + - address: grpc.server.request.message + regex: evil_exploit:[A-Za-z0-9+/=]+ + options: + case_sensitive: true + operator: match_regex + - parameters: + inputs: + - address: match.0.highlight + regex: "[A-Za-z0-9+/]+={0,3}$" + options: + case_sensitive: true + min_length: 4 + operator: match_regex + - parameters: + inputs: + - address: match.1.highlight + transformers: + - base64Decode + regex: \bexec\b + options: + case_sensitive: true + min_length: 5 + operator: match_regex + - id: exp-000-003 + name: 'Chained object' + tags: + type: java_code_injection + category: attack_attempt + conditions: + - parameters: + inputs: + - address: server.request.query + - address: server.request.body + - address: server.request.path_params + - address: server.request.headers.no_cookies + - address: grpc.server.request.message + regex: \bexec\( + options: + case_sensitive: true + min_length: 5 + operator: match_regex + - parameters: + inputs: + - address: match.0.object + transformers: + - keys_only + regex: \bcomment\b + options: + case_sensitive: true + min_length: 5 + operator: match_regex