diff --git a/appsec/src/extension/commands_helpers.c b/appsec/src/extension/commands_helpers.c index 7e7c16766e..a265ecaaf9 100644 --- a/appsec/src/extension/commands_helpers.c +++ b/appsec/src/extension/commands_helpers.c @@ -6,13 +6,13 @@ #include "commands_helpers.h" #include "backtrace.h" #include "commands_ctx.h" -#include "configuration.h" #include "ddappsec.h" #include "ddtrace.h" #include "logging.h" #include "msgpack_helpers.h" #include "request_abort.h" #include "tags.h" +#include "user_tracking.h" #include #include #include @@ -419,6 +419,8 @@ static void _command_process_stack_trace_parameters(mpack_node_t root) static dd_result _command_process_actions( mpack_node_t root, struct req_info *ctx); +static void dd_command_process_settings(mpack_node_t root); + /* * array( * 0: [<"ok" / "record" / "block" / "redirect">, @@ -435,9 +437,10 @@ static dd_result _command_process_actions( #define RESP_INDEX_ACTION_PARAMS 0 #define RESP_INDEX_APPSEC_SPAN_DATA 1 #define RESP_INDEX_FORCE_KEEP 2 -#define RESP_INDEX_SPAN_META 3 -#define RESP_INDEX_SPAN_METRICS 4 -#define RESP_INDEX_TELEMETRY_METRICS 5 +#define RESP_INDEX_SETTINGS 3 +#define RESP_INDEX_SPAN_META 4 +#define RESP_INDEX_SPAN_METRICS 5 +#define RESP_INDEX_TELEMETRY_METRICS 6 dd_result dd_command_proc_resp_verd_span_data( mpack_node_t root, void *unspecnull _ctx) @@ -461,6 +464,11 @@ dd_result dd_command_proc_resp_verd_span_data( dd_tags_set_sampling_priority(); } + if (mpack_node_array_length(root) >= RESP_INDEX_SETTINGS + 1) { + mpack_node_t settings = mpack_node_array_at(root, RESP_INDEX_SETTINGS); + dd_command_process_settings(settings); + } + if (mpack_node_array_length(root) >= RESP_INDEX_SPAN_METRICS + 1 && ctx->root_span) { zend_object *span = ctx->root_span; @@ -553,6 +561,48 @@ static void _set_appsec_span_data(mpack_node_t node) } } +static void dd_command_process_settings(mpack_node_t root) +{ + if (mpack_node_type(root) != mpack_type_map) { + return; + } + + size_t count = mpack_node_map_count(root); + + for (size_t i = 0; i < count; i++) { + mpack_node_t key = mpack_node_map_key_at(root, i); + mpack_node_t value = mpack_node_map_value_at(root, i); + + if (mpack_node_type(key) != mpack_type_str) { + mlog(dd_log_warning, "Failed to process unknown setting: " + "invalid type for key"); + continue; + } + if (mpack_node_type(value) != mpack_type_str) { + mlog(dd_log_warning, "Failed to process unknown setting: " + "invalid type for value"); + continue; + } + + const char *key_str = mpack_node_str(key); + const char *value_str = mpack_node_str(value); + size_t key_len = mpack_node_strlen(key); + size_t value_len = mpack_node_strlen(value); + + if (dd_string_equals_lc( + key_str, key_len, ZEND_STRL("auto_user_instrum"))) { + dd_parse_user_collection_mode_rc(value_str, value_len); + } else { + if (!get_global_DD_APPSEC_TESTING()) { + mlog(dd_log_warning, + "Failed to process user collection setting: " + "unknown key %.*s", + (int)key_len, key_str); + } + } + } +} + void dd_command_process_meta(mpack_node_t root, zend_object *nonnull span) { if (mpack_node_type(root) != mpack_type_map) { diff --git a/appsec/src/extension/user_tracking.c b/appsec/src/extension/user_tracking.c index f930293373..11ff319d55 100644 --- a/appsec/src/extension/user_tracking.c +++ b/appsec/src/extension/user_tracking.c @@ -6,12 +6,14 @@ #include "user_tracking.h" #include "commands/request_exec.h" +#include "compatibility.h" #include "ddappsec.h" #include "dddefs.h" #include "ddtrace.h" #include "helper_process.h" #include "logging.h" #include "php_compat.h" +#include "php_objects.h" #include "request_abort.h" #include "request_lifecycle.h" #include "string_helpers.h" @@ -21,6 +23,8 @@ #include static THREAD_LOCAL_ON_ZTS user_collection_mode _user_mode = user_mode_disabled; +static THREAD_LOCAL_ON_ZTS user_collection_mode _user_mode_rc = + user_mode_undefined; static zend_string *_user_mode_anon_zstr; static zend_string *_user_mode_ident_zstr; @@ -28,6 +32,7 @@ static zend_string *_user_mode_disabled_zstr; static zend_string *_sha256_algo_zstr; static void (*_ddtrace_set_user)(INTERNAL_FUNCTION_PARAMETERS) = NULL; +static void _register_test_objects(void); #if PHP_VERSION_ID < 80000 typedef const php_hash_ops *(*hash_fetch_ops_t)( @@ -78,6 +83,8 @@ void dd_user_tracking_startup(void) } #endif + _register_test_objects(); + if (!dd_trace_loaded()) { return; } @@ -181,6 +188,28 @@ bool dd_parse_user_collection_mode( return true; } +void dd_parse_user_collection_mode_rc( + const char *nonnull value, size_t value_len) +{ + if (dd_string_equals_lc(value, value_len, ZEND_STRL("undefined"))) { + _user_mode_rc = user_mode_undefined; + } else if (dd_string_equals_lc( + value, value_len, ZEND_STRL("identification"))) { + _user_mode_rc = user_mode_ident; + } else if (dd_string_equals_lc( + value, value_len, ZEND_STRL("anonymization"))) { + _user_mode_rc = user_mode_anon; + } else { // If the value is disabled or an unknown value, we disable user ID + // collection + if (!get_global_DD_APPSEC_TESTING()) { + mlog_g(dd_log_warning, + "Unknown or disabled remote config user collection mode: %.*s", + (int)value_len, value); + } + _user_mode_rc = user_mode_disabled; + } +} + zend_string *nullable dd_user_info_anonymize(zend_string *nonnull user_info) { zend_string *digest; @@ -240,17 +269,49 @@ zend_string *nullable dd_user_info_anonymize(zend_string *nonnull user_info) return anon_user_id; } -user_collection_mode dd_get_user_collection_mode() { return _user_mode; } +user_collection_mode dd_get_user_collection_mode() +{ + return _user_mode_rc != user_mode_undefined ? _user_mode_rc : _user_mode; +} zend_string *nonnull dd_get_user_collection_mode_zstr() { - if (_user_mode == user_mode_ident) { + user_collection_mode mode = dd_get_user_collection_mode(); + + if (mode == user_mode_ident) { return _user_mode_ident_zstr; } - if (_user_mode == user_mode_anon) { + if (mode == user_mode_anon) { return _user_mode_anon_zstr; } return _user_mode_disabled_zstr; } + +PHP_FUNCTION(datadog_appsec_testing_dump_user_collection_mode) +{ + if (zend_parse_parameters_none() == FAILURE) { + return; + } + + RETURN_STR(dd_get_user_collection_mode_zstr()); +} + +// clang-format off +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(dump_user_collection_mode_arginfo, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +static const zend_function_entry functions[] = { + ZEND_RAW_FENTRY(DD_TESTING_NS "dump_user_collection_mode", PHP_FN(datadog_appsec_testing_dump_user_collection_mode), dump_user_collection_mode_arginfo, 0, NULL, NULL) + PHP_FE_END +}; +// clang-format on + +static void _register_test_objects() +{ + if (!get_global_DD_APPSEC_TESTING()) { + return; + } + dd_phpobj_reg_funcs(functions); +} diff --git a/appsec/src/extension/user_tracking.h b/appsec/src/extension/user_tracking.h index fa722267f2..549652a16b 100644 --- a/appsec/src/extension/user_tracking.h +++ b/appsec/src/extension/user_tracking.h @@ -5,7 +5,7 @@ // (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. #pragma once -#include "configuration.h" +#include "zai_string/string.h" #include "attributes.h" #include @@ -13,6 +13,7 @@ typedef enum _user_collection_mode { user_mode_disabled = 0, user_mode_anon, user_mode_ident, + user_mode_undefined, } user_collection_mode; void dd_user_tracking_startup(void); @@ -23,6 +24,9 @@ void dd_find_and_apply_verdict_for_user(zend_string *nonnull user_id); bool dd_parse_user_collection_mode( zai_str value, zval *nonnull decoded_value, bool persistent); +void dd_parse_user_collection_mode_rc( + const char *nonnull value, size_t value_len); + zend_string *nullable dd_user_info_anonymize(zend_string *nonnull user_info); user_collection_mode dd_get_user_collection_mode(void); diff --git a/appsec/src/helper/client.cpp b/appsec/src/helper/client.cpp index ff2a709847..e9804c14ae 100644 --- a/appsec/src/helper/client.cpp +++ b/appsec/src/helper/client.cpp @@ -17,6 +17,7 @@ #include "network/broker.hpp" #include "network/proto.hpp" #include "service.hpp" +#include "service_config.hpp" #include "std_logging.hpp" using namespace std::chrono_literals; @@ -302,6 +303,10 @@ bool client::handle_command(network::request_init::request &command) context_.emplace(*service_->get_engine()); auto response = publish(command); + if (response) { + response->settings["auto_user_instrum"] = to_string_view( + service_->get_service_config()->get_auto_user_intrum_mode()); + } return send_message(response); } diff --git a/appsec/src/helper/json_helper.cpp b/appsec/src/helper/json_helper.cpp index a9c8bc7851..c36f121429 100644 --- a/appsec/src/helper/json_helper.cpp +++ b/appsec/src/helper/json_helper.cpp @@ -202,6 +202,26 @@ json_helper::get_field_of_type( return get_field_of_type(*parent_field, key, type); } +bool json_helper::field_exists( + const rapidjson::Value &parent_field, std::string_view key) +{ + return parent_field.FindMember(key.data()) != parent_field.MemberEnd(); +} + +bool json_helper::field_exists( + const rapidjson::Value::ConstMemberIterator &parent_field, + std::string_view key) +{ + return field_exists(parent_field->value, key); +} + +bool json_helper::field_exists( + const rapidjson::Value::ConstValueIterator parent_field, + std::string_view key) +{ + return field_exists(*parent_field, key); +} + bool json_helper::parse_json( std::string_view content, rapidjson::Document &output) { diff --git a/appsec/src/helper/json_helper.hpp b/appsec/src/helper/json_helper.hpp index 42e07c046e..98fec766d7 100644 --- a/appsec/src/helper/json_helper.hpp +++ b/appsec/src/helper/json_helper.hpp @@ -63,6 +63,11 @@ std::optional get_field_of_type( std::optional get_field_of_type( rapidjson::Value::ConstValueIterator parent_field, std::string_view key, rapidjson::Type type); +bool field_exists(const rapidjson::Value &parent_field, std::string_view key); +bool field_exists(const rapidjson::Value::ConstMemberIterator &parent_field, + std::string_view key); +bool field_exists(const rapidjson::Value::ConstValueIterator parent_field, + std::string_view key); bool parse_json(std::string_view content, rapidjson::Document &output); // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void merge_arrays(rapidjson::Value &destination, rapidjson::Value &source, diff --git a/appsec/src/helper/network/proto.hpp b/appsec/src/helper/network/proto.hpp index 07baae479a..3d1f243ffe 100644 --- a/appsec/src/helper/network/proto.hpp +++ b/appsec/src/helper/network/proto.hpp @@ -175,8 +175,9 @@ struct request_init { std::vector triggers; bool force_keep; + std::map settings; - MSGPACK_DEFINE(actions, triggers, force_keep); + MSGPACK_DEFINE(actions, triggers, force_keep, settings); }; }; @@ -211,8 +212,9 @@ struct request_exec { std::vector triggers; bool force_keep; + std::map settings; - MSGPACK_DEFINE(actions, triggers, force_keep); + MSGPACK_DEFINE(actions, triggers, force_keep, settings); }; }; @@ -290,6 +292,7 @@ struct request_shutdown { std::vector triggers; bool force_keep; + std::map settings; std::map meta; std::map metrics; @@ -297,8 +300,8 @@ struct request_shutdown { std::vector>> tel_metrics; - MSGPACK_DEFINE( - actions, triggers, force_keep, meta, metrics, tel_metrics); + MSGPACK_DEFINE(actions, triggers, force_keep, settings, meta, metrics, + tel_metrics); }; }; diff --git a/appsec/src/helper/remote_config/listeners/asm_features_listener.cpp b/appsec/src/helper/remote_config/listeners/asm_features_listener.cpp index 71c293a04d..02d32a1fc6 100644 --- a/appsec/src/helper/remote_config/listeners/asm_features_listener.cpp +++ b/appsec/src/helper/remote_config/listeners/asm_features_listener.cpp @@ -7,31 +7,36 @@ #include "../../json_helper.hpp" #include "../../utils.hpp" #include "../exception.hpp" -#include +#include "config_aggregators/asm_features_aggregator.hpp" #include -void dds::remote_config::asm_features_listener::on_update(const config &config) +namespace dds::remote_config { + +void asm_features_listener::init() { - rapidjson::Document serialized_doc; + ruleset_ = rapidjson::Document(rapidjson::kObjectType); + aggregator_ = std::make_unique(); + aggregator_->init(&ruleset_.GetAllocator()); +} - { - const mapped_memory contents{config.read()}; - if (!json_helper::parse_json(contents, serialized_doc)) { - throw error_applying_config("Invalid config contents"); - } - } +void asm_features_listener::on_update(const config &config) +{ + aggregator_->add(config); +} - auto asm_itr = json_helper::get_field_of_type( - serialized_doc, "asm", rapidjson::kObjectType); +void asm_features_listener::parse_asm_activation_config() +{ + auto asm_itr = + json_helper::get_field_of_type(ruleset_, "asm", rapidjson::kObjectType); if (!asm_itr) { - throw error_applying_config("Invalid config json encoded contents: " - "asm key missing or invalid"); + service_config_->unset_asm(); + return; } auto enabled_itr = asm_itr.value()->value.FindMember("enabled"); if (enabled_itr == asm_itr.value()->value.MemberEnd()) { - throw error_applying_config( - "Invalid config json encoded contents: enabled key missing"); + service_config_->unset_asm(); + return; } if (enabled_itr->value.GetType() == rapidjson::kStringType) { @@ -49,7 +54,53 @@ void dds::remote_config::asm_features_listener::on_update(const config &config) // when appsec should not be enabled service_config_->disable_asm(); } else { - throw error_applying_config( - "Invalid config json encoded contents: enabled key invalid"); + service_config_->unset_asm(); + } +} + +void asm_features_listener::parse_auto_user_instrum_config() +{ + if (!json_helper::field_exists(ruleset_, "auto_user_instrum")) { + service_config_->unset_auto_user_instrum(); + return; + } + + auto auto_user_instrum_itr = json_helper::get_field_of_type( + ruleset_, "auto_user_instrum", rapidjson::kObjectType); + if (!auto_user_instrum_itr) { + service_config_->set_auto_user_instrum(auto_user_instrum_mode::UNKNOWN); + return; + } + + auto mode_itr = auto_user_instrum_itr.value()->value.FindMember("mode"); + if (mode_itr == auto_user_instrum_itr.value()->value.MemberEnd()) { + service_config_->set_auto_user_instrum(auto_user_instrum_mode::UNKNOWN); + return; + } + + auto mode = auto_user_instrum_mode::UNKNOWN; + + if (mode_itr->value.GetType() == rapidjson::kStringType) { + if (dd_tolower(mode_itr->value.GetString()) == + std::string("identification")) { + mode = auto_user_instrum_mode::IDENTIFICATION; + } else if (dd_tolower(mode_itr->value.GetString()) == + std::string("anonymization")) { + mode = auto_user_instrum_mode::ANONYMIZATION; + } else if (dd_tolower(mode_itr->value.GetString()) == + std::string("disabled")) { + mode = auto_user_instrum_mode::DISABLED; + } } + + service_config_->set_auto_user_instrum(mode); +} + +void asm_features_listener::commit() +{ + aggregator_->aggregate(ruleset_); + parse_asm_activation_config(); + parse_auto_user_instrum_config(); } + +} // namespace dds::remote_config diff --git a/appsec/src/helper/remote_config/listeners/asm_features_listener.hpp b/appsec/src/helper/remote_config/listeners/asm_features_listener.hpp index d18aed760a..d7a9765781 100644 --- a/appsec/src/helper/remote_config/listeners/asm_features_listener.hpp +++ b/appsec/src/helper/remote_config/listeners/asm_features_listener.hpp @@ -5,10 +5,11 @@ // (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. #pragma once -#include "../../config.hpp" #include "../../service_config.hpp" #include "../product.hpp" +#include "config_aggregators/config_aggregator.hpp" #include "listener.hpp" +#include namespace dds::remote_config { @@ -17,23 +18,24 @@ class asm_features_listener : public listener_base { explicit asm_features_listener( std::shared_ptr service_config) : service_config_(std::move(service_config)){}; + + void init() override; void on_update(const config &config) override; - void on_unapply(const config & /*config*/) override - { - SPDLOG_DEBUG("reset ASM activation status"); - service_config_->unset_asm(); - } + void on_unapply(const config &config) override {} + void commit() override; [[nodiscard]] std::unordered_set get_supported_products() override { return {known_products::ASM_FEATURES}; } - void init() override {} - void commit() override {} - protected: + void parse_asm_activation_config(); + void parse_auto_user_instrum_config(); + std::shared_ptr service_config_; + rapidjson::Document ruleset_; + config_aggregator_base::unique_ptr aggregator_; }; } // namespace dds::remote_config diff --git a/appsec/src/helper/remote_config/listeners/config_aggregators/asm_features_aggregator.cpp b/appsec/src/helper/remote_config/listeners/config_aggregators/asm_features_aggregator.cpp new file mode 100644 index 0000000000..f03859421f --- /dev/null +++ b/appsec/src/helper/remote_config/listeners/config_aggregators/asm_features_aggregator.cpp @@ -0,0 +1,64 @@ +// 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 "asm_features_aggregator.hpp" +#include "../../exception.hpp" +#include +#include +#include + +namespace dds::remote_config { + +namespace { +constexpr std::array expected_keys{ + "asm", "auto_user_instrum", "attack_mode", "api_security"}; +} // namespace + +void asm_features_aggregator::init( + rapidjson::Document::AllocatorType *allocator) +{ + ruleset_ = rapidjson::Document(rapidjson::kObjectType, allocator); +} + +void asm_features_aggregator::add(const config &config) +{ + rapidjson::Document doc(&ruleset_.GetAllocator()); + if (!json_helper::parse_json(config.read(), doc)) { + throw error_applying_config("Invalid config contents"); + } + + if (!doc.IsObject()) { + throw error_applying_config("Invalid type for config, expected object"); + } + + std::vector available_keys; + available_keys.reserve(doc.MemberCount()); + + // Validate contents and extract available keys + for (const auto &key : expected_keys) { + auto doc_it = doc.FindMember(StringRef(key)); + if (doc_it != doc.MemberEnd()) { + auto &value = doc_it->value; + if (!value.IsObject()) { + throw error_applying_config( + "Invalid type for " + std::string(key)); + } + + available_keys.emplace_back(key); + } + } + + // All keys should be correct so no need to check for their type again. + for (const auto &key : available_keys) { + // Make sure we override the value with the latest config. + ruleset_.RemoveMember(StringRef(key)); + + auto doc_it = doc.FindMember(StringRef(key)); + ruleset_.AddMember( + StringRef(key), doc_it->value, ruleset_.GetAllocator()); + } +} + +} // namespace dds::remote_config diff --git a/appsec/src/helper/remote_config/listeners/config_aggregators/asm_features_aggregator.hpp b/appsec/src/helper/remote_config/listeners/config_aggregators/asm_features_aggregator.hpp new file mode 100644 index 0000000000..c58db85719 --- /dev/null +++ b/appsec/src/helper/remote_config/listeners/config_aggregators/asm_features_aggregator.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 "../../../json_helper.hpp" +#include "config_aggregator.hpp" +#include + +namespace dds::remote_config { + +class asm_features_aggregator : public config_aggregator_base { +public: + asm_features_aggregator() = default; + asm_features_aggregator(const asm_features_aggregator &) = delete; + asm_features_aggregator(asm_features_aggregator &&) = default; + asm_features_aggregator &operator=( + const asm_features_aggregator &) = delete; + asm_features_aggregator &operator=(asm_features_aggregator &&) = default; + ~asm_features_aggregator() override = default; + + void init(rapidjson::Document::AllocatorType *allocator) override; + void add(const config &config) override; + void remove(const config & /*config*/) override {} + void aggregate(rapidjson::Document &doc) override + { + json_helper::merge_objects(doc, ruleset_, doc.GetAllocator()); + } + +protected: + rapidjson::Document ruleset_; +}; + +} // namespace dds::remote_config diff --git a/appsec/src/helper/service_config.hpp b/appsec/src/helper/service_config.hpp index 7e2684e779..67da72461b 100644 --- a/appsec/src/helper/service_config.hpp +++ b/appsec/src/helper/service_config.hpp @@ -10,6 +10,13 @@ namespace dds { enum class enable_asm_status : unsigned { NOT_SET = 0, ENABLED, DISABLED }; +enum class auto_user_instrum_mode : unsigned { + UNDEFINED = 0, + UNKNOWN, + DISABLED, + IDENTIFICATION, + ANONYMIZATION +}; } // namespace dds template <> @@ -36,6 +43,36 @@ struct fmt::formatter> } }; +template <> +struct fmt::formatter> + : fmt::formatter { + auto format(const std::atomic &mode, + format_context &ctx) const + { + auto val = mode.load(); + std::string_view name{""}; + switch (val) { + case dds::auto_user_instrum_mode::UNDEFINED: + name = "undefined"; + break; + case dds::auto_user_instrum_mode::UNKNOWN: + name = "unknown"; + break; + case dds::auto_user_instrum_mode::DISABLED: + name = "disabled"; + break; + case dds::auto_user_instrum_mode::IDENTIFICATION: + name = "identification"; + break; + case dds::auto_user_instrum_mode::ANONYMIZATION: + name = "anonymization"; + break; + } + + return fmt::formatter::format(name, ctx); + } +}; + namespace dds { inline std::string_view to_string_view(enable_asm_status status) @@ -52,6 +89,23 @@ inline std::string_view to_string_view(enable_asm_status status) return "UNKNOWN"; } +inline std::string_view to_string_view(auto_user_instrum_mode mode) +{ + if (mode == auto_user_instrum_mode::UNDEFINED) { + return "undefined"; + } + if (mode == auto_user_instrum_mode::DISABLED) { + return "disabled"; + } + if (mode == auto_user_instrum_mode::IDENTIFICATION) { + return "identification"; + } + if (mode == auto_user_instrum_mode::ANONYMIZATION) { + return "anonymization"; + } + return "unknown"; +} + struct service_config { void enable_asm() { @@ -71,10 +125,30 @@ struct service_config { asm_enabled = enable_asm_status::NOT_SET; } + void set_auto_user_instrum(auto_user_instrum_mode mode) + { + SPDLOG_DEBUG("Setting auto_user_instrum mode, previous state: {}", + auto_user_instrum); + auto_user_instrum = mode; + } + + void unset_auto_user_instrum() + { + SPDLOG_DEBUG("Unsetting auto_user_instrum mode, previous state: {}", + auto_user_instrum); + auto_user_instrum = auto_user_instrum_mode::UNDEFINED; + } + enable_asm_status get_asm_enabled_status() { return asm_enabled; } + auto_user_instrum_mode get_auto_user_intrum_mode() + { + return auto_user_instrum; + } protected: std::atomic asm_enabled = {enable_asm_status::NOT_SET}; + std::atomic auto_user_instrum = { + auto_user_instrum_mode::UNDEFINED}; }; } // namespace dds diff --git a/appsec/tests/extension/inc/mock_helper.php b/appsec/tests/extension/inc/mock_helper.php index 7a5b7212dd..68ade87221 100644 --- a/appsec/tests/extension/inc/mock_helper.php +++ b/appsec/tests/extension/inc/mock_helper.php @@ -240,7 +240,8 @@ function response_request_init($message, $mergeWithEmpty = true) { ] ], [], //triggers - false //force_keep + false, //force_keep + [] // settings ], $message); } return response("request_init", $message); @@ -256,7 +257,8 @@ function response_request_exec($message, $mergeWithEmpty = true) { ] ], [], - false + false, + [] ], $message); } return response("request_exec", $message); @@ -275,6 +277,7 @@ function response_request_shutdown($message, $mergeWithEmpty = true) { false, [], [], + [], [] ], $message); } diff --git a/appsec/tests/extension/remote_config_auto_user_instrum_default_anon.phpt b/appsec/tests/extension/remote_config_auto_user_instrum_default_anon.phpt new file mode 100644 index 0000000000..df49ddbdb6 --- /dev/null +++ b/appsec/tests/extension/remote_config_auto_user_instrum_default_anon.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test auto user instrum mode parsing with anon default value. +--ENV-- +DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE=anon +--FILE-- + 'identification']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Second request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'disabled']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Third request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'anonymization']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Fourth request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'unknown']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Fifth request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'undefined']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request +]); + +echo "Default:\n"; +var_dump(dump_user_collection_mode()); + +rinit(); +echo "First request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Second request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Third request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Fourth request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Fifth request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); +?> +--EXPECTF-- +Default: +string(13) "anonymization" +First request: +string(14) "identification" +Second request: +string(8) "disabled" +Third request: +string(13) "anonymization" +Fourth request: +string(8) "disabled" +Fifth request: +string(13) "anonymization" diff --git a/appsec/tests/extension/remote_config_auto_user_instrum_default_disabled.phpt b/appsec/tests/extension/remote_config_auto_user_instrum_default_disabled.phpt new file mode 100644 index 0000000000..c1e9f31c80 --- /dev/null +++ b/appsec/tests/extension/remote_config_auto_user_instrum_default_disabled.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test auto user instrum mode parsing with disabled default value. +--ENV-- +DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE=disabled +--FILE-- + 'identification']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Second request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'disabled']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Third request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'anonymization']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Fourth request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'unknown']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Fifth request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'undefined']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request +]); + +echo "Default:\n"; +var_dump(dump_user_collection_mode()); + +rinit(); +echo "First request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Second request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Third request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Fourth request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Fifth request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); +?> +--EXPECTF-- +Default: +string(8) "disabled" +First request: +string(14) "identification" +Second request: +string(8) "disabled" +Third request: +string(13) "anonymization" +Fourth request: +string(8) "disabled" +Fifth request: +string(8) "disabled" diff --git a/appsec/tests/extension/remote_config_auto_user_instrum_default_ident.phpt b/appsec/tests/extension/remote_config_auto_user_instrum_default_ident.phpt new file mode 100644 index 0000000000..2bb9103972 --- /dev/null +++ b/appsec/tests/extension/remote_config_auto_user_instrum_default_ident.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test auto user instrum mode parsing with ident default value. +--ENV-- +DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE=ident +--FILE-- + 'identification']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Second request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'disabled']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Third request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'anonymization']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Fourth request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'unknown']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request + // Fifth request + response_list(response_request_init([[['ok', []]], [], false, ['auto_user_instrum' => 'undefined']])), + response_list(response_request_shutdown([[['ok', []]]])), //End of request +]); + +echo "Default:\n"; +var_dump(dump_user_collection_mode()); + +rinit(); +echo "First request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Second request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Third request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Fourth request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); + +rinit(); +echo "Fifth request:\n"; +var_dump(dump_user_collection_mode()); +rshutdown(); +?> +--EXPECTF-- +Default: +string(14) "identification" +First request: +string(14) "identification" +Second request: +string(8) "disabled" +Third request: +string(13) "anonymization" +Fourth request: +string(8) "disabled" +Fifth request: +string(14) "identification" diff --git a/appsec/tests/extension/rinit_record_span_tags.phpt b/appsec/tests/extension/rinit_record_span_tags.phpt index 97b2d2c8e6..7027881c7f 100644 --- a/appsec/tests/extension/rinit_record_span_tags.phpt +++ b/appsec/tests/extension/rinit_record_span_tags.phpt @@ -31,7 +31,7 @@ $helper = Helper::createInitedRun([ response_list(response_request_init([[['record', []]], ['{"found":"attack"}','{"another":"attack"}']])), response_list( response_request_shutdown( - [[['record', []]], ['{"yet another":"attack"}'], false, ["rshutdown_tag" => "rshutdown_value"], ["rshutdown_metric" => 2.1]] + [[['record', []]], ['{"yet another":"attack"}'], false, [], ["rshutdown_tag" => "rshutdown_value"], ["rshutdown_metric" => 2.1]] ) ), ], ['continuous' => true]); diff --git a/appsec/tests/helper/broker_test.cpp b/appsec/tests/helper/broker_test.cpp index 528753cba5..75b86a8c02 100644 --- a/appsec/tests/helper/broker_test.cpp +++ b/appsec/tests/helper/broker_test.cpp @@ -121,7 +121,7 @@ TEST(BrokerTest, SendRequestInit) packer.pack_array(1); // Array of messages packer.pack_array(2); // First message pack_str(packer, "request_init"); // Type - packer.pack_array(3); + packer.pack_array(4); packer.pack_array(1); // Array of actions packer.pack_array(2); // First action pack_str(packer, "block"); @@ -134,6 +134,9 @@ TEST(BrokerTest, SendRequestInit) pack_str(packer, "one"); pack_str(packer, "two"); packer.pack_true(); // Force_keep + packer.pack_map(1); + pack_str(packer, "auto_user_instrum"); + pack_str(packer, "DISABLED"); const auto &expected_data = ss.str(); @@ -149,6 +152,7 @@ TEST(BrokerTest, SendRequestInit) {"block", {{"status_code", "403"}, {"type", "auto"}}}); response->triggers = {"one", "two"}; response->force_keep = true; + response->settings["auto_user_instrum"] = "DISABLED"; std::vector> messages; messages.push_back(response); @@ -169,7 +173,7 @@ TEST(BrokerTest, SendRequestShutdown) packer.pack_array(1); // Array of messages packer.pack_array(2); // First message pack_str(packer, "request_shutdown"); // Type - packer.pack_array(6); + packer.pack_array(7); packer.pack_array(1); packer.pack_array(2); pack_str(packer, "block"); @@ -182,9 +186,10 @@ TEST(BrokerTest, SendRequestShutdown) pack_str(packer, "one"); pack_str(packer, "two"); packer.pack_true(); // Force keep - packer.pack_map(0); - packer.pack_map(0); - packer.pack_map(0); + packer.pack_map(0); // Settings + packer.pack_map(0); // Meta + packer.pack_map(0); // Metrics + packer.pack_map(0); // Tel_metrics const auto &expected_data = ss.str(); network::header_t h; @@ -219,7 +224,7 @@ TEST(BrokerTest, SendRequestExec) packer.pack_array(1); // Array of messages packer.pack_array(2); // First message pack_str(packer, "request_exec"); // Type - packer.pack_array(3); + packer.pack_array(4); packer.pack_array(1); packer.pack_array(2); pack_str(packer, "block"); @@ -232,6 +237,7 @@ TEST(BrokerTest, SendRequestExec) pack_str(packer, "one"); pack_str(packer, "two"); packer.pack_true(); // Force keep + packer.pack_map(0); // Settings const auto &expected_data = ss.str(); network::header_t h; diff --git a/appsec/tests/helper/client_test.cpp b/appsec/tests/helper/client_test.cpp index 9f4e9b1049..8c54a3a909 100644 --- a/appsec/tests/helper/client_test.cpp +++ b/appsec/tests/helper/client_test.cpp @@ -5,6 +5,7 @@ // (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. #include "common.hpp" #include "parameter.hpp" +#include "service_config.hpp" #include #include #include @@ -514,6 +515,7 @@ TEST(ClientTest, RequestInit) EXPECT_STREQ(msg_res->actions[0].verdict.c_str(), "record"); EXPECT_EQ(msg_res->triggers.size(), 1); EXPECT_TRUE(msg_res->force_keep); + EXPECT_EQ(msg_res->settings["auto_user_instrum"], "undefined"); } } @@ -616,6 +618,170 @@ TEST(ClientTest, RequestInitBlock) } } +TEST(ClientTest, RequestInitWithInstrumModeToIdentification) +{ + auto smanager = std::make_shared(); + auto broker = new mock::broker(); + + client c(smanager, std::unique_ptr(broker)); + + set_extension_configuration_to(broker, c, EXTENSION_CONFIGURATION_ENABLED); + + c.get_service()->get_service_config()->set_auto_user_instrum( + auto_user_instrum_mode::IDENTIFICATION); + + // Request Init + { + network::request_init::request msg; + msg.data = parameter::map(); + + auto headers = parameter::map(); + headers.add("user-agent", parameter::string("acunetix-product"sv)); + + msg.data.add("server.request.headers.no_cookies", std::move(headers)); + + network::request req(std::move(msg)); + + std::shared_ptr res; + EXPECT_CALL(*broker, recv(_)).WillOnce(Return(req)); + EXPECT_CALL(*broker, + send( + testing::An &>())) + .WillOnce(DoAll(testing::SaveArg<0>(&res), Return(true))); + + EXPECT_TRUE(c.run_request()); + auto msg_res = + dynamic_cast(res.get()); + EXPECT_STREQ(msg_res->actions[0].verdict.c_str(), "record"); + EXPECT_EQ(msg_res->triggers.size(), 1); + EXPECT_TRUE(msg_res->force_keep); + EXPECT_EQ(msg_res->settings["auto_user_instrum"], "identification"); + } +} + +TEST(ClientTest, RequestInitWithInstrumModeToAnonymization) +{ + auto smanager = std::make_shared(); + auto broker = new mock::broker(); + + client c(smanager, std::unique_ptr(broker)); + + set_extension_configuration_to(broker, c, EXTENSION_CONFIGURATION_ENABLED); + + c.get_service()->get_service_config()->set_auto_user_instrum( + auto_user_instrum_mode::ANONYMIZATION); + + // Request Init + { + network::request_init::request msg; + msg.data = parameter::map(); + + auto headers = parameter::map(); + headers.add("user-agent", parameter::string("acunetix-product"sv)); + + msg.data.add("server.request.headers.no_cookies", std::move(headers)); + + network::request req(std::move(msg)); + + std::shared_ptr res; + EXPECT_CALL(*broker, recv(_)).WillOnce(Return(req)); + EXPECT_CALL(*broker, + send( + testing::An &>())) + .WillOnce(DoAll(testing::SaveArg<0>(&res), Return(true))); + + EXPECT_TRUE(c.run_request()); + auto msg_res = + dynamic_cast(res.get()); + EXPECT_STREQ(msg_res->actions[0].verdict.c_str(), "record"); + EXPECT_EQ(msg_res->triggers.size(), 1); + EXPECT_TRUE(msg_res->force_keep); + EXPECT_EQ(msg_res->settings["auto_user_instrum"], "anonymization"); + } +} + +TEST(ClientTest, RequestInitWithInstrumModeToDisabled) +{ + auto smanager = std::make_shared(); + auto broker = new mock::broker(); + + client c(smanager, std::unique_ptr(broker)); + + set_extension_configuration_to(broker, c, EXTENSION_CONFIGURATION_ENABLED); + + c.get_service()->get_service_config()->set_auto_user_instrum( + auto_user_instrum_mode::DISABLED); + + // Request Init + { + network::request_init::request msg; + msg.data = parameter::map(); + + auto headers = parameter::map(); + headers.add("user-agent", parameter::string("acunetix-product"sv)); + + msg.data.add("server.request.headers.no_cookies", std::move(headers)); + + network::request req(std::move(msg)); + + std::shared_ptr res; + EXPECT_CALL(*broker, recv(_)).WillOnce(Return(req)); + EXPECT_CALL(*broker, + send( + testing::An &>())) + .WillOnce(DoAll(testing::SaveArg<0>(&res), Return(true))); + + EXPECT_TRUE(c.run_request()); + auto msg_res = + dynamic_cast(res.get()); + EXPECT_STREQ(msg_res->actions[0].verdict.c_str(), "record"); + EXPECT_EQ(msg_res->triggers.size(), 1); + EXPECT_TRUE(msg_res->force_keep); + EXPECT_EQ(msg_res->settings["auto_user_instrum"], "disabled"); + } +} + +TEST(ClientTest, RequestInitWithInstrumModeToUnknown) +{ + auto smanager = std::make_shared(); + auto broker = new mock::broker(); + + client c(smanager, std::unique_ptr(broker)); + + set_extension_configuration_to(broker, c, EXTENSION_CONFIGURATION_ENABLED); + + c.get_service()->get_service_config()->set_auto_user_instrum( + auto_user_instrum_mode::UNKNOWN); + + // Request Init + { + network::request_init::request msg; + msg.data = parameter::map(); + + auto headers = parameter::map(); + headers.add("user-agent", parameter::string("acunetix-product"sv)); + + msg.data.add("server.request.headers.no_cookies", std::move(headers)); + + network::request req(std::move(msg)); + + std::shared_ptr res; + EXPECT_CALL(*broker, recv(_)).WillOnce(Return(req)); + EXPECT_CALL(*broker, + send( + testing::An &>())) + .WillOnce(DoAll(testing::SaveArg<0>(&res), Return(true))); + + EXPECT_TRUE(c.run_request()); + auto msg_res = + dynamic_cast(res.get()); + EXPECT_STREQ(msg_res->actions[0].verdict.c_str(), "record"); + EXPECT_EQ(msg_res->triggers.size(), 1); + EXPECT_TRUE(msg_res->force_keep); + EXPECT_EQ(msg_res->settings["auto_user_instrum"], "unknown"); + } +} + TEST(ClientTest, EventWithMultipleActions) { auto smanager = std::make_shared(); diff --git a/appsec/tests/helper/remote_config/listeners/asm_features_listener_test.cpp b/appsec/tests/helper/remote_config/listeners/asm_features_listener_test.cpp index 5b8c10c60d..64226a7c28 100644 --- a/appsec/tests/helper/remote_config/listeners/asm_features_listener_test.cpp +++ b/appsec/tests/helper/remote_config/listeners/asm_features_listener_test.cpp @@ -4,268 +4,556 @@ // This product includes software developed at Datadog // (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. -#include "../../common.hpp" #include "../mocks.hpp" -#include "base64.h" #include "remote_config/exception.hpp" #include "remote_config/listeners/asm_features_listener.hpp" -#include "remote_config/product.hpp" +#include "service_config.hpp" namespace dds { namespace rcmock = remote_config::mock; -remote_config::config get_config_with_status(std::string status) +remote_config::config get_auto_user_instrum_config( + const std::string mode = "\"identification\"") { - return rcmock::get_config( - "ASM_FEATURES", "{\"asm\":{\"enabled\":" + status + "}}"); + const std::string content = + R"({"auto_user_instrum": { "mode": )" + mode + " }}"; + return rcmock::get_config("ASM_FEATURES", content); } -remote_config::config get_enabled_config(bool as_string = true) +remote_config::config get_asm_enabled_config(bool as_string = true) { - std::string quotes = as_string ? "\"" : ""; - return get_config_with_status(quotes + "true" + quotes); + std::string value = as_string ? "\"true\"" : "true"; + const std::string content = R"({"asm": { "enabled": )" + value + " }}"; + return rcmock::get_config("ASM_FEATURES", content); } -remote_config::config get_disabled_config(bool as_string = true) +remote_config::config get_asm_disabled_config(bool as_string = true) { - std::string quotes = as_string ? "\"" : ""; - return get_config_with_status(quotes + "false" + quotes); + std::string value = as_string ? "\"false\"" : "false"; + const std::string content = R"({"asm": { "enabled": )" + value + " }}"; + return rcmock::get_config("ASM_FEATURES", content); } -TEST(RemoteConfigAsmFeaturesListener, ByDefaultListenerIsNotSet) +TEST(RemoteConfigAsmFeaturesListener, NotSetByDefault) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerGetActiveWhenConfigSaysSoOnUpdateAsString) +TEST(RemoteConfigAsmFeaturesListener, AsmSetToEnabledAsString) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); try { - listener.on_update(get_enabled_config()); + listener.on_update(get_asm_enabled_config()); + listener.commit(); } catch (remote_config::error_applying_config &error) { std::cout << error.what() << std::endl; } EXPECT_EQ(enable_asm_status::ENABLED, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, AsmParserIsCaseInsensitive) +TEST(RemoteConfigAsmFeaturesListener, AsmSetToEnabledAsBool) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); - listener.on_update(get_config_with_status("\"TrUe\"")); + try { + listener.on_update(get_asm_enabled_config(false)); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } EXPECT_EQ(enable_asm_status::ENABLED, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerGetDeactivedWhenConfigSaysSoOnUpdateAsString) +TEST(RemoteConfigAsmFeaturesListener, AsmSetToDisabledAsString) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_asm_disabled_config()); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::DISABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, AsmSetToDisabledAsBool) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); try { - listener.on_update(get_disabled_config()); + listener.on_update(get_asm_disabled_config(false)); + listener.commit(); } catch (remote_config::error_applying_config &error) { std::cout << error.what() << std::endl; } EXPECT_EQ(enable_asm_status::DISABLED, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerGetActiveWhenConfigSaysSoOnUpdateAsBoolean) +TEST(RemoteConfigAsmFeaturesListener, AsmParserIsCaseInsensitive) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); try { - listener.on_update(get_enabled_config(false)); + const std::string content = R"({"asm": { "enabled": "TrUe" }})"; + listener.on_update(rcmock::get_config("ASM_FEATURES", content)); + listener.commit(); } catch (remote_config::error_applying_config &error) { std::cout << error.what() << std::endl; } EXPECT_EQ(enable_asm_status::ENABLED, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerGetDeactivedWhenConfigSaysSoOnUpdateAsBoolean) +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumSetToIdentication) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); try { - listener.on_update(get_disabled_config(false)); + listener.on_update(get_asm_enabled_config()); + listener.on_update(get_auto_user_instrum_config()); + listener.commit(); } catch (remote_config::error_applying_config &error) { std::cout << error.what() << std::endl; } - EXPECT_EQ(enable_asm_status::DISABLED, + EXPECT_EQ(enable_asm_status::ENABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::IDENTIFICATION, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumSetToAnon) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_asm_enabled_config()); + listener.on_update(get_auto_user_instrum_config("\"anonymization\"")); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::ENABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::ANONYMIZATION, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumSetToDisabled) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_asm_enabled_config()); + listener.on_update(get_auto_user_instrum_config("\"disabled\"")); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::ENABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::DISABLED, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumUnknownValue) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_asm_enabled_config()); + listener.on_update(get_auto_user_instrum_config("\"invalid\"")); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::ENABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNKNOWN, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumIgnoreInvalidType) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + std::string error_message = ""; + std::string expected_error_message = "Invalid type for auto_user_instrum"; + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + listener.on_update(get_asm_enabled_config()); + + try { + const std::string content = R"({"auto_user_instrum": 123 })"; + listener.on_update(rcmock::get_config("ASM_FEATURES", content)); + } catch (remote_config::error_applying_config &error) { + error_message = error.what(); + } + + listener.commit(); + + EXPECT_EQ(enable_asm_status::ENABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + EXPECT_EQ(0, error_message.compare(expected_error_message)); +} + +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumWithoutMode) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_asm_enabled_config()); + + const std::string content = R"({"auto_user_instrum": {}})"; + listener.on_update(rcmock::get_config("ASM_FEATURES", content)); + + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::ENABLED, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNKNOWN, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumInvalidModeType) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_asm_enabled_config()); + + const std::string content = R"({"auto_user_instrum": { "mode": 123 }})"; + listener.on_update(rcmock::get_config("ASM_FEATURES", content)); + + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::ENABLED, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNKNOWN, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerThrowsAnErrorWhenContentOfConfigAreNotValidBase64) +TEST(RemoteConfigAsmFeaturesListener, AutoUserInstrumWithoutAsm) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + try { + listener.on_update(get_auto_user_instrum_config()); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::IDENTIFICATION, + remote_config_service->get_auto_user_intrum_mode()); +} + +TEST(RemoteConfigAsmFeaturesListener, ErrorConfigInvalidContentBase64) +{ + auto remote_config_service = std::make_shared(); + remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + std::string invalid_content = "&&&"; std::string error_message = ""; std::string expected_error_message = "Invalid config contents"; + remote_config::config non_base_64_content_config = rcmock::get_config("ASM_FEATURES", invalid_content); try { listener.on_update(non_base_64_content_config); + listener.commit(); } catch (remote_config::error_applying_config &error) { error_message = error.what(); } EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); EXPECT_EQ(0, error_message.compare(0, expected_error_message.length(), expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerThrowsAnErrorWhenContentIsNotValidJson) +TEST(RemoteConfigAsmFeaturesListener, ErrorConfigInvalidJsonContent) { - std::string error_message = ""; - std::string expected_error_message = "Invalid config contents"; auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + std::string error_message = ""; + std::string expected_error_message = "Invalid config contents"; std::string invalid_content = "invalidJsonContent"; + remote_config::config config = rcmock::get_config("ASM_FEATURES", invalid_content); try { listener.on_update(config); + listener.commit(); } catch (remote_config::error_applying_config &error) { error_message = error.what(); } EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); EXPECT_EQ(0, error_message.compare(0, expected_error_message.length(), expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, ListenerThrowsAnErrorWhenAsmKeyMissing) +TEST(RemoteConfigAsmFeaturesListener, ErrorConfigAsmKeyMissing) { - std::string error_message = ""; - std::string expected_error_message = - "Invalid config json encoded contents: asm key missing or invalid"; auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); - remote_config::config asm_key_missing = - rcmock::get_config("ASM_FEATURES", "{}"); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + remote_config::config config = rcmock::get_config("ASM_FEATURES", "{}"); try { - listener.on_update(asm_key_missing); + listener.on_update(config); + listener.commit(); } catch (remote_config::error_applying_config &error) { - error_message = error.what(); + std::cout << error.what() << std::endl; } EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); - EXPECT_EQ(0, error_message.compare(expected_error_message)); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, ListenerThrowsAnErrorWhenAsmIsNotValid) +TEST(RemoteConfigAsmFeaturesListener, ErrorConfigInvalidAsmKey) { - std::string error_message = ""; - std::string expected_error_message = - "Invalid config json encoded contents: asm key missing or invalid"; auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); - remote_config::config invalid_asm_key = - rcmock::get_config("ASM_FEATURES", "{ \"asm\": 123}"); - - try { - listener.on_update(invalid_asm_key); - } catch (remote_config::error_applying_config &error) { - error_message = error.what(); - } + listener.init(); EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); - EXPECT_EQ(0, error_message.compare(expected_error_message)); -} + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); -TEST( - RemoteConfigAsmFeaturesListener, ListenerThrowsAnErrorWhenEnabledKeyMissing) -{ std::string error_message = ""; - std::string expected_error_message = - "Invalid config json encoded contents: enabled key missing"; - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); - remote_config::config enabled_key_missing = - rcmock::get_config("ASM_FEATURES", "{ \"asm\": {}}"); + std::string expected_error_message = "Invalid type for asm"; + + remote_config::config config = + rcmock::get_config("ASM_FEATURES", R"({ "asm": 123 })"); try { - listener.on_update(enabled_key_missing); + listener.on_update(config); + listener.commit(); } catch (remote_config::error_applying_config &error) { error_message = error.what(); } EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); EXPECT_EQ(0, error_message.compare(expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerThrowsAnErrorWhenEnabledKeyIsInvalid) +TEST(RemoteConfigAsmFeaturesListener, ErrorConfigEnabledKeyMissing) { - std::string error_message = ""; - std::string expected_error_message = - "Invalid config json encoded contents: enabled key invalid"; auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); - remote_config::config enabled_key_invalid = - rcmock::get_config("ASM_FEATURES", "{ \"asm\": { \"enabled\": 123}}"); + listener.init(); + + EXPECT_EQ(enable_asm_status::NOT_SET, + remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); + + remote_config::config config = + rcmock::get_config("ASM_FEATURES", R"({ "asm": { "disabled": true }})"); try { - listener.on_update(enabled_key_invalid); + listener.on_update(config); + listener.commit(); } catch (remote_config::error_applying_config &error) { - error_message = error.what(); + std::cout << error.what() << std::endl; } EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); - EXPECT_EQ(0, error_message.compare(expected_error_message)); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } -TEST(RemoteConfigAsmFeaturesListener, WhenListenerGetsUnapplyItGetsNotSet) +TEST(RemoteConfigAsmFeaturesListener, ErrorConfigInvalidEnabledKey) { auto remote_config_service = std::make_shared(); remote_config::asm_features_listener listener(remote_config_service); + listener.init(); - listener.on_update(get_enabled_config(false)); - EXPECT_EQ(enable_asm_status::ENABLED, + EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); - remote_config::config some_key; - listener.on_unapply(some_key); + remote_config::config config = + rcmock::get_config("ASM_FEATURES", R"({ "asm": { "enabled": 123 }})"); + + try { + listener.on_update(config); + listener.commit(); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } EXPECT_EQ(enable_asm_status::NOT_SET, remote_config_service->get_asm_enabled_status()); + EXPECT_EQ(auto_user_instrum_mode::UNDEFINED, + remote_config_service->get_auto_user_intrum_mode()); } - -} // namespace dds +} // namespace dds \ No newline at end of file diff --git a/appsec/tests/helper/remote_config/listeners/config_aggregators/asm_features_aggregator_test.cpp b/appsec/tests/helper/remote_config/listeners/config_aggregators/asm_features_aggregator_test.cpp new file mode 100644 index 0000000000..840d03fccb --- /dev/null +++ b/appsec/tests/helper/remote_config/listeners/config_aggregators/asm_features_aggregator_test.cpp @@ -0,0 +1,467 @@ +// 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 "../../mocks.hpp" +#include "remote_config/exception.hpp" +#include "remote_config/listeners/config_aggregators/asm_features_aggregator.hpp" +#include + +namespace dds::remote_config { +namespace { + +using mock::get_config; + +ACTION_P(SaveDocument, param) +{ + rapidjson::Document &document = + *reinterpret_cast(param); + + arg0.copy(document); +} + +TEST(RemoteConfigAsmFeaturesAggregator, EmptyCommit) +{ + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, EmptyConfigThrows) +{ + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + + aggregator.init(&doc.GetAllocator()); + EXPECT_THROW(aggregator.add(get_config("ASM_FEATURES", {})), + std::runtime_error); // mmap failure + + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, InvalidJson) +{ + const std::string rule_override = R"({"asm": [])"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + + EXPECT_THROW(aggregator.add(get_config("ASM_FEATURES", rule_override)), + remote_config::error_applying_config); + + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, IncorrectTypeThrows) +{ + const std::string rule_override = R"({"asm": []})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + + EXPECT_THROW(aggregator.add(get_config("ASM_FEATURES", rule_override)), + remote_config::error_applying_config); + + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, InvalidKeyName) +{ + const std::string rule_override = R"({"asn": { "enabled": true }})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesAsmEmpty) +{ + const std::string rule_override = R"({"asm": {}})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesAsmSingleConfig) +{ + const std::string rule_override = R"({"asm": { "enabled": true }})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &enabled = key["enabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_TRUE(enabled.GetBool()); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesInstrumModeEmpty) +{ + const std::string rule_override = R"({"auto_user_instrum": {}})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["auto_user_instrum"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesInstrumModeSingleConfig) +{ + const std::string rule_override = + R"({"auto_user_instrum": { "mode": "identification" }})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["auto_user_instrum"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &mode = key["mode"]; + EXPECT_TRUE(mode.IsString()); + EXPECT_EQ(mode.GetString(), "identification"sv); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesAttackModeEmpty) +{ + const std::string rule_override = R"({"attack_mode": {}})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["attack_mode"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesAttackModeSingleConfig) +{ + const std::string rule_override = + R"({"attack_mode": { "isAttackModeEnabled": true, "enabledServices": [] }})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["attack_mode"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 2); + + const auto &enabled = key["isAttackModeEnabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_TRUE(enabled.GetBool()); + + const auto &services = key["enabledServices"]; + EXPECT_TRUE(services.IsArray()); + EXPECT_TRUE(services.Empty()); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesApiSecurityEmpty) +{ + const std::string rule_override = R"({"api_security": {}})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["api_security"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesApiSecuritySingleConfig) +{ + const std::string rule_override = + R"({"api_security": { "request_sample_rate": 0.1 }})"; + + remote_config::asm_features_aggregator aggregator; + + rapidjson::Document doc(rapidjson::kObjectType); + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["api_security"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &rate = key["request_sample_rate"]; + EXPECT_TRUE(rate.IsFloat()); + EXPECT_EQ(rate.GetFloat(), 0.1f); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesOverrideSameConfig) +{ + const std::string rule_override = R"({"asm": { "enabled": true }})"; + + rapidjson::Document doc(rapidjson::kObjectType); + remote_config::asm_features_aggregator aggregator; + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &enabled = key["enabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_TRUE(enabled.GetBool()); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesOverrideDifferentConfig) +{ + const std::string rule_override = R"({"asm": { "enabled": true }})"; + const std::string second_rule_override = R"({"asm": { "enabled": false }})"; + + rapidjson::Document doc(rapidjson::kObjectType); + remote_config::asm_features_aggregator aggregator; + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.add(get_config("ASM_FEATURES", second_rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &enabled = key["enabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_FALSE(enabled.GetBool()); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesOverrideIgnoreInvalidConfigs) +{ + const std::string rule_override = R"({"asm": { "enabled": true }})"; + const std::string invalid = R"({"asm": [{ "enabled": false }]})"; + + rapidjson::Document doc(rapidjson::kObjectType); + remote_config::asm_features_aggregator aggregator; + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rule_override)); + + EXPECT_THROW(aggregator.add(get_config("ASM_FEATURES", invalid)), + remote_config::error_applying_config); + + aggregator.add(get_config("ASM_FEATURES", rule_override)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &enabled = key["enabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_TRUE(enabled.GetBool()); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesCustomConfigIgnoreInvalidKey) +{ + const std::string rules = + R"({"asm": { "enabled": true }, "invalid": false })"; + + rapidjson::Document doc(rapidjson::kObjectType); + remote_config::asm_features_aggregator aggregator; + aggregator.init(&doc.GetAllocator()); + aggregator.add(get_config("ASM_FEATURES", rules)); + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 1); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &enabled = key["enabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_TRUE(enabled.GetBool()); +} + +TEST(RemoteConfigAsmFeaturesAggregator, RulesCustomConfigInvalidType) +{ + const std::string rules = + R"({"asm": { "enabled": true }, "auto_user_instrum": false })"; + + rapidjson::Document doc(rapidjson::kObjectType); + remote_config::asm_features_aggregator aggregator; + aggregator.init(&doc.GetAllocator()); + + EXPECT_THROW(aggregator.add(get_config("ASM_FEATURES", rules)), + remote_config::error_applying_config); + + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 0); +} + +TEST(RemoteConfigAsmFeaturesAggregator, AllSingleConfigs) +{ + rapidjson::Document doc(rapidjson::kObjectType); + remote_config::asm_features_aggregator aggregator; + + aggregator.init(&doc.GetAllocator()); + { + const std::string rules = R"({"asm": { "enabled": true }})"; + + aggregator.add(get_config("ASM_FEATURES", rules)); + } + { + const std::string rules = + R"({"auto_user_instrum": { "mode": "identification"}})"; + + aggregator.add(get_config("ASM_FEATURES", rules)); + } + { + const std::string rules = + R"({"attack_mode": { "isAttackModeEnabled": true, "enabledServices": []}})"; + + aggregator.add(get_config("ASM_FEATURES", rules)); + } + { + const std::string rules = + R"({"api_security": { "request_sample_rate": 0.1}})"; + + aggregator.add(get_config("ASM_FEATURES", rules)); + } + aggregator.aggregate(doc); + + EXPECT_TRUE(doc.IsObject()); + EXPECT_EQ(doc.MemberCount(), 4); + + const auto &key = doc["asm"]; + EXPECT_TRUE(key.IsObject()); + EXPECT_EQ(key.MemberCount(), 1); + + const auto &enabled = key["enabled"]; + EXPECT_TRUE(enabled.IsBool()); + EXPECT_TRUE(enabled.GetBool()); + + const auto &auto_user_instrum = doc["auto_user_instrum"]; + EXPECT_TRUE(auto_user_instrum.IsObject()); + EXPECT_EQ(auto_user_instrum.MemberCount(), 1); + + const auto &mode = auto_user_instrum["mode"]; + EXPECT_TRUE(mode.IsString()); + EXPECT_EQ(mode.GetString(), "identification"sv); + + const auto &attack_mode = doc["attack_mode"]; + EXPECT_TRUE(attack_mode.IsObject()); + EXPECT_EQ(attack_mode.MemberCount(), 2); + + const auto &mode_enabled = attack_mode["isAttackModeEnabled"]; + EXPECT_TRUE(mode_enabled.IsBool()); + EXPECT_TRUE(mode_enabled.GetBool()); + + const auto &services = attack_mode["enabledServices"]; + EXPECT_TRUE(services.IsArray()); + EXPECT_TRUE(services.Empty()); + + const auto &api_security = doc["api_security"]; + EXPECT_TRUE(api_security.IsObject()); + EXPECT_EQ(api_security.MemberCount(), 1); + + const auto &rate = api_security["request_sample_rate"]; + EXPECT_TRUE(rate.IsFloat()); + EXPECT_EQ(rate.GetFloat(), 0.1f); +} + +} // namespace +} // namespace dds::remote_config diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy index f94c084556..16bc4235f8 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/CommonTests.groovy @@ -70,6 +70,7 @@ trait CommonTests { assert span.meta."_dd.appsec.usr.id" == 'Admin' assert span.meta."_dd.appsec.usr.login" == 'Login' assert span.meta."appsec.events.users.signup.track" == 'true' + assert span.meta."_dd.appsec.events.users.signup.auto.mode" == 'identification' } @Test @@ -101,6 +102,7 @@ trait CommonTests { assert span.meta."_dd.appsec.usr.id" == 'Admin' assert span.meta."_dd.appsec.usr.login" == 'Login' assert span.meta."appsec.events.users.login.success.track" == 'true' + assert span.meta."_dd.appsec.events.users.login.success.auto.mode" == 'identification' } @Test @@ -134,6 +136,7 @@ trait CommonTests { assert span.meta."appsec.events.users.login.failure.usr.login" == 'Login' assert span.meta."appsec.events.users.login.failure.usr.exists" == 'false' assert span.meta."appsec.events.users.login.failure.track" == 'true' + assert span.meta."_dd.appsec.events.users.login.failure.auto.mode" == 'identification' } @Test diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RemoteConfigTests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RemoteConfigTests.groovy index 1d788f02aa..ca1ee71785 100644 --- a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RemoteConfigTests.groovy +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/RemoteConfigTests.groovy @@ -1,5 +1,6 @@ package com.datadog.appsec.php.integration +import com.datadog.appsec.php.model.Span import com.datadog.appsec.php.docker.AppSecContainer import com.datadog.appsec.php.docker.FailOnUnmatchedTraces import com.datadog.appsec.php.mock_agent.rem_cfg.Capability @@ -347,6 +348,74 @@ class RemoteConfigTests { dropRemoteConfig(INITIAL_TARGET) } + @Test + void 'test identification auto user instrumentation'() { + applyRemoteConfig(INITIAL_TARGET, [ + 'datadog/2/ASM_FEATURES/asm_features_activation/config': [asm: [enabled: true]], + 'datadog/2/ASM_FEATURES/asm_auto_user_instrum_identification/config': [auto_user_instrum: [mode: "identification"]], + ]) + + def trace = CONTAINER.traceFromRequest('/user_login_success_automated.php') { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta."_dd.appsec.events.users.login.success.auto.mode" == 'identification' + + dropRemoteConfig(INITIAL_TARGET) + } + + @Test + void 'test anonymized auto user instrumentation'() { + applyRemoteConfig(INITIAL_TARGET, [ + 'datadog/2/ASM_FEATURES/asm_features_activation/config': [asm: [enabled: true]], + 'datadog/2/ASM_FEATURES/asm_auto_user_instrum_anonymization/config': [auto_user_instrum: [mode: "anonymization"]], + ]) + + def trace = CONTAINER.traceFromRequest('/user_login_success_automated.php') { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta."_dd.appsec.events.users.login.success.auto.mode" == 'anonymization' + + dropRemoteConfig(INITIAL_TARGET) + } + + @Test + void 'test disabled auto user instrumentation'() { + applyRemoteConfig(INITIAL_TARGET, [ + 'datadog/2/ASM_FEATURES/asm_features_activation/config': [asm: [enabled: true]], + 'datadog/2/ASM_FEATURES/asm_auto_user_instrum_disabled/config': [auto_user_instrum: [mode: "disabled"]], + ]) + + def trace = CONTAINER.traceFromRequest('/user_login_success_automated.php') { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert !span.meta.containsKey('_dd.appsec.events.users.login.success.auto.mode') + + dropRemoteConfig(INITIAL_TARGET) + } + + @Test + void 'test unknown auto user instrumentation'() { + applyRemoteConfig(INITIAL_TARGET, [ + 'datadog/2/ASM_FEATURES/asm_features_activation/config': [asm: [enabled: true]], + 'datadog/2/ASM_FEATURES/asm_auto_user_instrum_disabled/config': [auto_user_instrum: [mode: "unknown"]], + ]) + + def trace = CONTAINER.traceFromRequest('/user_login_success_automated.php') { HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert !span.meta.containsKey('_dd.appsec.events.users.login.success.auto.mode') + + dropRemoteConfig(INITIAL_TARGET) + } + private RemoteConfigRequest applyRemoteConfig(Target target, Map files) { CONTAINER.applyRemoteConfig(target, files).get() }