From 749b4fd925c594de70e70cb3b9ef1c570a6af134 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:06:34 +0100 Subject: [PATCH] Extend exists operator to support key paths and negation (#334) --- cmake/objects.cmake | 1 + src/condition/exists.cpp | 127 +++++++ src/condition/exists.hpp | 32 +- src/parser/expression_parser.cpp | 9 + tests/condition/exists_condition_test.cpp | 328 +++++++++++++++++ tests/integration/conditions/exists/test.cpp | 330 ++++++++++++++++++ .../conditions/exists/yaml/exists.yaml | 36 ++ .../exists/yaml/exists_negated.yaml | 23 ++ .../conditions/{ => transformers}/test.cpp | 16 +- .../yaml/global_transformer.yaml | 0 .../yaml/input_transformer.yaml | 0 .../yaml/overlapping_transformers.yaml | 0 tests/operator/exists_condition_test.cpp | 84 ----- tests/parser_v2_rules_test.cpp | 44 +++ ...ch.yaml => 002_rule1_exists_no_match.yaml} | 0 .../003_rule2_key_path_exists_match.yaml | 27 ++ .../004_rule2_key_path_exists_no_match.yaml | 17 + ...5_rule3_key_path_exists_negated_match.yaml | 27 ++ ...ule3_key_path_exists_negated_no_match.yaml | 17 + ...ilable_exists_negated_inconsequential.yaml | 17 + ...ilable_exists_negated_inconsequential.yaml | 14 + .../tests/rules/operators/exists/ruleset.yaml | 32 ++ 22 files changed, 1078 insertions(+), 103 deletions(-) create mode 100644 src/condition/exists.cpp create mode 100644 tests/condition/exists_condition_test.cpp create mode 100644 tests/integration/conditions/exists/test.cpp create mode 100644 tests/integration/conditions/exists/yaml/exists.yaml create mode 100644 tests/integration/conditions/exists/yaml/exists_negated.yaml rename tests/integration/conditions/{ => transformers}/test.cpp (96%) rename tests/integration/conditions/{ => transformers}/yaml/global_transformer.yaml (100%) rename tests/integration/conditions/{ => transformers}/yaml/input_transformer.yaml (100%) rename tests/integration/conditions/{ => transformers}/yaml/overlapping_transformers.yaml (100%) delete mode 100644 tests/operator/exists_condition_test.cpp rename validator/tests/rules/operators/exists/{002_rule1_no_exists_match.yaml => 002_rule1_exists_no_match.yaml} (100%) create mode 100644 validator/tests/rules/operators/exists/003_rule2_key_path_exists_match.yaml create mode 100644 validator/tests/rules/operators/exists/004_rule2_key_path_exists_no_match.yaml create mode 100644 validator/tests/rules/operators/exists/005_rule3_key_path_exists_negated_match.yaml create mode 100644 validator/tests/rules/operators/exists/006_rule3_key_path_exists_negated_no_match.yaml create mode 100644 validator/tests/rules/operators/exists/007_rule4_address_available_exists_negated_inconsequential.yaml create mode 100644 validator/tests/rules/operators/exists/008_rule4_address_unavailable_exists_negated_inconsequential.yaml diff --git a/cmake/objects.cmake b/cmake/objects.cmake index 0c50f21ff..efc8011de 100644 --- a/cmake/objects.cmake +++ b/cmake/objects.cmake @@ -47,6 +47,7 @@ set(LIBDDWAF_SOURCE ${libddwaf_SOURCE_DIR}/src/parser/exclusion_parser.cpp ${libddwaf_SOURCE_DIR}/src/processor/extract_schema.cpp ${libddwaf_SOURCE_DIR}/src/processor/fingerprint.cpp + ${libddwaf_SOURCE_DIR}/src/condition/exists.cpp ${libddwaf_SOURCE_DIR}/src/condition/lfi_detector.cpp ${libddwaf_SOURCE_DIR}/src/condition/sqli_detector.cpp ${libddwaf_SOURCE_DIR}/src/condition/ssrf_detector.cpp diff --git a/src/condition/exists.cpp b/src/condition/exists.cpp new file mode 100644 index 000000000..d96fb4256 --- /dev/null +++ b/src/condition/exists.cpp @@ -0,0 +1,127 @@ +// 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 +#include +#include +#include +#include +#include + +#include "argument_retriever.hpp" +#include "clock.hpp" +#include "condition/base.hpp" +#include "condition/exists.hpp" +#include "ddwaf.h" +#include "exception.hpp" +#include "exclusion/common.hpp" +#include "utils.hpp" + +namespace ddwaf { + +namespace { + +enum class search_outcome { found, not_found, unknown }; + +const ddwaf_object *find_key( + const ddwaf_object &parent, std::string_view key, const object_limits &limits) +{ + const std::size_t size = + std::min(static_cast(parent.nbEntries), limits.max_container_size); + for (std::size_t i = 0; i < size; ++i) { + const auto &child = parent.array[i]; + + if (child.parameterName == nullptr) [[unlikely]] { + continue; + } + const std::string_view child_key{ + child.parameterName, static_cast(child.parameterNameLength)}; + + if (key == child_key) { + return &child; + } + } + + return nullptr; +} + +search_outcome exists(const ddwaf_object *root, std::span key_path, + const exclusion::object_set_ref &objects_excluded, const object_limits &limits) +{ + if (key_path.empty()) { + return search_outcome::found; + } + + // Since there's a key path, the object must be a map + if (root->type != DDWAF_OBJ_MAP) { + return search_outcome::not_found; + } + + auto it = key_path.begin(); + + // The parser ensures that the key path is within the limits specified by + // the user, hence we don't need to check for depth + while ((root = find_key(*root, *it, limits)) != nullptr) { + if (objects_excluded.contains(root)) { + // We found the next root but it has been excluded, so we + // can't know for sure if the required key path exists + return search_outcome::unknown; + } + + if (++it == key_path.end()) { + return search_outcome::found; + } + + if (root->type != DDWAF_OBJ_MAP) { + return search_outcome::not_found; + } + } + + return search_outcome::not_found; +} + +} // namespace + +[[nodiscard]] eval_result exists_condition::eval_impl( + const variadic_argument &inputs, condition_cache &cache, + const exclusion::object_set_ref &objects_excluded, ddwaf::timer &deadline) const +{ + for (const auto &input : inputs) { + if (deadline.expired()) { + throw ddwaf::timeout_exception(); + } + + if (exists(input.value, input.key_path, objects_excluded, limits_) == + search_outcome::found) { + std::vector key_path{input.key_path.begin(), input.key_path.end()}; + cache.match = {{{{"input", {}, input.address, std::move(key_path)}}, {}, "exists", {}, + input.ephemeral}}; + return {true, input.ephemeral}; + } + } + return {false, false}; +} + +[[nodiscard]] eval_result exists_negated_condition::eval_impl( + const unary_argument &input, condition_cache &cache, + const exclusion::object_set_ref &objects_excluded, ddwaf::timer & /*deadline*/) const +{ + // We need to make sure the key path hasn't been found. If the result is + // unknown, we can't guarantee that the key path isn't actually present in + // the data set + if (exists(input.value, input.key_path, objects_excluded, limits_) != + search_outcome::not_found) { + return {false, false}; + } + + std::vector key_path{input.key_path.begin(), input.key_path.end()}; + cache.match = { + {{{"input", {}, input.address, std::move(key_path)}}, {}, "!exists", {}, input.ephemeral}}; + return {true, input.ephemeral}; +} + +} // namespace ddwaf diff --git a/src/condition/exists.hpp b/src/condition/exists.hpp index 74c5f0d85..c65078bec 100644 --- a/src/condition/exists.hpp +++ b/src/condition/exists.hpp @@ -7,6 +7,8 @@ #pragma once #include "condition/structured_condition.hpp" +#include "exception.hpp" +#include "iterator.hpp" namespace ddwaf { @@ -21,19 +23,27 @@ class exists_condition : public base_impl { protected: [[nodiscard]] eval_result eval_impl(const variadic_argument &inputs, - condition_cache &cache, const exclusion::object_set_ref & /*objects_excluded*/, - ddwaf::timer & /*deadline*/) const - { - if (inputs.empty()) { - return {false, false}; - } - // We only care about the first input - auto input = inputs.front(); - cache.match = {{{{"input", {}, input.address, {}}}, {}, "exists", {}, input.ephemeral}}; - return {true, input.ephemeral}; - } + condition_cache &cache, const exclusion::object_set_ref &objects_excluded, + ddwaf::timer &deadline) const; friend class base_impl; }; +class exists_negated_condition : public base_impl { +public: + static constexpr std::array param_names{"inputs"}; + + explicit exists_negated_condition( + std::vector args, const object_limits &limits = {}) + : base_impl(std::move(args), limits) + {} + +protected: + [[nodiscard]] eval_result eval_impl(const unary_argument &input, + condition_cache &cache, const exclusion::object_set_ref &objects_excluded, + ddwaf::timer & /*deadline*/) const; + + friend class base_impl; +}; + } // namespace ddwaf diff --git a/src/parser/expression_parser.cpp b/src/parser/expression_parser.cpp index 15e4922c9..a49d2013b 100644 --- a/src/parser/expression_parser.cpp +++ b/src/parser/expression_parser.cpp @@ -68,6 +68,10 @@ std::vector parse_arguments(const parameter::map ¶ms, d } auto kp = at>(input, "key_path", {}); + if (kp.size() > limits.max_container_depth) { + throw ddwaf::parsing_error("key_path beyond maximum container depth"); + } + for (const auto &path : kp) { if (path.empty()) { throw ddwaf::parsing_error("empty key_path"); @@ -131,6 +135,11 @@ std::shared_ptr parse_expression(const parameter::vector &conditions parse_arguments(params, source, transformers, addresses, limits); conditions.emplace_back( std::make_unique(std::move(arguments), limits)); + } else if (operator_name == "!exists") { + auto arguments = parse_arguments( + params, source, transformers, addresses, limits); + conditions.emplace_back( + std::make_unique(std::move(arguments), limits)); } else { auto [data_id, matcher] = parse_matcher(operator_name, params); diff --git a/tests/condition/exists_condition_test.cpp b/tests/condition/exists_condition_test.cpp new file mode 100644 index 000000000..416ec4cad --- /dev/null +++ b/tests/condition/exists_condition_test.cpp @@ -0,0 +1,328 @@ +// 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 "../test.hpp" +#include "condition/exists.hpp" +#include "utils.hpp" + +using namespace ddwaf; +using namespace std::literals; + +namespace { + +template std::vector gen_variadic_param(Args... addresses) +{ + return {{{{std::string{addresses}, get_target_index(addresses)}...}}}; +} + +TEST(TestExistsCondition, AddressAvailable) +{ + exists_condition cond{{gen_variadic_param("server.request.uri_raw")}}; + + ddwaf_object tmp; + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", ddwaf_object_invalid(&tmp)); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_TRUE(res.outcome); +} + +TEST(TestExistsCondition, KeyPathAvailable) +{ + exists_condition cond{{{{{{"server.request.uri_raw", get_target_index("server.request.uri_raw"), + {"path", "to", "object"}}}}}}}; + + ddwaf_object tmp; + ddwaf_object path; + ddwaf_object to; + ddwaf_object object; + + ddwaf_object_map(&object); + ddwaf_object_map_add(&object, "object", ddwaf_object_invalid(&tmp)); + + ddwaf_object_map(&to); + ddwaf_object_map_add(&to, "to", &object); + + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, "path", &to); + + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", &path); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_TRUE(res.outcome); +} + +TEST(TestExistsCondition, AddressNotAvaialble) +{ + exists_condition cond{{gen_variadic_param("server.request.uri_raw")}}; + + ddwaf_object tmp; + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_invalid(&tmp)); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_FALSE(res.outcome); +} + +TEST(TestExistsCondition, KeyPathNotAvailable) +{ + exists_condition cond{{{{{{"server.request.uri_raw", get_target_index("server.request.uri_raw"), + {"path", "to", "object"}}}}}}}; + + ddwaf_object tmp; + ddwaf_object path; + ddwaf_object to; + + ddwaf_object_map(&to); + ddwaf_object_map_add(&to, "to", ddwaf_object_invalid(&tmp)); + + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, "path", &to); + + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", &path); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_FALSE(res.outcome); +} + +TEST(TestExistsCondition, KeyPathAvailableButExcluded) +{ + exists_condition cond{{{{{{"server.request.uri_raw", get_target_index("server.request.uri_raw"), + {"path", "to", "object"}}}}}}}; + + ddwaf_object tmp; + ddwaf_object path; + ddwaf_object to; + ddwaf_object object; + + ddwaf_object_map(&object); + ddwaf_object_map_add(&object, "object", ddwaf_object_invalid(&tmp)); + + ddwaf_object_map(&to); + ddwaf_object_map_add(&to, "to", &object); + + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, "path", &to); + + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", &path); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + + std::unordered_set excluded = {&root.array[0]}; + + exclusion::object_set_ref excluded_ref; + excluded_ref.persistent = excluded; + + // While the key path is present, since part of the path was excluded + // the evaluation fails to determine the presence of the full key path, + // for that reason, no match is generated. + auto res = cond.eval(cache, store, excluded_ref, {}, deadline); + ASSERT_FALSE(res.outcome); +} + +TEST(TestExistsCondition, MultipleAddresses) +{ + exists_condition cond{ + {gen_variadic_param("server.request.uri_raw", "server.request.body", "usr.id")}}; + + auto validate_address = [&](const std::string &address, bool expected = true) { + ddwaf_object tmp; + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, address.c_str(), ddwaf_object_invalid(&tmp)); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_EQ(res.outcome, expected); + }; + + validate_address("usr.id"); + validate_address("server.request.body"); + validate_address("server.request.uri_raw"); + validate_address("server.request.query", false); + validate_address("usr.session_id", false); +} + +TEST(TestExistsCondition, MultipleAddressesAndKeyPaths) +{ + exists_condition cond{{{{{"server.request.uri_raw", get_target_index("server.request.uri_raw"), + {"path", "to", "object"}}, + {"usr.id", get_target_index("usr.id")}, + {"server.request.body", get_target_index("server.request.body"), {"key"}}}}}}; + + auto validate_address = [&](const std::string &address, const std::vector &kp, + bool expected = true) { + ddwaf_object tmp; + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_invalid(&tmp); + + // NOLINTNEXTLINE(modernize-loop-convert) + for (auto it = kp.rbegin(); it != kp.rend(); ++it) { + ddwaf_object path; + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, it->c_str(), &tmp); + + tmp = path; + } + + ddwaf_object_map_add(&root, address.c_str(), &tmp); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_EQ(res.outcome, expected); + }; + + validate_address("usr.id", {}); + validate_address("usr.id", {"whatever"}); + validate_address("server.request.uri_raw", {"path", "to", "object"}); + validate_address("server.request.body", {"key"}); + validate_address("server.request.body", {}, false); + validate_address("server.request.uri_raw", {"path", "to"}, false); + validate_address("server.request.uri_raw", {"path"}, false); + validate_address("server.request.uri_raw", {}, false); + validate_address("server.request.query", {}, false); + validate_address("usr.session_id", {}, false); +} + +TEST(TestExistsNegatedCondition, KeyPathAvailable) +{ + exists_negated_condition cond{{{{{{"server.request.uri_raw", + get_target_index("server.request.uri_raw"), {"path", "to", "object"}}}}}}}; + + ddwaf_object tmp; + ddwaf_object path; + ddwaf_object to; + ddwaf_object object; + + ddwaf_object_map(&object); + ddwaf_object_map_add(&object, "object", ddwaf_object_invalid(&tmp)); + + ddwaf_object_map(&to); + ddwaf_object_map_add(&to, "to", &object); + + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, "path", &to); + + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", &path); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_FALSE(res.outcome); +} + +TEST(TestExistsNegatedCondition, KeyPathNotAvailable) +{ + exists_negated_condition cond{{{{{{"server.request.uri_raw", + get_target_index("server.request.uri_raw"), {"path", "to", "object"}}}}}}}; + + ddwaf_object tmp; + ddwaf_object path; + ddwaf_object to; + + ddwaf_object_map(&to); + ddwaf_object_map_add(&to, "to", ddwaf_object_invalid(&tmp)); + + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, "path", &to); + + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", &path); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_TRUE(res.outcome); +} + +TEST(TestExistsNegatedCondition, KeyPathAvailableButExcluded) +{ + exists_negated_condition cond{{{{{{"server.request.uri_raw", + get_target_index("server.request.uri_raw"), {"path", "to", "object"}}}}}}}; + + ddwaf_object tmp; + ddwaf_object path; + ddwaf_object to; + + ddwaf_object_map(&to); + ddwaf_object_map_add(&to, "to", ddwaf_object_invalid(&tmp)); + + ddwaf_object_map(&path); + ddwaf_object_map_add(&path, "path", &to); + + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", &path); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + + std::unordered_set excluded = {&root.array[0]}; + + exclusion::object_set_ref excluded_ref; + excluded_ref.persistent = excluded; + + // While the key path is not present, since part of the path was excluded + // the evaluation fails to determine the presence of the full key path, + // for that reason, no match is generated. + auto res = cond.eval(cache, store, excluded_ref, {}, deadline); + ASSERT_FALSE(res.outcome); +} + +} // namespace diff --git a/tests/integration/conditions/exists/test.cpp b/tests/integration/conditions/exists/test.cpp new file mode 100644 index 000000000..1fd604ac0 --- /dev/null +++ b/tests/integration/conditions/exists/test.cpp @@ -0,0 +1,330 @@ +// 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 "../../../test_utils.hpp" + +using namespace ddwaf; + +namespace { +constexpr std::string_view base_dir = "integration/conditions/exists"; + +TEST(TestConditionExistsIntegration, AddressAvailable) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&map, "input-1", ddwaf_object_invalid(&value)); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + EXPECT_EVENTS(out, {.id = "1", + .name = "rule1-exists", + .tags = {{"type", "flow"}, {"category", "category"}}, + .matches = {{.op = "exists", + .highlight = "", + .args = {{ + .address = "input-1", + }}}}}); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsIntegration, AddressNotAvailable) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&map, "input", ddwaf_object_invalid(&value)); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_OK); + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsIntegration, KeyPathAvailable) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object intermediate = DDWAF_OBJECT_MAP; + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&intermediate, "path", ddwaf_object_invalid(&value)); + ddwaf_object_map_add(&map, "input-2", &intermediate); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + EXPECT_EVENTS(out, {.id = "2", + .name = "rule2-exists-kp", + .tags = {{"type", "flow"}, {"category", "category"}}, + .matches = {{.op = "exists", + .highlight = "", + .args = {{.address = "input-2", .path = {"path"}}}}}}); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsIntegration, KeyPathNotAvailable) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object intermediate = DDWAF_OBJECT_MAP; + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&intermediate, "poth", ddwaf_object_invalid(&value)); + ddwaf_object_map_add(&map, "input-2", &intermediate); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_OK); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsIntegration, AddressAvailableVariadicRule) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&map, "input-3-1", ddwaf_object_invalid(&value)); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + EXPECT_EVENTS(out, {.id = "3", + .name = "rule3-exists-multi", + .tags = {{"type", "flow"}, {"category", "category"}}, + .matches = {{.op = "exists", + .highlight = "", + .args = {{ + .address = "input-3-1", + }}}}}); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsIntegration, KeyPathAvailableVariadicRule) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object intermediate = DDWAF_OBJECT_MAP; + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&intermediate, "path", ddwaf_object_invalid(&value)); + ddwaf_object_map_add(&map, "input-3-2", &intermediate); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + EXPECT_EVENTS(out, {.id = "3", + .name = "rule3-exists-multi", + .tags = {{"type", "flow"}, {"category", "category"}}, + .matches = {{.op = "exists", + .highlight = "", + .args = {{.address = "input-3-2", .path = {"path"}}}}}}); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsIntegration, AddressAvailableKeyPathNotAvailableVariadicRule) +{ + auto rule = read_file("exists.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object intermediate = DDWAF_OBJECT_MAP; + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&intermediate, "poth", ddwaf_object_invalid(&value)); + ddwaf_object_map_add(&map, "input-3-2", &intermediate); + ddwaf_object_map_add(&map, "input-3-1", ddwaf_object_invalid(&value)); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + EXPECT_EVENTS(out, {.id = "3", + .name = "rule3-exists-multi", + .tags = {{"type", "flow"}, {"category", "category"}}, + .matches = {{.op = "exists", + .highlight = "", + .args = {{ + .address = "input-3-1", + }}}}}); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsNegatedIntegration, AddressAvailable) +{ + auto rule = read_file("exists_negated.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&map, "input-1", ddwaf_object_invalid(&value)); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_OK); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsNegatedIntegration, AddressNotAvailable) +{ + auto rule = read_file("exists_negated.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&map, "input", ddwaf_object_invalid(&value)); + + // Even though the address isn't present, this test shouldn't result in a match + // as the !exists operator only supports address + key path, since we can't + // assert the absence of an address given that these are provided in stages + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_OK); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsNegatedIntegration, KeyPathNotAvailable) +{ + auto rule = read_file("exists_negated.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object intermediate = DDWAF_OBJECT_MAP; + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&intermediate, "poth", ddwaf_object_invalid(&value)); + ddwaf_object_map_add(&map, "input-2", &intermediate); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + EXPECT_EVENTS(out, {.id = "2", + .name = "rule2-not-exists-kp", + .tags = {{"type", "flow"}, {"category", "category"}}, + .matches = {{.op = "!exists", + .highlight = "", + .args = {{.address = "input-2", .path = {"path"}}}}}}); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +TEST(TestConditionExistsNegatedIntegration, KeyPathAvailable) +{ + auto rule = read_file("exists_negated.yaml", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object intermediate = DDWAF_OBJECT_MAP; + ddwaf_object map = DDWAF_OBJECT_MAP; + ddwaf_object value; + ddwaf_object_map_add(&intermediate, "path", ddwaf_object_invalid(&value)); + ddwaf_object_map_add(&map, "input-2", &intermediate); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_OK); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + ddwaf_destroy(handle); +} + +} // namespace diff --git a/tests/integration/conditions/exists/yaml/exists.yaml b/tests/integration/conditions/exists/yaml/exists.yaml new file mode 100644 index 000000000..132b929e9 --- /dev/null +++ b/tests/integration/conditions/exists/yaml/exists.yaml @@ -0,0 +1,36 @@ +version: '2.1' +rules: + - id: 1 + name: rule1-exists + tags: + type: flow + category: category + conditions: + - operator: exists + parameters: + inputs: + - address: input-1 + - id: 2 + name: rule2-exists-kp + tags: + type: flow + category: category + conditions: + - operator: exists + parameters: + inputs: + - address: input-2 + key_path: ["path"] + - id: 3 + name: rule3-exists-multi + tags: + type: flow + category: category + conditions: + - operator: exists + parameters: + inputs: + - address: input-3-1 + - address: input-3-2 + key_path: ["path"] + diff --git a/tests/integration/conditions/exists/yaml/exists_negated.yaml b/tests/integration/conditions/exists/yaml/exists_negated.yaml new file mode 100644 index 000000000..67e7330e4 --- /dev/null +++ b/tests/integration/conditions/exists/yaml/exists_negated.yaml @@ -0,0 +1,23 @@ +version: '2.1' +rules: + - id: 1 + name: rule1-not-exists-inconsequential + tags: + type: flow + category: category + conditions: + - operator: "!exists" + parameters: + inputs: + - address: input-1 + - id: 2 + name: rule2-not-exists-kp + tags: + type: flow + category: category + conditions: + - operator: "!exists" + parameters: + inputs: + - address: input-2 + key_path: ["path"] diff --git a/tests/integration/conditions/test.cpp b/tests/integration/conditions/transformers/test.cpp similarity index 96% rename from tests/integration/conditions/test.cpp rename to tests/integration/conditions/transformers/test.cpp index 58144023d..533edd2b3 100644 --- a/tests/integration/conditions/test.cpp +++ b/tests/integration/conditions/transformers/test.cpp @@ -4,15 +4,15 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2021 Datadog, Inc. -#include "../../test_utils.hpp" +#include "../../../test_utils.hpp" #include "ddwaf.h" using namespace ddwaf; namespace { -constexpr std::string_view base_dir = "integration/conditions/"; +constexpr std::string_view base_dir = "integration/conditions/transformers/"; -TEST(TestConditionsIntegration, GlobalTransformer) +TEST(TestConditionTransformersIntegration, GlobalTransformer) { auto rule = read_file("global_transformer.yaml", base_dir); ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); @@ -75,7 +75,7 @@ TEST(TestConditionsIntegration, GlobalTransformer) ddwaf_destroy(handle); } -TEST(TestConditionsIntegration, GlobalTransformerKeysOnly) +TEST(TestConditionTransformersIntegration, GlobalTransformerKeysOnly) { auto rule = read_file("global_transformer.yaml", base_dir); ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); @@ -146,7 +146,7 @@ TEST(TestConditionsIntegration, GlobalTransformerKeysOnly) ddwaf_destroy(handle); } -TEST(TestConditionsIntegration, InputTransformer) +TEST(TestConditionTransformersIntegration, InputTransformer) { auto rule = read_file("input_transformer.yaml", base_dir); ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); @@ -209,7 +209,7 @@ TEST(TestConditionsIntegration, InputTransformer) ddwaf_destroy(handle); } -TEST(TestConditionsIntegration, InputTransformerKeysOnly) +TEST(TestConditionTransformersIntegration, InputTransformerKeysOnly) { auto rule = read_file("input_transformer.yaml", base_dir); ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); @@ -280,7 +280,7 @@ TEST(TestConditionsIntegration, InputTransformerKeysOnly) ddwaf_destroy(handle); } -TEST(TestConditionsIntegration, OverlappingTransformer) +TEST(TestConditionTransformersIntegration, OverlappingTransformer) { auto rule = read_file("overlapping_transformers.yaml", base_dir); ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); @@ -379,7 +379,7 @@ TEST(TestConditionsIntegration, OverlappingTransformer) ddwaf_destroy(handle); } -TEST(TestConditionsIntegration, OverlappingTransformerKeysOnly) +TEST(TestConditionTransformersIntegration, OverlappingTransformerKeysOnly) { auto rule = read_file("overlapping_transformers.yaml", base_dir); ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); diff --git a/tests/integration/conditions/yaml/global_transformer.yaml b/tests/integration/conditions/transformers/yaml/global_transformer.yaml similarity index 100% rename from tests/integration/conditions/yaml/global_transformer.yaml rename to tests/integration/conditions/transformers/yaml/global_transformer.yaml diff --git a/tests/integration/conditions/yaml/input_transformer.yaml b/tests/integration/conditions/transformers/yaml/input_transformer.yaml similarity index 100% rename from tests/integration/conditions/yaml/input_transformer.yaml rename to tests/integration/conditions/transformers/yaml/input_transformer.yaml diff --git a/tests/integration/conditions/yaml/overlapping_transformers.yaml b/tests/integration/conditions/transformers/yaml/overlapping_transformers.yaml similarity index 100% rename from tests/integration/conditions/yaml/overlapping_transformers.yaml rename to tests/integration/conditions/transformers/yaml/overlapping_transformers.yaml diff --git a/tests/operator/exists_condition_test.cpp b/tests/operator/exists_condition_test.cpp deleted file mode 100644 index c238cc516..000000000 --- a/tests/operator/exists_condition_test.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// 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 "../test.hpp" -#include "condition/exists.hpp" -#include "utils.hpp" - -using namespace ddwaf; -using namespace std::literals; - -namespace { - -template std::vector gen_variadic_param(Args... addresses) -{ - return {{{{std::string{addresses}, get_target_index(addresses)}...}}}; -} - -TEST(TestExistsCondition, AddressAvailable) -{ - exists_condition cond{{gen_variadic_param("server.request.uri_raw")}}; - - ddwaf_object tmp; - ddwaf_object root; - ddwaf_object_map(&root); - ddwaf_object_map_add(&root, "server.request.uri_raw", ddwaf_object_invalid(&tmp)); - - object_store store; - store.insert(root); - - ddwaf::timer deadline{2s}; - condition_cache cache; - auto res = cond.eval(cache, store, {}, {}, deadline); - ASSERT_TRUE(res.outcome); -} - -TEST(TestExistsCondition, AddressNotAvaialble) -{ - exists_condition cond{{gen_variadic_param("server.request.uri_raw")}}; - - ddwaf_object tmp; - ddwaf_object root; - ddwaf_object_map(&root); - ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_invalid(&tmp)); - - object_store store; - store.insert(root); - - ddwaf::timer deadline{2s}; - condition_cache cache; - auto res = cond.eval(cache, store, {}, {}, deadline); - ASSERT_FALSE(res.outcome); -} - -TEST(TestExistsCondition, MultipleAddresses) -{ - exists_condition cond{ - {gen_variadic_param("server.request.uri_raw", "server.request.body", "usr.id")}}; - - auto validate_address = [&](const std::string &address, bool expected = true) { - ddwaf_object tmp; - ddwaf_object root; - ddwaf_object_map(&root); - ddwaf_object_map_add(&root, address.c_str(), ddwaf_object_invalid(&tmp)); - - object_store store; - store.insert(root); - - ddwaf::timer deadline{2s}; - condition_cache cache; - auto res = cond.eval(cache, store, {}, {}, deadline); - ASSERT_EQ(res.outcome, expected); - }; - - validate_address("usr.id"); - validate_address("server.request.body"); - validate_address("server.request.uri_raw"); - validate_address("server.request.query", false); - validate_address("usr.session_id", false); -} - -} // namespace diff --git a/tests/parser_v2_rules_test.cpp b/tests/parser_v2_rules_test.cpp index 07f9ca937..5b66d3696 100644 --- a/tests/parser_v2_rules_test.cpp +++ b/tests/parser_v2_rules_test.cpp @@ -368,4 +368,48 @@ TEST(TestParserV2Rules, ParseMultipleRulesOneDuplicate) EXPECT_STR(rule.tags["category"], "category1"); } } + +TEST(TestParserV2Rules, KeyPathTooLong) +{ + ddwaf::object_limits limits; + limits.max_container_depth = 2; + ddwaf::ruleset_info::section_info section; + std::unordered_map rule_data_ids; + + auto rule_object = yaml_to_object( + R"([{id: 1, name: rule1, tags: {type: flow1, category: category1}, conditions: [{operator: match_regex, parameters: {inputs: [{address: arg1}], regex: .*}}, {operator: match_regex, parameters: {inputs: [{address: arg2, key_path: [x, y, z]}], regex: .*}}]}])"); + + auto rule_array = static_cast(parameter(rule_object)); + EXPECT_EQ(rule_array.size(), 1); + + auto rules = parser::v2::parse_rules(rule_array, section, rule_data_ids, limits); + ddwaf_object_free(&rule_object); + + { + ddwaf::parameter root; + section.to_object(root); + + auto root_map = static_cast(root); + + auto loaded = ddwaf::parser::at(root_map, "loaded"); + EXPECT_EQ(loaded.size(), 0); + + auto failed = ddwaf::parser::at(root_map, "failed"); + EXPECT_EQ(failed.size(), 1); + EXPECT_NE(failed.find("1"), failed.end()); + + auto errors = ddwaf::parser::at(root_map, "errors"); + EXPECT_EQ(errors.size(), 1); + auto it = errors.find("key_path beyond maximum container depth"); + EXPECT_NE(it, errors.end()); + + auto error_rules = static_cast(it->second); + EXPECT_EQ(error_rules.size(), 1); + EXPECT_NE(error_rules.find("1"), error_rules.end()); + + ddwaf_object_free(&root); + } + + EXPECT_EQ(rules.size(), 0); +} } // namespace diff --git a/validator/tests/rules/operators/exists/002_rule1_no_exists_match.yaml b/validator/tests/rules/operators/exists/002_rule1_exists_no_match.yaml similarity index 100% rename from validator/tests/rules/operators/exists/002_rule1_no_exists_match.yaml rename to validator/tests/rules/operators/exists/002_rule1_exists_no_match.yaml diff --git a/validator/tests/rules/operators/exists/003_rule2_key_path_exists_match.yaml b/validator/tests/rules/operators/exists/003_rule2_key_path_exists_match.yaml new file mode 100644 index 000000000..61320bb93 --- /dev/null +++ b/validator/tests/rules/operators/exists/003_rule2_key_path_exists_match.yaml @@ -0,0 +1,27 @@ +{ + name: "Basic run with exists operator with key_path", + runs: [ + { + persistent-input: { + rule2-input: { + "path": { + "to": { + "object": "something else" + } + } + } + }, + rules: [ + { + 2: [ + { + address: rule2-input, + key_path: ["path", "to", "object"] + } + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/operators/exists/004_rule2_key_path_exists_no_match.yaml b/validator/tests/rules/operators/exists/004_rule2_key_path_exists_no_match.yaml new file mode 100644 index 000000000..50ffca65e --- /dev/null +++ b/validator/tests/rules/operators/exists/004_rule2_key_path_exists_no_match.yaml @@ -0,0 +1,17 @@ +{ + name: "Basic run with exists operator with key_path and no match", + runs: [ + { + persistent-input: { + rule2-input: { + "path": { + "to": { + "nowhere": "something else" + } + } + } + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/exists/005_rule3_key_path_exists_negated_match.yaml b/validator/tests/rules/operators/exists/005_rule3_key_path_exists_negated_match.yaml new file mode 100644 index 000000000..9524e6272 --- /dev/null +++ b/validator/tests/rules/operators/exists/005_rule3_key_path_exists_negated_match.yaml @@ -0,0 +1,27 @@ +{ + name: "Basic run with !exists operator with key_path", + runs: [ + { + persistent-input: { + rule3-input: { + "path": { + "to": { + "nowhere": "something else" + } + } + } + }, + rules: [ + { + 3: [ + { + address: rule3-input, + key_path: ["path", "to", "object"] + } + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/operators/exists/006_rule3_key_path_exists_negated_no_match.yaml b/validator/tests/rules/operators/exists/006_rule3_key_path_exists_negated_no_match.yaml new file mode 100644 index 000000000..e2005b6fa --- /dev/null +++ b/validator/tests/rules/operators/exists/006_rule3_key_path_exists_negated_no_match.yaml @@ -0,0 +1,17 @@ +{ + name: "Basic run with !exists operator with key_path and no match", + runs: [ + { + persistent-input: { + rule3-input: { + "path": { + "to": { + "object": "something else" + } + } + } + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/exists/007_rule4_address_available_exists_negated_inconsequential.yaml b/validator/tests/rules/operators/exists/007_rule4_address_available_exists_negated_inconsequential.yaml new file mode 100644 index 000000000..f52ee1c12 --- /dev/null +++ b/validator/tests/rules/operators/exists/007_rule4_address_available_exists_negated_inconsequential.yaml @@ -0,0 +1,17 @@ +{ + name: "Validate that the !exists operator without key path doesn't match when an address is present", + runs: [ + { + persistent-input: { + rule4-input: { + "path": { + "to": { + "nowhere": "something else" + } + } + } + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/exists/008_rule4_address_unavailable_exists_negated_inconsequential.yaml b/validator/tests/rules/operators/exists/008_rule4_address_unavailable_exists_negated_inconsequential.yaml new file mode 100644 index 000000000..514546cec --- /dev/null +++ b/validator/tests/rules/operators/exists/008_rule4_address_unavailable_exists_negated_inconsequential.yaml @@ -0,0 +1,14 @@ +{ + name: "Validate that the !exists operator without key path doesn't match when an address + isn't present Note that this is due to the fact that", + runs: [ + { + persistent-input: { + random-input: { + "nowhere": "something else" + } + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/exists/ruleset.yaml b/validator/tests/rules/operators/exists/ruleset.yaml index fe5982507..d546ac318 100644 --- a/validator/tests/rules/operators/exists/ruleset.yaml +++ b/validator/tests/rules/operators/exists/ruleset.yaml @@ -10,3 +10,35 @@ rules: parameters: inputs: - address: rule1-input + - id: "2" + name: rule2-exists-keypath + tags: + type: flow2 + category: category + conditions: + - operator: exists + parameters: + inputs: + - address: rule2-input + key_path: ["path", "to", "object"] + - id: "3" + name: rule3-does-not-exist-keypath + tags: + type: flow3 + category: category + conditions: + - operator: "!exists" + parameters: + inputs: + - address: rule3-input + key_path: ["path", "to", "object"] + - id: "4" + name: rule4-does-not-exist-invalid + tags: + type: flow4 + category: category + conditions: + - operator: "!exists" + parameters: + inputs: + - address: rule4-input