From 8593bcc60f48449477bf3efa883d43b2f9b6f9b3 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:46:09 +0100 Subject: [PATCH 1/8] Threshold rule --- src/threshold_rule.hpp | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/threshold_rule.hpp diff --git a/src/threshold_rule.hpp b/src/threshold_rule.hpp new file mode 100644 index 000000000..0b3f50caf --- /dev/null +++ b/src/threshold_rule.hpp @@ -0,0 +1,57 @@ +// 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 "clock.hpp" +#include "event.hpp" +#include "exclusion/common.hpp" +#include "expression.hpp" +#include "iterator.hpp" +#include "matcher/base.hpp" +#include "object_store.hpp" + +namespace ddwaf { + +class threshold_rule { +public: + using cache_type = expression::cache_type; + + threshold_rule(std::string id, std::string name, std::unordered_map tags, + std::shared_ptr expr, uint64_t threshold, const std::chrono::milliseconds &period, + std::vector actions = {}, bool enabled = true) + : enabled_(enabled), id_(std::move(id)), name_(std::move(name)), + tags_(std::move(tags)), expr_(std::move(expr)), threshold_(threshold), + period_(period), actions_(std::move(actions)) + { + if (!expr_) { + throw std::invalid_argument("threshold rule constructed with null expression"); + } + } + ~threshold_rule() = default; + threshold_rule(const threshold_rule &) = delete; + threshold_rule &operator=(const threshold_rule &) = delete; + threshold_rule(threshold_rule &&rhs) noexcept = default; + threshold_rule &operator=(threshold_rule &&rhs) noexcept = default; + +protected: + bool enabled_{true}; + std::string id_; + std::string name_; + std::unordered_map tags_; + std::shared_ptr expr_; + uint64_t threshold_; + std::chrono::milliseconds period_; + std::vector actions_; +}; + +} // namespace ddwaf From 0050aa43a9ca9e25b1e44c3a31b97efbc3abf0a2 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:09:01 +0100 Subject: [PATCH 2/8] Add threshold rules and timed counter --- cmake/objects.cmake | 1 + src/event.cpp | 4 +- src/event.hpp | 4 +- src/rule.cpp | 35 +++++++ src/rule.hpp | 180 ++++++++++++++++++++++++++--------- src/threshold_rule.hpp | 57 ----------- src/timed_counter.hpp | 147 ++++++++++++++++++++++++++++ tests/timed_counter_test.cpp | 102 ++++++++++++++++++++ 8 files changed, 422 insertions(+), 108 deletions(-) create mode 100644 src/rule.cpp delete mode 100644 src/threshold_rule.hpp create mode 100644 src/timed_counter.hpp create mode 100644 tests/timed_counter_test.cpp diff --git a/cmake/objects.cmake b/cmake/objects.cmake index edb90268b..4960c6eb6 100644 --- a/cmake/objects.cmake +++ b/cmake/objects.cmake @@ -21,6 +21,7 @@ set(LIBDDWAF_SOURCE ${libddwaf_SOURCE_DIR}/src/waf.cpp ${libddwaf_SOURCE_DIR}/src/platform.cpp ${libddwaf_SOURCE_DIR}/src/uuid.cpp + ${libddwaf_SOURCE_DIR}/src/rule.cpp ${libddwaf_SOURCE_DIR}/src/action_mapper.cpp ${libddwaf_SOURCE_DIR}/src/exclusion/input_filter.cpp ${libddwaf_SOURCE_DIR}/src/exclusion/object_filter.cpp diff --git a/src/event.cpp b/src/event.cpp index f7b60b792..c4d249fdb 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -141,7 +141,7 @@ void add_action_to_tracker(action_tracker &actions, std::string_view id, action_ } } -void serialize_rule(const ddwaf::rule &rule, ddwaf_object &rule_map) +void serialize_rule(const ddwaf::base_rule &rule, ddwaf_object &rule_map) { ddwaf_object tmp; ddwaf_object tags_map; @@ -173,7 +173,7 @@ void serialize_empty_rule(ddwaf_object &rule_map) ddwaf_object_map_add(&rule_map, "tags", &tags_map); } -void serialize_and_consolidate_rule_actions(const ddwaf::rule &rule, ddwaf_object &rule_map, +void serialize_and_consolidate_rule_actions(const ddwaf::base_rule &rule, ddwaf_object &rule_map, action_type action_override, action_tracker &actions, ddwaf_object &stack_id) { const auto &rule_actions = rule.get_actions(); diff --git a/src/event.hpp b/src/event.hpp index 226c6b06d..32bc77084 100644 --- a/src/event.hpp +++ b/src/event.hpp @@ -15,10 +15,10 @@ namespace ddwaf { -class rule; +class base_rule; struct event { - const ddwaf::rule *rule{nullptr}; + const ddwaf::base_rule *rule{nullptr}; std::vector matches; bool ephemeral{false}; action_type action_override{action_type::none}; diff --git a/src/rule.cpp b/src/rule.cpp new file mode 100644 index 000000000..090463555 --- /dev/null +++ b/src/rule.cpp @@ -0,0 +1,35 @@ +// 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 "rule.hpp" + +namespace ddwaf { + +std::optional threshold_rule::eval(const object_store &store, global_cache_type &gcache, + local_cache_type &lcache, ddwaf::timer &deadline) const +{ + expression::cache_type expr_cache; + auto res = expr_->eval(expr_cache, store, {}, {}, deadline); + if (!res.outcome) { + return std::nullopt; + } + + return {ddwaf::event{this, expression::get_matches(lcache), res.ephemeral}}; +} + +std::optional indexed_threshold_rule::eval(const object_store &store, + global_cache_type &gcache, local_cache_type &lcache, ddwaf::timer &deadline) const +{ + expression::cache_type expr_cache; + auto res = expr_->eval(expr_cache, store, {}, {}, deadline); + if (!res.outcome) { + return std::nullopt; + } + + return {ddwaf::event{this, expression::get_matches(lcache), res.ephemeral}}; +} + +} // namespace ddwaf diff --git a/src/rule.hpp b/src/rule.hpp index 50d1cdfdb..49dc60fc4 100644 --- a/src/rule.hpp +++ b/src/rule.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include "clock.hpp" @@ -19,48 +21,84 @@ #include "iterator.hpp" #include "matcher/base.hpp" #include "object_store.hpp" +#include "timed_counter.hpp" namespace ddwaf { -class rule { +class base_rule { public: - enum class source_type : uint8_t { base = 1, user = 2 }; - - using cache_type = expression::cache_type; - - rule(std::string id, std::string name, std::unordered_map tags, + base_rule(std::string id, std::string name, std::unordered_map tags, std::shared_ptr expr, std::vector actions = {}, - bool enabled = true, source_type source = source_type::base) - : enabled_(enabled), source_(source), id_(std::move(id)), name_(std::move(name)), - tags_(std::move(tags)), expr_(std::move(expr)), actions_(std::move(actions)) + bool enabled = true) + : enabled_(enabled), id_(std::move(id)), name_(std::move(name)), tags_(std::move(tags)), + expr_(std::move(expr)), actions_(std::move(actions)) { if (!expr_) { throw std::invalid_argument("rule constructed with null expression"); } } - rule(const rule &) = delete; - rule &operator=(const rule &) = delete; + base_rule(const base_rule &) = delete; + base_rule &operator=(const base_rule &) = delete; - rule(rule &&rhs) noexcept - : enabled_(rhs.enabled_), source_(rhs.source_), id_(std::move(rhs.id_)), - name_(std::move(rhs.name_)), tags_(std::move(rhs.tags_)), expr_(std::move(rhs.expr_)), - actions_(std::move(rhs.actions_)) - {} + base_rule(base_rule &&rhs) noexcept = default; + base_rule &operator=(base_rule &&rhs) noexcept = default; - rule &operator=(rule &&rhs) noexcept + virtual ~base_rule() = default; + + [[nodiscard]] bool is_enabled() const { return enabled_; } + void toggle(bool value) { enabled_ = value; } + + const std::string &get_id() const { return id_; } + const std::string &get_name() const { return name_; } + + std::string_view get_tag(const std::string &tag) const { - enabled_ = rhs.enabled_; - source_ = rhs.source_; - id_ = std::move(rhs.id_); - name_ = std::move(rhs.name_); - tags_ = std::move(rhs.tags_); - expr_ = std::move(rhs.expr_); - actions_ = std::move(rhs.actions_); - return *this; + auto it = tags_.find(tag); + return it == tags_.end() ? std::string_view() : it->second; } - virtual ~rule() = default; + const std::unordered_map &get_tags() const { return tags_; } + + const std::vector &get_actions() const { return actions_; } + + void get_addresses(std::unordered_map &addresses) const + { + return expr_->get_addresses(addresses); + } + + void set_actions(std::vector new_actions) { actions_ = std::move(new_actions); } + +protected: + bool enabled_{true}; + std::string id_; + std::string name_; + std::unordered_map tags_; + std::shared_ptr expr_; + std::vector actions_; +}; + +class rule : public base_rule { +public: + enum class source_type : uint8_t { base = 1, user = 2 }; + + using cache_type = expression::cache_type; + + rule(std::string id, std::string name, std::unordered_map tags, + std::shared_ptr expr, std::vector actions = {}, + bool enabled = true, source_type source = source_type::base) + : base_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), + std::move(actions), enabled), + source_(source) + {} + + rule(const rule &) = delete; + rule &operator=(const rule &) = delete; + + rule(rule &&rhs) noexcept = default; + rule &operator=(rule &&rhs) noexcept = default; + + ~rule() override = default; virtual std::optional match(const object_store &store, cache_type &cache, const exclusion::object_set_ref &objects_excluded, @@ -80,38 +118,86 @@ class rule { return {ddwaf::event{this, expression::get_matches(cache), res.ephemeral}}; } - [[nodiscard]] bool is_enabled() const { return enabled_; } - void toggle(bool value) { enabled_ = value; } - source_type get_source() const { return source_; } - const std::string &get_id() const { return id_; } - const std::string &get_name() const { return name_; } - std::string_view get_tag(const std::string &tag) const +protected: + source_type source_; +}; + +class threshold_rule : public base_rule { +public: + struct evaluation_criteria { + std::size_t threshold; + std::chrono::milliseconds period; + }; + + using local_cache_type = expression::cache_type; + using global_cache_type = timed_counter_ms; + + threshold_rule(std::string id, std::string name, + std::unordered_map tags, std::shared_ptr expr, + evaluation_criteria criteria, std::vector actions = {}, bool enabled = true) + : base_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), + std::move(actions), enabled), + criteria_(criteria) + {} + + ~threshold_rule() override = default; + threshold_rule(const threshold_rule &) = delete; + threshold_rule &operator=(const threshold_rule &) = delete; + threshold_rule(threshold_rule &&rhs) noexcept = default; + threshold_rule &operator=(threshold_rule &&rhs) noexcept = default; + + std::optional eval(const object_store &store, global_cache_type &gcache, + local_cache_type &lcache, ddwaf::timer &deadline) const; + + global_cache_type init_global_cache() const { - auto it = tags_.find(tag); - return it == tags_.end() ? std::string_view() : it->second; + auto max_window_size = criteria_.threshold * 2; + return timed_counter_ms{criteria_.period, max_window_size}; } - const std::unordered_map &get_tags() const { return tags_; } +protected: + evaluation_criteria criteria_; +}; - const std::vector &get_actions() const { return actions_; } +class indexed_threshold_rule : public base_rule { +public: + struct evaluation_criteria { + std::string name; + target_index target; + std::size_t threshold; + std::chrono::milliseconds period; + }; + + using local_cache_type = expression::cache_type; + using global_cache_type = indexed_timed_counter_ms; + + indexed_threshold_rule(std::string id, std::string name, + std::unordered_map tags, std::shared_ptr expr, + evaluation_criteria criteria, std::vector actions = {}, bool enabled = true) + : base_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), + std::move(actions), enabled), + criteria_(std::move(criteria)) + {} - void get_addresses(std::unordered_map &addresses) const + ~indexed_threshold_rule() override = default; + indexed_threshold_rule(const indexed_threshold_rule &) = delete; + indexed_threshold_rule &operator=(const indexed_threshold_rule &) = delete; + indexed_threshold_rule(indexed_threshold_rule &&rhs) noexcept = default; + indexed_threshold_rule &operator=(indexed_threshold_rule &&rhs) noexcept = default; + + std::optional eval(const object_store &store, global_cache_type &gcache, + local_cache_type &lcache, ddwaf::timer &deadline) const; + + global_cache_type init_global_cache() const { - return expr_->get_addresses(addresses); + auto max_window_size = criteria_.threshold * 2; + return indexed_timed_counter_ms{criteria_.period, 128, max_window_size}; } - void set_actions(std::vector new_actions) { actions_ = std::move(new_actions); } - protected: - bool enabled_{true}; - source_type source_; - std::string id_; - std::string name_; - std::unordered_map tags_; - std::shared_ptr expr_; - std::vector actions_; + evaluation_criteria criteria_; }; } // namespace ddwaf diff --git a/src/threshold_rule.hpp b/src/threshold_rule.hpp deleted file mode 100644 index 0b3f50caf..000000000 --- a/src/threshold_rule.hpp +++ /dev/null @@ -1,57 +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. - -#pragma once - -#include -#include -#include -#include -#include - -#include "clock.hpp" -#include "event.hpp" -#include "exclusion/common.hpp" -#include "expression.hpp" -#include "iterator.hpp" -#include "matcher/base.hpp" -#include "object_store.hpp" - -namespace ddwaf { - -class threshold_rule { -public: - using cache_type = expression::cache_type; - - threshold_rule(std::string id, std::string name, std::unordered_map tags, - std::shared_ptr expr, uint64_t threshold, const std::chrono::milliseconds &period, - std::vector actions = {}, bool enabled = true) - : enabled_(enabled), id_(std::move(id)), name_(std::move(name)), - tags_(std::move(tags)), expr_(std::move(expr)), threshold_(threshold), - period_(period), actions_(std::move(actions)) - { - if (!expr_) { - throw std::invalid_argument("threshold rule constructed with null expression"); - } - } - ~threshold_rule() = default; - threshold_rule(const threshold_rule &) = delete; - threshold_rule &operator=(const threshold_rule &) = delete; - threshold_rule(threshold_rule &&rhs) noexcept = default; - threshold_rule &operator=(threshold_rule &&rhs) noexcept = default; - -protected: - bool enabled_{true}; - std::string id_; - std::string name_; - std::unordered_map tags_; - std::shared_ptr expr_; - uint64_t threshold_; - std::chrono::milliseconds period_; - std::vector actions_; -}; - -} // namespace ddwaf diff --git a/src/timed_counter.hpp b/src/timed_counter.hpp new file mode 100644 index 000000000..cd26750d8 --- /dev/null +++ b/src/timed_counter.hpp @@ -0,0 +1,147 @@ +// Unless explicitly stated otherwise all files in this repository are +// dual-licensed under the Apache-2.0 License or BSD-3-Clause License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2021 Datadog, Inc. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "utils.hpp" + +namespace ddwaf { + +template +concept is_duration = std::is_same_v, T>; + +template + requires is_duration +class timed_counter { +public: + explicit timed_counter(T period, std::size_t max_window_size = 100) : period_(period) + { + time_points_.resize(max_window_size); + } + + std::size_t add_timepoint_and_count(T point) + { + // Discard old elements + auto window_begin = point - period_; + while (count > 0 && time_points_[left] <= window_begin) { + left = (left + 1) % time_points_.size(); + count -= 1; + } + + if (count < time_points_.size()) { + // Add a new element + time_points_[right] = point; + right = (right + 1) % time_points_.size(); + count += 1; + } else if (count == time_points_.size()) { + // Discard the oldest one + time_points_[right] = point; + right = (right + 1) % time_points_.size(); + left = (left + 1) % time_points_.size(); + } + + return count; + } + + T last_timepoint() + { + if (count == 0) { + [[unlikely]] return static_cast(0); + } + + auto index = (right + time_points_.size() - 1) % time_points_.size(); + return time_points_[index]; + } + + T update_count(T point) + { + // Discard old elements + auto window_begin = point - period_; + while (count > 0 && time_points_[left] <= window_begin) { + left = (left + 1) % time_points_.size(); + count -= 1; + } + return count; + } + + void reset() { left = right = count = 0; } + +protected: + std::chrono::milliseconds period_; + std::vector time_points_; + std::size_t left{0}; + std::size_t right{0}; + std::size_t count{0}; +}; + +template + requires is_duration +class indexed_timed_counter { +public: + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) + explicit indexed_timed_counter( + Duration period, std::size_t max_index_size = 32, std::size_t max_window_size = 100) + : period_(period), max_index_size_(max_index_size), max_window_size_(max_window_size) + { + index_.reserve(max_index_size_); + } + + std::size_t add_timepoint_and_count(Key key, Duration point) + { + auto it = index_.find(key); + if (it == index_.end()) { + if (index_.size() == max_index_size_) { + remove_oldest_entry(point); + } + + auto [new_it, res] = + index_.emplace(key, timed_counter{period_, max_window_size_}); + if (!res) { + return 0; + } + + it = new_it; + } + + return it->second.add_timepoint_and_count(point); + } + +protected: + void remove_oldest_entry(Duration point) + { + using iterator_type = typename decltype(index_)::iterator; + + Duration max_delta{0}; + iterator_type oldest_it; + for (auto it = index_.begin(); it != index_.end(); ++it) { + auto window_last = it->second.last_timepoint(); + auto delta = point - window_last; + if (delta > max_delta) { + max_delta = delta; + oldest_it = it; + } + } + + index_.erase(oldest_it); + } + + Duration period_; + std::size_t max_index_size_; + std::size_t max_window_size_; + std::unordered_map> index_; +}; + +using timed_counter_ms = timed_counter; +using indexed_timed_counter_ms = indexed_timed_counter; + +} // namespace ddwaf diff --git a/tests/timed_counter_test.cpp b/tests/timed_counter_test.cpp new file mode 100644 index 000000000..cbf44dee7 --- /dev/null +++ b/tests/timed_counter_test.cpp @@ -0,0 +1,102 @@ +// 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 "test_utils.hpp" +#include "timed_counter.hpp" + +using namespace std::chrono_literals; + +namespace { + +TEST(TestTimedCounter, BasicMs) +{ + ddwaf::timed_counter window{10ms, 5}; + + EXPECT_EQ(window.add_timepoint_and_count(1ms), 1); + EXPECT_EQ(window.add_timepoint_and_count(11ms), 1); + EXPECT_EQ(window.add_timepoint_and_count(12ms), 2); + EXPECT_EQ(window.add_timepoint_and_count(13ms), 3); + EXPECT_EQ(window.add_timepoint_and_count(14ms), 4); + EXPECT_EQ(window.add_timepoint_and_count(15ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(16ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(17ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(18ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(19ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(20ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(21ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(40ms), 1); +} + +TEST(TestTimedCounter, BasicS) +{ + ddwaf::timed_counter window{10s, 5}; + + EXPECT_EQ(window.add_timepoint_and_count(1s), 1); + EXPECT_EQ(window.add_timepoint_and_count(11s), 1); + EXPECT_EQ(window.add_timepoint_and_count(12s), 2); + EXPECT_EQ(window.add_timepoint_and_count(13s), 3); + EXPECT_EQ(window.add_timepoint_and_count(14s), 4); + EXPECT_EQ(window.add_timepoint_and_count(15s), 5); + EXPECT_EQ(window.add_timepoint_and_count(16s), 5); + EXPECT_EQ(window.add_timepoint_and_count(17s), 5); + EXPECT_EQ(window.add_timepoint_and_count(18s), 5); + EXPECT_EQ(window.add_timepoint_and_count(19s), 5); + EXPECT_EQ(window.add_timepoint_and_count(20s), 5); + EXPECT_EQ(window.add_timepoint_and_count(21s), 5); + EXPECT_EQ(window.add_timepoint_and_count(40s), 1); +} + +TEST(TestIndexedTimedCounter, BasicString) +{ + ddwaf::indexed_timed_counter window{10s, 5, 5}; + + EXPECT_EQ(window.add_timepoint_and_count("admin", 1s), 1); + EXPECT_EQ(window.add_timepoint_and_count("user", 10s), 1); + EXPECT_EQ(window.add_timepoint_and_count("docker", 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("nobody", 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("root", 11s), 1); + // Admin should be removed, as it's the latest + EXPECT_EQ(window.add_timepoint_and_count("mail", 11s), 1); + + // User will now be removed + EXPECT_EQ(window.add_timepoint_and_count("admin", 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("admin", 12s), 2); + EXPECT_EQ(window.add_timepoint_and_count("admin", 13s), 3); + EXPECT_EQ(window.add_timepoint_and_count("admin", 14s), 4); + EXPECT_EQ(window.add_timepoint_and_count("admin", 15s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin", 16s), 5); + + EXPECT_EQ(window.add_timepoint_and_count("docker", 17s), 2); + EXPECT_EQ(window.add_timepoint_and_count("nobody", 17s), 2); + EXPECT_EQ(window.add_timepoint_and_count("root", 17s), 2); + EXPECT_EQ(window.add_timepoint_and_count("mail", 17s), 2); + + EXPECT_EQ(window.add_timepoint_and_count("docker", 18s), 3); + EXPECT_EQ(window.add_timepoint_and_count("nobody", 18s), 3); + EXPECT_EQ(window.add_timepoint_and_count("root", 18s), 3); + EXPECT_EQ(window.add_timepoint_and_count("mail", 18s), 3); + + EXPECT_EQ(window.add_timepoint_and_count("docker", 19s), 4); + EXPECT_EQ(window.add_timepoint_and_count("nobody", 19s), 4); + EXPECT_EQ(window.add_timepoint_and_count("root", 19s), 4); + EXPECT_EQ(window.add_timepoint_and_count("mail", 19s), 4); + + EXPECT_EQ(window.add_timepoint_and_count("docker", 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("nobody", 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("root", 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("mail", 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin", 20s), 5); + + EXPECT_EQ(window.add_timepoint_and_count("nobody", 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("root", 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("mail", 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin", 21s), 5); + // Docker will now be removed + EXPECT_EQ(window.add_timepoint_and_count("user", 21s), 1); +} + +} // namespace From 63fcc892a4786ac4121ed53211e84a41fa035880 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:42:20 +0100 Subject: [PATCH 3/8] Small changes --- src/context.hpp | 3 -- src/global_context.hpp | 36 ++++++++++++++++++++ src/timed_counter.hpp | 65 ++++++++++++++++++++++++------------ tests/timed_counter_test.cpp | 1 + 4 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 src/global_context.hpp diff --git a/src/context.hpp b/src/context.hpp index 12b6bef3a..38f72375e 100644 --- a/src/context.hpp +++ b/src/context.hpp @@ -7,7 +7,6 @@ #pragma once #include -#include #include #include "context_allocator.hpp" @@ -16,8 +15,6 @@ #include "exclusion/common.hpp" #include "exclusion/input_filter.hpp" #include "exclusion/rule_filter.hpp" -#include "obfuscator.hpp" -#include "rule.hpp" #include "ruleset.hpp" #include "utils.hpp" diff --git a/src/global_context.hpp b/src/global_context.hpp new file mode 100644 index 000000000..adf2d7df2 --- /dev/null +++ b/src/global_context.hpp @@ -0,0 +1,36 @@ +// 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 "rule.hpp" + +namespace ddwaf { + +class global_context { +public: + using local_cache_type = std::unordered_map; + + global_context() = default; + global_context(const global_context &) = delete; + global_context &operator=(const global_context &) = delete; + global_context(global_context &&) = default; + global_context &operator=(global_context &&) = delete; + ~global_context() = default; + + void eval(std::vector &events, const object_store &store, + local_cache_type &lcache, ddwaf::timer &deadline) const; + +protected: + using global_cache_variants = std::variant; + + std::vector> rules_; + std::unordered_map rule_cache_{}; +}; + +} diff --git a/src/timed_counter.hpp b/src/timed_counter.hpp index cd26750d8..89da7393b 100644 --- a/src/timed_counter.hpp +++ b/src/timed_counter.hpp @@ -32,56 +32,77 @@ class timed_counter { std::size_t add_timepoint_and_count(T point) { // Discard old elements - auto window_begin = point - period_; - while (count > 0 && time_points_[left] <= window_begin) { - left = (left + 1) % time_points_.size(); - count -= 1; + update_count(point); + + // Check if the latest element is beyond the current one (concurrent writers) + auto index = decrement(right); + if (buckets > 0 && time_points_[index].point > point) { + ++time_points_[index].count; + return ++count; } - if (count < time_points_.size()) { + if (buckets < time_points_.size()) { // Add a new element - time_points_[right] = point; - right = (right + 1) % time_points_.size(); - count += 1; - } else if (count == time_points_.size()) { + time_points_[right].point = point; + time_points_[right].count = 1; + right = increment(right); + ++count; + ++buckets; + } else if (buckets == time_points_.size()) { // Discard the oldest one - time_points_[right] = point; - right = (right + 1) % time_points_.size(); - left = (left + 1) % time_points_.size(); + time_points_[right].point = point; + count -= (time_points_[right].count - 1); + time_points_[right].count = 1; + right = increment(right); + left = increment(left); } return count; } - T last_timepoint() + T last_timepoint() const { - if (count == 0) { + if (buckets == 0) { [[unlikely]] return static_cast(0); } - auto index = (right + time_points_.size() - 1) % time_points_.size(); - return time_points_[index]; + auto index = decrement(right); + return time_points_[index].point; } - T update_count(T point) + std::size_t update_count(T point) { // Discard old elements auto window_begin = point - period_; - while (count > 0 && time_points_[left] <= window_begin) { - left = (left + 1) % time_points_.size(); - count -= 1; + while (buckets > 0 && time_points_[left].point <= window_begin) { + count -= time_points_[left].count; + --buckets; + left = increment(left); } return count; } - void reset() { left = right = count = 0; } + void reset() { left = right = count = buckets = 0; } protected: + std::size_t increment(std::size_t value) const { return (value + 1) % time_points_.size(); } + + std::size_t decrement(std::size_t value) const + { + return (value + time_points_.size() - 1) % time_points_.size(); + } + + struct time_bucket { + T point; + std::size_t count; + }; + std::chrono::milliseconds period_; - std::vector time_points_; + std::vector time_points_; std::size_t left{0}; std::size_t right{0}; std::size_t count{0}; + std::size_t buckets{0}; }; template diff --git a/tests/timed_counter_test.cpp b/tests/timed_counter_test.cpp index cbf44dee7..0def8460a 100644 --- a/tests/timed_counter_test.cpp +++ b/tests/timed_counter_test.cpp @@ -23,6 +23,7 @@ TEST(TestTimedCounter, BasicMs) EXPECT_EQ(window.add_timepoint_and_count(14ms), 4); EXPECT_EQ(window.add_timepoint_and_count(15ms), 5); EXPECT_EQ(window.add_timepoint_and_count(16ms), 5); + EXPECT_EQ(window.add_timepoint_and_count(17ms), 5); EXPECT_EQ(window.add_timepoint_and_count(18ms), 5); EXPECT_EQ(window.add_timepoint_and_count(19ms), 5); From 3af4268fc096176d88d9f08e6ebcffe772cc6349 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:48:16 +0100 Subject: [PATCH 4/8] Connect all pieces --- cmake/objects.cmake | 1 + src/global_context.cpp | 33 ++++++++++++++ src/global_context.hpp | 17 ++++---- src/rule.cpp | 34 ++++++++++++--- src/rule.hpp | 68 +++++++++++++++-------------- src/ruleset_builder.hpp | 4 ++ src/timed_counter.hpp | 78 ++++++++++++++++++++++++++------- tests/timed_counter_test.cpp | 85 ++++++++++++++++++------------------ 8 files changed, 215 insertions(+), 105 deletions(-) create mode 100644 src/global_context.cpp diff --git a/cmake/objects.cmake b/cmake/objects.cmake index 4960c6eb6..24cd50b15 100644 --- a/cmake/objects.cmake +++ b/cmake/objects.cmake @@ -4,6 +4,7 @@ set(LIBDDWAF_SOURCE ${libddwaf_SOURCE_DIR}/src/parameter.cpp ${libddwaf_SOURCE_DIR}/src/interface.cpp ${libddwaf_SOURCE_DIR}/src/context.cpp + ${libddwaf_SOURCE_DIR}/src/global_context.cpp ${libddwaf_SOURCE_DIR}/src/context_allocator.cpp ${libddwaf_SOURCE_DIR}/src/event.cpp ${libddwaf_SOURCE_DIR}/src/object.cpp diff --git a/src/global_context.cpp b/src/global_context.cpp new file mode 100644 index 000000000..4bd0b960d --- /dev/null +++ b/src/global_context.cpp @@ -0,0 +1,33 @@ +// 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 "global_context.hpp" +#include "rule.hpp" + +namespace ddwaf { +void global_context::eval(std::vector &events, const object_store &store, cache_type &cache, + ddwaf::timer &deadline) +{ + auto timepoint = monotonic_clock::now(); + + for (const auto &rule : rules_) { + auto cache_it = cache.find(rule.get()); + if (cache_it == cache.end()) { + auto [new_it, res] = cache.emplace(rule.get(), base_threshold_rule::cache_type{}); + if (!res) { + continue; + } + cache_it = new_it; + } + + auto opt_evt = rule->eval(store, cache_it->second, timepoint, deadline); + if (opt_evt.has_value()) { + events.emplace_back(std::move(opt_evt.value())); + } + } +} + +} // namespace ddwaf diff --git a/src/global_context.hpp b/src/global_context.hpp index adf2d7df2..099a4ebb6 100644 --- a/src/global_context.hpp +++ b/src/global_context.hpp @@ -14,23 +14,22 @@ namespace ddwaf { class global_context { public: - using local_cache_type = std::unordered_map; + using cache_type = std::unordered_map; - global_context() = default; + explicit global_context(std::vector> rules) + : rules_(std::move(rules)) + {} global_context(const global_context &) = delete; global_context &operator=(const global_context &) = delete; global_context(global_context &&) = default; global_context &operator=(global_context &&) = delete; ~global_context() = default; - void eval(std::vector &events, const object_store &store, - local_cache_type &lcache, ddwaf::timer &deadline) const; + void eval(std::vector &events, const object_store &store, cache_type &lcache, + ddwaf::timer &deadline); protected: - using global_cache_variants = std::variant; - - std::vector> rules_; - std::unordered_map rule_cache_{}; + std::vector> rules_; }; -} +} // namespace ddwaf diff --git a/src/rule.cpp b/src/rule.cpp index 090463555..5312114b7 100644 --- a/src/rule.cpp +++ b/src/rule.cpp @@ -5,11 +5,12 @@ // Copyright 2021 Datadog, Inc. #include "rule.hpp" +#include "timed_counter.hpp" namespace ddwaf { -std::optional threshold_rule::eval(const object_store &store, global_cache_type &gcache, - local_cache_type &lcache, ddwaf::timer &deadline) const +std::optional threshold_rule::eval(const object_store &store, cache_type &cache, + monotonic_clock::time_point now, ddwaf::timer &deadline) { expression::cache_type expr_cache; auto res = expr_->eval(expr_cache, store, {}, {}, deadline); @@ -17,19 +18,40 @@ std::optional threshold_rule::eval(const object_store &store, global_cach return std::nullopt; } - return {ddwaf::event{this, expression::get_matches(lcache), res.ephemeral}}; + auto ms = std::chrono::duration_cast(now.time_since_epoch()); + auto count = counter_.add_timepoint_and_count(ms); + if (count > criteria_.threshold) { + // Match should be generated differently + return {ddwaf::event{this, expression::get_matches(cache), res.ephemeral}}; + } + + return std::nullopt; } -std::optional indexed_threshold_rule::eval(const object_store &store, - global_cache_type &gcache, local_cache_type &lcache, ddwaf::timer &deadline) const +std::optional indexed_threshold_rule::eval(const object_store &store, cache_type &lcache, + monotonic_clock::time_point now, ddwaf::timer &deadline) { + auto [obj, attr] = store.get_target(criteria_.target); + if (obj == nullptr || obj->type != DDWAF_OBJ_STRING) { + return std::nullopt; + } + expression::cache_type expr_cache; auto res = expr_->eval(expr_cache, store, {}, {}, deadline); if (!res.outcome) { return std::nullopt; } - return {ddwaf::event{this, expression::get_matches(lcache), res.ephemeral}}; + auto ms = std::chrono::duration_cast(now.time_since_epoch()); + std::string_view key{obj->stringValue, static_cast(obj->nbEntries)}; + + auto count = counter_.add_timepoint_and_count(key, ms); + if (count > criteria_.threshold) { + // Match should be generated differently + return {ddwaf::event{this, expression::get_matches(lcache), res.ephemeral}}; + } + + return std::nullopt; } } // namespace ddwaf diff --git a/src/rule.hpp b/src/rule.hpp index 49dc60fc4..9dfc9bae9 100644 --- a/src/rule.hpp +++ b/src/rule.hpp @@ -124,44 +124,56 @@ class rule : public base_rule { source_type source_; }; -class threshold_rule : public base_rule { +class base_threshold_rule : public base_rule { +public: + using cache_type = expression::cache_type; + + base_threshold_rule(std::string id, std::string name, + std::unordered_map tags, std::shared_ptr expr, + std::vector actions = {}, bool enabled = true) + : base_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), + std::move(actions), enabled) + {} + ~base_threshold_rule() override = default; + base_threshold_rule(const base_threshold_rule &) = delete; + base_threshold_rule &operator=(const base_threshold_rule &) = delete; + base_threshold_rule(base_threshold_rule &&rhs) noexcept = default; + base_threshold_rule &operator=(base_threshold_rule &&rhs) noexcept = default; + + virtual std::optional eval(const object_store &store, cache_type &cache, + monotonic_clock::time_point now, ddwaf::timer &deadline) = 0; +}; + +class threshold_rule : public base_threshold_rule { public: struct evaluation_criteria { std::size_t threshold; std::chrono::milliseconds period; }; - using local_cache_type = expression::cache_type; - using global_cache_type = timed_counter_ms; - threshold_rule(std::string id, std::string name, std::unordered_map tags, std::shared_ptr expr, evaluation_criteria criteria, std::vector actions = {}, bool enabled = true) - : base_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), + : base_threshold_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), std::move(actions), enabled), - criteria_(criteria) + criteria_(criteria), counter_(criteria_.period, criteria_.threshold * 2) {} ~threshold_rule() override = default; threshold_rule(const threshold_rule &) = delete; threshold_rule &operator=(const threshold_rule &) = delete; - threshold_rule(threshold_rule &&rhs) noexcept = default; - threshold_rule &operator=(threshold_rule &&rhs) noexcept = default; + threshold_rule(threshold_rule &&rhs) noexcept = delete; + threshold_rule &operator=(threshold_rule &&rhs) noexcept = delete; - std::optional eval(const object_store &store, global_cache_type &gcache, - local_cache_type &lcache, ddwaf::timer &deadline) const; - - global_cache_type init_global_cache() const - { - auto max_window_size = criteria_.threshold * 2; - return timed_counter_ms{criteria_.period, max_window_size}; - } + std::optional eval(const object_store &store, cache_type &cache, + monotonic_clock::time_point now, ddwaf::timer &deadline) override; protected: evaluation_criteria criteria_; + timed_counter_ts_ms counter_; }; -class indexed_threshold_rule : public base_rule { +class indexed_threshold_rule : public base_threshold_rule { public: struct evaluation_criteria { std::string name; @@ -170,34 +182,26 @@ class indexed_threshold_rule : public base_rule { std::chrono::milliseconds period; }; - using local_cache_type = expression::cache_type; - using global_cache_type = indexed_timed_counter_ms; - indexed_threshold_rule(std::string id, std::string name, std::unordered_map tags, std::shared_ptr expr, evaluation_criteria criteria, std::vector actions = {}, bool enabled = true) - : base_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), + : base_threshold_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), std::move(actions), enabled), - criteria_(std::move(criteria)) + criteria_(std::move(criteria)), counter_(criteria_.period, 128, criteria_.threshold * 2) {} ~indexed_threshold_rule() override = default; indexed_threshold_rule(const indexed_threshold_rule &) = delete; indexed_threshold_rule &operator=(const indexed_threshold_rule &) = delete; - indexed_threshold_rule(indexed_threshold_rule &&rhs) noexcept = default; - indexed_threshold_rule &operator=(indexed_threshold_rule &&rhs) noexcept = default; - - std::optional eval(const object_store &store, global_cache_type &gcache, - local_cache_type &lcache, ddwaf::timer &deadline) const; + indexed_threshold_rule(indexed_threshold_rule &&rhs) noexcept = delete; + indexed_threshold_rule &operator=(indexed_threshold_rule &&rhs) noexcept = delete; - global_cache_type init_global_cache() const - { - auto max_window_size = criteria_.threshold * 2; - return indexed_timed_counter_ms{criteria_.period, 128, max_window_size}; - } + std::optional eval(const object_store &store, cache_type &cache, + monotonic_clock::time_point now, ddwaf::timer &deadline) override; protected: evaluation_criteria criteria_; + indexed_timed_counter_ts_ms counter_; }; } // namespace ddwaf diff --git a/src/ruleset_builder.hpp b/src/ruleset_builder.hpp index 4b1a5bbeb..ec36e1813 100644 --- a/src/ruleset_builder.hpp +++ b/src/ruleset_builder.hpp @@ -11,6 +11,7 @@ #include #include +#include "global_context.hpp" #include "indexer.hpp" #include "parameter.hpp" #include "parser/specification.hpp" @@ -107,6 +108,9 @@ class ruleset_builder { // Actions std::shared_ptr actions_; + + // Global Context + std::shared_ptr gctx_; }; } // namespace ddwaf diff --git a/src/timed_counter.hpp b/src/timed_counter.hpp index 89da7393b..770e188fa 100644 --- a/src/timed_counter.hpp +++ b/src/timed_counter.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include "utils.hpp" @@ -20,10 +21,12 @@ namespace ddwaf { template concept is_duration = std::is_same_v, T>; +// Perhaps this should use an std::chrono::time_point instead template requires is_duration class timed_counter { public: + timed_counter() = default; explicit timed_counter(T period, std::size_t max_window_size = 100) : period_(period) { time_points_.resize(max_window_size); @@ -85,9 +88,12 @@ class timed_counter { void reset() { left = right = count = buckets = 0; } protected: - std::size_t increment(std::size_t value) const { return (value + 1) % time_points_.size(); } + [[nodiscard]] std::size_t increment(std::size_t value) const + { + return (value + 1) % time_points_.size(); + } - std::size_t decrement(std::size_t value) const + [[nodiscard]] std::size_t decrement(std::size_t value) const { return (value + time_points_.size() - 1) % time_points_.size(); } @@ -97,7 +103,7 @@ class timed_counter { std::size_t count; }; - std::chrono::milliseconds period_; + std::chrono::milliseconds period_{}; std::vector time_points_; std::size_t left{0}; std::size_t right{0}; @@ -105,20 +111,58 @@ class timed_counter { std::size_t buckets{0}; }; +template + requires is_duration +class timed_counter_ts : protected timed_counter { +public: + timed_counter_ts() = default; + explicit timed_counter_ts(T period, std::size_t max_window_size = 100) + : timed_counter(period, max_window_size) + {} + std::size_t add_timepoint_and_count(T point) + { + std::lock_guard lock(mtx_); + return timed_counter::add_timepoint_and_count(point); + } + + T last_timepoint() const + { + std::lock_guard lock(mtx_); + return timed_counter::last_timepoint(); + } + + std::size_t update_count(T point) + { + std::lock_guard lock(mtx_); + return timed_counter::update_count(point); + } + + void reset() + { + std::lock_guard lock(mtx_); + timed_counter::reset(); + } + +protected: + mutable std::mutex mtx_; +}; + template requires is_duration -class indexed_timed_counter { +class indexed_timed_counter_ts { public: - // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) - explicit indexed_timed_counter( + indexed_timed_counter_ts() = default; + explicit indexed_timed_counter_ts( + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) Duration period, std::size_t max_index_size = 32, std::size_t max_window_size = 100) : period_(period), max_index_size_(max_index_size), max_window_size_(max_window_size) - { - index_.reserve(max_index_size_); - } + {} - std::size_t add_timepoint_and_count(Key key, Duration point) + template + requires std::is_constructible_v + std::size_t add_timepoint_and_count(T key, Duration point) { + std::lock_guard lock(mtx_); auto it = index_.find(key); if (it == index_.end()) { if (index_.size() == max_index_size_) { @@ -126,7 +170,7 @@ class indexed_timed_counter { } auto [new_it, res] = - index_.emplace(key, timed_counter{period_, max_window_size_}); + index_.emplace(Key{key}, timed_counter{period_, max_window_size_}); if (!res) { return 0; } @@ -157,12 +201,14 @@ class indexed_timed_counter { } Duration period_; - std::size_t max_index_size_; - std::size_t max_window_size_; - std::unordered_map> index_; + std::size_t max_index_size_{}; + std::size_t max_window_size_{}; + std::map, std::less<>> index_; + mutable std::mutex mtx_; }; -using timed_counter_ms = timed_counter; -using indexed_timed_counter_ms = indexed_timed_counter; +using timed_counter_ts_ms = timed_counter_ts; +using indexed_timed_counter_ts_ms = + indexed_timed_counter_ts; } // namespace ddwaf diff --git a/tests/timed_counter_test.cpp b/tests/timed_counter_test.cpp index 0def8460a..df91c19c5 100644 --- a/tests/timed_counter_test.cpp +++ b/tests/timed_counter_test.cpp @@ -8,13 +8,14 @@ #include "test_utils.hpp" #include "timed_counter.hpp" +using namespace std::literals; using namespace std::chrono_literals; namespace { TEST(TestTimedCounter, BasicMs) { - ddwaf::timed_counter window{10ms, 5}; + ddwaf::timed_counter_ts window{10ms, 5}; EXPECT_EQ(window.add_timepoint_and_count(1ms), 1); EXPECT_EQ(window.add_timepoint_and_count(11ms), 1); @@ -34,7 +35,7 @@ TEST(TestTimedCounter, BasicMs) TEST(TestTimedCounter, BasicS) { - ddwaf::timed_counter window{10s, 5}; + ddwaf::timed_counter_ts window{10s, 5}; EXPECT_EQ(window.add_timepoint_and_count(1s), 1); EXPECT_EQ(window.add_timepoint_and_count(11s), 1); @@ -53,51 +54,51 @@ TEST(TestTimedCounter, BasicS) TEST(TestIndexedTimedCounter, BasicString) { - ddwaf::indexed_timed_counter window{10s, 5, 5}; + ddwaf::indexed_timed_counter_ts window{10s, 5, 5}; - EXPECT_EQ(window.add_timepoint_and_count("admin", 1s), 1); - EXPECT_EQ(window.add_timepoint_and_count("user", 10s), 1); - EXPECT_EQ(window.add_timepoint_and_count("docker", 11s), 1); - EXPECT_EQ(window.add_timepoint_and_count("nobody", 11s), 1); - EXPECT_EQ(window.add_timepoint_and_count("root", 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 1s), 1); + EXPECT_EQ(window.add_timepoint_and_count("user"sv, 10s), 1); + EXPECT_EQ(window.add_timepoint_and_count("docker"sv, 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("nobody"sv, 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("root"sv, 11s), 1); // Admin should be removed, as it's the latest - EXPECT_EQ(window.add_timepoint_and_count("mail", 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("mail"sv, 11s), 1); // User will now be removed - EXPECT_EQ(window.add_timepoint_and_count("admin", 11s), 1); - EXPECT_EQ(window.add_timepoint_and_count("admin", 12s), 2); - EXPECT_EQ(window.add_timepoint_and_count("admin", 13s), 3); - EXPECT_EQ(window.add_timepoint_and_count("admin", 14s), 4); - EXPECT_EQ(window.add_timepoint_and_count("admin", 15s), 5); - EXPECT_EQ(window.add_timepoint_and_count("admin", 16s), 5); - - EXPECT_EQ(window.add_timepoint_and_count("docker", 17s), 2); - EXPECT_EQ(window.add_timepoint_and_count("nobody", 17s), 2); - EXPECT_EQ(window.add_timepoint_and_count("root", 17s), 2); - EXPECT_EQ(window.add_timepoint_and_count("mail", 17s), 2); - - EXPECT_EQ(window.add_timepoint_and_count("docker", 18s), 3); - EXPECT_EQ(window.add_timepoint_and_count("nobody", 18s), 3); - EXPECT_EQ(window.add_timepoint_and_count("root", 18s), 3); - EXPECT_EQ(window.add_timepoint_and_count("mail", 18s), 3); - - EXPECT_EQ(window.add_timepoint_and_count("docker", 19s), 4); - EXPECT_EQ(window.add_timepoint_and_count("nobody", 19s), 4); - EXPECT_EQ(window.add_timepoint_and_count("root", 19s), 4); - EXPECT_EQ(window.add_timepoint_and_count("mail", 19s), 4); - - EXPECT_EQ(window.add_timepoint_and_count("docker", 20s), 5); - EXPECT_EQ(window.add_timepoint_and_count("nobody", 20s), 5); - EXPECT_EQ(window.add_timepoint_and_count("root", 20s), 5); - EXPECT_EQ(window.add_timepoint_and_count("mail", 20s), 5); - EXPECT_EQ(window.add_timepoint_and_count("admin", 20s), 5); - - EXPECT_EQ(window.add_timepoint_and_count("nobody", 21s), 5); - EXPECT_EQ(window.add_timepoint_and_count("root", 21s), 5); - EXPECT_EQ(window.add_timepoint_and_count("mail", 21s), 5); - EXPECT_EQ(window.add_timepoint_and_count("admin", 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 11s), 1); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 12s), 2); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 13s), 3); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 14s), 4); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 15s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 16s), 5); + + EXPECT_EQ(window.add_timepoint_and_count("docker"sv, 17s), 2); + EXPECT_EQ(window.add_timepoint_and_count("nobody"sv, 17s), 2); + EXPECT_EQ(window.add_timepoint_and_count("root"sv, 17s), 2); + EXPECT_EQ(window.add_timepoint_and_count("mail"sv, 17s), 2); + + EXPECT_EQ(window.add_timepoint_and_count("docker"sv, 18s), 3); + EXPECT_EQ(window.add_timepoint_and_count("nobody"sv, 18s), 3); + EXPECT_EQ(window.add_timepoint_and_count("root"sv, 18s), 3); + EXPECT_EQ(window.add_timepoint_and_count("mail"sv, 18s), 3); + + EXPECT_EQ(window.add_timepoint_and_count("docker"sv, 19s), 4); + EXPECT_EQ(window.add_timepoint_and_count("nobody"sv, 19s), 4); + EXPECT_EQ(window.add_timepoint_and_count("root"sv, 19s), 4); + EXPECT_EQ(window.add_timepoint_and_count("mail"sv, 19s), 4); + + EXPECT_EQ(window.add_timepoint_and_count("docker"sv, 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("nobody"sv, 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("root"sv, 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("mail"sv, 20s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 20s), 5); + + EXPECT_EQ(window.add_timepoint_and_count("nobody"sv, 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("root"sv, 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("mail"sv, 21s), 5); + EXPECT_EQ(window.add_timepoint_and_count("admin"sv, 21s), 5); // Docker will now be removed - EXPECT_EQ(window.add_timepoint_and_count("user", 21s), 1); + EXPECT_EQ(window.add_timepoint_and_count("user"sv, 21s), 1); } } // namespace From f2c5fa190daf07e63594a618e854a7fd0595f811 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:18:50 +0100 Subject: [PATCH 5/8] Parser --- src/parser/parser_v2.cpp | 119 +++++++++++++++++++++++++++++++++++++++ src/rule.hpp | 2 +- src/ruleset.hpp | 3 + 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/parser/parser_v2.cpp b/src/parser/parser_v2.cpp index f037532a0..debf9a5e0 100644 --- a/src/parser/parser_v2.cpp +++ b/src/parser/parser_v2.cpp @@ -482,6 +482,88 @@ void add_addresses_to_info(const address_container &addresses, base_section_info for (const auto &address : addresses.optional) { info.add_optional_address(address); } } +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +std::unique_ptr parse_threshold_rule( + std::string id, parameter::map &rule, parameter::map &criteria_map, const object_limits &limits) +{ + std::vector rule_transformers; + auto data_source = ddwaf::data_source::values; + auto transformers = at(rule, "transformers", {}); + rule_transformers = parse_transformers(transformers, data_source); + + auto conditions_array = at(rule, "conditions"); + + address_container addresses; + auto expr = parse_simplified_expression(conditions_array, addresses, limits); + std::unordered_map tags; + for (auto &[key, value] : at(rule, "tags")) { + try { + tags.emplace(key, std::string(value)); + } catch (const bad_cast &e) { + throw invalid_type(std::string(key), e); + } + } + + if (tags.find("type") == tags.end()) { + throw ddwaf::parsing_error("missing key 'type'"); + } + + indexed_threshold_rule::evaluation_criteria criteria; + criteria.threshold = at(criteria_map, "threshold"); + criteria.period = std::chrono::milliseconds(at(criteria_map, "period")); + criteria.name = at(criteria_map, "input"); + criteria.target = get_target_index(criteria.name); + + return std::make_unique(std::move(id), at(rule, "name"), + std::move(tags), std::move(expr), criteria, + at>(rule, "on_match", {}), at(rule, "enabled", true)); +} + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) +std::unique_ptr parse_indexed_threshold_rule( + std::string id, parameter::map &rule, parameter::map &criteria_map, const object_limits &limits) +{ + std::vector rule_transformers; + auto data_source = ddwaf::data_source::values; + auto transformers = at(rule, "transformers", {}); + rule_transformers = parse_transformers(transformers, data_source); + + auto conditions_array = at(rule, "conditions"); + + address_container addresses; + auto expr = parse_simplified_expression(conditions_array, addresses, limits); + std::unordered_map tags; + for (auto &[key, value] : at(rule, "tags")) { + try { + tags.emplace(key, std::string(value)); + } catch (const bad_cast &e) { + throw invalid_type(std::string(key), e); + } + } + + if (tags.find("type") == tags.end()) { + throw ddwaf::parsing_error("missing key 'type'"); + } + + threshold_rule::evaluation_criteria criteria; + criteria.threshold = at(criteria_map, "threshold"); + criteria.period = std::chrono::milliseconds(at(criteria_map, "period")); + + return std::make_unique(std::move(id), at(rule, "name"), + std::move(tags), std::move(expr), criteria, + at>(rule, "on_match", {}), at(rule, "enabled", true)); +} + +std::unique_ptr parse_global_rule( + std::string id, parameter::map &rule, const object_limits &limits) +{ + auto criteria = at(rule, "criteria"); + if (criteria.contains("input")) { + return parse_indexed_threshold_rule(std::move(id), rule, criteria, limits); + } + return parse_threshold_rule(std::move(id), rule, criteria, limits); +} + } // namespace rule_spec_container parse_rules(parameter::vector &rule_array, base_section_info &info, @@ -800,4 +882,41 @@ indexer parse_scanners(parameter::vector &scanner_array, base_sec return scanners; } +std::shared_ptr parse_global_rules( + parameter::vector &rule_array, base_section_info &info, const object_limits &limits) +{ + std::vector> rules; + + std::unordered_set ids; + for (unsigned i = 0; i < rule_array.size(); ++i) { + const auto &rule_param = rule_array[i]; + auto rule_map = static_cast(rule_param); + std::string id; + try { + address_container addresses; + id = at(rule_map, "id"); + if (ids.find(id) != ids.end()) { + DDWAF_WARN("Duplicate global rule {}", id); + info.add_failed(id, "duplicate rule"); + continue; + } + + auto rule = parse_global_rule(id, rule_map, limits); + DDWAF_DEBUG("Parsed global rule {}", id); + info.add_loaded(id); + add_addresses_to_info(addresses, info); + + rules.emplace_back(std::move(rule)); + } catch (const std::exception &e) { + if (id.empty()) { + id = index_to_id(i); + } + DDWAF_WARN("Failed to parse rule '{}': {}", id, e.what()); + info.add_failed(id, e.what()); + } + } + + return std::make_shared(std::move(rules)); +} + } // namespace ddwaf::parser::v2 diff --git a/src/rule.hpp b/src/rule.hpp index 9dfc9bae9..483e35dbf 100644 --- a/src/rule.hpp +++ b/src/rule.hpp @@ -148,7 +148,7 @@ class threshold_rule : public base_threshold_rule { public: struct evaluation_criteria { std::size_t threshold; - std::chrono::milliseconds period; + std::chrono::milliseconds period{}; }; threshold_rule(std::string id, std::string name, diff --git a/src/ruleset.hpp b/src/ruleset.hpp index cffbaa07a..aeae1e189 100644 --- a/src/ruleset.hpp +++ b/src/ruleset.hpp @@ -14,6 +14,7 @@ #include "collection.hpp" #include "exclusion/input_filter.hpp" #include "exclusion/rule_filter.hpp" +#include "global_context.hpp" #include "obfuscator.hpp" #include "processor.hpp" #include "rule.hpp" @@ -152,6 +153,8 @@ struct ruleset { // Root addresses, lazily computed std::vector root_addresses; + + std::shared_ptr gcontext; }; } // namespace ddwaf From abfef9ba95567f5287c3d66ecd6a176a6ab1d2ad Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:41:05 +0100 Subject: [PATCH 6/8] Run global rules --- src/context.cpp | 4 ++++ src/context.hpp | 2 ++ src/event.cpp | 2 +- src/global_context.cpp | 1 + src/parser/parser.hpp | 3 +++ src/parser/parser_v2.cpp | 8 ++++---- src/rule.cpp | 31 ++++++++++++++++++++++++------- src/rule.hpp | 8 ++++++-- src/ruleset.hpp | 2 +- src/ruleset_builder.cpp | 20 ++++++++++++++++++++ src/ruleset_builder.hpp | 1 + 11 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/context.cpp b/src/context.cpp index f71a9b3a7..8b56a3a2c 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -69,6 +69,10 @@ DDWAF_RET_CODE context::run(optional_ref persistent, try { eval_preprocessors(derived, deadline); + if (ruleset_->gctx) { + ruleset_->gctx->eval(events, store_, gctx_cache_, deadline); + } + // If no rule targets are available, there is no point in evaluating them const bool should_eval_rules = check_new_rule_targets(); const bool should_eval_filters = should_eval_rules || check_new_filter_targets(); diff --git a/src/context.hpp b/src/context.hpp index 38f72375e..aed4a2178 100644 --- a/src/context.hpp +++ b/src/context.hpp @@ -86,6 +86,8 @@ class context { // Cache of collections to avoid processing once a result has been obtained memory::unordered_map collection_cache_{}; + + global_context::cache_type gctx_cache_{}; }; class context_wrapper { diff --git a/src/event.cpp b/src/event.cpp index c4d249fdb..ef98e6cc8 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -63,7 +63,7 @@ void serialize_match(const condition_match &match, ddwaf_object &match_map, auto } // Scalar case - if (match.args.size() == 1 || match.args[0].name == "input") { + if (match.args.size() == 1 && match.args[0].name == "input") { const auto &arg = match.args[0]; ddwaf_object key_path; diff --git a/src/global_context.cpp b/src/global_context.cpp index 4bd0b960d..ffa6aba6a 100644 --- a/src/global_context.cpp +++ b/src/global_context.cpp @@ -13,6 +13,7 @@ void global_context::eval(std::vector &events, const object_store &store, { auto timepoint = monotonic_clock::now(); + DDWAF_DEBUG("Evaluating global rules"); for (const auto &rule : rules_) { auto cache_it = cache.find(rule.get()); if (cache_it == cache.end()) { diff --git a/src/parser/parser.hpp b/src/parser/parser.hpp index 5867be33f..047eed301 100644 --- a/src/parser/parser.hpp +++ b/src/parser/parser.hpp @@ -50,5 +50,8 @@ indexer parse_scanners(parameter::vector &scanner_array, base_sec std::shared_ptr parse_actions( parameter::vector &actions_array, base_section_info &info); +std::shared_ptr parse_global_rules( + parameter::vector &rule_array, base_section_info &info, const object_limits &limits); + } // namespace v2 } // namespace ddwaf::parser diff --git a/src/parser/parser_v2.cpp b/src/parser/parser_v2.cpp index debf9a5e0..018da411c 100644 --- a/src/parser/parser_v2.cpp +++ b/src/parser/parser_v2.cpp @@ -483,7 +483,7 @@ void add_addresses_to_info(const address_container &addresses, base_section_info } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -std::unique_ptr parse_threshold_rule( +std::unique_ptr parse_indexed_threshold_rule( std::string id, parameter::map &rule, parameter::map &criteria_map, const object_limits &limits) { std::vector rule_transformers; @@ -491,7 +491,7 @@ std::unique_ptr parse_threshold_rule( auto transformers = at(rule, "transformers", {}); rule_transformers = parse_transformers(transformers, data_source); - auto conditions_array = at(rule, "conditions"); + auto conditions_array = at(rule, "conditions", {}); address_container addresses; auto expr = parse_simplified_expression(conditions_array, addresses, limits); @@ -520,7 +520,7 @@ std::unique_ptr parse_threshold_rule( } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -std::unique_ptr parse_indexed_threshold_rule( +std::unique_ptr parse_threshold_rule( std::string id, parameter::map &rule, parameter::map &criteria_map, const object_limits &limits) { std::vector rule_transformers; @@ -528,7 +528,7 @@ std::unique_ptr parse_indexed_threshold_rule( auto transformers = at(rule, "transformers", {}); rule_transformers = parse_transformers(transformers, data_source); - auto conditions_array = at(rule, "conditions"); + auto conditions_array = at(rule, "conditions", {}); address_container addresses; auto expr = parse_simplified_expression(conditions_array, addresses, limits); diff --git a/src/rule.cpp b/src/rule.cpp index 5312114b7..0abe43363 100644 --- a/src/rule.cpp +++ b/src/rule.cpp @@ -7,13 +7,19 @@ #include "rule.hpp" #include "timed_counter.hpp" +using namespace std::literals; + namespace ddwaf { std::optional threshold_rule::eval(const object_store &store, cache_type &cache, monotonic_clock::time_point now, ddwaf::timer &deadline) { - expression::cache_type expr_cache; - auto res = expr_->eval(expr_cache, store, {}, {}, deadline); + if (expression::get_result(cache)) { + // An event was already produced, so we skip the rule + return std::nullopt; + } + + auto res = expr_->eval(cache, store, {}, {}, deadline); if (!res.outcome) { return std::nullopt; } @@ -22,22 +28,29 @@ std::optional threshold_rule::eval(const object_store &store, cache_type auto count = counter_.add_timepoint_and_count(ms); if (count > criteria_.threshold) { // Match should be generated differently - return {ddwaf::event{this, expression::get_matches(cache), res.ephemeral}}; + // Match should be generated differently + auto matches = expression::get_matches(cache); + matches.emplace_back(condition_match{{}, {}, "threshold", threshold_str_, false}); + return {ddwaf::event{this, std::move(matches), false}}; } return std::nullopt; } -std::optional indexed_threshold_rule::eval(const object_store &store, cache_type &lcache, +std::optional indexed_threshold_rule::eval(const object_store &store, cache_type &cache, monotonic_clock::time_point now, ddwaf::timer &deadline) { + if (expression::get_result(cache)) { + // An event was already produced, so we skip the rule + return std::nullopt; + } + auto [obj, attr] = store.get_target(criteria_.target); if (obj == nullptr || obj->type != DDWAF_OBJ_STRING) { return std::nullopt; } - expression::cache_type expr_cache; - auto res = expr_->eval(expr_cache, store, {}, {}, deadline); + auto res = expr_->eval(cache, store, {}, {}, deadline); if (!res.outcome) { return std::nullopt; } @@ -48,7 +61,11 @@ std::optional indexed_threshold_rule::eval(const object_store &store, cac auto count = counter_.add_timepoint_and_count(key, ms); if (count > criteria_.threshold) { // Match should be generated differently - return {ddwaf::event{this, expression::get_matches(lcache), res.ephemeral}}; + auto matches = expression::get_matches(cache); + matches.emplace_back( + condition_match{{{"input"sv, object_to_string(*obj), criteria_.name, {}}}, {}, + "threshold", threshold_str_, false}); + return {ddwaf::event{this, std::move(matches), false}}; } return std::nullopt; diff --git a/src/rule.hpp b/src/rule.hpp index 483e35dbf..d98cc7a98 100644 --- a/src/rule.hpp +++ b/src/rule.hpp @@ -156,7 +156,8 @@ class threshold_rule : public base_threshold_rule { evaluation_criteria criteria, std::vector actions = {}, bool enabled = true) : base_threshold_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), std::move(actions), enabled), - criteria_(criteria), counter_(criteria_.period, criteria_.threshold * 2) + criteria_(criteria), counter_(criteria_.period, criteria_.threshold * 2), + threshold_str_(to_string(criteria_.threshold)) {} ~threshold_rule() override = default; @@ -171,6 +172,7 @@ class threshold_rule : public base_threshold_rule { protected: evaluation_criteria criteria_; timed_counter_ts_ms counter_; + std::string threshold_str_; }; class indexed_threshold_rule : public base_threshold_rule { @@ -187,7 +189,8 @@ class indexed_threshold_rule : public base_threshold_rule { evaluation_criteria criteria, std::vector actions = {}, bool enabled = true) : base_threshold_rule(std::move(id), std::move(name), std::move(tags), std::move(expr), std::move(actions), enabled), - criteria_(std::move(criteria)), counter_(criteria_.period, 128, criteria_.threshold * 2) + criteria_(std::move(criteria)), counter_(criteria_.period, 128, criteria_.threshold * 2), + threshold_str_(to_string(criteria_.threshold)) {} ~indexed_threshold_rule() override = default; @@ -202,6 +205,7 @@ class indexed_threshold_rule : public base_threshold_rule { protected: evaluation_criteria criteria_; indexed_timed_counter_ts_ms counter_; + std::string threshold_str_; }; } // namespace ddwaf diff --git a/src/ruleset.hpp b/src/ruleset.hpp index aeae1e189..904fb892a 100644 --- a/src/ruleset.hpp +++ b/src/ruleset.hpp @@ -154,7 +154,7 @@ struct ruleset { // Root addresses, lazily computed std::vector root_addresses; - std::shared_ptr gcontext; + std::shared_ptr gctx; }; } // namespace ddwaf diff --git a/src/ruleset_builder.cpp b/src/ruleset_builder.cpp index 550a896de..0125f0ea4 100644 --- a/src/ruleset_builder.cpp +++ b/src/ruleset_builder.cpp @@ -202,6 +202,7 @@ std::shared_ptr ruleset_builder::build(parameter::map &root, base_rules rs->actions = actions_; rs->free_fn = free_fn_; rs->event_obfuscator = event_obfuscator_; + rs->gctx = gctx_; return rs; } @@ -391,6 +392,25 @@ ruleset_builder::change_state ruleset_builder::load(parameter::map &root, base_r } } + it = root.find("global_rules"); + if (it != root.end()) { + DDWAF_DEBUG("Parsing global rules"); + auto §ion = info.add_section("global_rules"); + try { + auto global_rules = static_cast(it->second); + if (!global_rules.empty()) { + gctx_ = parser::v2::parse_global_rules(global_rules, section, limits_); + } else { + DDWAF_DEBUG("Clearing all global rules"); + gctx_ = nullptr; + } + state = state | change_state::global_rules; + } catch (const std::exception &e) { + DDWAF_WARN("Failed to parse global rules: {}", e.what()); + section.set_error(e.what()); + } + } + return state; } diff --git a/src/ruleset_builder.hpp b/src/ruleset_builder.hpp index ec36e1813..b06d89a40 100644 --- a/src/ruleset_builder.hpp +++ b/src/ruleset_builder.hpp @@ -53,6 +53,7 @@ class ruleset_builder { processors = 32, scanners = 64, actions = 128, + global_rules = 256, }; friend constexpr change_state operator|(change_state lhs, change_state rhs); From 2e14a81e7a3b50e575c4fa9d2d50f727863f09b1 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:20:13 +0100 Subject: [PATCH 7/8] Add WAF streamer --- tools/CMakeLists.txt | 2 +- tools/waf_streamer.cpp | 139 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tools/waf_streamer.cpp diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 3379ca540..b1a5303a5 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -5,7 +5,7 @@ foreach(TOOL ${LIBDDWAF_TOOL_SOURCE}) get_filename_component(TOOL_NAME ${TOOL} NAME_WLE) add_executable(${TOOL_NAME} ${TOOL} ${LIBDDWAF_TOOL_COMMON_SOURCE}) - target_link_libraries(${TOOL_NAME} PRIVATE libddwaf_objects lib_yamlcpp lib_rapidjson) + target_link_libraries(${TOOL_NAME} PRIVATE libddwaf_objects lib_yamlcpp lib_rapidjson readline) target_include_directories(${TOOL_NAME} PRIVATE ${LIBDDWAF_PRIVATE_INCLUDES}) set_target_properties(${TOOL_NAME} PROPERTIES diff --git a/tools/waf_streamer.cpp b/tools/waf_streamer.cpp new file mode 100644 index 000000000..826b1594d --- /dev/null +++ b/tools/waf_streamer.cpp @@ -0,0 +1,139 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include + +#include "common/utils.hpp" +#include "ddwaf.h" + +// NOLINTNEXTLINE +auto parse_args(int argc, char *argv[]) +{ + const std::map> arg_mapping{ + {"-r", "--ruleset"}, {"--ruleset", "--ruleset"}}; + + std::unordered_map args; + auto last_arg = args.end(); + for (int i = 1; i < argc; i++) { + std::string_view arg = argv[i]; + if (arg.starts_with('-')) { + if (auto long_arg = arg_mapping.find(arg); long_arg != arg_mapping.end()) { + arg = long_arg->second; + } else { + continue; // Unknown option + } + + auto [it, res] = args.emplace(arg, std::string{}); + last_arg = it; + } else if (last_arg != args.end()) { + last_arg->second = arg; + } + } + return args; +} + +int main(int argc, char *argv[]) +{ + auto args = parse_args(argc, argv); + + std::string ruleset = args["--ruleset"]; + if (ruleset.empty()) { + std::cout << "Usage: " << argv[0] << " --ruleset [..]\n"; + return EXIT_FAILURE; + } + + auto rule = YAML::Load(read_file(ruleset)).as(); + const ddwaf_config config{{0, 0, 0}, {nullptr, nullptr}, ddwaf_object_free}; + auto handle = ddwaf_init(&rule, &config, nullptr); + ddwaf_object_free(&rule); + if (handle == nullptr) { + std::cout << "Failed to load " << ruleset << '\n'; + return EXIT_FAILURE; + } + + while (true) { + char *inpt = readline("Input: "); + add_history(inpt); + std::string json_str = inpt; + + + ddwaf_context context = ddwaf_context_init(handle); + if (context == nullptr) { + ddwaf_destroy(handle); + std::cout << "Failed to initialise context\n"; + return EXIT_FAILURE; + } + + auto input = YAML::Load(json_str).as(); + + ddwaf_result ret; + auto code = + ddwaf_run(context, &input, nullptr, &ret, std::numeric_limits::max()); + + if (code == DDWAF_MATCH) { + std::cout << "Evaluating " << json_str << " --> Match!\n"; + } else if (code == DDWAF_OK) { + std::cout << "Evaluating " << json_str << " --> No match!\n"; + } else { + std::cout << "Evaluating " << json_str << " --> Error!\n"; + } + + if (code == DDWAF_MATCH && ddwaf_object_size(&ret.events) > 0) { + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + out.SetMapFormat(YAML::Block); + out.SetSeqFormat(YAML::Block); + out << object_to_yaml(ret.events); + + std::cout << "Events:\n" << ss.str() << "\n\n"; + } + + if (code == DDWAF_MATCH && ddwaf_object_size(&ret.actions) > 0) { + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + out.SetMapFormat(YAML::Block); + out.SetSeqFormat(YAML::Block); + out << object_to_yaml(ret.actions); + + std::cout << "Actions:\n" << ss.str() << "\n\n"; + } + + if (ddwaf_object_size(&ret.derivatives) > 0) { + std::stringstream ss; + YAML::Emitter out(ss); + out.SetIndent(2); + out.SetMapFormat(YAML::Block); + out.SetSeqFormat(YAML::Block); + out << object_to_yaml(ret.derivatives); + + std::cout << "Derivatives:\n" << ss.str() << "\n\n"; + } + + ddwaf_result_free(&ret); + ddwaf_context_destroy(context); + } + + ddwaf_destroy(handle); + + return EXIT_SUCCESS; +} From dae2e06cc78b7a54733e77bd6e6795f762a6a7b6 Mon Sep 17 00:00:00 2001 From: Anil Mahtani <929854+Anilm3@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:34:47 +0100 Subject: [PATCH 8/8] Fixes --- src/context.cpp | 10 +++------- src/context.hpp | 3 ++- src/global_context.cpp | 2 +- tools/waf_streamer.cpp | 4 +++- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/context.cpp b/src/context.cpp index 8b56a3a2c..1a3e0fee2 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -84,7 +84,7 @@ DDWAF_RET_CODE context::run(optional_ref persistent, const auto &policy = eval_filters(deadline); if (should_eval_rules) { - events = eval_rules(policy, deadline); + eval_rules(events, policy, deadline); } } @@ -195,11 +195,9 @@ exclusion::context_policy &context::eval_filters(ddwaf::timer &deadline) return exclusion_policy_; } -std::vector context::eval_rules( - const exclusion::context_policy &policy, ddwaf::timer &deadline) +void context::eval_rules(std::vector &events, const exclusion::context_policy &policy, + ddwaf::timer &deadline) { - std::vector events; - auto eval_collection = [&](const auto &type, const auto &collection) { auto it = collection_cache_.find(type); if (it == collection_cache_.end()) { @@ -232,8 +230,6 @@ std::vector context::eval_rules( DDWAF_DEBUG("Evaluating base collection '{}'", type); eval_collection(type, collection); } - - return events; } } // namespace ddwaf diff --git a/src/context.hpp b/src/context.hpp index aed4a2178..5cc6d905e 100644 --- a/src/context.hpp +++ b/src/context.hpp @@ -47,7 +47,8 @@ class context { // This function below returns a reference to an internal object, // however using them this way helps with testing exclusion::context_policy &eval_filters(ddwaf::timer &deadline); - std::vector eval_rules(const exclusion::context_policy &policy, ddwaf::timer &deadline); + void eval_rules(std::vector &events, const exclusion::context_policy &policy, + ddwaf::timer &deadline); protected: bool is_first_run() const { return collection_cache_.empty(); } diff --git a/src/global_context.cpp b/src/global_context.cpp index ffa6aba6a..21d5879c7 100644 --- a/src/global_context.cpp +++ b/src/global_context.cpp @@ -23,7 +23,7 @@ void global_context::eval(std::vector &events, const object_store &store, } cache_it = new_it; } - + DDWAF_DEBUG("Evaluating rule {}", rule->get_id()); auto opt_evt = rule->eval(store, cache_it->second, timepoint, deadline); if (opt_evt.has_value()) { events.emplace_back(std::move(opt_evt.value())); diff --git a/tools/waf_streamer.cpp b/tools/waf_streamer.cpp index 826b1594d..7ed3cd35c 100644 --- a/tools/waf_streamer.cpp +++ b/tools/waf_streamer.cpp @@ -52,6 +52,8 @@ auto parse_args(int argc, char *argv[]) int main(int argc, char *argv[]) { + //ddwaf_set_log_cb(log_cb, DDWAF_LOG_TRACE); + auto args = parse_args(argc, argv); std::string ruleset = args["--ruleset"]; @@ -62,7 +64,7 @@ int main(int argc, char *argv[]) auto rule = YAML::Load(read_file(ruleset)).as(); const ddwaf_config config{{0, 0, 0}, {nullptr, nullptr}, ddwaf_object_free}; - auto handle = ddwaf_init(&rule, &config, nullptr); + auto *handle = ddwaf_init(&rule, &config, nullptr); ddwaf_object_free(&rule); if (handle == nullptr) { std::cout << "Failed to load " << ruleset << '\n';