From 81e47db07d5242745a062d168b1f1f46155607fc Mon Sep 17 00:00:00 2001 From: xiaoshuai <49056817+xiaoshuai7038@users.noreply.github.com> Date: Fri, 15 May 2026 22:56:25 +0800 Subject: [PATCH] Add runtime regex check configuration --- README.md | 44 +++ dooked/CMakeLists.txt | 11 + dooked/examples/regex-checks.json | 21 ++ dooked/include/checks/regex_checks.hpp | 44 +++ dooked/include/cli_preprocessor.hpp | 3 + dooked/include/utils/containers.hpp | 13 +- dooked/include/utils/exceptions.hpp | 1 + dooked/source/checks/regex_checks.cpp | 379 ++++++++++++++++++++++++ dooked/source/cli_preprocessor.cpp | 15 + dooked/source/dns/dns_resolver.cpp | 12 +- dooked/source/http/requests_handler.cpp | 15 +- dooked/source/http/resolver.cpp | 12 +- dooked/source/main.cpp | 2 + dooked/test/regex_checks_test.cpp | 94 ++++++ 14 files changed, 648 insertions(+), 18 deletions(-) create mode 100644 dooked/examples/regex-checks.json create mode 100644 dooked/include/checks/regex_checks.hpp create mode 100644 dooked/source/checks/regex_checks.cpp create mode 100644 dooked/test/regex_checks_test.cpp diff --git a/README.md b/README.md index f1a761c..841c279 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,47 @@ make ## Usage For comprehensive help, use `dooked --help` + +### Runtime regex checks + +Use `--checks ` or `--check-config ` to load custom alert checks +from a JSON configuration file while dooked is running: + +``` +dooked -i domains.txt -o results --checks checks.json +``` + +The config may be either a JSON array or an object with a `checks` array. Each +check requires `field`, `regex`, and `alert`. `pattern` is accepted as an alias +for `regex`, `message` is accepted as an alias for `alert`, and checks are +case-sensitive unless `ignore_case: true` or `case_sensitive: false` is set. + +```json +{ + "checks": [ + { + "field": "domain", + "regex": "(dev|test)", + "alert": "domain contains an environment marker", + "ignore_case": true + }, + { + "field": "content", + "regex": "Copyright 2020", + "alert": "outdated copyright banner" + }, + { + "field": "rdata", + "regex": "v=spf1", + "alert": "SPF TXT record found" + } + ] +} +``` + +Supported fields are domain aliases (`domain`, `domain_name`), DNS record fields +(`type`, `info`, `rdata`, `ttl`), HTTP fields (`content_length`, `http_code`, +`http_status`, `code_string`), and response body aliases (`body`, +`response_body`, `page_content`, `content`). Response bodies are kept only in +memory for matching, capped at the first 64 KiB, and are not written to the JSON +result file. diff --git a/dooked/CMakeLists.txt b/dooked/CMakeLists.txt index c43ff38..a97d091 100644 --- a/dooked/CMakeLists.txt +++ b/dooked/CMakeLists.txt @@ -63,6 +63,7 @@ message("${PROJECT_NAME}: CURR BIN DIR: ${CMAKE_CURRENT_BINARY_DIR}") # Source Files set(SRC_FILES + ./source/checks/regex_checks.cpp ./source/dns/dns.cpp ./source/dns/dns_resolver.cpp ./source/http/resolver.cpp @@ -80,6 +81,7 @@ source_group("Sources" FILES ${SRC_FILES}) # Header Files set(HEADERS_FILES + ./include/checks/regex_checks.hpp ./include/dns/dns.hpp ./include/dns/dns_resolver.hpp ./include/http/resolver.hpp @@ -103,6 +105,15 @@ add_executable(${PROJECT_NAME} ${SRC_FILES} ${HEADERS_FILES} ) +option(DOOKED_BUILD_TESTS "Build dooked test targets" OFF) +if(DOOKED_BUILD_TESTS) + add_executable(regex_checks_test + ./test/regex_checks_test.cpp + ./source/checks/regex_checks.cpp + ./source/utils/constants.cpp + ) +endif() + if(NOT MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -O3") if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") diff --git a/dooked/examples/regex-checks.json b/dooked/examples/regex-checks.json new file mode 100644 index 0000000..d57f695 --- /dev/null +++ b/dooked/examples/regex-checks.json @@ -0,0 +1,21 @@ +{ + "checks": [ + { + "field": "domain", + "regex": "(dev|test)", + "alert": "domain contains an environment marker", + "ignore_case": true + }, + { + "field": "content", + "regex": "Copyright 2020", + "alert": "outdated copyright banner" + }, + { + "field": "rdata", + "regex": "v=spf1", + "alert": "SPF TXT record found", + "ignore_case": true + } + ] +} diff --git a/dooked/include/checks/regex_checks.hpp b/dooked/include/checks/regex_checks.hpp new file mode 100644 index 0000000..8b08f04 --- /dev/null +++ b/dooked/include/checks/regex_checks.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "utils/containers.hpp" +#include "utils/probe_result.hpp" +#include +#include +#include +#include +#include + +namespace dooked { + +enum class regex_check_field_e { + domain, + type, + rdata, + ttl, + content_length, + http_code, + code_string, + body +}; + +struct regex_check_t { + regex_check_field_e field{}; + std::string field_name{}; + std::string pattern{}; + std::string alert{}; + bool ignore_case{}; + std::regex compiled_pattern{}; +}; + +using regex_check_list_t = std::vector; + +std::optional +parse_regex_checks_config(std::istream &input, std::string &error_message); + +std::optional +load_regex_checks(std::string const &filename, std::string &error_message); + +void run_regex_checks(map_container_t const &result_map, + regex_check_list_t const &checks); + +} // namespace dooked diff --git a/dooked/include/cli_preprocessor.hpp b/dooked/include/cli_preprocessor.hpp index 43fa1ba..d9564e1 100644 --- a/dooked/include/cli_preprocessor.hpp +++ b/dooked/include/cli_preprocessor.hpp @@ -1,5 +1,6 @@ #pragma once +#include "checks/regex_checks.hpp" #include "dns/dns_resolver.hpp" #include "utils/io_utils.hpp" #include @@ -19,6 +20,7 @@ struct cli_args_t { std::string resolver_filename{}; std::string output_filename{}; std::string input_filename{}; + std::string check_config_filename{}; int file_type{}; int post_http_request{}; @@ -34,6 +36,7 @@ struct runtime_args_t { std::unique_ptr output_file{}; std::string output_filename{}; http_process_e http_request_time_{}; + regex_check_list_t regex_checks{}; int thread_count{}; int content_length{-1}; }; diff --git a/dooked/include/utils/containers.hpp b/dooked/include/utils/containers.hpp index 7c955f6..b2934ab 100644 --- a/dooked/include/utils/containers.hpp +++ b/dooked/include/utils/containers.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace dooked { @@ -31,6 +32,7 @@ template class circular_queue_t { struct http_response_t { int content_length_{}; int http_status_{}; + std::string body_{}; }; template struct http_dns_response_t { @@ -52,9 +54,11 @@ template class map_container_t { } void insert_impl(std::string const &name, int const len, - int const http_status) { + int const http_status, + std::string const &body) { map_[name].http_result_.content_length_ = len; map_[name].http_result_.http_status_ = http_status; + map_[name].http_result_.body_ = body; } public: @@ -74,12 +78,13 @@ template class map_container_t { append_impl(key, value); } - void insert(std::string const &name, int const len, int const http_status) { + void insert(std::string const &name, int const len, int const http_status, + std::string const &body = {}) { if (!opt_mutex_) { - return insert_impl(name, len, http_status); + return insert_impl(name, len, http_status, body); } std::lock_guard lock_g{*opt_mutex_}; - insert_impl(name, len, http_status); + insert_impl(name, len, http_status, body); } // only used by main thread, after all "computations" has been // done. There's no need for locks here. diff --git a/dooked/include/utils/exceptions.hpp b/dooked/include/utils/exceptions.hpp index a749a1b..846d544 100644 --- a/dooked/include/utils/exceptions.hpp +++ b/dooked/include/utils/exceptions.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace dooked { diff --git a/dooked/source/checks/regex_checks.cpp b/dooked/source/checks/regex_checks.cpp new file mode 100644 index 0000000..a5f1d1f --- /dev/null +++ b/dooked/source/checks/regex_checks.cpp @@ -0,0 +1,379 @@ +#include "checks/regex_checks.hpp" +#include "utils/io_utils.hpp" +#include +#include +#include +#include +#include + +namespace dooked { +namespace { + +std::string normalize_field_name(std::string field) { + std::transform(field.begin(), field.end(), field.begin(), + [](unsigned char c) { + if (c == '-') { + return '_'; + } + return static_cast(std::tolower(c)); + }); + return field; +} + +std::optional +field_from_name(std::string const &field_name) { + if (field_name == "domain" || field_name == "domain_name") { + return regex_check_field_e::domain; + } + if (field_name == "type") { + return regex_check_field_e::type; + } + if (field_name == "info" || field_name == "rdata") { + return regex_check_field_e::rdata; + } + if (field_name == "ttl") { + return regex_check_field_e::ttl; + } + if (field_name == "content_length") { + return regex_check_field_e::content_length; + } + if (field_name == "http_code" || field_name == "http_status") { + return regex_check_field_e::http_code; + } + if (field_name == "code_string") { + return regex_check_field_e::code_string; + } + if (field_name == "body" || field_name == "content" || + field_name == "page_content" || field_name == "response_body" || + field_name == "http_body") { + return regex_check_field_e::body; + } + return std::nullopt; +} + +std::string canonical_field_name(regex_check_field_e const field) { + switch (field) { + case regex_check_field_e::domain: + return "domain"; + case regex_check_field_e::type: + return "type"; + case regex_check_field_e::rdata: + return "rdata"; + case regex_check_field_e::ttl: + return "ttl"; + case regex_check_field_e::content_length: + return "content_length"; + case regex_check_field_e::http_code: + return "http_code"; + case regex_check_field_e::code_string: + return "code_string"; + case regex_check_field_e::body: + return "body"; + } + return {}; +} + +std::string supported_field_names() { + return "domain/domain_name, type, info/rdata, ttl, content_length, " + "http_code/http_status, code_string, body/response_body/content"; +} + +std::optional json_string(json const &object, + char const *field_name) { + auto const iter = object.find(field_name); + if (iter == object.end() || !iter->is_string()) { + return std::nullopt; + } + return iter->get(); +} + +std::optional json_bool(json const &object, char const *field_name, + std::string &error_message, + std::size_t const index) { + auto const iter = object.find(field_name); + if (iter == object.end()) { + return std::nullopt; + } + if (!iter->is_boolean()) { + error_message = "check #" + std::to_string(index) + " has a non-boolean `" + + field_name + "` value"; + return std::nullopt; + } + return iter->get(); +} + +std::optional parse_ignore_case(json const &check_json, + std::string &error_message, + std::size_t const index) { + auto const ignore_case = + json_bool(check_json, "ignore_case", error_message, index); + if (!error_message.empty()) { + return std::nullopt; + } + + auto const case_sensitive = + json_bool(check_json, "case_sensitive", error_message, index); + if (!error_message.empty()) { + return std::nullopt; + } + + if (ignore_case && case_sensitive && (*ignore_case == *case_sensitive)) { + error_message = "check #" + std::to_string(index) + + " has conflicting `ignore_case` and `case_sensitive` " + "values"; + return std::nullopt; + } + + if (case_sensitive) { + return !*case_sensitive; + } + return ignore_case.value_or(false); +} + +bool is_domain_or_http_field(regex_check_field_e const field) { + return field == regex_check_field_e::domain || + field == regex_check_field_e::content_length || + field == regex_check_field_e::http_code || + field == regex_check_field_e::code_string || + field == regex_check_field_e::body; +} + +bool is_dns_record_field(regex_check_field_e const field) { + return field == regex_check_field_e::type || + field == regex_check_field_e::rdata || + field == regex_check_field_e::ttl; +} + +std::string http_field_value(http_response_t const &response, + regex_check_field_e const field) { + switch (field) { + case regex_check_field_e::content_length: + return std::to_string(response.content_length_); + case regex_check_field_e::http_code: + return std::to_string(response.http_status_); + case regex_check_field_e::code_string: + return code_string(response.http_status_); + case regex_check_field_e::body: + return response.body_; + default: + return {}; + } +} + +std::string dns_field_value(probe_result_t const &record, + regex_check_field_e const field) { + switch (field) { + case regex_check_field_e::type: + return dns_record_type_to_str(record.type); + case regex_check_field_e::rdata: + return record.rdata; + case regex_check_field_e::ttl: + return std::to_string(record.ttl); + default: + return {}; + } +} + +std::string preview_match(std::string value) { + constexpr std::size_t max_preview_size = 160; + for (auto &c : value) { + auto const uc = static_cast(c); + if (c == '\n' || c == '\r' || c == '\t' || std::iscntrl(uc)) { + c = ' '; + } + } + if (value.size() > max_preview_size) { + value.resize(max_preview_size); + value += "..."; + } + return value; +} + +void report_match(std::string const &domain_name, regex_check_t const &check, + std::string const &value) { + std::smatch match; + if (!std::regex_search(value, match, check.compiled_pattern)) { + return; + } + + auto matched_value = match.empty() ? value : match.str(0); + spdlog::warn("[REGEX][{}][{}] {} (matched: `{}`)", domain_name, + check.field_name, check.alert, + preview_match(std::move(matched_value))); +} + +void report_match(std::string const &domain_name, regex_check_t const &check, + probe_result_t const &record, std::string const &value) { + std::smatch match; + if (!std::regex_search(value, match, check.compiled_pattern)) { + return; + } + + auto matched_value = match.empty() ? value : match.str(0); + spdlog::warn("[REGEX][{}][{}][{}] {} (matched: `{}`)", domain_name, + check.field_name, dns_record_type_to_str(record.type), + check.alert, preview_match(std::move(matched_value))); +} + +} // namespace + +std::optional +parse_regex_checks_config(std::istream &input, std::string &error_message) { + error_message.clear(); + + json root; + try { + root = json::parse(input); + } catch (json::exception const &e) { + error_message = "invalid JSON check config: " + std::string(e.what()); + return std::nullopt; + } + + json const *checks_json = nullptr; + if (root.is_array()) { + checks_json = &root; + } else if (root.is_object()) { + auto const iter = root.find("checks"); + if (iter != root.end() && iter->is_array()) { + checks_json = &(*iter); + } + } + + if (!checks_json) { + error_message = + "check config must be a JSON array or an object with a `checks` array"; + return std::nullopt; + } + + regex_check_list_t checks; + checks.reserve(checks_json->size()); + + std::size_t index = 0; + for (auto const &check_json : *checks_json) { + ++index; + if (!check_json.is_object()) { + error_message = "check #" + std::to_string(index) + " must be an object"; + return std::nullopt; + } + + auto raw_field = json_string(check_json, "field"); + if (!raw_field || raw_field->empty()) { + error_message = + "check #" + std::to_string(index) + " is missing a string `field`"; + return std::nullopt; + } + + auto raw_pattern = json_string(check_json, "regex"); + if (!raw_pattern) { + raw_pattern = json_string(check_json, "pattern"); + } + if (!raw_pattern || raw_pattern->empty()) { + error_message = + "check #" + std::to_string(index) + " is missing a string `regex`"; + return std::nullopt; + } + + auto raw_alert = json_string(check_json, "alert"); + if (!raw_alert) { + raw_alert = json_string(check_json, "message"); + } + if (!raw_alert || raw_alert->empty()) { + error_message = + "check #" + std::to_string(index) + " is missing a string `alert`"; + return std::nullopt; + } + + auto const normalized_field = normalize_field_name(*raw_field); + auto const field = field_from_name(normalized_field); + if (!field) { + error_message = "check #" + std::to_string(index) + + " uses unsupported field `" + *raw_field + + "`; supported fields: " + supported_field_names(); + return std::nullopt; + } + + auto const ignore_case = + parse_ignore_case(check_json, error_message, index); + if (!ignore_case) { + return std::nullopt; + } + + auto flags = std::regex_constants::ECMAScript; + if (*ignore_case) { + flags |= std::regex_constants::icase; + } + + try { + checks.push_back({*field, canonical_field_name(*field), *raw_pattern, + *raw_alert, *ignore_case, + std::regex(*raw_pattern, flags)}); + } catch (std::regex_error const &e) { + error_message = "check #" + std::to_string(index) + + " has invalid regex `" + *raw_pattern + + "`: " + e.what(); + return std::nullopt; + } + } + + if (checks.empty()) { + error_message = "check config does not contain any checks"; + return std::nullopt; + } + + return checks; +} + +std::optional +load_regex_checks(std::string const &filename, std::string &error_message) { + error_message.clear(); + + std::ifstream file{filename}; + if (!file) { + error_message = "unable to open check config `" + filename + "`"; + return std::nullopt; + } + + auto checks = parse_regex_checks_config(file, error_message); + if (!checks && !error_message.empty()) { + error_message += " in `" + filename + "`"; + } + return checks; +} + +void run_regex_checks(map_container_t const &result_map, + regex_check_list_t const &checks) { + if (checks.empty() || result_map.empty()) { + return; + } + + for (auto const &result_pair : result_map.cresult()) { + auto const &domain_name = result_pair.first; + auto const &domain_result = result_pair.second; + + for (auto const &check : checks) { + if (!is_domain_or_http_field(check.field)) { + continue; + } + + if (check.field == regex_check_field_e::domain) { + report_match(domain_name, check, domain_name); + } else { + report_match(domain_name, check, + http_field_value(domain_result.http_result_, check.field)); + } + } + + for (auto const &record : domain_result.dns_result_list_) { + for (auto const &check : checks) { + if (!is_dns_record_field(check.field)) { + continue; + } + + report_match(domain_name, check, record, + dns_field_value(record, check.field)); + } + } + } +} + +} // namespace dooked diff --git a/dooked/source/cli_preprocessor.cpp b/dooked/source/cli_preprocessor.cpp index c08d7fb..24b3c6d 100644 --- a/dooked/source/cli_preprocessor.cpp +++ b/dooked/source/cli_preprocessor.cpp @@ -354,6 +354,7 @@ void start_name_checking(runtime_args_t &&rt_args) { spdlog::info("Writing JSON output"); } write_json_result(result_map, rt_args); + run_regex_checks(result_map, rt_args.regex_checks); // compare old with new result -- only if we had previous record if (rt_args.previous_data) { @@ -380,6 +381,20 @@ void start_name_checking(runtime_args_t &&rt_args) { void run_program(cli_args_t const &cli_args) { runtime_args_t rt_args{}; + + if (!cli_args.check_config_filename.empty()) { + std::string error_message{}; + auto regex_checks = + load_regex_checks(cli_args.check_config_filename, error_message); + if (!regex_checks) { + return spdlog::error(error_message); + } + rt_args.regex_checks = std::move(*regex_checks); + if (!silent) { + spdlog::info("Loaded {} regex check(s)", rt_args.regex_checks.size()); + } + } + // settle resolvers. std::vector resolver_strings{}; if (cli_args.resolver_filename.empty()) { diff --git a/dooked/source/dns/dns_resolver.cpp b/dooked/source/dns/dns_resolver.cpp index 851745f..14ba9ee 100644 --- a/dooked/source/dns/dns_resolver.cpp +++ b/dooked/source/dns/dns_resolver.cpp @@ -417,11 +417,11 @@ void custom_resolver_socket_t::http_result_obtained( switch (rt) { case response_type_e::bad_request: { - result_map_.insert(name_, content_length, 400); + result_map_.insert(name_, content_length, 400, response_string); return dns_continue_probe(); } case response_type_e::forbidden: { - result_map_.insert(name_, content_length, 403); + result_map_.insert(name_, content_length, 403, response_string); return dns_continue_probe(); } case response_type_e::cannot_resolve_name: { @@ -447,11 +447,11 @@ void custom_resolver_socket_t::http_result_obtained( return send_https_request(response_string); } case response_type_e::not_found: { // HTTP(S) 404 - result_map_.insert(name_, content_length, 404); + result_map_.insert(name_, content_length, 404, response_string); return dns_continue_probe(); } case response_type_e::ok: { - result_map_.insert(name_, content_length, 200); + result_map_.insert(name_, content_length, 200, response_string); return dns_continue_probe(); } case response_type_e::recv_timed_out: { // retry, wait timeout @@ -477,11 +477,11 @@ void custom_resolver_socket_t::http_result_obtained( return send_https_request(response_string); } case response_type_e::server_error: { - result_map_.insert(name_, content_length, 503); + result_map_.insert(name_, content_length, 503, response_string); return dns_continue_probe(); } default: { - result_map_.insert(name_, 0, 0); + result_map_.insert(name_, 0, 0, response_string); return dns_continue_probe(); } } // end switch diff --git a/dooked/source/http/requests_handler.cpp b/dooked/source/http/requests_handler.cpp index d21a592..dc34ab7 100644 --- a/dooked/source/http/requests_handler.cpp +++ b/dooked/source/http/requests_handler.cpp @@ -9,6 +9,17 @@ extern bool no_bytes_count; extern bool silent; namespace dooked { +namespace { + +std::string response_body_for_checks(std::string const &body) { + constexpr std::size_t max_body_bytes = 64 * 1024; + if (body.size() <= max_body_bytes) { + return body; + } + return body.substr(0, max_body_bytes); +} + +} // namespace http_request_handler_t::http_request_handler_t(net::io_context &io_context, std::string domain_name) @@ -134,7 +145,7 @@ void http_request_handler_t::on_data_received( } auto const http_status_code = response_->result_int(); int const status_code_simple = http_status_code / 100; - std::string response_string{}; + std::string response_string{response_body_for_checks(response_->body())}; if (status_code_simple == 2) { response_int = response_type_e::ok; @@ -360,7 +371,7 @@ void https_request_handler_t::on_data_received( } int const status_code = response_->result_int(); int const status_code_simple = status_code / 100; - std::string response_string{}; + std::string response_string{response_body_for_checks(response_->body())}; if (status_code_simple == 2) { response_int = response_type_e::ok; diff --git a/dooked/source/http/resolver.cpp b/dooked/source/http/resolver.cpp index 95332a4..4061f4f 100644 --- a/dooked/source/http/resolver.cpp +++ b/dooked/source/http/resolver.cpp @@ -65,11 +65,11 @@ void http_resolver_t::tcp_request_result(response_type_e const rt, std::string const &response_string) { switch (rt) { case response_type_e::bad_request: { - result_map_.insert(name_, content_length, 400); + result_map_.insert(name_, content_length, 400, response_string); return send_next_request(); } case response_type_e::forbidden: { - result_map_.insert(name_, content_length, 403); + result_map_.insert(name_, content_length, 403, response_string); return send_next_request(); } case response_type_e::cannot_resolve_name: { @@ -97,11 +97,11 @@ void http_resolver_t::tcp_request_result(response_type_e const rt, return send_https_request(response_string); } case response_type_e::not_found: { // HTTP(S) 404 - result_map_.insert(name_, content_length, 404); + result_map_.insert(name_, content_length, 404, response_string); return send_next_request(); } case response_type_e::ok: { - result_map_.insert(name_, content_length, 200); + result_map_.insert(name_, content_length, 200, response_string); return send_next_request(); } case response_type_e::recv_timed_out: { // retry, wait timeout @@ -122,11 +122,11 @@ void http_resolver_t::tcp_request_result(response_type_e const rt, return switch_ssl_method(response_string); } case response_type_e::server_error: { - result_map_.insert(name_, content_length, 503); + result_map_.insert(name_, content_length, 503, response_string); return send_next_request(); } default: { - result_map_.insert(name_, 0, 0); + result_map_.insert(name_, 0, 0, response_string); return send_next_request(); } } // end switch diff --git a/dooked/source/main.cpp b/dooked/source/main.cpp index cf29460..ab3f290 100644 --- a/dooked/source/main.cpp +++ b/dooked/source/main.cpp @@ -39,6 +39,8 @@ int main(int argc, char **argv) { app.add_flag( "--defer", cli_args.post_http_request, "defers http request until after all DNS requests have been completed"); + app.add_option("--checks,--check-config", cli_args.check_config_filename, + "load runtime regex checks from a JSON config file"); app.add_flag("--compare-cl", compare_cl, "compare content-length of HTTP requests"); diff --git a/dooked/test/regex_checks_test.cpp b/dooked/test/regex_checks_test.cpp new file mode 100644 index 0000000..de69eac --- /dev/null +++ b/dooked/test/regex_checks_test.cpp @@ -0,0 +1,94 @@ +#include "checks/regex_checks.hpp" +#include +#include +#include +#include + +int main() { + using namespace dooked; + + std::string error; + std::istringstream object_config{R"json( + { + "checks": [ + { + "field": "domain_name", + "regex": "(dev|test)", + "alert": "environment marker", + "ignore_case": true + }, + { + "field": "content", + "pattern": "Copyright 2020", + "message": "outdated copyright", + "case_sensitive": true + }, + { + "field": "http_status", + "regex": "^500$", + "alert": "server error" + }, + { + "field": "info", + "regex": "v=spf1", + "alert": "SPF record" + } + ] + } + )json"}; + + auto checks = parse_regex_checks_config(object_config, error); + assert(checks); + assert(error.empty()); + assert(checks->size() == 4); + assert((*checks)[0].field == regex_check_field_e::domain); + assert((*checks)[0].field_name == "domain"); + assert((*checks)[0].ignore_case); + std::string test_domain{"TEST"}; + assert(std::regex_search(test_domain, (*checks)[0].compiled_pattern)); + assert((*checks)[1].field == regex_check_field_e::body); + assert(!(*checks)[1].ignore_case); + assert((*checks)[2].field == regex_check_field_e::http_code); + assert((*checks)[3].field == regex_check_field_e::rdata); + + std::istringstream array_config{R"json([ + {"field": "ttl", "regex": "^300$", "alert": "ttl match"} + ])json"}; + checks = parse_regex_checks_config(array_config, error); + assert(checks); + assert(checks->size() == 1); + assert((*checks)[0].field == regex_check_field_e::ttl); + + std::istringstream invalid_field_config{R"json( + {"checks": [{"field": "missing", "regex": "x", "alert": "bad"}]} + )json"}; + checks = parse_regex_checks_config(invalid_field_config, error); + assert(!checks); + assert(error.find("unsupported field") != std::string::npos); + + std::istringstream invalid_regex_config{R"json( + {"checks": [{"field": "domain", "regex": "(", "alert": "bad"}]} + )json"}; + checks = parse_regex_checks_config(invalid_regex_config, error); + assert(!checks); + assert(error.find("invalid regex") != std::string::npos); + + std::istringstream conflicting_case_config{R"json( + { + "checks": [ + { + "field": "domain", + "regex": "dev", + "alert": "bad flags", + "ignore_case": true, + "case_sensitive": true + } + ] + } + )json"}; + checks = parse_regex_checks_config(conflicting_case_config, error); + assert(!checks); + assert(error.find("conflicting") != std::string::npos); + + return 0; +}