diff --git a/appsec/src/helper/client.cpp b/appsec/src/helper/client.cpp index d7f859e790..eff230748c 100644 --- a/appsec/src/helper/client.cpp +++ b/appsec/src/helper/client.cpp @@ -439,10 +439,8 @@ bool client::handle_command(network::request_shutdown::request &command) try { auto sampler = service_->get_schema_sampler(); - std::optional scope; if (sampler) { - scope = sampler->get(); - if (scope.has_value()) { + if (sampler->get()) { parameter context_processor = parameter::map(); context_processor.add( "extract-schema", parameter::as_boolean(true)); diff --git a/appsec/src/helper/remote_config/client_handler.cpp b/appsec/src/helper/remote_config/client_handler.cpp index c0e33ba829..58d5c2733e 100644 --- a/appsec/src/helper/remote_config/client_handler.cpp +++ b/appsec/src/helper/remote_config/client_handler.cpp @@ -47,10 +47,11 @@ client_handler::ptr client_handler::from_settings(service_identifier &&id, } std::vector listeners = {}; - if (dynamic_enablement) { + if (dynamic_enablement || eng_settings.schema_extraction.enabled) { listeners.emplace_back( std::make_shared( - service_config)); + service_config, dynamic_enablement, + eng_settings.schema_extraction.enabled)); } if (eng_settings.rules_file.empty()) { 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 edd8ae0f66..60340a3faa 100644 --- a/appsec/src/helper/remote_config/listeners/asm_features_listener.cpp +++ b/appsec/src/helper/remote_config/listeners/asm_features_listener.cpp @@ -9,25 +9,56 @@ #include "remote_config/exception.hpp" #include "utils.hpp" #include -#include -void dds::remote_config::asm_features_listener::on_update(const config &config) +void dds::remote_config::asm_features_listener::parse_api_security( + const rapidjson::Document &serialized_doc) { - rapidjson::Document serialized_doc; - if (!json_helper::get_json_base64_encoded_content( - config.contents, serialized_doc)) { - throw error_applying_config("Invalid config contents"); + rapidjson::Value::ConstMemberIterator const api_security_itr = + serialized_doc.FindMember("api_security"); + + if (api_security_itr == serialized_doc.MemberEnd()) { + return; + } + + if (rapidjson::kObjectType != api_security_itr->value.GetType()) { + throw error_applying_config("Invalid config json encoded contents: " + "api_security key invalid"); + } + + auto request_sample_rate_itr = + api_security_itr->value.FindMember("request_sample_rate"); + if (request_sample_rate_itr == api_security_itr->value.MemberEnd()) { + throw error_applying_config("Invalid config json encoded contents: " + "request_sample_rate key missing"); + } + + if (request_sample_rate_itr->value.GetType() != rapidjson::kNumberType || + !request_sample_rate_itr->value.IsDouble()) { + throw error_applying_config("Invalid config json encoded contents: " + "request_sample_rate is not double"); + } + + service_config_->set_request_sample_rate( + request_sample_rate_itr->value.GetDouble()); +} + +void dds::remote_config::asm_features_listener::parse_asm( + const rapidjson::Document &serialized_doc) +{ + rapidjson::Value::ConstMemberIterator const asm_itr = + serialized_doc.FindMember("asm"); + + if (asm_itr == serialized_doc.MemberEnd()) { + return; } - auto asm_itr = json_helper::get_field_of_type( - serialized_doc, "asm", rapidjson::kObjectType); - if (!asm_itr) { + if (rapidjson::kObjectType != asm_itr->value.GetType()) { throw error_applying_config("Invalid config json encoded contents: " - "asm key missing or invalid"); + "asm key invalid"); } - auto enabled_itr = asm_itr.value()->value.FindMember("enabled"); - if (enabled_itr == asm_itr.value()->value.MemberEnd()) { + auto enabled_itr = asm_itr->value.FindMember("enabled"); + if (enabled_itr == asm_itr->value.MemberEnd()) { throw error_applying_config( "Invalid config json encoded contents: enabled key missing"); } @@ -36,18 +67,31 @@ void dds::remote_config::asm_features_listener::on_update(const config &config) if (dd_tolower(enabled_itr->value.GetString()) == std::string("true")) { service_config_->enable_asm(); } else { - // This scenario should not happen since RC would remove the file - // when appsec should not be enabled service_config_->disable_asm(); } } else if (enabled_itr->value.GetType() == rapidjson::kTrueType) { service_config_->enable_asm(); } else if (enabled_itr->value.GetType() == rapidjson::kFalseType) { - // This scenario should not happen since RC would remove the file - // when appsec should not be enabled service_config_->disable_asm(); } else { throw error_applying_config( "Invalid config json encoded contents: enabled key invalid"); } } + +void dds::remote_config::asm_features_listener::on_update(const config &config) +{ + rapidjson::Document serialized_doc; + if (!json_helper::get_json_base64_encoded_content( + config.contents, serialized_doc)) { + throw error_applying_config("Invalid config contents"); + } + + if (dynamic_enablement_) { + parse_asm(serialized_doc); + } + + if (api_security_enabled_) { + parse_api_security(serialized_doc); + } +} 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 ab91cc42be..7137457e32 100644 --- a/appsec/src/helper/remote_config/listeners/asm_features_listener.hpp +++ b/appsec/src/helper/remote_config/listeners/asm_features_listener.hpp @@ -8,14 +8,18 @@ #include "config.hpp" #include "listener.hpp" #include "service_config.hpp" +#include namespace dds::remote_config { class asm_features_listener : public listener_base { public: explicit asm_features_listener( - std::shared_ptr service_config) - : service_config_(std::move(service_config)){}; + std::shared_ptr service_config, + bool dynamic_enablement, bool api_security_enabled) + : service_config_(std::move(service_config)), + dynamic_enablement_(dynamic_enablement), + api_security_enabled_(api_security_enabled){}; void on_update(const config &config) override; void on_unapply(const config & /*config*/) override { @@ -25,14 +29,32 @@ class asm_features_listener : public listener_base { [[nodiscard]] std::unordered_map get_supported_products() override { - return {{"ASM_FEATURES", protocol::capabilities_e::ASM_ACTIVATION}}; + protocol::capabilities_e capabilities = protocol::capabilities_e::NONE; + + if (dynamic_enablement_) { + capabilities = protocol::capabilities_e::ASM_ACTIVATION; + } + if (api_security_enabled_) { + capabilities |= + protocol::capabilities_e::ASM_API_SECURITY_SAMPLE_RATE; + } + + if (capabilities != protocol::capabilities_e::NONE) { + return {{asm_features, capabilities}}; + } + return {}; } void init() override {} void commit() override {} protected: + static constexpr std::string_view asm_features = "ASM_FEATURES"; + void parse_asm(const rapidjson::Document &serialized_doc); + void parse_api_security(const rapidjson::Document &serialized_doc); std::shared_ptr service_config_; + bool dynamic_enablement_; + bool api_security_enabled_; }; } // namespace dds::remote_config diff --git a/appsec/src/helper/remote_config/protocol/client.hpp b/appsec/src/helper/remote_config/protocol/client.hpp index 1e13d08a7c..107d4ef1f6 100644 --- a/appsec/src/helper/remote_config/protocol/client.hpp +++ b/appsec/src/helper/remote_config/protocol/client.hpp @@ -27,6 +27,7 @@ enum class capabilities_e : uint16_t { ASM_CUSTOM_RULES = 1 << 8, ASM_CUSTOM_BLOCKING_RESPONSE = 1 << 9, ASM_TRUSTED_IPS = 1 << 10, + ASM_API_SECURITY_SAMPLE_RATE = 1 << 11, }; constexpr capabilities_e operator|( diff --git a/appsec/src/helper/sampler.hpp b/appsec/src/helper/sampler.hpp index 4a3dc2cc8c..f7c868341d 100644 --- a/appsec/src/helper/sampler.hpp +++ b/appsec/src/helper/sampler.hpp @@ -13,79 +13,34 @@ #include #include +#include "service_config.hpp" + namespace dds { static const double min_rate = 0.0001; class sampler { public: - sampler(double sample_rate) : sample_rate_(sample_rate) + sampler(double sampler_rate) { - // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) - if (sample_rate_ <= 0) { - sample_rate_ = 0; - } else if (sample_rate_ > 1) { - sample_rate_ = 1; - } else if (sample_rate_ < min_rate) { - sample_rate_ = min_rate; - } - // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) - } - class scope { - public: - explicit scope(std::atomic &concurrent) : concurrent_(&concurrent) - { - concurrent_->store(true, std::memory_order_relaxed); - } - - scope(const scope &) = delete; - scope &operator=(const scope &) = delete; - scope(scope &&oth) noexcept - { - concurrent_ = oth.concurrent_; - oth.concurrent_ = nullptr; - } - scope &operator=(scope &&oth) - { - concurrent_ = oth.concurrent_; - oth.concurrent_ = nullptr; - - return *this; - } - - ~scope() - { - if (concurrent_ != nullptr) { - concurrent_->store(false, std::memory_order_relaxed); - } + if (sampler_rate <= 0) { + sampler_rate = 0; + } else if (sampler_rate > 1) { + sampler_rate = 1; + } else if (sampler_rate < min_rate) { + sampler_rate = min_rate; } - protected: - std::atomic *concurrent_; - }; + sample_rate_ = sampler_rate; + } - std::optional get() + bool get() { - const std::lock_guard lock_guard(mtx_); - - std::optional result = std::nullopt; - - if (!concurrent_ && floor(request_ * sample_rate_) != - floor((request_ + 1) * sample_rate_)) { - result = {scope{concurrent_}}; - } - - if (request_ < std::numeric_limits::max()) { - request_++; - } else { - request_ = 1; - } - - return result; + unsigned prev = request_.fetch_add(1, std::memory_order_relaxed); + return floor(prev * sample_rate_) != floor((prev + 1) * sample_rate_); } -protected: - unsigned request_{1}; - double sample_rate_; - std::atomic concurrent_{false}; - std::mutex mtx_; + double get_sample_rate() { return sample_rate_; } + + std::atomic request_{0}; + double sample_rate_{0}; }; } // namespace dds diff --git a/appsec/src/helper/service.cpp b/appsec/src/helper/service.cpp index 561675deb3..7ead85688b 100644 --- a/appsec/src/helper/service.cpp +++ b/appsec/src/helper/service.cpp @@ -30,6 +30,8 @@ service::service(std::shared_ptr engine, sample_rate = 0; } + service_config_->set_request_sampler_listener( + [this](double rate) { set_sampler_rate(rate); }); schema_sampler_ = std::make_shared(sample_rate); } diff --git a/appsec/src/helper/service.hpp b/appsec/src/helper/service.hpp index 72e3a8a5dd..7231d0aaf4 100644 --- a/appsec/src/helper/service.hpp +++ b/appsec/src/helper/service.hpp @@ -72,7 +72,13 @@ class service { [[nodiscard]] std::shared_ptr get_schema_sampler() { - return schema_sampler_; + return std::atomic_load(&schema_sampler_); + } + + void set_sampler_rate(double rate) + { + auto new_sampler = std::make_shared(rate); + std::atomic_store(&schema_sampler_, new_sampler); } protected: diff --git a/appsec/src/helper/service_config.hpp b/appsec/src/helper/service_config.hpp index 475ddf8a37..642e815949 100644 --- a/appsec/src/helper/service_config.hpp +++ b/appsec/src/helper/service_config.hpp @@ -19,9 +19,18 @@ struct service_config { void disable_asm() { asm_enabled = enable_asm_status::DISABLED; } void unset_asm() { asm_enabled = enable_asm_status::NOT_SET; } enable_asm_status get_asm_enabled_status() { return asm_enabled; } + void set_request_sampler_listener(std::function listener) + { + request_sample_rate_listener = listener; + } + void set_request_sample_rate(double sample_rate) + { + request_sample_rate_listener(sample_rate); + } protected: std::atomic asm_enabled = {enable_asm_status::NOT_SET}; + std::function request_sample_rate_listener; }; } // namespace dds diff --git a/appsec/tests/helper/client_test.cpp b/appsec/tests/helper/client_test.cpp index 7a76029992..2bf6ab4162 100644 --- a/appsec/tests/helper/client_test.cpp +++ b/appsec/tests/helper/client_test.cpp @@ -2408,4 +2408,77 @@ TEST(ClientTest, SchemasOverTheLimitAreCompressed) } } +TEST(ClientTest, SchemaSamplerRateIsUpdatedFromService) +{ + auto smanager = std::make_shared(); + auto broker = new mock::broker(); + client c(smanager, std::unique_ptr(broker)); + + network::client_init::request msg = get_default_client_init_msg(); + msg.enabled_configuration = EXTENSION_CONFIGURATION_ENABLED; + msg.engine_settings.schema_extraction.enabled = true; + msg.engine_settings.schema_extraction.sample_rate = + 1; // All request should be get + + send_client_init(broker, c, std::move(msg)); + + { // First request + request_init(broker, c); + + // Request Shutdown + { + network::request_shutdown::request msg; + msg.data = parameter::map(); + msg.data.add("server.request.headers.no_cookies", + parameter::string("acunetix-product"sv)); + + network::request req(std::move(msg)); + + std::shared_ptr res; + EXPECT_CALL(*broker, recv(_)).WillOnce(Return(req)); + EXPECT_CALL(*broker, + send(testing::An< + const std::shared_ptr &>())) + .WillOnce(DoAll(testing::SaveArg<0>(&res), Return(true))); + + EXPECT_TRUE(c.run_request()); + auto msg_res = + dynamic_cast(res.get()); + EXPECT_FALSE(msg_res->meta.empty()); + EXPECT_GT(count_schemas(msg_res->meta), 0); + EXPECT_STREQ( + msg_res->meta["_dd.appsec.s.req.headers.no_cookies"].c_str(), + "[8]"); + } + } + // Setting this to zero means no more request get schema extraction + c.get_service()->get_service_config()->set_request_sample_rate(0); + { // First request + request_init(broker, c); + + // Request Shutdown + { + network::request_shutdown::request msg; + msg.data = parameter::map(); + msg.data.add("server.request.headers.no_cookies", + parameter::string("acunetix-product"sv)); + + network::request req(std::move(msg)); + + std::shared_ptr res; + EXPECT_CALL(*broker, recv(_)).WillOnce(Return(req)); + EXPECT_CALL(*broker, + send(testing::An< + const std::shared_ptr &>())) + .WillOnce(DoAll(testing::SaveArg<0>(&res), Return(true))); + + EXPECT_TRUE(c.run_request()); + auto msg_res = + dynamic_cast(res.get()); + EXPECT_FALSE(msg_res->meta.empty()); + EXPECT_EQ(count_schemas(msg_res->meta), 0); + } + } +} + } // namespace dds 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 5d1af34b73..7766f56e4c 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 @@ -25,9 +25,22 @@ remote_config::config get_config(const std::string &content, bool encode = true) ""}; } +remote_config::config get_config_with_status_and_sample_rate( + std::string status, double sample_rate) +{ + return get_config("{\"asm\":{\"enabled\":" + status + + "}, \"api_security\": { \"request_sample_rate\": " + + std::to_string(sample_rate) + "}}"); +} + remote_config::config get_config_with_status(std::string status) { - return get_config("{\"asm\":{\"enabled\":" + status + "}}"); + return get_config_with_status_and_sample_rate(status, 0.1); +} + +remote_config::config get_config_with_sample_rate(double sample_rate) +{ + return get_config_with_status_and_sample_rate("true", sample_rate); } remote_config::config get_enabled_config(bool as_string = true) @@ -42,20 +55,31 @@ remote_config::config get_disabled_config(bool as_string = true) return get_config_with_status(quotes + "false" + quotes); } -TEST(RemoteConfigAsmFeaturesListener, ByDefaultListenerIsNotSet) +class RemoteConfigAsmFeaturesListenerTest : public ::testing::Test { +public: + std::shared_ptr service_config; + double rate_; + + void SetUp() + { + service_config = std::make_shared(); + service_config->set_request_sampler_listener( + [this](double rate) { rate_ = rate; }); + } +}; + +TEST_F(RemoteConfigAsmFeaturesListenerTest, ByDefaultListenerIsNotSet) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); - EXPECT_EQ(enable_asm_status::NOT_SET, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); } -TEST(RemoteConfigAsmFeaturesListener, +TEST_F(RemoteConfigAsmFeaturesListenerTest, ListenerGetActiveWhenConfigSaysSoOnUpdateAsString) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); try { listener.on_update(get_enabled_config()); @@ -63,29 +87,27 @@ TEST(RemoteConfigAsmFeaturesListener, std::cout << error.what() << std::endl; } - EXPECT_EQ(enable_asm_status::ENABLED, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::ENABLED, service_config->get_asm_enabled_status()); } -TEST(RemoteConfigAsmFeaturesListener, AsmParserIsCaseInsensitive) +TEST_F(RemoteConfigAsmFeaturesListenerTest, AsmParserIsCaseInsensitive) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); - EXPECT_EQ(enable_asm_status::NOT_SET, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); listener.on_update(get_config_with_status("\"TrUe\"")); - EXPECT_EQ(enable_asm_status::ENABLED, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::ENABLED, service_config->get_asm_enabled_status()); } -TEST(RemoteConfigAsmFeaturesListener, +TEST_F(RemoteConfigAsmFeaturesListenerTest, ListenerGetDeactivedWhenConfigSaysSoOnUpdateAsString) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); try { listener.on_update(get_disabled_config()); @@ -93,15 +115,14 @@ TEST(RemoteConfigAsmFeaturesListener, std::cout << error.what() << std::endl; } - EXPECT_EQ(enable_asm_status::DISABLED, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::DISABLED, service_config->get_asm_enabled_status()); } -TEST(RemoteConfigAsmFeaturesListener, +TEST_F(RemoteConfigAsmFeaturesListenerTest, ListenerGetActiveWhenConfigSaysSoOnUpdateAsBoolean) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); try { listener.on_update(get_enabled_config(false)); @@ -109,15 +130,14 @@ TEST(RemoteConfigAsmFeaturesListener, std::cout << error.what() << std::endl; } - EXPECT_EQ(enable_asm_status::ENABLED, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::ENABLED, service_config->get_asm_enabled_status()); } -TEST(RemoteConfigAsmFeaturesListener, +TEST_F(RemoteConfigAsmFeaturesListenerTest, ListenerGetDeactivedWhenConfigSaysSoOnUpdateAsBoolean) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); try { listener.on_update(get_disabled_config(false)); @@ -125,15 +145,14 @@ TEST(RemoteConfigAsmFeaturesListener, std::cout << error.what() << std::endl; } - EXPECT_EQ(enable_asm_status::DISABLED, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::DISABLED, service_config->get_asm_enabled_status()); } -TEST(RemoteConfigAsmFeaturesListener, +TEST_F(RemoteConfigAsmFeaturesListenerTest, ListenerThrowsAnErrorWhenContentOfConfigAreNotValidBase64) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + remote_config::asm_features_listener listener(service_config, true, false); std::string invalid_content = "&&&"; std::string error_message = ""; std::string expected_error_message = "Invalid config contents"; @@ -146,19 +165,18 @@ TEST(RemoteConfigAsmFeaturesListener, error_message = error.what(); } - EXPECT_EQ(enable_asm_status::NOT_SET, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); EXPECT_EQ(0, error_message.compare(0, expected_error_message.length(), expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, +TEST_F(RemoteConfigAsmFeaturesListenerTest, ListenerThrowsAnErrorWhenContentIsNotValidJson) { 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); + remote_config::asm_features_listener listener(service_config, true, false); std::string invalid_content = "invalidJsonContent"; remote_config::config config = get_config(invalid_content); @@ -168,109 +186,255 @@ TEST(RemoteConfigAsmFeaturesListener, error_message = error.what(); } - EXPECT_EQ(enable_asm_status::NOT_SET, - remote_config_service->get_asm_enabled_status()); + EXPECT_EQ( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); EXPECT_EQ(0, error_message.compare(0, expected_error_message.length(), expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, ListenerThrowsAnErrorWhenAsmKeyMissing) +TEST_F( + RemoteConfigAsmFeaturesListenerTest, ListenerThrowsAnErrorWhenAsmIsNotValid) { 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 = get_config("{}"); + "Invalid config json encoded contents: asm key invalid"; + remote_config::asm_features_listener listener(service_config, true, false); + remote_config::config invalid_asm_key = get_config("{ \"asm\": 123}"); try { - listener.on_update(asm_key_missing); + listener.on_update(invalid_asm_key); } 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( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); EXPECT_EQ(0, error_message.compare(expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, ListenerThrowsAnErrorWhenAsmIsNotValid) +TEST_F(RemoteConfigAsmFeaturesListenerTest, + ListenerThrowsAnErrorWhenEnabledKeyMissing) { 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 = get_config("{ \"asm\": 123}"); + "Invalid config json encoded contents: enabled key missing"; + remote_config::asm_features_listener listener(service_config, true, false); + remote_config::config enabled_key_missing = get_config( + "{ \"asm\": {}, \"api_security\": { \"request_sample_rate\": 0.1}}"); try { - listener.on_update(invalid_asm_key); + listener.on_update(enabled_key_missing); } 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( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); EXPECT_EQ(0, error_message.compare(expected_error_message)); } -TEST( - RemoteConfigAsmFeaturesListener, ListenerThrowsAnErrorWhenEnabledKeyMissing) +TEST_F(RemoteConfigAsmFeaturesListenerTest, + ListenerThrowsAnErrorWhenEnabledKeyIsInvalid) { 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 = get_config("{ \"asm\": {}}"); + "Invalid config json encoded contents: enabled key invalid"; + remote_config::asm_features_listener listener(service_config, true, false); + remote_config::config enabled_key_invalid = + get_config("{ \"asm\": { \"enabled\": 123}, \"api_security\": { " + "\"request_sample_rate\": 0.1}}"); try { - listener.on_update(enabled_key_missing); + listener.on_update(enabled_key_invalid); } 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( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); EXPECT_EQ(0, error_message.compare(expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, - ListenerThrowsAnErrorWhenEnabledKeyIsInvalid) +TEST_F(RemoteConfigAsmFeaturesListenerTest, WhenListenerGetsUnapplyItGetsNotSet) +{ + remote_config::asm_features_listener listener(service_config, true, false); + + listener.on_update(get_enabled_config(false)); + EXPECT_EQ( + enable_asm_status::ENABLED, service_config->get_asm_enabled_status()); + + remote_config::config some_key; + listener.on_unapply(some_key); + + EXPECT_EQ( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); +} + +TEST_F( + RemoteConfigAsmFeaturesListenerTest, ThrowsErrorWhenApiSecurityHasWrongType) { 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 = - get_config("{ \"asm\": { \"enabled\": 123}}"); + "Invalid config json encoded contents: api_security key invalid"; + remote_config::asm_features_listener listener(service_config, true, true); + remote_config::config payload = + get_config("{ \"asm\": { \"enabled\": true}, \"api_security\": 1234}"); try { - listener.on_update(enabled_key_invalid); + listener.on_update(payload); } 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(0, error_message.compare(expected_error_message)); } -TEST(RemoteConfigAsmFeaturesListener, WhenListenerGetsUnapplyItGetsNotSet) +TEST_F(RemoteConfigAsmFeaturesListenerTest, + ThrowsErrorWhenNoRequestSampleRatePresent) { - auto remote_config_service = std::make_shared(); - remote_config::asm_features_listener listener(remote_config_service); + std::string error_message = ""; + std::string expected_error_message = + "Invalid config json encoded contents: request_sample_rate key missing"; + remote_config::asm_features_listener listener(service_config, true, true); + remote_config::config payload = + get_config("{ \"asm\": { \"enabled\": true}, \"api_security\": {}}"); - listener.on_update(get_enabled_config(false)); - EXPECT_EQ(enable_asm_status::ENABLED, - remote_config_service->get_asm_enabled_status()); + try { + listener.on_update(payload); + } catch (remote_config::error_applying_config &error) { + error_message = error.what(); + } - remote_config::config some_key; - listener.on_unapply(some_key); + EXPECT_EQ(0, error_message.compare(expected_error_message)); +} + +TEST_F(RemoteConfigAsmFeaturesListenerTest, + ThrowsErrorWhenRequestSampleRateHasWrongType) +{ + std::string error_message = ""; + std::string expected_error_message = + "Invalid config json encoded contents: request_sample_rate is not " + "double"; + remote_config::asm_features_listener listener(service_config, true, true); + remote_config::config payload = + get_config("{ \"asm\": { \"enabled\": true}, \"api_security\": { " + "\"request_sample_rate\": true}}"); + + try { + listener.on_update(payload); + } catch (remote_config::error_applying_config &error) { + error_message = error.what(); + } + + EXPECT_EQ(0, error_message.compare(expected_error_message)); +} + +TEST_F(RemoteConfigAsmFeaturesListenerTest, RequestSampleRateIsParsed) +{ + remote_config::asm_features_listener listener(service_config, true, true); + + { // It parses floats + auto sample_rate = 0.12; + try { + listener.on_update(get_config_with_sample_rate(sample_rate)); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + EXPECT_EQ(sample_rate, rate_); + } + + { // It parses integers + auto sample_rate = 0; + try { + listener.on_update(get_config_with_sample_rate(sample_rate)); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + EXPECT_EQ(sample_rate, rate_); + } +} + +TEST_F(RemoteConfigAsmFeaturesListenerTest, DynamicEnablementIsDisabled) +{ + remote_config::asm_features_listener listener(service_config, false, true); - EXPECT_EQ(enable_asm_status::NOT_SET, - remote_config_service->get_asm_enabled_status()); + try { + listener.on_update(get_config_with_status_and_sample_rate("true", 0.2)); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + EXPECT_EQ(0.2, rate_); + EXPECT_EQ( + enable_asm_status::NOT_SET, service_config->get_asm_enabled_status()); +} + +TEST_F(RemoteConfigAsmFeaturesListenerTest, ApiSecurityIsDisabled) +{ + auto some_rate = 0.123; + service_config->set_request_sample_rate(some_rate); + remote_config::asm_features_listener listener(service_config, true, false); + + { // Api security is not parsed if not enabled + try { + listener.on_update( + get_config_with_status_and_sample_rate("true", 0.2)); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + EXPECT_EQ(some_rate, rate_); + EXPECT_EQ(enable_asm_status::ENABLED, + service_config->get_asm_enabled_status()); + } + + { // Api security can be missing + try { + auto missing_api_security = + get_config("{ \"asm\": { \"enabled\": true}}"); + listener.on_update(missing_api_security); + } catch (remote_config::error_applying_config &error) { + std::cout << error.what() << std::endl; + } + EXPECT_EQ(some_rate, + rate_); // same as before + EXPECT_EQ(enable_asm_status::ENABLED, + service_config->get_asm_enabled_status()); + } +} + +TEST_F(RemoteConfigAsmFeaturesListenerTest, ProductsAreDynamic) +{ + { // All disabled + remote_config::asm_features_listener listener( + service_config, false, false); + EXPECT_EQ(0, listener.get_supported_products().size()); + } + + { // Asm disabled + remote_config::asm_features_listener listener( + service_config, true, false); + EXPECT_EQ(1, listener.get_supported_products().size()); + EXPECT_EQ(dds::remote_config::protocol::capabilities_e::ASM_ACTIVATION, + listener.get_supported_products()["ASM_FEATURES"]); + } + + { // Api security disabled + remote_config::asm_features_listener listener( + service_config, false, true); + EXPECT_EQ(1, listener.get_supported_products().size()); + EXPECT_EQ(dds::remote_config::protocol::capabilities_e:: + ASM_API_SECURITY_SAMPLE_RATE, + listener.get_supported_products()["ASM_FEATURES"]); + } + + { // All enabled + remote_config::asm_features_listener listener( + service_config, true, true); + EXPECT_EQ(1, listener.get_supported_products().size()); + EXPECT_EQ(dds::remote_config::protocol::capabilities_e::ASM_ACTIVATION | + dds::remote_config::protocol::capabilities_e:: + ASM_API_SECURITY_SAMPLE_RATE, + listener.get_supported_products()["ASM_FEATURES"]); + } } } // namespace dds diff --git a/appsec/tests/helper/sampler_test.cpp b/appsec/tests/helper/sampler_test.cpp index eb2c90d8a8..02e6f9b613 100644 --- a/appsec/tests/helper/sampler_test.cpp +++ b/appsec/tests/helper/sampler_test.cpp @@ -4,6 +4,7 @@ // This product includes software developed at Datadog // (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc. #include "common.hpp" +#include "service_config.hpp" #include #include @@ -13,9 +14,9 @@ namespace mock { class sampler : public dds::sampler { public: - sampler(double sample_rate) : dds::sampler(sample_rate) {} + sampler(double sampler_rate) : dds::sampler(sampler_rate) {} void set_request(unsigned int i) { request_ = i; } - auto get_request() { return request_; } + unsigned get_request() { return request_; } }; } // namespace mock @@ -25,8 +26,7 @@ std::atomic picked = 0; void count_picked(dds::sampler &sampler, int iterations) { for (int i = 0; i < iterations; i++) { - auto is_pick = sampler.get(); - if (is_pick != std::nullopt) { + if (sampler.get()) { picked++; } } @@ -34,6 +34,8 @@ void count_picked(dds::sampler &sampler, int iterations) TEST(SamplerTest, ItPicksAllWhenRateIs1) { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(1); picked = 0; count_picked(s, 100); @@ -43,6 +45,8 @@ TEST(SamplerTest, ItPicksAllWhenRateIs1) TEST(SamplerTest, ItPicksNoneWhenRateIs0) { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0); picked = 0; count_picked(s, 100); @@ -52,6 +56,8 @@ TEST(SamplerTest, ItPicksNoneWhenRateIs0) TEST(SamplerTest, ItPicksHalfWhenPortionGiven) { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.5); picked = 0; count_picked(s, 100); @@ -61,6 +67,8 @@ TEST(SamplerTest, ItPicksHalfWhenPortionGiven) TEST(SamplerTest, ItResetTokensAfter100Calls) { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(1); picked = 0; @@ -73,6 +81,8 @@ TEST(SamplerTest, ItResetTokensAfter100Calls) TEST(SamplerTest, ItWorksWithDifferentMagnitudes) { { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.1); picked = 0; count_picked(s, 10); @@ -80,6 +90,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(1, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.5); picked = 0; count_picked(s, 10); @@ -87,6 +99,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(5, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.01); picked = 0; count_picked(s, 100); @@ -94,6 +108,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(1, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.02); picked = 0; count_picked(s, 100); @@ -101,6 +117,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(2, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.001); picked = 0; count_picked(s, 1000); @@ -108,6 +126,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(1, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.003); picked = 0; count_picked(s, 1000); @@ -115,6 +135,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(3, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.0001); picked = 0; count_picked(s, 10000); @@ -122,6 +144,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(1, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.0007); picked = 0; count_picked(s, 10000); @@ -129,6 +153,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(7, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.123); picked = 0; count_picked(s, 1000); @@ -136,6 +162,8 @@ TEST(SamplerTest, ItWorksWithDifferentMagnitudes) EXPECT_EQ(123, picked); } { + auto service_config = std::make_shared(); + service_config->enable_asm(); sampler s(0.6); picked = 0; count_picked(s, 10); @@ -199,28 +227,6 @@ TEST(SamplerTest, TestOverflow) mock::sampler s(0); s.set_request(UINT_MAX); s.get(); - EXPECT_EQ(1, s.get_request()); -} - -TEST(ScopeTest, TestConcurrent) -{ - std::atomic concurrent = false; - { - auto s = sampler::scope(std::ref(concurrent)); - EXPECT_TRUE(concurrent); - } - EXPECT_FALSE(concurrent); -} - -TEST(ScopeTest, TestItDoesNotPickTokenUntilScopeReleased) -{ - sampler sampler(1); - auto is_pick = sampler.get(); - EXPECT_TRUE(is_pick != std::nullopt); - is_pick = sampler.get(); - EXPECT_FALSE(is_pick != std::nullopt); - is_pick.reset(); - is_pick = sampler.get(); - EXPECT_TRUE(is_pick != std::nullopt); + EXPECT_EQ(0, s.get_request()); } } // namespace dds diff --git a/appsec/tests/helper/service_test.cpp b/appsec/tests/helper/service_test.cpp index 5af59db440..f0c79784bf 100644 --- a/appsec/tests/helper/service_test.cpp +++ b/appsec/tests/helper/service_test.cpp @@ -57,7 +57,9 @@ TEST(ServiceTest, ServicePickSchemaExtractionSamples) auto s = service( engine, service_config, nullptr, {true, all_requests_are_picked}); - EXPECT_TRUE(s.get_schema_sampler()->get().has_value()); + EXPECT_EQ( + all_requests_are_picked, s.get_schema_sampler()->get_sample_rate()); + EXPECT_TRUE(s.get_schema_sampler()->get()); } { // Constructor. It does not pick based on rate @@ -65,7 +67,9 @@ TEST(ServiceTest, ServicePickSchemaExtractionSamples) auto s = service( engine, service_config, nullptr, {true, no_request_is_picked}); - EXPECT_FALSE(s.get_schema_sampler()->get().has_value()); + EXPECT_EQ( + no_request_is_picked, s.get_schema_sampler()->get_sample_rate()); + EXPECT_FALSE(s.get_schema_sampler()->get()); } { // Constructor. It does not pick if disabled @@ -74,7 +78,8 @@ TEST(ServiceTest, ServicePickSchemaExtractionSamples) auto s = service(engine, service_config, nullptr, {schema_extraction_disabled, all_requests_are_picked}); - EXPECT_FALSE(s.get_schema_sampler()->get().has_value()); + EXPECT_EQ(0, s.get_schema_sampler()->get_sample_rate()); + EXPECT_FALSE(s.get_schema_sampler()->get()); } { // Static constructor. It picks based on rate @@ -83,7 +88,7 @@ TEST(ServiceTest, ServicePickSchemaExtractionSamples) auto service = service::from_settings( service_identifier(sid), engine_settings, {}, meta, metrics, false); - EXPECT_TRUE(service->get_schema_sampler()->get().has_value()); + EXPECT_TRUE(service->get_schema_sampler()->get()); } { // Static constructor. It does not pick based on rate @@ -92,7 +97,7 @@ TEST(ServiceTest, ServicePickSchemaExtractionSamples) auto service = service::from_settings( service_identifier(sid), engine_settings, {}, meta, metrics, false); - EXPECT_FALSE(service->get_schema_sampler()->get().has_value()); + EXPECT_FALSE(service->get_schema_sampler()->get()); } { // Static constructor. It does not pick if disabled @@ -101,7 +106,7 @@ TEST(ServiceTest, ServicePickSchemaExtractionSamples) auto service = service::from_settings( service_identifier(sid), engine_settings, {}, meta, metrics, false); - EXPECT_FALSE(service->get_schema_sampler()->get().has_value()); + EXPECT_FALSE(service->get_schema_sampler()->get()); } }