From 1ba06740accb3b06673432c315467342b02933c8 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:21:20 +0000 Subject: [PATCH] Action semantics and related improvements (#277) --- .clang-tidy | 2 +- .github/workflows/test.yml | 11 +- cmake/objects.cmake | 3 + src/action_mapper.cpp | 85 +++ src/action_mapper.hpp | 71 ++ src/collection.cpp | 8 +- src/context.cpp | 5 +- src/event.cpp | 209 ++++-- src/event.hpp | 9 +- src/parameter.cpp | 27 + src/parameter.hpp | 6 + src/parser/actions_parser.cpp | 85 +++ src/parser/common.hpp | 2 + src/parser/parser.hpp | 3 + src/parser/parser_v2.cpp | 2 - src/ruleset.hpp | 5 +- src/ruleset_builder.cpp | 23 +- src/ruleset_builder.hpp | 4 + src/utils.hpp | 2 +- src/uuid.cpp | 62 ++ src/uuid.hpp | 14 + src/waf.cpp | 1 + tests/action_mapper_builder_test.cpp | 197 +++++ tests/context_test.cpp | 101 ++- tests/event_serializer_test.cpp | 201 ++++- tests/integration/actions/test.cpp | 310 ++++++++ .../actions/yaml/default_actions.yaml | 62 ++ tests/integration/custom_rules/test.cpp | 43 +- tests/interface_test.cpp | 154 ++-- tests/parser_v2_actions_test.cpp | 687 ++++++++++++++++++ tests/rule_filter_test.cpp | 6 +- tests/test_utils.cpp | 152 ++-- tests/test_utils.hpp | 56 +- tests/transformer/transformer_utils.hpp | 2 +- tests/uuid_test.cpp | 30 + tools/waf_runner.cpp | 26 +- validator/assert.hpp | 15 +- validator/runner.cpp | 31 +- validator/runner.hpp | 1 + ...009_rule1_monitored_through_condition.yaml | 2 +- .../conditional/010_rule_no_exclusions.yaml | 8 +- .../actions/001_rule1_match_with_actions.yaml | 9 +- .../actions/002_rule2_undefined_action.yaml | 22 + .../003_rule3_generate_stack_trace.yaml | 26 + validator/tests/rules/actions/ruleset.yaml | 41 ++ validator/utils.hpp | 2 +- 46 files changed, 2494 insertions(+), 329 deletions(-) create mode 100644 src/action_mapper.cpp create mode 100644 src/action_mapper.hpp create mode 100644 src/parser/actions_parser.cpp create mode 100644 src/uuid.cpp create mode 100644 src/uuid.hpp create mode 100644 tests/action_mapper_builder_test.cpp create mode 100644 tests/integration/actions/test.cpp create mode 100644 tests/integration/actions/yaml/default_actions.yaml create mode 100644 tests/parser_v2_actions_test.cpp create mode 100644 tests/uuid_test.cpp create mode 100644 validator/tests/rules/actions/002_rule2_undefined_action.yaml create mode 100644 validator/tests/rules/actions/003_rule3_generate_stack_trace.yaml diff --git a/.clang-tidy b/.clang-tidy index e9f9a5a5e..07fc4979d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,7 +1,7 @@ --- # readability-function-cognitive-complexity temporarily disabled until clang-tidy is fixed # right now emalloc causes it to misbehave -Checks: '*,misc-const-correctness,-bugprone-reserved-identifier,-hicpp-signed-bitwise,-llvmlibc-restrict-system-libc-headers,-altera-unroll-loops,-hicpp-named-parameter,-cert-dcl37-c,-cert-dcl51-cpp,-read,-cppcoreguidelines-init-variables,-cppcoreguidelines-avoid-non-const-global-variables,-altera-id-dependent-backward-branch,-performance-no-int-to-ptr,-altera-struct-pack-align,-google-readability-casting,-modernize-use-trailing-return-type,-llvmlibc-implementation-in-namespace,-llvmlibc-callee-namespace,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-fuchsia-default-arguments-declarations,-fuchsia-overloaded-operator,-cppcoreguidelines-pro-type-union-access,-fuchsia-default-arguments-calls,-cppcoreguidelines-non-private-member-variables-in-classes,-misc-non-private-member-variables-in-classes,-google-readability-todo,-llvm-header-guard,-readability-function-cognitive-complexity,-readability-identifier-length,-cppcoreguidelines-owning-memory,-cert-err58-cpp,-fuchsia-statically-constructed-objects,-google-build-using-namespace,-hicpp-avoid-goto,-cppcoreguidelines-avoid-goto,-hicpp-no-array-decay,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-abseil-string-find-str-contains,-bugprone-unchecked-optional-access' +Checks: '*,misc-const-correctness,-bugprone-reserved-identifier,-hicpp-signed-bitwise,-llvmlibc-restrict-system-libc-headers,-altera-unroll-loops,-hicpp-named-parameter,-cert-dcl37-c,-cert-dcl51-cpp,-read,-cppcoreguidelines-init-variables,-cppcoreguidelines-avoid-non-const-global-variables,-altera-id-dependent-backward-branch,-performance-no-int-to-ptr,-altera-struct-pack-align,-google-readability-casting,-modernize-use-trailing-return-type,-llvmlibc-implementation-in-namespace,-llvmlibc-callee-namespace,-cppcoreguidelines-pro-bounds-pointer-arithmetic,-fuchsia-default-arguments-declarations,-fuchsia-overloaded-operator,-cppcoreguidelines-pro-type-union-access,-fuchsia-default-arguments-calls,-cppcoreguidelines-non-private-member-variables-in-classes,-misc-non-private-member-variables-in-classes,-google-readability-todo,-llvm-header-guard,-readability-function-cognitive-complexity,-readability-identifier-length,-cppcoreguidelines-owning-memory,-cert-err58-cpp,-fuchsia-statically-constructed-objects,-google-build-using-namespace,-hicpp-avoid-goto,-cppcoreguidelines-avoid-goto,-hicpp-no-array-decay,-cppcoreguidelines-pro-bounds-array-to-pointer-decay,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers,-abseil-string-find-str-contains,-bugprone-unchecked-optional-access,-readability-use-anyofallof' WarningsAsErrors: '*' HeaderFilterRegex: '' AnalyzeTemporaryDtors: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8750f453e..45ddaea41 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,12 +36,17 @@ jobs: - name: Install dependencies if: matrix.arch == 'arm64' - run: sudo apt update ; sudo apt install -y cmake gcc-12 g++-12 git make curl + run: sudo apt update ; sudo apt install -y cmake git make curl + + - name: Install clang-{tidy,format} + run: | + sudo .github/workflows/scripts/llvm.sh 17 + sudo apt-get install -y clang-17 clang++-17 - name: CMake env: - CC: gcc-12 - CXX: g++-12 + CC: clang-17 + CXX: clang++-17 run: | cmake .. -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS="-fsanitize=address,leak,undefined -DASAN_BUILD" \ diff --git a/cmake/objects.cmake b/cmake/objects.cmake index d0891d881..edb90268b 100644 --- a/cmake/objects.cmake +++ b/cmake/objects.cmake @@ -20,10 +20,13 @@ set(LIBDDWAF_SOURCE ${libddwaf_SOURCE_DIR}/src/utils.cpp ${libddwaf_SOURCE_DIR}/src/waf.cpp ${libddwaf_SOURCE_DIR}/src/platform.cpp + ${libddwaf_SOURCE_DIR}/src/uuid.cpp + ${libddwaf_SOURCE_DIR}/src/action_mapper.cpp ${libddwaf_SOURCE_DIR}/src/exclusion/input_filter.cpp ${libddwaf_SOURCE_DIR}/src/exclusion/object_filter.cpp ${libddwaf_SOURCE_DIR}/src/exclusion/rule_filter.cpp ${libddwaf_SOURCE_DIR}/src/generator/extract_schema.cpp + ${libddwaf_SOURCE_DIR}/src/parser/actions_parser.cpp ${libddwaf_SOURCE_DIR}/src/parser/common.cpp ${libddwaf_SOURCE_DIR}/src/parser/parser.cpp ${libddwaf_SOURCE_DIR}/src/parser/parser_v1.cpp diff --git a/src/action_mapper.cpp b/src/action_mapper.cpp new file mode 100644 index 000000000..457267df9 --- /dev/null +++ b/src/action_mapper.cpp @@ -0,0 +1,85 @@ +// 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 "action_mapper.hpp" +#include "uuid.hpp" + +namespace ddwaf { +action_type action_type_from_string(std::string_view type) +{ + if (type == "block_request") { + return action_type::block_request; + } + if (type == "redirect_request") { + return action_type::redirect_request; + } + if (type == "generate_stack") { + return action_type::generate_stack; + } + if (type == "generate_schema") { + return action_type::generate_schema; + } + if (type == "monitor") { + return action_type::monitor; + } + // Unknown actions are valid, but provide no semantic value + return action_type::unknown; +} + +void action_mapper_builder::alias_default_action_to(std::string_view default_id, std::string alias) +{ + auto it = default_actions_.find(default_id); + if (it == default_actions_.end()) { + throw std::runtime_error( + "attempting to add alias to non-existent default action " + std::string(default_id)); + } + action_by_id_.emplace(std::move(alias), it->second); +} + +void action_mapper_builder::set_action( + std::string id, std::string type, std::unordered_map parameters) +{ + if (action_by_id_.find(id) != action_by_id_.end()) { + throw std::runtime_error("duplicate action '" + id + '\''); + } + + action_by_id_.emplace(std::move(id), + action_spec{action_type_from_string(type), std::move(type), std::move(parameters)}); +} + +[[nodiscard]] const action_spec &action_mapper_builder::get_default_action(std::string_view id) +{ + auto it = default_actions_.find(id); + if (it == default_actions_.end()) { + throw std::out_of_range("unknown action " + std::string(id)); + } + return it->second; +} + +std::shared_ptr action_mapper_builder::build_shared() +{ + return std::make_shared(build()); +} + +action_mapper action_mapper_builder::build() +{ + for (const auto &[action_id, action_spec] : default_actions_) { + action_by_id_.try_emplace(action_id, action_spec); + } + + return std::move(action_by_id_); +} + +const std::map> action_mapper_builder::default_actions_ = { + {"block", {action_type::block_request, "block_request", + {{"status_code", "403"}, {"type", "auto"}, {"grpc_status_code", "10"}}}}, + {"stack_trace", {action_type::generate_stack, "generate_stack", {}}}, + {"extract_schema", {action_type::generate_schema, "generate_schema", {}}}, + {"monitor", {action_type::monitor, "monitor", {}}}}; + +} // namespace ddwaf diff --git a/src/action_mapper.hpp b/src/action_mapper.hpp new file mode 100644 index 000000000..14275dee5 --- /dev/null +++ b/src/action_mapper.hpp @@ -0,0 +1,71 @@ +// 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 "utils.hpp" + +namespace ddwaf { + +enum class action_type : uint8_t { + none = 0, + unknown = 1, + generate_stack = 2, + generate_schema = 3, + monitor = 4, + block_request = 5, + redirect_request = 6, // Redirect must always be the last action + // as the value is used to serve as precedence +}; + +action_type action_type_from_string(std::string_view type); + +inline bool is_blocking_action(action_type type) +{ + return type == action_type::block_request || type == action_type::redirect_request; +} + +struct action_spec { + action_type type; + std::string type_str; + std::unordered_map parameters; +}; + +using action_mapper = std::map>; + +class action_mapper_builder { +public: + action_mapper_builder() = default; + ~action_mapper_builder() = default; + action_mapper_builder(const action_mapper_builder &) = delete; + action_mapper_builder(action_mapper_builder &&) = delete; + action_mapper_builder &operator=(const action_mapper_builder &) = delete; + action_mapper_builder &operator=(action_mapper_builder &&) = delete; + + void alias_default_action_to(std::string_view default_id, std::string alias); + + void set_action( + std::string id, std::string type, std::unordered_map parameters); + + [[nodiscard]] static const action_spec &get_default_action(std::string_view id); + + std::shared_ptr build_shared(); + + // Used for testing + action_mapper build(); + +protected: + std::map> action_by_id_; + static const std::map> default_actions_; +}; + +} // namespace ddwaf diff --git a/src/collection.cpp b/src/collection.cpp index e8c5b9a16..41076ad98 100644 --- a/src/collection.cpp +++ b/src/collection.cpp @@ -29,7 +29,7 @@ std::optional match_rule(rule *rule, const object_store &store, return std::nullopt; } - bool skip_actions = false; + action_type action_override = action_type::none; auto exclusion = policy.find(rule); if (exclusion.mode == exclusion::filter_mode::bypass) { DDWAF_DEBUG("Bypassing rule '{}'", id); @@ -38,7 +38,7 @@ std::optional match_rule(rule *rule, const object_store &store, if (exclusion.mode == exclusion::filter_mode::monitor) { DDWAF_DEBUG("Monitoring rule '{}'", id); - skip_actions = true; + action_override = action_type::monitor; } DDWAF_DEBUG("Evaluating rule '{}'", id); @@ -54,8 +54,8 @@ std::optional match_rule(rule *rule, const object_store &store, std::optional event; event = rule->match(store, rule_cache, exclusion.objects, dynamic_matchers, deadline); - if (event.has_value() && skip_actions) { - event->skip_actions = true; + if (event.has_value()) { + event->action_override = action_override; } return event; diff --git a/src/context.cpp b/src/context.cpp index b7de10d4e..f71a9b3a7 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -4,13 +4,10 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2021 Datadog, Inc. -#include - #include "context.hpp" #include "exception.hpp" #include "log.hpp" #include "utils.hpp" -#include "waf.hpp" namespace ddwaf { @@ -59,7 +56,7 @@ DDWAF_RET_CODE context::run(optional_ref persistent, return DDWAF_OK; } - const event_serializer serializer(*ruleset_->event_obfuscator); + const event_serializer serializer(*ruleset_->event_obfuscator, *ruleset_->actions); optional_ref derived; if (res.has_value()) { diff --git a/src/event.cpp b/src/event.cpp index 1f7c3a363..f7b60b792 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -6,9 +6,11 @@ #include +#include "action_mapper.hpp" #include "ddwaf.h" #include "event.hpp" #include "rule.hpp" +#include "uuid.hpp" namespace ddwaf { @@ -16,7 +18,7 @@ namespace { bool redact_match(const ddwaf::obfuscator &obfuscator, const condition_match &match) { - for (auto arg : match.args) { + for (const auto &arg : match.args) { for (const auto &key : arg.key_path) { if (obfuscator.is_sensitive_key(key)) { return true; @@ -56,7 +58,7 @@ void serialize_match(const condition_match &match, ddwaf_object &match_map, auto ddwaf_object highlight_arr; ddwaf_object_array(&highlight_arr); - for (auto highlight : match.highlights) { + for (const auto &highlight : match.highlights) { ddwaf_object_array_add(&highlight_arr, to_object(tmp, highlight, redact)); } @@ -103,61 +105,185 @@ void serialize_match(const condition_match &match, ddwaf_object &match_map, auto ddwaf_object_map_add(&match_map, "parameters", ¶meters); } -} // namespace +// This structure is used to collect and deduplicate all actions, keep track of the +// blocking action with the highest precedence and of the relevant stack trace ID +struct action_tracker { + // The blocking action refers to either a block_request or redirect_request + // action, the latter having precedence over the former. + std::string_view blocking_action{}; + action_type blocking_action_type{action_type::none}; -void event_serializer::serialize(const std::vector &events, ddwaf_result &output) const + // Stack trace ID + std::string stack_id{}; + + // This set contains all remaining actions other than the blocking action + std::unordered_set non_blocking_actions{}; + + const action_mapper &mapper; +}; + +void add_action_to_tracker(action_tracker &actions, std::string_view id, action_type type) +{ + if (is_blocking_action(type)) { + if (type > actions.blocking_action_type) { + // Only keep a single blocking action + actions.blocking_action_type = type; + actions.blocking_action = id; + } + } else { + if (type == action_type::generate_stack && actions.stack_id.empty()) { + // Stack trace actions require a dynamic stack ID, however we + // only provide a single stack ID per run + actions.stack_id = uuidv4_generate_pseudo(); + } + + actions.non_blocking_actions.emplace(id); + } +} + +void serialize_rule(const ddwaf::rule &rule, ddwaf_object &rule_map) +{ + ddwaf_object tmp; + ddwaf_object tags_map; + + ddwaf_object_map(&rule_map); + ddwaf_object_map(&tags_map); + + ddwaf_object_map_add(&rule_map, "id", to_object(tmp, rule.get_id())); + ddwaf_object_map_add(&rule_map, "name", to_object(tmp, rule.get_name())); + + for (const auto &[key, value] : rule.get_tags()) { + ddwaf_object_map_addl(&tags_map, key.c_str(), key.size(), to_object(tmp, value)); + } + ddwaf_object_map_add(&rule_map, "tags", &tags_map); +} + +void serialize_empty_rule(ddwaf_object &rule_map) { ddwaf_object tmp; + ddwaf_object tags_map; + + ddwaf_object_map(&tags_map); + ddwaf_object_map_add(&tags_map, "type", to_object(tmp, "")); + ddwaf_object_map_add(&tags_map, "category", to_object(tmp, "")); + + ddwaf_object_map(&rule_map); + ddwaf_object_map_add(&rule_map, "id", to_object(tmp, "")); + ddwaf_object_map_add(&rule_map, "name", to_object(tmp, "")); + ddwaf_object_map_add(&rule_map, "tags", &tags_map); +} + +void serialize_and_consolidate_rule_actions(const ddwaf::rule &rule, ddwaf_object &rule_map, + action_type action_override, action_tracker &actions, ddwaf_object &stack_id) +{ + const auto &rule_actions = rule.get_actions(); + if (rule_actions.empty()) { + return; + } + ddwaf_object tmp; + ddwaf_object actions_array; + ddwaf_object_array(&actions_array); + + if (action_override == action_type::monitor) { + ddwaf_object_array_add(&actions_array, to_object(tmp, "monitor")); + } + + for (const auto &action_id : rule_actions) { + auto action_it = actions.mapper.find(action_id); + if (action_it != actions.mapper.end()) { + const auto &[type, type_str, parameters] = action_it->second; + if (action_override == action_type::monitor && + (type == action_type::monitor || is_blocking_action(type))) { + // If the rule was in monitor mode, ignore blocking and monitor actions + continue; + } + + add_action_to_tracker(actions, action_id, type); + + // The stack ID will be generated when adding the action to the tracker + if (type == action_type::generate_stack && stack_id.type == DDWAF_OBJ_INVALID) { + to_object(stack_id, actions.stack_id); + } + } + // If an action is unspecified, add it and move on + ddwaf_object_array_add(&actions_array, to_object(tmp, action_id)); + } + + ddwaf_object_map_add(&rule_map, "on_match", &actions_array); +} + +void serialize_action(std::string_view id, ddwaf_object &action_map, const action_tracker &actions) +{ + auto action_it = actions.mapper.find(id); + if (action_it == actions.mapper.end()) { + // If the action has no spec, we don't report it + return; + } + + const auto &[type, type_str, parameters] = action_it->second; + if (type == action_type::monitor) { + return; + } + + ddwaf_object tmp; + ddwaf_object param_map; + ddwaf_object_map(¶m_map); + if (type != action_type::generate_stack) { + for (const auto &[k, v] : parameters) { + ddwaf_object_map_addl( + ¶m_map, k.c_str(), k.size(), ddwaf_object_stringl(&tmp, v.c_str(), v.size())); + } + } else { + ddwaf_object_map_addl( + ¶m_map, "stack_id", sizeof("stack_id") - 1, to_object(tmp, actions.stack_id)); + } + + ddwaf_object_map_addl(&action_map, type_str.data(), type_str.size(), ¶m_map); +} + +void serialize_actions(ddwaf_object &action_map, const action_tracker &actions) +{ + ddwaf_object_map(&action_map); + + if (actions.blocking_action_type != action_type::none) { + serialize_action(actions.blocking_action, action_map, actions); + } + + for (const auto &id : actions.non_blocking_actions) { + serialize_action(id, action_map, actions); + } +} + +} // namespace + +void event_serializer::serialize(const std::vector &events, ddwaf_result &output) const +{ if (events.empty()) { return; } - ddwaf_object_array(&output.events); - ddwaf_object_array(&output.actions); + action_tracker actions{.mapper = actions_}; - std::unordered_set all_actions; + ddwaf_object_array(&output.events); for (const auto &event : events) { ddwaf_object root_map; ddwaf_object rule_map; - ddwaf_object tags_map; ddwaf_object match_array; ddwaf_object_map(&root_map); - ddwaf_object_map(&rule_map); - ddwaf_object_map(&tags_map); ddwaf_object_array(&match_array); + ddwaf_object stack_id; + ddwaf_object_invalid(&stack_id); if (event.rule != nullptr) { - for (const auto &[key, value] : event.rule->get_tags()) { - ddwaf_object_map_addl(&tags_map, key.c_str(), key.size(), to_object(tmp, value)); - } - - ddwaf_object_map_add(&rule_map, "id", to_object(tmp, event.rule->get_id())); - ddwaf_object_map_add(&rule_map, "name", to_object(tmp, event.rule->get_name())); - - const auto &actions = event.rule->get_actions(); - if (!actions.empty()) { - ddwaf_object actions_array; - ddwaf_object_array(&actions_array); - if (!event.skip_actions) { - for (const auto &action : actions) { - all_actions.emplace(action); - ddwaf_object_array_add(&actions_array, to_object(tmp, action)); - } - } else { - ddwaf_object_array_add(&actions_array, to_object(tmp, "monitor")); - } - ddwaf_object_map_add(&rule_map, "on_match", &actions_array); - } + serialize_rule(*event.rule, rule_map); + serialize_and_consolidate_rule_actions( + *event.rule, rule_map, event.action_override, actions, stack_id); } else { // This will only be used for testing - ddwaf_object_map_add(&rule_map, "id", to_object(tmp, "")); - ddwaf_object_map_add(&rule_map, "name", to_object(tmp, "")); - ddwaf_object_map_add(&tags_map, "type", to_object(tmp, "")); - ddwaf_object_map_add(&tags_map, "category", to_object(tmp, "")); + serialize_empty_rule(rule_map); } - ddwaf_object_map_add(&rule_map, "tags", &tags_map); for (const auto &match : event.matches) { ddwaf_object match_map; @@ -168,17 +294,14 @@ void event_serializer::serialize(const std::vector &events, ddwaf_result ddwaf_object_map_add(&root_map, "rule", &rule_map); ddwaf_object_map_add(&root_map, "rule_matches", &match_array); + if (stack_id.type == DDWAF_OBJ_STRING) { + ddwaf_object_map_add(&root_map, "stack_id", &stack_id); + } ddwaf_object_array_add(&output.events, &root_map); } - if (!all_actions.empty()) { - for (const auto &action : all_actions) { - ddwaf_object string_action; - ddwaf_object_stringl(&string_action, action.data(), action.size()); - ddwaf_object_array_add(&output.actions, &string_action); - } - } + serialize_actions(output.actions, actions); } } // namespace ddwaf diff --git a/src/event.hpp b/src/event.hpp index 7e7e7ecbe..226c6b06d 100644 --- a/src/event.hpp +++ b/src/event.hpp @@ -8,6 +8,7 @@ #include +#include "action_mapper.hpp" #include "condition/base.hpp" #include "ddwaf.h" #include "obfuscator.hpp" @@ -20,21 +21,23 @@ struct event { const ddwaf::rule *rule{nullptr}; std::vector matches; bool ephemeral{false}; - bool skip_actions{false}; + action_type action_override{action_type::none}; }; using optional_event = std::optional; class event_serializer { public: - explicit event_serializer(const ddwaf::obfuscator &event_obfuscator) - : obfuscator_(event_obfuscator) + explicit event_serializer( + const ddwaf::obfuscator &event_obfuscator, const action_mapper &actions) + : obfuscator_(event_obfuscator), actions_(actions) {} void serialize(const std::vector &events, ddwaf_result &output) const; protected: const ddwaf::obfuscator &obfuscator_; + const action_mapper &actions_; }; } // namespace ddwaf diff --git a/src/parameter.cpp b/src/parameter.cpp index b6ed690be..284ab2a7e 100644 --- a/src/parameter.cpp +++ b/src/parameter.cpp @@ -271,4 +271,31 @@ parameter::operator std::vector() const return data; } +parameter::operator std::unordered_map() const +{ + if (type != DDWAF_OBJ_MAP) { + throw bad_cast("map", strtype(type)); + } + + if (array == nullptr || nbEntries == 0) { + return {}; + } + + std::unordered_map data; + data.reserve(nbEntries); + for (unsigned i = 0; i < nbEntries; i++) { + if (array[i].type != DDWAF_OBJ_STRING) { + throw malformed_object("item in map not a string, can't cast to string map"); + } + + std::string key{ + array[i].parameterName, static_cast(array[i].parameterNameLength)}; + std::string value{array[i].stringValue, static_cast(array[i].nbEntries)}; + + data.emplace(std::move(key), std::move(value)); + } + + return data; +} + } // namespace ddwaf diff --git a/src/parameter.hpp b/src/parameter.hpp index 7ef399742..d00cdf9d6 100644 --- a/src/parameter.hpp +++ b/src/parameter.hpp @@ -24,6 +24,7 @@ class parameter : public ddwaf_object { using string_set = std::unordered_set; parameter() = default; + // NOLINTNEXTLINE(google-explicit-constructor) parameter(const ddwaf_object &arg) : _ddwaf_object() { *((ddwaf_object *)this) = arg; } parameter(const parameter &) = default; @@ -45,6 +46,7 @@ class parameter : public ddwaf_object { explicit operator bool() const; explicit operator std::vector() const; explicit operator std::vector() const; + explicit operator std::unordered_map() const; ~parameter() = default; }; @@ -81,4 +83,8 @@ template <> struct parameter_traits> { static const char *name() { return "std::vector"; } }; +template <> struct parameter_traits> { + static const char *name() { return "std::unordered_map"; } +}; + } // namespace ddwaf diff --git a/src/parser/actions_parser.cpp b/src/parser/actions_parser.cpp new file mode 100644 index 000000000..3184361a1 --- /dev/null +++ b/src/parser/actions_parser.cpp @@ -0,0 +1,85 @@ +// 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 "log.hpp" +#include "parser/common.hpp" +#include "parser/parser.hpp" + +namespace ddwaf::parser::v2 { + +void validate_and_add_block(auto &id, auto &type, auto ¶meters, action_mapper_builder &builder) +{ + if (!parameters.contains("status_code") || !parameters.contains("grpc_status_code") || + !parameters.contains("type")) { + // If any of the parameters are missing, add the relevant default value + // We could also avoid the above check ... + auto default_params = action_mapper_builder::get_default_action("block"); + for (const auto &[k, v] : default_params.parameters) { parameters.try_emplace(k, v); } + } + builder.set_action(id, std::move(type), std::move(parameters)); +} + +void validate_and_add_redirect( + auto &id, auto &type, auto ¶meters, action_mapper_builder &builder) +{ + auto it = parameters.find("location"); + if (it == parameters.end() || it->second.empty()) { + builder.alias_default_action_to("block", id); + return; + } + + it = parameters.find("status_code"); + if (it != parameters.end()) { + auto [res, code] = ddwaf::from_string(it->second); + if (!res || code < 300 || code > 399) { + it->second = "303"; + } + } else { + parameters.emplace("status_code", "303"); + } + + builder.set_action(id, std::move(type), std::move(parameters)); +} + +std::shared_ptr parse_actions( + parameter::vector &actions_array, base_section_info &info) +{ + action_mapper_builder builder; + + for (unsigned i = 0; i < actions_array.size(); i++) { + const auto &node_param = actions_array[i]; + auto node = static_cast(node_param); + + std::string id; + try { + id = at(node, "id"); + auto type = at(node, "type"); + auto parameters = at>(node, "parameters"); + + // Block and redirect actions should be validated and aliased + if (type == "redirect_request") { + validate_and_add_redirect(id, type, parameters, builder); + } else if (type == "block_request") { + validate_and_add_block(id, type, parameters, builder); + } else { + builder.set_action(id, std::move(type), std::move(parameters)); + } + + DDWAF_DEBUG("Parsed action {} of type {}", id, type); + info.add_loaded(id); + } catch (const std::exception &e) { + if (id.empty()) { + id = index_to_id(i); + } + DDWAF_WARN("Failed to parse action '{}': {}", id, e.what()); + info.add_failed(id, e.what()); + } + } + + return builder.build_shared(); +} + +} // namespace ddwaf::parser::v2 diff --git a/src/parser/common.hpp b/src/parser/common.hpp index 30a15edb3..40d173c7c 100644 --- a/src/parser/common.hpp +++ b/src/parser/common.hpp @@ -44,4 +44,6 @@ T at(const parameter::map &map, const Key &key, const T &default_) std::optional transformer_from_string(std::string_view str); +inline std::string index_to_id(unsigned idx) { return "index:" + to_string(idx); } + } // namespace ddwaf::parser diff --git a/src/parser/parser.hpp b/src/parser/parser.hpp index 7c97c736b..5867be33f 100644 --- a/src/parser/parser.hpp +++ b/src/parser/parser.hpp @@ -47,5 +47,8 @@ processor_container parse_processors( indexer parse_scanners(parameter::vector &scanner_array, base_section_info &info); +std::shared_ptr parse_actions( + parameter::vector &actions_array, base_section_info &info); + } // namespace v2 } // namespace ddwaf::parser diff --git a/src/parser/parser_v2.cpp b/src/parser/parser_v2.cpp index 8d1e215bb..f037532a0 100644 --- a/src/parser/parser_v2.cpp +++ b/src/parser/parser_v2.cpp @@ -475,8 +475,6 @@ std::unique_ptr parse_scanner_matcher(const parameter::map &root) return std::move(matcher); } -std::string index_to_id(unsigned idx) { return "index:" + to_string(idx); } - void add_addresses_to_info(const address_container &addresses, base_section_info &info) { for (const auto &address : addresses.required) { info.add_required_address(address); } diff --git a/src/ruleset.hpp b/src/ruleset.hpp index 3bcbff6b6..cffbaa07a 100644 --- a/src/ruleset.hpp +++ b/src/ruleset.hpp @@ -6,16 +6,14 @@ #pragma once -#include #include -#include #include #include +#include "action_mapper.hpp" #include "collection.hpp" #include "exclusion/input_filter.hpp" #include "exclusion/rule_filter.hpp" -#include "mkmap.hpp" #include "obfuscator.hpp" #include "processor.hpp" #include "rule.hpp" @@ -138,6 +136,7 @@ struct ruleset { std::unordered_map> dynamic_matchers; std::vector> scanners; + std::shared_ptr actions; // The key used to organise collections is rule.type std::unordered_set collection_types; diff --git a/src/ruleset_builder.cpp b/src/ruleset_builder.cpp index 5b1cabd3a..550a896de 100644 --- a/src/ruleset_builder.cpp +++ b/src/ruleset_builder.cpp @@ -4,7 +4,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2021 Datadog, Inc. // -#include #include #include "exception.hpp" @@ -200,6 +199,7 @@ std::shared_ptr ruleset_builder::build(parameter::map &root, base_rules rs->insert_postprocessors(postprocessors_); rs->dynamic_matchers = dynamic_matchers_; rs->scanners = scanners_.items(); + rs->actions = actions_; rs->free_fn = free_fn_; rs->event_obfuscator = event_obfuscator_; @@ -216,7 +216,26 @@ ruleset_builder::change_state ruleset_builder::load(parameter::map &root, base_r info.set_ruleset_version(rules_version); } - auto it = root.find("rules"); + auto it = root.find("actions"); + if (it != root.end()) { + DDWAF_DEBUG("Parsing actions"); + auto §ion = info.add_section("actions"); + try { + // If the actions array is empty, an empty action mapper will be + // generated. Note that this mapper will still contain the default + // actions. + auto actions = static_cast(it->second); + actions_ = parser::v2::parse_actions(actions, section); + state = state | change_state::actions; + } catch (const std::exception &e) { + DDWAF_WARN("Failed to parse actions: {}", e.what()); + section.set_error(e.what()); + } + } else if (!actions_) { + actions_ = action_mapper_builder().build_shared(); + } + + it = root.find("rules"); if (it != root.end()) { DDWAF_DEBUG("Parsing base rules"); auto §ion = info.add_section("rules"); diff --git a/src/ruleset_builder.hpp b/src/ruleset_builder.hpp index 5a9a75c92..4b1a5bbeb 100644 --- a/src/ruleset_builder.hpp +++ b/src/ruleset_builder.hpp @@ -51,6 +51,7 @@ class ruleset_builder { data = 16, processors = 32, scanners = 64, + actions = 128, }; friend constexpr change_state operator|(change_state lhs, change_state rhs); @@ -103,6 +104,9 @@ class ruleset_builder { // Scanners indexer scanners_; + + // Actions + std::shared_ptr actions_; }; } // namespace ddwaf diff --git a/src/utils.hpp b/src/utils.hpp index 604cd00a9..b01f7fd0e 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -32,7 +32,7 @@ template using optional_ref = std::optional +#include +#include +#include +#include + +#include "uuid.hpp" + +namespace ddwaf { + +namespace { +// System clock is used to provide a more unique seed compared to the +// monotonic clock, which is backed by a steady clock, in practice it +// likely doesn't make a difference. +using clock = std::chrono::system_clock; + +auto init_rng() +{ + return std::mt19937_64{static_cast(clock::now().time_since_epoch().count())}; +} + +} // namespace + +std::string uuidv4_generate_pseudo() +{ + static thread_local auto rng = init_rng(); + + union { + // NOLINTNEXTLINE + uint8_t byte[16]; + // NOLINTNEXTLINE + uint64_t qword[2]; + } uuid_bytes{}; + + uuid_bytes.qword[0] = rng(); + uuid_bytes.qword[1] = rng(); + + uuid_bytes.byte[6] = 0x4F & (0x40 | uuid_bytes.byte[4]); + uuid_bytes.byte[8] = 0x1b; + + std::string result; + result.resize(36); + char *buffer = result.data(); + static constexpr auto hex_chars = std::array{"0123456789abcdef"}; + + for (int i = 0, j = 0; i < 16; ++i) { + if (i == 4 || i == 6 || i == 8 || i == 10) { + buffer[j++] = '-'; + } + buffer[j++] = hex_chars[(uuid_bytes.byte[i] >> 4) & 0x0F]; + buffer[j++] = hex_chars[uuid_bytes.byte[i] & 0x0F]; + } + return result; +} + +} // namespace ddwaf diff --git a/src/uuid.hpp b/src/uuid.hpp new file mode 100644 index 000000000..31f7b11b0 --- /dev/null +++ b/src/uuid.hpp @@ -0,0 +1,14 @@ +// 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 + +namespace ddwaf { + +std::string uuidv4_generate_pseudo(); + +} // namespace ddwaf diff --git a/src/waf.cpp b/src/waf.cpp index b907f3e23..a903a613f 100644 --- a/src/waf.cpp +++ b/src/waf.cpp @@ -28,6 +28,7 @@ waf::waf(ddwaf::parameter input, ddwaf::base_ruleset_info &info, ddwaf::object_l ddwaf::ruleset rs; rs.free_fn = free_fn; rs.event_obfuscator = event_obfuscator; + rs.actions = std::make_shared(); DDWAF_DEBUG("Parsing ruleset with schema version 1.x"); parser::v1::parse(input_map, info, rs, limits); ruleset_ = std::make_shared(std::move(rs)); diff --git a/tests/action_mapper_builder_test.cpp b/tests/action_mapper_builder_test.cpp new file mode 100644 index 000000000..aef24f43c --- /dev/null +++ b/tests/action_mapper_builder_test.cpp @@ -0,0 +1,197 @@ +// 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 "action_mapper.hpp" +#include "test.hpp" + +using namespace ddwaf; +using namespace std::literals; + +namespace { + +TEST(TestActionMapperBuilder, TypeToString) +{ + EXPECT_EQ(action_type_from_string("block_request"), action_type::block_request); + EXPECT_EQ(action_type_from_string("redirect_request"), action_type::redirect_request); + EXPECT_EQ(action_type_from_string("generate_stack"), action_type::generate_stack); + EXPECT_EQ(action_type_from_string("generate_schema"), action_type::generate_schema); + EXPECT_EQ(action_type_from_string("monitor"), action_type::monitor); +} + +TEST(TestActionMapperBuilder, DefaultActions) +{ + action_mapper actions = action_mapper_builder().build(); + + EXPECT_TRUE(actions.contains("block")); + EXPECT_TRUE(actions.contains("stack_trace")); + EXPECT_TRUE(actions.contains("extract_schema")); + EXPECT_TRUE(actions.contains("monitor")); + + { + const auto &action = actions.at("block"); + EXPECT_EQ(action.type, action_type::block_request); + EXPECT_STR(action.type_str, "block_request"); + + EXPECT_EQ(action.parameters.size(), 3); + EXPECT_STRV(action.parameters.at("status_code"), "403"); + EXPECT_STRV(action.parameters.at("type"), "auto"); + EXPECT_STRV(action.parameters.at("grpc_status_code"), "10"); + } + + { + const auto &action = actions.at("stack_trace"); + EXPECT_EQ(action.type, action_type::generate_stack); + EXPECT_STR(action.type_str, "generate_stack"); + EXPECT_EQ(action.parameters.size(), 0); + } + + { + const auto &action = actions.at("extract_schema"); + EXPECT_EQ(action.type, action_type::generate_schema); + EXPECT_STR(action.type_str, "generate_schema"); + EXPECT_EQ(action.parameters.size(), 0); + } + + { + const auto &action = actions.at("monitor"); + EXPECT_EQ(action.type, action_type::monitor); + EXPECT_STR(action.type_str, "monitor"); + EXPECT_EQ(action.parameters.size(), 0); + } +} + +TEST(TestActionMapperBuilder, UnknownAction) +{ + action_mapper actions = action_mapper_builder().build(); + + EXPECT_FALSE(actions.contains("blorck")); + EXPECT_FALSE(actions.contains("stack_traces")); + EXPECT_FALSE(actions.contains("extract_scherma")); + EXPECT_FALSE(actions.contains("mornitor")); + + EXPECT_THROW(auto _ = actions.at("blorck"), std::out_of_range); + EXPECT_THROW(auto _ = actions.at("stack_traces"), std::out_of_range); + EXPECT_THROW(auto _ = actions.at("extract_scherma"), std::out_of_range); + EXPECT_THROW(auto _ = actions.at("mornitor"), std::out_of_range); +} + +TEST(TestActionMapperBuilder, SetAction) +{ + action_mapper_builder builder; + builder.set_action( + "redirect", "redirect_request", {{"status_code", "33"}, {"location", "datadoghq"}}); + + auto actions = builder.build(); + + EXPECT_TRUE(actions.contains("block")); + EXPECT_TRUE(actions.contains("stack_trace")); + EXPECT_TRUE(actions.contains("extract_schema")); + EXPECT_TRUE(actions.contains("monitor")); + + EXPECT_TRUE(actions.contains("redirect")); + + { + const auto &action = actions.at("redirect"); + EXPECT_EQ(action.type, action_type::redirect_request); + EXPECT_STR(action.type_str, "redirect_request"); + + EXPECT_EQ(action.parameters.size(), 2); + EXPECT_STRV(action.parameters.at("status_code"), "33"); + EXPECT_STRV(action.parameters.at("location"), "datadoghq"); + } +} + +TEST(TestActionMapperBuilder, SetActionAlias) +{ + action_mapper_builder builder; + builder.alias_default_action_to("block", "redirect"); + + auto actions = builder.build(); + + EXPECT_TRUE(actions.contains("block")); + EXPECT_TRUE(actions.contains("stack_trace")); + EXPECT_TRUE(actions.contains("extract_schema")); + EXPECT_TRUE(actions.contains("monitor")); + + EXPECT_TRUE(actions.contains("redirect")); + + { + const auto &action = actions.at("redirect"); + EXPECT_EQ(action.type, action_type::block_request); + EXPECT_STR(action.type_str, "block_request"); + + EXPECT_EQ(action.parameters.size(), 3); + EXPECT_STRV(action.parameters.at("status_code"), "403"); + EXPECT_STRV(action.parameters.at("type"), "auto"); + EXPECT_STRV(action.parameters.at("grpc_status_code"), "10"); + } +} + +TEST(TestActionMapperBuilder, SetInvalidActionAlias) +{ + action_mapper_builder builder; + EXPECT_THROW(builder.alias_default_action_to("blorck", "redirect"), std::runtime_error); +} + +TEST(TestActionMapperBuilder, OverrideDefaultAction) +{ + action_mapper_builder builder; + builder.set_action( + "block", "redirect_request", {{"status_code", "33"}, {"location", "datadoghq"}}); + + auto actions = builder.build(); + + EXPECT_TRUE(actions.contains("block")); + EXPECT_TRUE(actions.contains("stack_trace")); + EXPECT_TRUE(actions.contains("extract_schema")); + EXPECT_TRUE(actions.contains("monitor")); + + { + const auto &action = actions.at("block"); + EXPECT_EQ(action.type, action_type::redirect_request); + EXPECT_STR(action.type_str, "redirect_request"); + + EXPECT_EQ(action.parameters.size(), 2); + EXPECT_STRV(action.parameters.at("status_code"), "33"); + EXPECT_STRV(action.parameters.at("location"), "datadoghq"); + } +} + +TEST(TestActionMapperBuilder, DuplicateAction) +{ + action_mapper_builder builder; + builder.set_action( + "redirect", "redirect_request", {{"status_code", "33"}, {"location", "datadoghq"}}); + EXPECT_THROW(builder.set_action("redirect", "redirect_request", {}), std::runtime_error); +} + +TEST(TestActionMapperBuilder, DuplicateDefaultAction) +{ + action_mapper_builder builder; + builder.set_action( + "block", "redirect_request", {{"status_code", "33"}, {"location", "datadoghq"}}); + EXPECT_THROW(builder.set_action("block", "redirect_request", {}), std::runtime_error); + auto actions = builder.build(); + + EXPECT_TRUE(actions.contains("block")); + EXPECT_TRUE(actions.contains("stack_trace")); + EXPECT_TRUE(actions.contains("extract_schema")); + EXPECT_TRUE(actions.contains("monitor")); + + { + const auto &action = actions.at("block"); + EXPECT_EQ(action.type, action_type::redirect_request); + EXPECT_STR(action.type_str, "redirect_request"); + + EXPECT_EQ(action.parameters.size(), 2); + EXPECT_STRV(action.parameters.at("status_code"), "33"); + EXPECT_STRV(action.parameters.at("location"), "datadoghq"); + } +} + +} // namespace diff --git a/tests/context_test.cpp b/tests/context_test.cpp index 4b6d41f2b..ee70b8fee 100644 --- a/tests/context_test.cpp +++ b/tests/context_test.cpp @@ -4,6 +4,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2021 Datadog, Inc. +#include "action_mapper.hpp" #include "context.hpp" #include "exception.hpp" #include "exclusion/input_filter.hpp" @@ -122,10 +123,9 @@ TEST(TestContext, PreprocessorEval) EXPECT_CALL(*proc, eval(_, _, _, _)).InSequence(seq); EXPECT_CALL(*rule, match(_, _, _, _, _)).InSequence(seq).WillOnce(Return(std::nullopt)); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ruleset->preprocessors.emplace("id", proc); - ruleset->event_obfuscator = std::make_shared(); ddwaf::context ctx(ruleset); @@ -155,10 +155,9 @@ TEST(TestContext, PostprocessorEval) EXPECT_CALL(*rule, match(_, _, _, _, _)).InSequence(seq).WillOnce(Return(std::nullopt)); EXPECT_CALL(*proc, eval(_, _, _, _)).InSequence(seq); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ruleset->postprocessors.emplace("id", proc); - ruleset->event_obfuscator = std::make_shared(); ddwaf::context ctx(ruleset); @@ -182,9 +181,8 @@ TEST(TestContext, SkipRuleNoTargets) auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); - ruleset->event_obfuscator = std::make_shared(); EXPECT_CALL(*rule, match(_, _, _, _, _)).Times(0); @@ -210,7 +208,7 @@ TEST(TestContext, MatchTimeout) auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ddwaf::timer deadline{0s}; @@ -237,7 +235,7 @@ TEST(TestContext, NoMatch) auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ddwaf::timer deadline{2s}; @@ -265,7 +263,7 @@ TEST(TestContext, Match) auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ddwaf::timer deadline{2s}; @@ -283,7 +281,7 @@ TEST(TestContext, Match) TEST(TestContext, MatchMultipleRulesInCollectionSingleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -347,7 +345,7 @@ TEST(TestContext, MatchMultipleRulesInCollectionSingleRun) TEST(TestContext, MatchMultipleRulesWithPrioritySingleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -422,7 +420,7 @@ TEST(TestContext, MatchMultipleRulesWithPrioritySingleRun) TEST(TestContext, MatchMultipleRulesInCollectionDoubleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -498,7 +496,7 @@ TEST(TestContext, MatchMultipleRulesInCollectionDoubleRun) TEST(TestContext, MatchMultipleRulesWithPriorityDoubleRunPriorityLast) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -595,7 +593,7 @@ TEST(TestContext, MatchMultipleRulesWithPriorityDoubleRunPriorityLast) TEST(TestContext, MatchMultipleRulesWithPriorityDoubleRunPriorityFirst) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -674,7 +672,7 @@ TEST(TestContext, MatchMultipleRulesWithPriorityDoubleRunPriorityFirst) TEST(TestContext, MatchMultipleRulesWithPriorityUntilAllActionsMet) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -769,7 +767,7 @@ TEST(TestContext, MatchMultipleRulesWithPriorityUntilAllActionsMet) TEST(TestContext, MatchMultipleCollectionsSingleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -816,7 +814,7 @@ TEST(TestContext, MatchMultipleCollectionsSingleRun) TEST(TestContext, MatchMultiplePriorityCollectionsSingleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -865,7 +863,7 @@ TEST(TestContext, MatchMultiplePriorityCollectionsSingleRun) TEST(TestContext, MatchMultipleCollectionsDoubleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -924,7 +922,7 @@ TEST(TestContext, MatchMultipleCollectionsDoubleRun) TEST(TestContext, MatchMultiplePriorityCollectionsDoubleRun) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -985,7 +983,7 @@ TEST(TestContext, MatchMultiplePriorityCollectionsDoubleRun) TEST(TestContext, SkipRuleFilterNoTargets) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1018,7 +1016,6 @@ TEST(TestContext, SkipRuleFilterNoTargets) ruleset->insert_filter(filter); } - ruleset->event_obfuscator = std::make_shared(); EXPECT_CALL(*rule, match(_, _, _, _, _)).Times(0); EXPECT_CALL(*filter, match(_, _, _)).Times(0); @@ -1035,7 +1032,7 @@ TEST(TestContext, SkipRuleFilterNoTargets) TEST(TestContext, SkipRuleButNotRuleFilterNoTargets) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1068,7 +1065,6 @@ TEST(TestContext, SkipRuleButNotRuleFilterNoTargets) ruleset->insert_filter(filter); } - ruleset->event_obfuscator = std::make_shared(); EXPECT_CALL(*rule, match(_, _, _, _, _)).Times(0); EXPECT_CALL(*filter, match(_, _, _)).WillOnce(Return(std::nullopt)); @@ -1085,7 +1081,7 @@ TEST(TestContext, SkipRuleButNotRuleFilterNoTargets) TEST(TestContext, RuleFilterWithCondition) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1137,7 +1133,7 @@ TEST(TestContext, RuleFilterWithCondition) TEST(TestContext, RuleFilterWithEphemeralConditionMatch) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1198,7 +1194,7 @@ TEST(TestContext, RuleFilterWithEphemeralConditionMatch) TEST(TestContext, OverlappingRuleFiltersEphemeralBypassPersistentMonitor) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1242,8 +1238,6 @@ TEST(TestContext, OverlappingRuleFiltersEphemeralBypassPersistentMonitor) ruleset->insert_filter(filter); } - ruleset->event_obfuscator = std::make_shared(); - ddwaf::test::context ctx(ruleset); { @@ -1277,7 +1271,7 @@ TEST(TestContext, OverlappingRuleFiltersEphemeralBypassPersistentMonitor) TEST(TestContext, OverlappingRuleFiltersEphemeralMonitorPersistentBypass) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1321,8 +1315,6 @@ TEST(TestContext, OverlappingRuleFiltersEphemeralMonitorPersistentBypass) ruleset->insert_filter(filter); } - ruleset->event_obfuscator = std::make_shared(); - ddwaf::test::context ctx(ruleset); { @@ -1353,7 +1345,7 @@ TEST(TestContext, OverlappingRuleFiltersEphemeralMonitorPersistentBypass) TEST(TestContext, RuleFilterTimeout) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1400,7 +1392,7 @@ TEST(TestContext, RuleFilterTimeout) TEST(TestContext, NoRuleFilterWithCondition) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1451,7 +1443,7 @@ TEST(TestContext, NoRuleFilterWithCondition) TEST(TestContext, MultipleRuleFiltersNonOverlappingRules) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule constexpr unsigned num_rules = 9; @@ -1524,7 +1516,7 @@ TEST(TestContext, MultipleRuleFiltersNonOverlappingRules) TEST(TestContext, MultipleRuleFiltersOverlappingRules) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule constexpr unsigned num_rules = 9; @@ -1634,7 +1626,7 @@ TEST(TestContext, MultipleRuleFiltersOverlappingRules) TEST(TestContext, MultipleRuleFiltersNonOverlappingRulesWithConditions) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule constexpr unsigned num_rules = 10; @@ -1721,7 +1713,7 @@ TEST(TestContext, MultipleRuleFiltersNonOverlappingRulesWithConditions) TEST(TestContext, MultipleRuleFiltersOverlappingRulesWithConditions) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule constexpr unsigned num_rules = 10; @@ -1810,7 +1802,7 @@ TEST(TestContext, MultipleRuleFiltersOverlappingRulesWithConditions) TEST(TestContext, SkipInputFilterNoTargets) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1840,7 +1832,6 @@ TEST(TestContext, SkipInputFilterNoTargets) "1", std::make_shared(), std::move(eval_filters), std::move(obj_filter)); ruleset->insert_filter(filter); } - ruleset->event_obfuscator = std::make_shared(); EXPECT_CALL(*rule, match(_, _, _, _, _)).Times(0); EXPECT_CALL(*filter, match(_, _, _)).Times(0); @@ -1857,7 +1848,7 @@ TEST(TestContext, SkipInputFilterNoTargets) TEST(TestContext, SkipRuleButNotInputFilterNoTargets) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); // Generate rule std::shared_ptr rule; @@ -1887,7 +1878,6 @@ TEST(TestContext, SkipRuleButNotInputFilterNoTargets) "1", std::make_shared(), std::move(eval_filters), std::move(obj_filter)); ruleset->insert_filter(filter); } - ruleset->event_obfuscator = std::make_shared(); EXPECT_CALL(*rule, match(_, _, _, _, _)).Times(0); EXPECT_CALL(*filter, match(_, _, _)).WillOnce(Return(std::nullopt)); @@ -1921,7 +1911,7 @@ TEST(TestContext, InputFilterExclude) auto filter = std::make_shared( "1", std::make_shared(), std::move(eval_filters), std::move(obj_filter)); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ruleset->insert_filter(filter); @@ -1961,10 +1951,9 @@ TEST(TestContext, InputFilterExcludeEphemeral) auto filter = std::make_shared( "1", std::make_shared(), std::move(eval_filters), std::move(obj_filter)); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ruleset->insert_filter(filter); - ruleset->event_obfuscator = std::make_shared(); ddwaf::test::context ctx(ruleset); @@ -2013,10 +2002,9 @@ TEST(TestContext, InputFilterExcludeEphemeralReuseObject) auto filter = std::make_shared( "1", std::make_shared(), std::move(eval_filters), std::move(obj_filter)); - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); ruleset->insert_rule(rule); ruleset->insert_filter(filter); - ruleset->event_obfuscator = std::make_shared(); ruleset->free_fn = nullptr; ddwaf::test::context ctx(ruleset); @@ -2047,7 +2035,7 @@ TEST(TestContext, InputFilterExcludeRule) std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); @@ -2102,7 +2090,7 @@ TEST(TestContext, InputFilterExcludeRuleEphemeral) std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); @@ -2152,7 +2140,7 @@ TEST(TestContext, InputFilterMonitorRuleEphemeral) std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); @@ -2207,7 +2195,7 @@ TEST(TestContext, InputFilterExcluderRuleEphemeralAndPersistent) std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); @@ -2268,7 +2256,7 @@ TEST(TestContext, InputFilterMonitorRuleEphemeralAndPersistent) std::unordered_map tags{{"type", "type"}, {"category", "category"}}; - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); auto rule = std::make_shared("id", "name", std::move(tags), builder.build()); ruleset->insert_rule(rule); @@ -2326,7 +2314,7 @@ TEST(TestContext, InputFilterMonitorRuleEphemeralAndPersistent) TEST(TestContext, InputFilterWithCondition) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -2415,8 +2403,7 @@ TEST(TestContext, InputFilterWithCondition) TEST(TestContext, InputFilterWithEphemeralCondition) { - auto ruleset = std::make_shared(); - ruleset->event_obfuscator = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -2476,7 +2463,7 @@ TEST(TestContext, InputFilterWithEphemeralCondition) TEST(TestContext, InputFilterMultipleRules) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -2589,7 +2576,7 @@ TEST(TestContext, InputFilterMultipleRules) TEST(TestContext, InputFilterMultipleRulesMultipleFilters) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); @@ -2715,7 +2702,7 @@ TEST(TestContext, InputFilterMultipleRulesMultipleFilters) TEST(TestContext, InputFilterMultipleRulesMultipleFiltersMultipleObjects) { - auto ruleset = std::make_shared(); + auto ruleset = test::get_default_ruleset(); { test::expression_builder builder(1); builder.start_condition(); diff --git a/tests/event_serializer_test.cpp b/tests/event_serializer_test.cpp index 266996eb8..36d9227bf 100644 --- a/tests/event_serializer_test.cpp +++ b/tests/event_serializer_test.cpp @@ -17,8 +17,9 @@ namespace { TEST(TestEventSerializer, SerializeNothing) { + ddwaf::action_mapper actions; ddwaf::obfuscator obfuscator; - ddwaf::event_serializer serializer(obfuscator); + ddwaf::event_serializer serializer(obfuscator, actions); ddwaf_result output = DDWAF_RESULT_INITIALISER; serializer.serialize({}, output); @@ -29,7 +30,7 @@ TEST(TestEventSerializer, SerializeNothing) TEST(TestEventSerializer, SerializeEmptyEvent) { ddwaf::obfuscator obfuscator; - ddwaf::event_serializer serializer(obfuscator); + ddwaf::event_serializer serializer(obfuscator, action_mapper_builder().build()); ddwaf_result output = DDWAF_RESULT_INITIALISER; serializer.serialize({ddwaf::event{}}, output); @@ -41,22 +42,25 @@ TEST(TestEventSerializer, SerializeEmptyEvent) TEST(TestEventSerializer, SerializeSingleEventSingleMatch) { ddwaf::rule rule{"xasd1022", "random rule", {{"type", "test"}, {"category", "none"}}, - std::make_shared(), {"block", "monitor"}}; + std::make_shared(), {"block", "monitor_request"}}; ddwaf::event event; event.rule = &rule; event.matches = {{{{"input", "value", "query", {"root", "key"}}}, {"val"}, "random", "val"}}; + ddwaf::action_mapper_builder builder; + builder.set_action("monitor_request", "monitor_request", {}); + auto actions = builder.build(); + ddwaf::obfuscator obfuscator; - ddwaf::event_serializer serializer(obfuscator); + ddwaf::event_serializer serializer(obfuscator, actions); ddwaf_result output = DDWAF_RESULT_INITIALISER; serializer.serialize({event}, output); - EXPECT_EVENTS(output, {.id = "xasd1022", .name = "random rule", .tags = {{"type", "test"}, {"category", "none"}}, - .actions = {"block", "monitor"}, + .actions = {"block", "monitor_request"}, .matches = {{.op = "random", .op_value = "val", .highlight = "val", @@ -65,7 +69,9 @@ TEST(TestEventSerializer, SerializeSingleEventSingleMatch) .address = "query", .path = {"root", "key"}}}}}}); - EXPECT_THAT(output.actions, WithActions({"block", "monitor"})); + EXPECT_ACTIONS(output, + {{"block_request", {{"status_code", "403"}, {"grpc_status_code", "10"}, {"type", "auto"}}}, + {"monitor_request", {}}}); ddwaf_result_free(&output); } @@ -73,7 +79,7 @@ TEST(TestEventSerializer, SerializeSingleEventSingleMatch) TEST(TestEventSerializer, SerializeSingleEventMultipleMatches) { ddwaf::rule rule{"xasd1022", "random rule", {{"type", "test"}, {"category", "none"}}, - std::make_shared(), {"block", "monitor"}}; + std::make_shared(), {"block", "monitor_request"}}; ddwaf::event event; event.rule = &rule; @@ -82,8 +88,12 @@ TEST(TestEventSerializer, SerializeSingleEventMultipleMatches) {{{"input", "192.168.0.1", "client.ip"}}, {"192.168.0.1"}, "ip_match", ""}, {{{"input", "