From 08826d4a413d75112337a9e52584822a7f10cfbe Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:00:33 +0100 Subject: [PATCH] Negated scalar condition for matchers --- src/condition/scalar_condition.cpp | 101 ++++++++++++++---- src/condition/scalar_condition.hpp | 46 +++++++- src/parser/expression_parser.cpp | 23 +++- .../ip_match/009-rule3_ipv4_cidr_match.yaml | 21 ++++ .../ip_match/010_rule3_ipv6_cidr_match.yaml | 21 ++++ .../011_rule3_ipv4_cidr_no_match.yaml | 11 ++ .../012_rule3_ipv6_cidr_no_match.yaml | 11 ++ .../rules/operators/ip_match/ruleset.yaml | 13 +++ 8 files changed, 218 insertions(+), 29 deletions(-) create mode 100644 validator/tests/rules/operators/ip_match/009-rule3_ipv4_cidr_match.yaml create mode 100644 validator/tests/rules/operators/ip_match/010_rule3_ipv6_cidr_match.yaml create mode 100644 validator/tests/rules/operators/ip_match/011_rule3_ipv4_cidr_no_match.yaml create mode 100644 validator/tests/rules/operators/ip_match/012_rule3_ipv6_cidr_no_match.yaml diff --git a/src/condition/scalar_condition.cpp b/src/condition/scalar_condition.cpp index 4e2a74417..6d07adc24 100644 --- a/src/condition/scalar_condition.cpp +++ b/src/condition/scalar_condition.cpp @@ -31,8 +31,8 @@ namespace ddwaf { namespace { -template -std::optional eval_object(Iterator &it, std::string_view address, bool ephemeral, +template +ResultType eval_object(Iterator &it, std::string_view address, bool ephemeral, const matcher::base &matcher, const std::span &transformers, const object_limits &limits) { @@ -60,8 +60,12 @@ std::optional eval_object(Iterator &it, std::string_view addres DDWAF_TRACE("Target {} matched parameter value {}", address, highlight); - return {{{{"input"sv, object_to_string(dst), address, it.get_current_path()}}, - {std::move(highlight)}, matcher.name(), matcher.to_string(), ephemeral}}; + if constexpr (std::is_same_v) { + return true; + } else { + return {{{{"input"sv, object_to_string(dst), address, it.get_current_path()}}, + {std::move(highlight)}, matcher.name(), matcher.to_string(), ephemeral}}; + } } } } @@ -73,12 +77,16 @@ std::optional eval_object(Iterator &it, std::string_view addres DDWAF_TRACE("Target {} matched parameter value {}", address, highlight); - return {{{{"input"sv, object_to_string(src), address, it.get_current_path()}}, - {std::move(highlight)}, matcher.name(), matcher.to_string(), ephemeral}}; + if constexpr (std::is_same_v) { + return true; + } else { + return {{{{"input"sv, object_to_string(src), address, it.get_current_path()}}, + {std::move(highlight)}, matcher.name(), matcher.to_string(), ephemeral}}; + } } -template -std::optional eval_target(Iterator &it, std::string_view address, bool ephemeral, +template +ResultType eval_target(Iterator &it, std::string_view address, bool ephemeral, const matcher::base &matcher, const std::span &transformers, const object_limits &limits, ddwaf::timer &deadline) { @@ -91,8 +99,8 @@ std::optional eval_target(Iterator &it, std::string_view addres continue; } - auto match = eval_object(it, address, ephemeral, matcher, transformers, limits); - if (match.has_value()) { + auto match = eval_object(it, address, ephemeral, matcher, transformers, limits); + if (match) { // If this target matched, we can stop processing return match; } @@ -101,16 +109,15 @@ std::optional eval_target(Iterator &it, std::string_view addres return {}; } -} // namespace - -const matcher::base *scalar_condition::get_matcher( - const std::unordered_map> &dynamic_matchers) const +const matcher::base *get_matcher(const std::unique_ptr &matcher, + const std::string &data_id, + const std::unordered_map> &dynamic_matchers) { - if (matcher_ || data_id_.empty()) { - return matcher_.get(); + if (matcher || data_id.empty()) { + return matcher.get(); } - auto it = dynamic_matchers.find(data_id_); + auto it = dynamic_matchers.find(data_id); if (it != dynamic_matchers.end()) { return it->second.get(); } @@ -118,12 +125,14 @@ const matcher::base *scalar_condition::get_matcher( return nullptr; } +} // namespace + eval_result scalar_condition::eval(condition_cache &cache, const object_store &store, const exclusion::object_set_ref &objects_excluded, const std::unordered_map> &dynamic_matchers, ddwaf::timer &deadline) const { - const auto *matcher = get_matcher(dynamic_matchers); + const auto *matcher = get_matcher(matcher_, data_id_, dynamic_matchers); if (matcher == nullptr) { return {}; } @@ -152,11 +161,11 @@ eval_result scalar_condition::eval(condition_cache &cache, const object_store &s // TODO: iterators could be cached to avoid reinitialisation if (target.source == data_source::keys) { object::key_iterator it(object, target.key_path, objects_excluded, limits_); - match = eval_target( + match = eval_target>( it, target.name, ephemeral, *matcher, target.transformers, limits_, deadline); } else { object::value_iterator it(object, target.key_path, objects_excluded, limits_); - match = eval_target( + match = eval_target>( it, target.name, ephemeral, *matcher, target.transformers, limits_, deadline); } @@ -169,4 +178,56 @@ eval_result scalar_condition::eval(condition_cache &cache, const object_store &s return {false, false}; } +eval_result scalar_negated_condition::eval(condition_cache &cache, const object_store &store, + const exclusion::object_set_ref &objects_excluded, + const std::unordered_map> &dynamic_matchers, + ddwaf::timer &deadline) const +{ + if (deadline.expired()) { + throw ddwaf::timeout_exception(); + } + + const auto *matcher = get_matcher(matcher_, data_id_, dynamic_matchers); + if (matcher == nullptr) { + return {}; + } + + if (cache.targets.size() != 1) { + cache.targets.assign(1, nullptr); + } + + // This type of scalar condition only accepts a single target + const auto &target = targets_[0]; + + auto [object, attr] = store.get_target(target.index); + if (object == nullptr || object == cache.targets[0]) { + return {}; + } + + const bool ephemeral = (attr == object_store::attribute::ephemeral); + if (!ephemeral) { + cache.targets[0] = object; + } + + bool match = false; + if (target.source == data_source::keys) { + object::key_iterator it(object, target.key_path, objects_excluded, limits_); + match = eval_target( + it, target.name, ephemeral, *matcher, target.transformers, limits_, deadline); + } else { + object::value_iterator it(object, target.key_path, objects_excluded, limits_); + match = eval_target( + it, target.name, ephemeral, *matcher, target.transformers, limits_, deadline); + } + + if (!match) { + cache.match = {{{{"input"sv, object_to_string(*object), target.name, + {target.key_path.begin(), target.key_path.end()}}}, + {}, matcher_name_, matcher->to_string(), ephemeral}}; + return {true, ephemeral}; + } + + return {false, false}; +} + } // namespace ddwaf diff --git a/src/condition/scalar_condition.hpp b/src/condition/scalar_condition.hpp index 6d7c9843f..e98d8b481 100644 --- a/src/condition/scalar_condition.hpp +++ b/src/condition/scalar_condition.hpp @@ -39,17 +39,55 @@ class scalar_condition : public base_condition { static constexpr auto arguments() { - return std::array{{{"inputs", true, false}}}; + return std::array{{{"inputs", /*variadic*/ true, false}}}; } protected: - [[nodiscard]] const matcher::base *get_matcher( - const std::unordered_map> &dynamic_matchers) - const; + std::unique_ptr matcher_; + std::string data_id_; + std::vector targets_; + const object_limits limits_; +}; +class scalar_negated_condition : public base_condition { +public: + scalar_negated_condition(std::unique_ptr &&matcher, std::string data_id, + std::vector args, std::string matcher_name, + const object_limits &limits = {}) + : matcher_(std::move(matcher)), data_id_(std::move(data_id)), + matcher_name_(std::move(matcher_name)), limits_(limits) + { + if (args.size() > 1) { + throw std::invalid_argument("Matcher initialised with more than one argument"); + } + + if (args.empty()) { + throw std::invalid_argument("Matcher initialised without arguments"); + } + + targets_ = std::move(args[0].targets); + } + + eval_result eval(condition_cache &cache, const object_store &store, + const exclusion::object_set_ref &objects_excluded, + const std::unordered_map> &dynamic_matchers, + ddwaf::timer &deadline) const override; + + void get_addresses(std::unordered_map &addresses) const override + { + for (const auto &target : targets_) { addresses.emplace(target.index, target.name); } + } + + static constexpr auto arguments() + { + return std::array{{{"inputs", /*variadic*/ false, false}}}; + } + +protected: std::unique_ptr matcher_; std::string data_id_; std::vector targets_; + std::string matcher_name_; const object_limits limits_; }; diff --git a/src/parser/expression_parser.cpp b/src/parser/expression_parser.cpp index a49d2013b..573fa0781 100644 --- a/src/parser/expression_parser.cpp +++ b/src/parser/expression_parser.cpp @@ -141,17 +141,30 @@ std::shared_ptr parse_expression(const parameter::vector &conditions conditions.emplace_back( std::make_unique(std::move(arguments), limits)); } else { + auto raw_operator_name = operator_name; + auto negated = operator_name.starts_with('!'); + if (negated) { + operator_name = operator_name.substr(1); + } + auto [data_id, matcher] = parse_matcher(operator_name, params); if (!matcher && !data_id.empty()) { data_ids_to_type.emplace(data_id, operator_name); } - auto arguments = - parse_arguments(params, source, transformers, addresses, limits); - - conditions.emplace_back(std::make_unique( - std::move(matcher), data_id, std::move(arguments), limits)); + if (!negated) { + auto arguments = parse_arguments( + params, source, transformers, addresses, limits); + conditions.emplace_back(std::make_unique( + std::move(matcher), data_id, std::move(arguments), limits)); + } else { + auto arguments = parse_arguments( + params, source, transformers, addresses, limits); + conditions.emplace_back( + std::make_unique(std::move(matcher), data_id, + std::move(arguments), std::string{raw_operator_name}, limits)); + } } } diff --git a/validator/tests/rules/operators/ip_match/009-rule3_ipv4_cidr_match.yaml b/validator/tests/rules/operators/ip_match/009-rule3_ipv4_cidr_match.yaml new file mode 100644 index 000000000..6b87e9dc3 --- /dev/null +++ b/validator/tests/rules/operators/ip_match/009-rule3_ipv4_cidr_match.yaml @@ -0,0 +1,21 @@ +{ + name: "negated ip_match operator, match IPv4 CIDR", + runs: [ + { + persistent-input: { + rule3-input: 192.187.25.1 + }, + rules: [ + { + 3: [ + { + address: rule3-input, + value: 192.187.25.1 + } + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/operators/ip_match/010_rule3_ipv6_cidr_match.yaml b/validator/tests/rules/operators/ip_match/010_rule3_ipv6_cidr_match.yaml new file mode 100644 index 000000000..f887f7992 --- /dev/null +++ b/validator/tests/rules/operators/ip_match/010_rule3_ipv6_cidr_match.yaml @@ -0,0 +1,21 @@ +{ + name: "negated ip_match operator, match IPv6 CIDR", + runs: [ + { + persistent-input: { + rule3-input: "abce::1234:0:ab11:0" + }, + rules: [ + { + 3: [ + { + address: rule3-input, + value: "abce::1234:0:ab11:0" + } + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/operators/ip_match/011_rule3_ipv4_cidr_no_match.yaml b/validator/tests/rules/operators/ip_match/011_rule3_ipv4_cidr_no_match.yaml new file mode 100644 index 000000000..e9ad83129 --- /dev/null +++ b/validator/tests/rules/operators/ip_match/011_rule3_ipv4_cidr_no_match.yaml @@ -0,0 +1,11 @@ +{ + name: "negated ip_match operator, no match on IPv4 CIDR", + runs: [ + { + persistent-input: { + rule3-input: 192.188.25.1 + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/ip_match/012_rule3_ipv6_cidr_no_match.yaml b/validator/tests/rules/operators/ip_match/012_rule3_ipv6_cidr_no_match.yaml new file mode 100644 index 000000000..d8b3fb0ef --- /dev/null +++ b/validator/tests/rules/operators/ip_match/012_rule3_ipv6_cidr_no_match.yaml @@ -0,0 +1,11 @@ +{ + name: "negated ip_match operator, no match on IPv6 CIDR", + runs: [ + { + persistent-input: { + rule3-input: "abcd::1234:0:ab11:0" + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/ip_match/ruleset.yaml b/validator/tests/rules/operators/ip_match/ruleset.yaml index 9d64896fd..b2a2318ea 100644 --- a/validator/tests/rules/operators/ip_match/ruleset.yaml +++ b/validator/tests/rules/operators/ip_match/ruleset.yaml @@ -40,3 +40,16 @@ rules: list: - "192.188.0.0/16" - "abcd::1234:0:0:0/96" + - id: "3" + name: rule3-ip-match-negated-with-cidr + tags: + type: flow3 + category: category + conditions: + - operator: "!ip_match" + parameters: + inputs: + - address: rule3-input + list: + - "192.188.0.0/16" + - "abcd::1234:0:0:0/96"