From fbe911732091fc53503b9b829506aad47ef33928 Mon Sep 17 00:00:00 2001 From: Da7-Tech <286182457+Da7-Tech@users.noreply.github.com> Date: Wed, 20 May 2026 09:21:39 +0300 Subject: [PATCH] Add runtime regex notification checks --- README.md | 30 ++++ dooked/CMakeLists.txt | 2 + dooked/include/cli_preprocessor.hpp | 3 + dooked/include/dns/dns_resolver.hpp | 8 +- dooked/include/http/requests_handler.hpp | 3 +- dooked/include/http/resolver.hpp | 8 +- dooked/include/utils/exceptions.hpp | 1 + dooked/include/utils/regex_checks.hpp | 31 ++++ dooked/source/cli_preprocessor.cpp | 13 +- dooked/source/dns/dns_resolver.cpp | 34 +++- dooked/source/http/requests_handler.cpp | 29 +-- dooked/source/http/resolver.cpp | 34 +++- dooked/source/main.cpp | 2 + dooked/source/utils/regex_checks.cpp | 218 +++++++++++++++++++++++ 14 files changed, 382 insertions(+), 34 deletions(-) create mode 100644 dooked/include/utils/regex_checks.hpp create mode 100644 dooked/source/utils/regex_checks.cpp diff --git a/README.md b/README.md index f1a761c..1a6533c 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,33 @@ make ## Usage For comprehensive help, use `dooked --help` + +### Runtime regex checks + +Use `--checks ` or `--check-config ` to load custom +notifications without changing the output JSON format. + +The config can be a JSON array, or an object with a `checks` array: + +```json +{ + "checks": [ + { + "field": "domain", + "regex": "(dev|test)", + "alert": "development-looking domain" + }, + { + "field": "body", + "regex": "copyright 2020", + "alert": "outdated copyright banner", + "ignore_case": true + } + ] +} +``` + +Supported fields are `domain`, DNS fields (`type`, `info`, `rdata`, `ttl`), +HTTP metadata (`http_code`, `code_string`, `content_length`), and response +`body`. Body checks are evaluated in memory while probing and are not written +to the JSON output. diff --git a/dooked/CMakeLists.txt b/dooked/CMakeLists.txt index c43ff38..f0954ab 100644 --- a/dooked/CMakeLists.txt +++ b/dooked/CMakeLists.txt @@ -69,6 +69,7 @@ set(SRC_FILES ./source/http/requests_handler.cpp ./source/utils/constants.cpp ./source/utils/io_utils.cpp + ./source/utils/regex_checks.cpp ./source/utils/string_utils.cpp ./source/utils/random_utils.cpp ./source/utils/ucstring.cpp @@ -91,6 +92,7 @@ set(HEADERS_FILES ./include/utils/io_utils.hpp ./include/utils/probe_result.hpp ./include/utils/random_utils.hpp + ./include/utils/regex_checks.hpp ./include/utils/string_utils.hpp ./include/utils/ucstring.hpp ./include/cli_preprocessor.hpp diff --git a/dooked/include/cli_preprocessor.hpp b/dooked/include/cli_preprocessor.hpp index 43fa1ba..a2cd82c 100644 --- a/dooked/include/cli_preprocessor.hpp +++ b/dooked/include/cli_preprocessor.hpp @@ -2,6 +2,7 @@ #include "dns/dns_resolver.hpp" #include "utils/io_utils.hpp" +#include "utils/regex_checks.hpp" #include // maximum sockets to open regardless of the number of threads @@ -19,6 +20,7 @@ struct cli_args_t { std::string resolver_filename{}; std::string output_filename{}; std::string input_filename{}; + std::string checks_filename{}; int file_type{}; int post_http_request{}; @@ -36,6 +38,7 @@ struct runtime_args_t { http_process_e http_request_time_{}; int thread_count{}; int content_length{-1}; + regex_check_list_t regex_checks{}; }; void run_program(cli_args_t const &cli_args); diff --git a/dooked/include/dns/dns_resolver.hpp b/dooked/include/dns/dns_resolver.hpp index 498cc28..d54ae3b 100644 --- a/dooked/include/dns/dns_resolver.hpp +++ b/dooked/include/dns/dns_resolver.hpp @@ -3,6 +3,7 @@ #include "dns.hpp" #include "http/requests_handler.hpp" #include "utils/containers.hpp" +#include "utils/regex_checks.hpp" #include #include #include @@ -33,6 +34,7 @@ class custom_resolver_socket_t { domain_list_t &names_; resolver_list_t &resolvers_; map_container_t &result_map_; + regex_check_list_t const *regex_checks_{nullptr}; net::ssl::context *ssl_context_{nullptr}; std::optional udp_stream_{}; std::optional default_ep_{}; @@ -67,7 +69,8 @@ class custom_resolver_socket_t { // http related "handlers" void perform_http_request(); - void http_result_obtained(response_type_e, int, std::string const &); + void http_result_obtained(response_type_e, int, std::string const &, + std::string const &); void on_http_resolve_error(); void send_https_request(std::string const &address); void send_http_request(std::string const &address); @@ -77,7 +80,8 @@ class custom_resolver_socket_t { public: custom_resolver_socket_t(net::io_context &, net::ssl::context *, domain_list_t &, resolver_list_t &, - map_container_t &); + map_container_t &, + regex_check_list_t const * = nullptr); void defer_http_request(bool const defer); void start(); }; diff --git a/dooked/include/http/requests_handler.hpp b/dooked/include/http/requests_handler.hpp index 63e4f15..bac49b2 100644 --- a/dooked/include/http/requests_handler.hpp +++ b/dooked/include/http/requests_handler.hpp @@ -32,7 +32,8 @@ enum class ssl_method_e { undefined }; -using completion_cb_t = std::function; +using completion_cb_t = + std::function; class http_request_handler_t { net::io_context &io_; diff --git a/dooked/include/http/resolver.hpp b/dooked/include/http/resolver.hpp index c657b38..f47224b 100644 --- a/dooked/include/http/resolver.hpp +++ b/dooked/include/http/resolver.hpp @@ -3,6 +3,7 @@ #include "http/requests_handler.hpp" #include "utils/containers.hpp" #include "utils/probe_result.hpp" +#include "utils/regex_checks.hpp" namespace dooked { @@ -14,6 +15,7 @@ class http_resolver_t { std::optional http_request_handler_{}; std::optional tls_holder_{}; std::string name_{}; + regex_check_list_t const *regex_checks_{nullptr}; int http_redirects_count_{}; int http_retries_count_{}; // this should have been a boolean but it's an int to keep the alignment @@ -23,14 +25,16 @@ class http_resolver_t { void perform_http_request(); void switch_ssl_method(std::string const &); void send_next_request(); - void tcp_request_result(response_type_e, int, std::string const &); + void tcp_request_result(response_type_e, int, std::string const &, + std::string const &); void send_http_request(std::string const &address); void send_https_request(std::string const &address); void on_resolve_error(); public: http_resolver_t(net::io_context &, ssl::context *, domain_list_t &, - map_container_t &); + map_container_t &, + regex_check_list_t const * = nullptr); void start() { send_next_request(); } }; 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/include/utils/regex_checks.hpp b/dooked/include/utils/regex_checks.hpp new file mode 100644 index 0000000..5a3ad24 --- /dev/null +++ b/dooked/include/utils/regex_checks.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "utils/containers.hpp" +#include "utils/probe_result.hpp" +#include +#include +#include +#include + +namespace dooked { + +struct regex_check_t { + std::string field{}; + std::string pattern{}; + std::string alert{}; + std::regex expression{}; +}; + +using regex_check_list_t = std::vector; + +std::optional +load_regex_checks(std::string const &filename); + +void run_result_checks(regex_check_list_t const &checks, + map_container_t const &result_map); + +void run_body_checks(regex_check_list_t const &checks, + std::string const &domain_name, int const http_code, + int const content_length, std::string const &body); + +} // namespace dooked diff --git a/dooked/source/cli_preprocessor.cpp b/dooked/source/cli_preprocessor.cpp index c08d7fb..bf2c882 100644 --- a/dooked/source/cli_preprocessor.cpp +++ b/dooked/source/cli_preprocessor.cpp @@ -179,7 +179,7 @@ void dns_functor(net::io_context &io_context, net::ssl::context *ssl_context, for (std::size_t i = 0; i < socket_count; ++i) { sockets[i] = std::make_unique( io_context, ssl_context, *rt_args.names, *rt_args.resolvers, - result_map); + result_map, &rt_args.regex_checks); sockets[i]->defer_http_request(deferring); sockets[i]->start(); } @@ -194,7 +194,8 @@ void http_functor(net::io_context &io_context, net::ssl::context *ssl_context, sockets.resize(socket_count); for (std::size_t i = 0; i < socket_count; ++i) { sockets[i] = std::make_unique(io_context, ssl_context, - *rt_args.names, result_map); + *rt_args.names, result_map, + &rt_args.regex_checks); sockets[i]->start(); } io_context.run(); @@ -354,6 +355,7 @@ void start_name_checking(runtime_args_t &&rt_args) { spdlog::info("Writing JSON output"); } write_json_result(result_map, rt_args); + run_result_checks(rt_args.regex_checks, result_map); // compare old with new result -- only if we had previous record if (rt_args.previous_data) { @@ -404,6 +406,13 @@ void run_program(cli_args_t const &cli_args) { if (!read_input_file(cli_args, rt_args)) { return; } + if (!cli_args.checks_filename.empty()) { + auto checks = load_regex_checks(cli_args.checks_filename); + if (!checks) { + return; + } + rt_args.regex_checks = std::move(*checks); + } // try opening an output file { std::string filename{}; diff --git a/dooked/source/dns/dns_resolver.cpp b/dooked/source/dns/dns_resolver.cpp index 851745f..7bf81bd 100644 --- a/dooked/source/dns/dns_resolver.cpp +++ b/dooked/source/dns/dns_resolver.cpp @@ -9,6 +9,17 @@ extern bool silent; namespace dooked { +namespace { +void run_final_body_checks(regex_check_list_t const *checks, + std::string const &name, int const http_code, + int const content_length, + std::string const &body) { + if (checks != nullptr) { + run_body_checks(*checks, name, http_code, content_length, body); + } +} +} // namespace + dns_rec_list_t const dns_supported_record_type_t::supported_types{ dns_record_type_e::DNS_REC_A, dns_record_type_e::DNS_REC_AAAA, dns_record_type_e::DNS_REC_CNAME, dns_record_type_e::DNS_REC_MX, @@ -111,9 +122,11 @@ void dns_create_query(std::string const &name, std::uint16_t record_type, custom_resolver_socket_t::custom_resolver_socket_t( net::io_context &io_context, net::ssl::context *ssl_context, domain_list_t &domain_list, resolver_list_t &resolvers, - map_container_t &result_map) + map_container_t &result_map, + regex_check_list_t const *regex_checks) : io_{io_context}, names_{domain_list}, resolvers_{resolvers}, - result_map_{result_map}, ssl_context_{ssl_context}, + result_map_{result_map}, regex_checks_{regex_checks}, + ssl_context_{ssl_context}, supported_dns_record_size_( dns_supported_record_type_t::supported_types.size()) {} @@ -382,8 +395,9 @@ void custom_resolver_socket_t::send_http_request(std::string const &address) { http_request_handler_->request_.emplace( io_, uri_t{address}.host()); http_request.start([this](response_type_e const rt, int const content_length, - std::string const &response) { - http_result_obtained(rt, content_length, response); + std::string const &response, + std::string const &body) { + http_result_obtained(rt, content_length, response, body); }); } @@ -392,8 +406,9 @@ void custom_resolver_socket_t::send_https_request(std::string const &address) { http_request_handler_->request_.emplace( io_, *ssl_context_, uri_t{address}.host()); return https_request.start( - [this](auto const rt, auto const len, auto const &rstr) { - http_result_obtained(rt, len, rstr); + [this](auto const rt, auto const len, auto const &rstr, + auto const &body) { + http_result_obtained(rt, len, rstr, body); }); } @@ -413,14 +428,16 @@ void custom_resolver_socket_t::on_http_resolve_error() { void custom_resolver_socket_t::http_result_obtained( response_type_e const rt, int const content_length, - std::string const &response_string) { + std::string const &response_string, std::string const &body) { switch (rt) { case response_type_e::bad_request: { + run_final_body_checks(regex_checks_, name_, 400, content_length, body); result_map_.insert(name_, content_length, 400); return dns_continue_probe(); } case response_type_e::forbidden: { + run_final_body_checks(regex_checks_, name_, 403, content_length, body); result_map_.insert(name_, content_length, 403); return dns_continue_probe(); } @@ -447,10 +464,12 @@ void custom_resolver_socket_t::http_result_obtained( return send_https_request(response_string); } case response_type_e::not_found: { // HTTP(S) 404 + run_final_body_checks(regex_checks_, name_, 404, content_length, body); result_map_.insert(name_, content_length, 404); return dns_continue_probe(); } case response_type_e::ok: { + run_final_body_checks(regex_checks_, name_, 200, content_length, body); result_map_.insert(name_, content_length, 200); return dns_continue_probe(); } @@ -477,6 +496,7 @@ void custom_resolver_socket_t::http_result_obtained( return send_https_request(response_string); } case response_type_e::server_error: { + run_final_body_checks(regex_checks_, name_, 503, content_length, body); result_map_.insert(name_, content_length, 503); return dns_continue_probe(); } diff --git a/dooked/source/http/requests_handler.cpp b/dooked/source/http/requests_handler.cpp index d21a592..92b567a 100644 --- a/dooked/source/http/requests_handler.cpp +++ b/dooked/source/http/requests_handler.cpp @@ -46,7 +46,7 @@ void http_request_handler_t::establish_connection() { void http_request_handler_t::resolve_name() { if (resolver_) { if (callback_) { - callback_(response_type_e::cannot_resolve_name, 0, ""); + callback_(response_type_e::cannot_resolve_name, 0, "", ""); } return; } @@ -55,7 +55,8 @@ void http_request_handler_t::resolve_name() { domain_, "http", [this](auto const &error, auto const &results) { if (error) { if (callback_) { - callback_(response_type_e::cannot_resolve_name, 0, error.message()); + callback_(response_type_e::cannot_resolve_name, 0, error.message(), + ""); } return; } @@ -78,7 +79,7 @@ void http_request_handler_t::on_connected(beast::error_code const ec) { void http_request_handler_t::reconnect() { if (++connect_retries_ >= 3) { if (callback_) { - callback_(response_type_e::cannot_connect, 0, {}); + callback_(response_type_e::cannot_connect, 0, {}, {}); } return; } @@ -99,7 +100,7 @@ void http_request_handler_t::send_http_data() { void http_request_handler_t::resend_data() { if (++send_retries_ >= 3) { if (callback_) { - return callback_(response_type_e::cannot_send, 0, {}); + return callback_(response_type_e::cannot_send, 0, {}, {}); } } else { send_http_data(); @@ -128,7 +129,7 @@ void http_request_handler_t::on_data_received( } #endif // _DEBUG if (callback_) { - callback_(response_type_e::recv_timed_out, 0, {}); + callback_(response_type_e::recv_timed_out, 0, {}, {}); } return; } @@ -184,7 +185,7 @@ void http_request_handler_t::on_data_received( } } if (callback_) { - callback_(response_int, content_length, response_string); + callback_(response_int, content_length, response_string, response_->body()); } } @@ -236,7 +237,7 @@ void https_request_handler_t::on_ssl_handshake( error_type = response_type_e::ssl_change_context; } if (callback_) { - callback_(error_type, 0, domain_name_); + callback_(error_type, 0, domain_name_, ""); } return; } @@ -254,7 +255,7 @@ void https_request_handler_t::send_https_data() { void https_request_handler_t::on_data_sent(beast::error_code ec, std::size_t) { if (ec) { if (callback_) { - callback_(response_type_e::cannot_send, 0, ec.message()); + callback_(response_type_e::cannot_send, 0, ec.message(), ""); } return; } @@ -302,7 +303,7 @@ void https_request_handler_t::connect() { void https_request_handler_t::reconnect() { if (++reconnect_count_ >= 3) { if (callback_) { - callback_(response_type_e::cannot_connect, 0, {}); + callback_(response_type_e::cannot_connect, 0, {}, {}); } return; } @@ -324,7 +325,7 @@ void https_request_handler_t::on_connect(beast::error_code const ec) { void https_request_handler_t::resolve_name() { if (resolver_) { // we have tried resolving the name earlier if (callback_) { - callback_(response_type_e::cannot_resolve_name, 0, ""); + callback_(response_type_e::cannot_resolve_name, 0, "", ""); } return; } @@ -333,7 +334,8 @@ void https_request_handler_t::resolve_name() { domain_name_, "https", [this](auto const &error, auto const &results) { if (error) { if (callback_) { - callback_(response_type_e::cannot_resolve_name, 0, error.message()); + callback_(response_type_e::cannot_resolve_name, 0, error.message(), + ""); return; } } @@ -354,7 +356,7 @@ void https_request_handler_t::on_data_received( response_int = response_type_e::recv_timed_out; } if (callback_) { - callback_(response_int, 0, domain_name_); + callback_(response_int, 0, domain_name_, ""); } return; } @@ -405,7 +407,8 @@ void https_request_handler_t::on_data_received( } } if (callback_) { - callback_(response_int, content_length, response_string); + callback_(response_int, content_length, response_string, + response_->body()); } } diff --git a/dooked/source/http/resolver.cpp b/dooked/source/http/resolver.cpp index 95332a4..6b1f96c 100644 --- a/dooked/source/http/resolver.cpp +++ b/dooked/source/http/resolver.cpp @@ -4,11 +4,23 @@ namespace dooked { +namespace { +void run_final_body_checks(regex_check_list_t const *checks, + std::string const &name, int const http_code, + int const content_length, + std::string const &body) { + if (checks != nullptr) { + run_body_checks(*checks, name, http_code, content_length, body); + } +} +} // namespace + http_resolver_t::http_resolver_t(net::io_context &ioc, ssl::context *sslc, domain_list_t &names, - map_container_t &result_map) + map_container_t &result_map, + regex_check_list_t const *regex_checks) : io_context_(ioc), default_tls_context_(sslc), names_(names), - result_map_(result_map) {} + result_map_(result_map), regex_checks_{regex_checks} {} void http_resolver_t::send_next_request() { try { @@ -33,8 +45,9 @@ void http_resolver_t::send_http_request(std::string const &address) { http_request_handler_->request_.emplace( io_context_, uri_t{address}.host()); http_request.start([this](response_type_e const rt, int const content_length, - std::string const &response) { - tcp_request_result(rt, content_length, response); + std::string const &response, + std::string const &body) { + tcp_request_result(rt, content_length, response, body); }); } @@ -43,8 +56,9 @@ void http_resolver_t::send_https_request(std::string const &address) { http_request_handler_->request_.emplace( io_context_, *default_tls_context_, uri_t{address}.host()); return https_request.start( - [this](auto const rt, auto const len, auto const &rstr) { - tcp_request_result(rt, len, rstr); + [this](auto const rt, auto const len, auto const &rstr, + auto const &body) { + tcp_request_result(rt, len, rstr, body); }); } @@ -62,13 +76,16 @@ void http_resolver_t::on_resolve_error() { void http_resolver_t::tcp_request_result(response_type_e const rt, int const content_length, - std::string const &response_string) { + std::string const &response_string, + std::string const &body) { switch (rt) { case response_type_e::bad_request: { + run_final_body_checks(regex_checks_, name_, 400, content_length, body); result_map_.insert(name_, content_length, 400); return send_next_request(); } case response_type_e::forbidden: { + run_final_body_checks(regex_checks_, name_, 403, content_length, body); result_map_.insert(name_, content_length, 403); return send_next_request(); } @@ -97,10 +114,12 @@ 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 + run_final_body_checks(regex_checks_, name_, 404, content_length, body); result_map_.insert(name_, content_length, 404); return send_next_request(); } case response_type_e::ok: { + run_final_body_checks(regex_checks_, name_, 200, content_length, body); result_map_.insert(name_, content_length, 200); return send_next_request(); } @@ -122,6 +141,7 @@ void http_resolver_t::tcp_request_result(response_type_e const rt, return switch_ssl_method(response_string); } case response_type_e::server_error: { + run_final_body_checks(regex_checks_, name_, 503, content_length, body); result_map_.insert(name_, content_length, 503); return send_next_request(); } diff --git a/dooked/source/main.cpp b/dooked/source/main.cpp index cf29460..0861ee5 100644 --- a/dooked/source/main.cpp +++ b/dooked/source/main.cpp @@ -28,6 +28,8 @@ int main(int argc, char **argv) { app.add_option("-r,--resolver", cli_args.resolver, "a (possible) list of resolvers separated by comma. If -l " "and -r isn't specified, -r is defaulted to 8.8.8.8"); + app.add_option("--checks,--check-config", cli_args.checks_filename, + "JSON file containing runtime regex notification checks"); app.add_option("-t,--threads", cli_args.thread_count, "total threads to use(default: " + std::to_string(DOOKED_SUPPORTED_THREADS) + ")"); diff --git a/dooked/source/utils/regex_checks.cpp b/dooked/source/utils/regex_checks.cpp new file mode 100644 index 0000000..9a0fbeb --- /dev/null +++ b/dooked/source/utils/regex_checks.cpp @@ -0,0 +1,218 @@ +#include "utils/regex_checks.hpp" +#include "utils/constants.hpp" +#include +#include +#include +#include + +namespace dooked { +namespace { + +using json = nlohmann::json; + +std::string normalize_field(std::string const &field) { + std::string normalized{}; + normalized.reserve(field.size()); + for (auto const ch : field) { + if (ch == '_' || ch == '-') { + continue; + } + normalized.push_back((char)std::tolower((unsigned char)ch)); + } + return normalized; +} + +bool is_domain_field(std::string const &field) { + auto const key = normalize_field(field); + return key == "domain" || key == "domainname" || key == "name"; +} + +bool is_dns_field(std::string const &field) { + auto const key = normalize_field(field); + return key == "type" || key == "dnstype" || key == "info" || + key == "rdata" || key == "ttl"; +} + +bool is_http_field(std::string const &field) { + auto const key = normalize_field(field); + return key == "httpcode" || key == "status" || key == "codestatus" || + key == "codestring" || key == "contentlength"; +} + +bool is_body_field(std::string const &field) { + auto const key = normalize_field(field); + return key == "body" || key == "content" || key == "page" || + key == "pagecontent" || key == "responsebody"; +} + +bool is_supported_field(std::string const &field) { + return is_domain_field(field) || is_dns_field(field) || + is_http_field(field) || is_body_field(field); +} + +std::optional dns_value(std::string const &field, + probe_result_t const &record) { + auto const key = normalize_field(field); + if (key == "type" || key == "dnstype") { + return dns_record_type_to_str(record.type); + } + if (key == "info" || key == "rdata") { + return record.rdata; + } + if (key == "ttl") { + return std::to_string(record.ttl); + } + return std::nullopt; +} + +std::optional http_value(std::string const &field, + int const http_code, + int const content_length) { + auto const key = normalize_field(field); + if (key == "httpcode" || key == "status" || key == "codestatus") { + return std::to_string(http_code); + } + if (key == "codestring") { + return code_string(http_code); + } + if (key == "contentlength") { + return std::to_string(content_length); + } + return std::nullopt; +} + +void maybe_report(std::string const &domain_name, regex_check_t const &check, + std::string const &value) { + if (std::regex_search(value, check.expression)) { + spdlog::warn("[REGEX][{}][{}] {}", domain_name, check.field, check.alert); + } +} + +std::optional parse_check(json const &item, + std::size_t const index) { + if (!item.is_object()) { + spdlog::error("regex check {} must be a JSON object", index); + return std::nullopt; + } + if (!item.contains("field") || !item.contains("regex")) { + spdlog::error("regex check {} must contain field and regex", index); + return std::nullopt; + } + + regex_check_t check{}; + check.field = item.at("field").get(); + check.pattern = item.at("regex").get(); + if (item.contains("alert")) { + check.alert = item.at("alert").get(); + } else if (item.contains("message")) { + check.alert = item.at("message").get(); + } else { + check.alert = "matched configured check"; + } + + if (!is_supported_field(check.field)) { + spdlog::error("regex check {} has unsupported field `{}`", index, + check.field); + return std::nullopt; + } + + auto flags = std::regex::ECMAScript; + if (item.value("ignore_case", false) || + item.value("case_insensitive", false)) { + flags |= std::regex::icase; + } + + try { + check.expression = std::regex(check.pattern, flags); + } catch (std::regex_error const &e) { + spdlog::error("regex check {} has invalid regex `{}`: {}", index, + check.pattern, e.what()); + return std::nullopt; + } + return check; +} + +} // namespace + +std::optional +load_regex_checks(std::string const &filename) { + std::ifstream input_file(filename); + if (!input_file) { + spdlog::error("unable to open regex checks file `{}`", filename); + return std::nullopt; + } + + try { + auto const root = json::parse(input_file); + auto const checks_json = + root.is_array() ? root : root.value("checks", json::array()); + if (!checks_json.is_array()) { + spdlog::error("regex checks file must be an array or contain checks[]"); + return std::nullopt; + } + + regex_check_list_t checks{}; + for (std::size_t index = 0; index < checks_json.size(); ++index) { + auto check = parse_check(checks_json.at(index), index); + if (!check) { + return std::nullopt; + } + checks.push_back(std::move(*check)); + } + return checks; + } catch (std::exception const &e) { + spdlog::error("unable to parse regex checks file `{}`: {}", filename, + e.what()); + return std::nullopt; + } +} + +void run_result_checks(regex_check_list_t const &checks, + map_container_t const &result_map) { + 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; + auto const http_code = domain_result.http_result_.http_status_; + auto const content_length = domain_result.http_result_.content_length_; + + for (auto const &check : checks) { + if (is_body_field(check.field)) { + continue; + } + if (is_domain_field(check.field)) { + maybe_report(domain_name, check, domain_name); + continue; + } + if (is_http_field(check.field)) { + if (auto value = http_value(check.field, http_code, content_length)) { + maybe_report(domain_name, check, *value); + } + continue; + } + for (auto const &record : domain_result.dns_result_list_) { + if (auto value = dns_value(check.field, record)) { + maybe_report(domain_name, check, *value); + } + } + } + } +} + +void run_body_checks(regex_check_list_t const &checks, + std::string const &domain_name, int const, + int const, std::string const &body) { + if (checks.empty() || body.empty()) { + return; + } + for (auto const &check : checks) { + if (is_body_field(check.field)) { + maybe_report(domain_name, check, body); + } + } +} + +} // namespace dooked