diff --git a/src/collection.cpp b/src/collection.cpp index 69795b4ee..97f0baa57 100644 --- a/src/collection.cpp +++ b/src/collection.cpp @@ -71,7 +71,7 @@ std::optional match_rule(rule *rule, const object_store &store, } template -void base_collection::match(std::vector &events, const object_store &store, +void base_collection::match(std::vector &events, object_store &store, collection_cache &cache, const exclusion::context_policy &exclusion, const std::unordered_map> &dynamic_matchers, ddwaf::timer &deadline) const diff --git a/src/collection.hpp b/src/collection.hpp index 3f5659a1c..6f4e883ce 100644 --- a/src/collection.hpp +++ b/src/collection.hpp @@ -44,7 +44,7 @@ template class base_collection { void insert(const std::shared_ptr &rule) { rules_.emplace_back(rule.get()); } - void match(std::vector &events, const object_store &store, collection_cache &cache, + void match(std::vector &events, object_store &store, collection_cache &cache, const exclusion::context_policy &exclusion, const std::unordered_map> &dynamic_matchers, ddwaf::timer &deadline) const; diff --git a/src/condition/exists.hpp b/src/condition/exists.hpp new file mode 100644 index 000000000..d66d27baa --- /dev/null +++ b/src/condition/exists.hpp @@ -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. + +#pragma once + +#include "condition/structured_condition.hpp" + +namespace ddwaf { + +class exists_condition : public base_impl { +public: + static constexpr std::array param_names{"inputs"}; + + explicit exists_condition( + std::vector args, const object_limits &limits = {}) + : base_impl(std::move(args), limits) + {} + +protected: + [[nodiscard]] eval_result eval_impl(const unary_argument &input, + condition_cache &cache, const exclusion::object_set_ref & /*objects_excluded*/, + ddwaf::timer & /*deadline*/) const + { + cache.match = {{{{"input", {}, input.address, {}}}, {}, "exists", {}, input.ephemeral}}; + return {true, input.ephemeral}; + } + + friend class base_impl; +}; + +} // namespace ddwaf diff --git a/src/context.cpp b/src/context.cpp index f99e2b82d..7e12e4b0f 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -11,8 +11,31 @@ namespace ddwaf { +namespace { using attribute = object_store::attribute; +// This function adds the waf.context.event "virtual" address, specifically +// meant to be used to tryigger post-processors when there has been an event +// during the lifecycle of the context. +// Since post-processors aren't typically used with ephemeral addresses or +// composite requests in general, we don't need to make this address dependent +// on whether the events were ephemeral or not. +void set_context_event_address(object_store &store) +{ + static std::string_view event_addr = "waf.context.event"; + static auto event_addr_idx = get_target_index(event_addr); + + if (store.has_target(event_addr_idx)) { + return; + } + + ddwaf_object true_obj; + ddwaf_object_bool(&true_obj, true); + store.insert(event_addr_idx, event_addr, true_obj, attribute::none); +} + +} // namespace + // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) DDWAF_RET_CODE context::run(optional_ref persistent, optional_ref ephemeral, optional_ref res, uint64_t timeout) @@ -81,6 +104,9 @@ DDWAF_RET_CODE context::run(optional_ref persistent, if (should_eval_rules) { events = eval_rules(policy, deadline); + if (!events.empty()) { + set_context_event_address(store_); + } } } diff --git a/src/parser/expression_parser.cpp b/src/parser/expression_parser.cpp index f6f805061..b15990e6d 100644 --- a/src/parser/expression_parser.cpp +++ b/src/parser/expression_parser.cpp @@ -4,6 +4,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2021 Datadog, Inc. +#include "condition/exists.hpp" #include "condition/lfi_detector.hpp" #include "condition/scalar_condition.hpp" #include "condition/shi_detector.hpp" @@ -114,6 +115,11 @@ std::shared_ptr parse_expression(const parameter::vector &conditions auto arguments = parse_arguments(params, source, transformers, addresses, limits); conditions.emplace_back(std::make_unique(std::move(arguments), limits)); + } else if (operator_name == "exists") { + auto arguments = + parse_arguments(params, source, transformers, addresses, limits); + conditions.emplace_back( + std::make_unique(std::move(arguments), limits)); } else { auto [data_id, matcher] = parse_matcher(operator_name, params); diff --git a/tests/integration/context/ruleset/context_event_address.json b/tests/integration/context/ruleset/context_event_address.json new file mode 100644 index 000000000..e09ebc375 --- /dev/null +++ b/tests/integration/context/ruleset/context_event_address.json @@ -0,0 +1,76 @@ +{ + "version": "2.2", + "metadata": { + "rules_version": "1.8.0" + }, + "rules": [ + { + "id": "rule1", + "name": "rule1", + "tags": { + "type": "flow1", + "category": "category1" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "waf.trigger" + } + ], + "regex": "rule" + }, + "operator": "match_regex" + } + ] + } + ], + "processors": [ + { + "id": "processor-001", + "generator": "http_endpoint_fingerprint", + "conditions": [ + { + "operator": "exists", + "parameters": { + "inputs": [ + { + "address": "waf.context.event" + } + ] + } + } + ], + "parameters": { + "mappings": [ + { + "method": [ + { + "address": "server.request.method" + } + ], + "uri_raw": [ + { + "address": "server.request.uri.raw" + } + ], + "body": [ + { + "address": "server.request.body" + } + ], + "query": [ + { + "address": "server.request.query" + } + ], + "output": "_dd.appsec.fp.http.endpoint" + } + ] + }, + "evaluate": false, + "output": true + } + ] +} diff --git a/tests/integration/context/test.cpp b/tests/integration/context/test.cpp index c9a6d04b8..86cf4b24d 100644 --- a/tests/integration/context/test.cpp +++ b/tests/integration/context/test.cpp @@ -862,4 +862,81 @@ TEST(TestContextIntegration, PersistentPriorityAndEphemeralNonPriority) ddwaf_destroy(handle); } +TEST(TestContextIntegration, WafContextEventAddress) +{ + auto rule = read_json_file("context_event_address.json", base_dir); + ASSERT_TRUE(rule.type != DDWAF_OBJ_INVALID); + ddwaf_handle handle = ddwaf_init(&rule, nullptr, nullptr); + ASSERT_NE(handle, nullptr); + ddwaf_object_free(&rule); + + ddwaf_object tmp; + + { + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + + ddwaf_object body = DDWAF_OBJECT_MAP; + ddwaf_object_map_add(&body, "key", ddwaf_object_invalid(&tmp)); + ddwaf_object_map_add(&map, "server.request.body", &body); + + ddwaf_object query = DDWAF_OBJECT_MAP; + ddwaf_object_map_add(&query, "key", ddwaf_object_invalid(&tmp)); + ddwaf_object_map_add(&map, "server.request.query", &query); + + ddwaf_object_map_add( + &map, "server.request.uri.raw", ddwaf_object_string(&tmp, "/path/to/resource/?key=")); + ddwaf_object_map_add(&map, "server.request.method", ddwaf_object_string(&tmp, "PuT")); + + ddwaf_object_map_add(&map, "waf.trigger", ddwaf_object_string(&tmp, "irrelevant")); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_OK); + EXPECT_FALSE(out.timeout); + + EXPECT_EQ(ddwaf_object_size(&out.derivatives), 0); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + } + + { + ddwaf_context context = ddwaf_context_init(handle); + ASSERT_NE(context, nullptr); + + ddwaf_object map = DDWAF_OBJECT_MAP; + + ddwaf_object body = DDWAF_OBJECT_MAP; + ddwaf_object_map_add(&body, "key", ddwaf_object_invalid(&tmp)); + ddwaf_object_map_add(&map, "server.request.body", &body); + + ddwaf_object query = DDWAF_OBJECT_MAP; + ddwaf_object_map_add(&query, "key", ddwaf_object_invalid(&tmp)); + ddwaf_object_map_add(&map, "server.request.query", &query); + + ddwaf_object_map_add( + &map, "server.request.uri.raw", ddwaf_object_string(&tmp, "/path/to/resource/?key=")); + ddwaf_object_map_add(&map, "server.request.method", ddwaf_object_string(&tmp, "PuT")); + + ddwaf_object_map_add(&map, "waf.trigger", ddwaf_object_string(&tmp, "rule")); + + ddwaf_result out; + ASSERT_EQ(ddwaf_run(context, &map, nullptr, &out, LONG_TIME), DDWAF_MATCH); + EXPECT_FALSE(out.timeout); + + EXPECT_EQ(ddwaf_object_size(&out.derivatives), 1); + + auto json = test::object_to_json(out.derivatives); + EXPECT_STR( + json, R"({"_dd.appsec.fp.http.endpoint":"http-put-729d56c3-2c70e12b-2c70e12b"})"); + + ddwaf_result_free(&out); + ddwaf_context_destroy(context); + } + + ddwaf_destroy(handle); +} + } // namespace diff --git a/tests/operator/exists_condition_test.cpp b/tests/operator/exists_condition_test.cpp new file mode 100644 index 000000000..43ea12870 --- /dev/null +++ b/tests/operator/exists_condition_test.cpp @@ -0,0 +1,56 @@ +// Unless explicitly stated otherwise all files in this repository are +// dual-licensed under the Apache-2.0 License or BSD-3-Clause License. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2021 Datadog, Inc. + +#include "../test.hpp" +#include "condition/exists.hpp" + +using namespace ddwaf; +using namespace std::literals; + +namespace { + +template std::vector gen_param_def(Args... addresses) +{ + return {{{{std::string{addresses}, get_target_index(addresses)}}}...}; +} + +TEST(TestExistsCondition, AddressAvailable) +{ + exists_condition cond{{gen_param_def("server.request.uri_raw")}}; + + ddwaf_object tmp; + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.uri_raw", ddwaf_object_invalid(&tmp)); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_TRUE(res.outcome); +} + +TEST(TestExistsCondition, AddressNotAvaialble) +{ + exists_condition cond{{gen_param_def("server.request.uri_raw")}}; + + ddwaf_object tmp; + ddwaf_object root; + ddwaf_object_map(&root); + ddwaf_object_map_add(&root, "server.request.query", ddwaf_object_invalid(&tmp)); + + object_store store; + store.insert(root); + + ddwaf::timer deadline{2s}; + condition_cache cache; + auto res = cond.eval(cache, store, {}, {}, deadline); + ASSERT_FALSE(res.outcome); +} + +} // namespace diff --git a/tests/waf_test.cpp b/tests/waf_test.cpp index 55699258f..f51a28715 100644 --- a/tests/waf_test.cpp +++ b/tests/waf_test.cpp @@ -62,55 +62,25 @@ TEST(TestWaf, RuleDisabledInRuleset) TEST(TestWaf, AddressUniqueness) { - std::unordered_set indices; + std::array addresses{"grpc.server.method", "grpc.server.request.message", + "grpc.server.request.metadata", "grpc.server.response.message", + "grpc.server.response.metadata.headers", "grpc.server.response.metadata.trailers", + "grpc.server.response.status", "graphql.server.all_resolvers", "graphql.server.resolver", + "http.client_ip", "server.request.body", "server.request.headers.no_cookies", + "server.request.path_params", "server.request.query", "server.request.uri.raw", + "server.request.trailers", "server.request.cookies", "server.response.body", + "server.response.headers.no_cookies", "server.response.status", "usr.id", "usr.session_id", + "waf.context.processor", "waf.context.event", "_dd.appsec.fp.http.endpoint", + "_dd.appsec.fp.http.header", "_dd.appsec.fp.http.network", + "_dd.appsec.fp.session" + "_dd.appsec.s.req.body", + "_dd.appsec.s.req.cookies", "_dd.appsec.s.req.query", "_dd.appsec.s.req.params", + "_dd.appsec.s.res.body", "_dd.appsec.s.graphql.all_resolvers", + "_dd.appsec.s.graphql.resolver", "_dd.appsec.s.req.headers", "_dd.appsec.s.res.headers"}; - { - std::size_t hash = std::hash()("grpc.server.request.message"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("grpc.server.request.metadata"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("http.client_ip"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("server.request.body"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("server.request.headers.no_cookies"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("server.request.path_params"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("server.request.query"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("server.request.uri.raw"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("server.response.status"); - EXPECT_EQ(indices.find(hash), indices.end()); - indices.insert(hash); - } - { - std::size_t hash = std::hash()("usr.id"); + std::unordered_set indices; + for (auto addr : addresses) { + std::size_t hash = std::hash()(addr); EXPECT_EQ(indices.find(hash), indices.end()); indices.insert(hash); } diff --git a/validator/tests/rules/operators/exists/001_rule1_exists_match.yaml b/validator/tests/rules/operators/exists/001_rule1_exists_match.yaml new file mode 100644 index 000000000..bd56ab484 --- /dev/null +++ b/validator/tests/rules/operators/exists/001_rule1_exists_match.yaml @@ -0,0 +1,20 @@ +{ + name: "Basic run with exists operator", + runs: [ + { + persistent-input: { + rule1-input: "something else" + }, + rules: [ + { + 1: [ + { + address: rule1-input, + } + ] + } + ], + code: match + } + ] +} diff --git a/validator/tests/rules/operators/exists/002_rule1_no_exists_match.yaml b/validator/tests/rules/operators/exists/002_rule1_no_exists_match.yaml new file mode 100644 index 000000000..d6c5e9c1a --- /dev/null +++ b/validator/tests/rules/operators/exists/002_rule1_no_exists_match.yaml @@ -0,0 +1,11 @@ +{ + name: "Basic run with exists operator and no match", + runs: [ + { + persistent-input: { + rule2-input: "something" + }, + code: ok + } + ] +} diff --git a/validator/tests/rules/operators/exists/ruleset.yaml b/validator/tests/rules/operators/exists/ruleset.yaml new file mode 100644 index 000000000..fe5982507 --- /dev/null +++ b/validator/tests/rules/operators/exists/ruleset.yaml @@ -0,0 +1,12 @@ +version: '2.1' +rules: + - id: "1" + name: rule1-exists + tags: + type: flow1 + category: category + conditions: + - operator: exists + parameters: + inputs: + - address: rule1-input