From 04541780e36dd9811fa9bac77a47d501cc40c4ae Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 21 May 2026 16:48:01 +0700 Subject: [PATCH] Add configurable regex alerts --- README.md | 15 +++++ dooked/CMakeLists.txt | 2 + dooked/include/cli_preprocessor.hpp | 2 + dooked/include/regex_alerts.hpp | 21 +++++++ dooked/source/cli_preprocessor.cpp | 3 + dooked/source/main.cpp | 2 + dooked/source/regex_alerts.cpp | 92 +++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+) create mode 100644 dooked/include/regex_alerts.hpp create mode 100644 dooked/source/regex_alerts.cpp diff --git a/README.md b/README.md index f1a761c..fe2ddc7 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,18 @@ make ## Usage For comprehensive help, use `dooked --help` + +### Regex alerts + +Use `--regex-alerts ` to log custom alerts when a DNS/HTTP probe result matches a regex. The config can be a JSON array or an object with a `checks` array: + +```json +{ + "checks": [ + {"field": "rdata", "regex": "(?i)github", "alert": "github infrastructure"}, + {"field": "http_code", "regex": "^30[12]$", "alert": "redirect detected"} + ] +} +``` + +Supported fields: `domain`/`name`, `http_code`, `content_length`, `dns_type`/`type`, `rdata`/`info`. diff --git a/dooked/CMakeLists.txt b/dooked/CMakeLists.txt index c43ff38..99d9931 100644 --- a/dooked/CMakeLists.txt +++ b/dooked/CMakeLists.txt @@ -72,6 +72,7 @@ set(SRC_FILES ./source/utils/string_utils.cpp ./source/utils/random_utils.cpp ./source/utils/ucstring.cpp + ./source/regex_alerts.cpp ./source/cli_preprocessor.cpp ./source/main.cpp ) @@ -93,6 +94,7 @@ set(HEADERS_FILES ./include/utils/random_utils.hpp ./include/utils/string_utils.hpp ./include/utils/ucstring.hpp + ./include/regex_alerts.hpp ./include/cli_preprocessor.hpp ) diff --git a/dooked/include/cli_preprocessor.hpp b/dooked/include/cli_preprocessor.hpp index 43fa1ba..b15ca86 100644 --- a/dooked/include/cli_preprocessor.hpp +++ b/dooked/include/cli_preprocessor.hpp @@ -19,6 +19,7 @@ struct cli_args_t { std::string resolver_filename{}; std::string output_filename{}; std::string input_filename{}; + std::string regex_alert_filename{}; int file_type{}; int post_http_request{}; @@ -33,6 +34,7 @@ struct runtime_args_t { std::optional> previous_data{}; std::unique_ptr output_file{}; std::string output_filename{}; + std::string regex_alert_filename{}; http_process_e http_request_time_{}; int thread_count{}; int content_length{-1}; diff --git a/dooked/include/regex_alerts.hpp b/dooked/include/regex_alerts.hpp new file mode 100644 index 0000000..c4b910d --- /dev/null +++ b/dooked/include/regex_alerts.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "utils/containers.hpp" +#include "utils/probe_result.hpp" +#include +#include +#include + +namespace dooked { + +struct regex_alert_rule_t { + std::string field{}; + std::string pattern{}; + std::string alert{}; +}; + +std::vector load_regex_alert_rules(std::string const &filename); +void run_regex_alerts(std::vector const &rules, + map_container_t const &result_map); + +} // namespace dooked diff --git a/dooked/source/cli_preprocessor.cpp b/dooked/source/cli_preprocessor.cpp index c08d7fb..df2e0cc 100644 --- a/dooked/source/cli_preprocessor.cpp +++ b/dooked/source/cli_preprocessor.cpp @@ -1,5 +1,6 @@ #include "cli_preprocessor.hpp" #include "dns/dns_resolver.hpp" +#include "regex_alerts.hpp" #include "http/resolver.hpp" #include "utils/exceptions.hpp" #include "utils/random_utils.hpp" @@ -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_regex_alerts(load_regex_alert_rules(rt_args.regex_alert_filename), result_map); // compare old with new result -- only if we had previous record if (rt_args.previous_data) { @@ -477,6 +479,7 @@ void run_program(cli_args_t const &cli_args) { static_cast(cli_args.post_http_request); rt_args.thread_count = cli_args.thread_count; rt_args.content_length = cli_args.content_length; + rt_args.regex_alert_filename = cli_args.regex_alert_filename; return start_name_checking(std::move(rt_args)); } diff --git a/dooked/source/main.cpp b/dooked/source/main.cpp index cf29460..a2c79c9 100644 --- a/dooked/source/main.cpp +++ b/dooked/source/main.cpp @@ -34,6 +34,8 @@ int main(int argc, char **argv) { app.add_option( "-c,--content-length", cli_args.content_length, "show content lengths that changed more than --content-length"); + app.add_option("--regex-alerts", cli_args.regex_alert_filename, + "JSON config containing runtime regex alert checks"); app.add_flag("-d,--include-date", cli_args.include_date, "append present datetime(-ddMMyyyy_hhmmss) in output name"); app.add_flag( diff --git a/dooked/source/regex_alerts.cpp b/dooked/source/regex_alerts.cpp new file mode 100644 index 0000000..86f5010 --- /dev/null +++ b/dooked/source/regex_alerts.cpp @@ -0,0 +1,92 @@ +#include "regex_alerts.hpp" +#include "utils/io_utils.hpp" +#include +#include +#include + +namespace dooked { + +namespace { +std::string dns_type_to_string(probe_result_t const &record) { + return dns_record_type_to_str(record.type); +} + +std::vector values_for_field( + std::string const &field, std::string const &domain, + http_dns_response_t const &response) { + std::vector values{}; + if (field == "domain" || field == "name") { + values.push_back(domain); + } else if (field == "http_code") { + values.push_back(std::to_string(response.http_result_.http_status_)); + } else if (field == "content_length") { + values.push_back(std::to_string(response.http_result_.content_length_)); + } else if (field == "dns_type" || field == "type") { + for (auto const &record : response.dns_result_list_) { + values.push_back(dns_type_to_string(record)); + } + } else if (field == "rdata" || field == "info") { + for (auto const &record : response.dns_result_list_) { + values.push_back(record.rdata); + } + } + return values; +} +} // namespace + +std::vector +load_regex_alert_rules(std::string const &filename) { + if (filename.empty()) { + return {}; + } + + std::ifstream file{filename}; + if (!file) { + spdlog::error("Unable to open regex alert config `{}`", filename); + return {}; + } + + try { + auto const parsed = json::parse(file); + auto const rule_list = parsed.contains("checks") ? parsed.at("checks") : parsed; + std::vector rules{}; + for (auto const &rule_json : rule_list) { + regex_alert_rule_t rule{}; + rule.field = rule_json.at("field").get(); + rule.pattern = rule_json.at("regex").get(); + rule.alert = rule_json.at("alert").get(); + std::regex{rule.pattern}; + rules.push_back(std::move(rule)); + } + return rules; + } catch (std::exception const &e) { + spdlog::error("Invalid regex alert config `{}`: {}", filename, e.what()); + return {}; + } +} + +void run_regex_alerts(std::vector const &rules, + map_container_t const &result_map) { + if (rules.empty()) { + return; + } + + std::vector> compiled_rules{}; + compiled_rules.reserve(rules.size()); + for (auto const &rule : rules) { + compiled_rules.emplace_back(rule, std::regex{rule.pattern}); + } + + for (auto const &[domain, response] : result_map.cresult()) { + for (auto const &[rule, matcher] : compiled_rules) { + for (auto const &value : values_for_field(rule.field, domain, response)) { + if (std::regex_search(value, matcher)) { + spdlog::info("[REGEX ALERT][{}][{}] {} matched `{}`", + domain, rule.field, rule.alert, value); + } + } + } + } +} + +} // namespace dooked