diff --git a/cmake/objects.cmake b/cmake/objects.cmake index edb90268b..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 @@ -21,6 +22,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/context.cpp b/src/context.cpp index f71a9b3a7..1a3e0fee2 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(); @@ -80,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); } } @@ -191,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()) { @@ -228,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 12b6bef3a..5cc6d905e 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" @@ -50,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(); } @@ -89,6 +87,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 f7b60b792..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; @@ -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/global_context.cpp b/src/global_context.cpp new file mode 100644 index 000000000..21d5879c7 --- /dev/null +++ b/src/global_context.cpp @@ -0,0 +1,34 @@ +// 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(); + + DDWAF_DEBUG("Evaluating global rules"); + 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; + } + 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())); + } + } +} + +} // namespace ddwaf diff --git a/src/global_context.hpp b/src/global_context.hpp new file mode 100644 index 000000000..099a4ebb6 --- /dev/null +++ b/src/global_context.hpp @@ -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. + +#pragma once + +#include + +#include "rule.hpp" + +namespace ddwaf { + +class global_context { +public: + using cache_type = std::unordered_map; + + 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, cache_type &lcache, + ddwaf::timer &deadline); + +protected: + std::vector> rules_; +}; + +} // namespace ddwaf 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 f037532a0..018da411c 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_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'"); + } + + 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_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.cpp b/src/rule.cpp new file mode 100644 index 000000000..0abe43363 --- /dev/null +++ b/src/rule.cpp @@ -0,0 +1,74 @@ +// 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" +#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) +{ + 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; + } + + 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 + // 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 &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; + } + + auto res = expr_->eval(cache, store, {}, {}, deadline); + if (!res.outcome) { + return std::nullopt; + } + + 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 + 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; +} + +} // namespace ddwaf diff --git a/src/rule.hpp b/src/rule.hpp index 50d1cdfdb..d98cc7a98 100644 --- a/src/rule.hpp +++ b/src/rule.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include #include "clock.hpp" @@ -19,71 +21,34 @@ #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; - - 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_)) - {} - - rule &operator=(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_); - return *this; - } + base_rule(const base_rule &) = delete; + base_rule &operator=(const base_rule &) = delete; - virtual ~rule() = default; + base_rule(base_rule &&rhs) noexcept = default; + base_rule &operator=(base_rule &&rhs) noexcept = default; - virtual std::optional match(const object_store &store, cache_type &cache, - const exclusion::object_set_ref &objects_excluded, - const std::unordered_map> &dynamic_matchers, - ddwaf::timer &deadline) const - { - if (expression::get_result(cache)) { - // An event was already produced, so we skip the rule - return std::nullopt; - } - - auto res = expr_->eval(cache, store, objects_excluded, dynamic_matchers, deadline); - if (!res.outcome) { - return std::nullopt; - } - - return {ddwaf::event{this, expression::get_matches(cache), res.ephemeral}}; - } + virtual ~base_rule() = default; [[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_; } @@ -106,7 +71,6 @@ class rule { protected: bool enabled_{true}; - source_type source_; std::string id_; std::string name_; std::unordered_map tags_; @@ -114,4 +78,134 @@ class rule { 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, + const std::unordered_map> &dynamic_matchers, + ddwaf::timer &deadline) const + { + if (expression::get_result(cache)) { + // An event was already produced, so we skip the rule + return std::nullopt; + } + + auto res = expr_->eval(cache, store, objects_excluded, dynamic_matchers, deadline); + if (!res.outcome) { + return std::nullopt; + } + + return {ddwaf::event{this, expression::get_matches(cache), res.ephemeral}}; + } + + source_type get_source() const { return source_; } + +protected: + source_type source_; +}; + +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{}; + }; + + 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_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), + threshold_str_(to_string(criteria_.threshold)) + {} + + ~threshold_rule() override = default; + threshold_rule(const threshold_rule &) = delete; + threshold_rule &operator=(const threshold_rule &) = delete; + threshold_rule(threshold_rule &&rhs) noexcept = delete; + threshold_rule &operator=(threshold_rule &&rhs) noexcept = delete; + + 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_; + std::string threshold_str_; +}; + +class indexed_threshold_rule : public base_threshold_rule { +public: + struct evaluation_criteria { + std::string name; + target_index target; + std::size_t threshold; + std::chrono::milliseconds period; + }; + + 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_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), + threshold_str_(to_string(criteria_.threshold)) + {} + + ~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 = delete; + indexed_threshold_rule &operator=(indexed_threshold_rule &&rhs) noexcept = delete; + + 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_; + std::string threshold_str_; +}; + } // namespace ddwaf diff --git a/src/ruleset.hpp b/src/ruleset.hpp index cffbaa07a..904fb892a 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 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 4b1a5bbeb..b06d89a40 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" @@ -52,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); @@ -107,6 +109,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 new file mode 100644 index 000000000..770e188fa --- /dev/null +++ b/src/timed_counter.hpp @@ -0,0 +1,214 @@ +// Unless explicitly stated otherwise all files in this repository are +// dual-licensed under the Apache-2.0 License or BSD-3-Clause License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2021 Datadog, Inc. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "utils.hpp" + +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); + } + + std::size_t add_timepoint_and_count(T point) + { + // Discard old elements + 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 (buckets < time_points_.size()) { + // Add a new element + 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 = point; + count -= (time_points_[right].count - 1); + time_points_[right].count = 1; + right = increment(right); + left = increment(left); + } + + return count; + } + + T last_timepoint() const + { + if (buckets == 0) { + [[unlikely]] return static_cast(0); + } + + auto index = decrement(right); + return time_points_[index].point; + } + + std::size_t update_count(T point) + { + // Discard old elements + auto window_begin = point - period_; + 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 = buckets = 0; } + +protected: + [[nodiscard]] std::size_t increment(std::size_t value) const + { + return (value + 1) % time_points_.size(); + } + + [[nodiscard]] 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::size_t left{0}; + std::size_t right{0}; + std::size_t count{0}; + 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_ts { +public: + 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) + {} + + 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_) { + remove_oldest_entry(point); + } + + auto [new_it, res] = + index_.emplace(Key{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::map, std::less<>> index_; + mutable std::mutex mtx_; +}; + +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 new file mode 100644 index 000000000..df91c19c5 --- /dev/null +++ b/tests/timed_counter_test.cpp @@ -0,0 +1,104 @@ +// 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::literals; +using namespace std::chrono_literals; + +namespace { + +TEST(TestTimedCounter, BasicMs) +{ + 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); + 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_ts 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_ts window{10s, 5, 5}; + + 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"sv, 11s), 1); + + // User will now be removed + 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"sv, 21s), 1); +} + +} // namespace 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..7ed3cd35c --- /dev/null +++ b/tools/waf_streamer.cpp @@ -0,0 +1,141 @@ +// 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[]) +{ + //ddwaf_set_log_cb(log_cb, DDWAF_LOG_TRACE); + + 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; +}