diff options
Diffstat (limited to '')
-rw-r--r-- | src/detect-engine-analyzer.c | 1770 |
1 files changed, 1770 insertions, 0 deletions
diff --git a/src/detect-engine-analyzer.c b/src/detect-engine-analyzer.c new file mode 100644 index 0000000..9729046 --- /dev/null +++ b/src/detect-engine-analyzer.c @@ -0,0 +1,1770 @@ +/* Copyright (C) 2007-2023 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Eileen Donlon <emdonlo@gmail.com> + * \author Victor Julien <victor@inliniac.net> + * + * Rule analyzers for the detection engine + */ + +#include "suricata-common.h" +#include "suricata.h" +#include "rust.h" +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-analyzer.h" +#include "detect-engine-mpm.h" +#include "conf.h" +#include "detect-content.h" +#include "detect-pcre.h" +#include "detect-bytejump.h" +#include "detect-bytetest.h" +#include "detect-flow.h" +#include "detect-tcp-flags.h" +#include "detect-ipopts.h" +#include "feature.h" +#include "util-print.h" +#include "util-time.h" +#include "util-validate.h" +#include "util-conf.h" + +static int rule_warnings_only = 0; + +/* Details for each buffer being tracked */ +typedef struct DetectEngineAnalyzerItems { + int16_t item_id; + bool item_seen; + bool export_item_seen; + bool check_encoding_match; + const char *item_name; + const char *display_name; +} DetectEngineAnalyzerItems; + +typedef struct FpPatternStats_ { + uint16_t min; + uint16_t max; + uint32_t cnt; + uint64_t tot; +} FpPatternStats; + +/* Track which items require the item_seen value to be exposed */ +struct ExposedItemSeen { + const char *bufname; + bool *item_seen_ptr; +}; + +typedef struct EngineAnalysisCtx_ { + + FILE *rule_engine_analysis_fp; + FILE *fp_engine_analysis_fp; + + DetectEngineAnalyzerItems *analyzer_items; + char *file_prefix; + pcre2_code *percent_re; + + /* + * This array contains the map between the `analyzer_items` array listed above and + * the item ids returned by DetectBufferTypeGetByName. Iterating signature's sigmatch + * array provides list_ids. The map converts those ids into elements of the + * analyzer items array. + * + * Ultimately, the g_buffer_type_hash is searched for each buffer name. The size of that + * hashlist is 256, so that's the value we use here. + */ + int16_t analyzer_item_map[256]; + FpPatternStats fp_pattern_stats[DETECT_SM_LIST_MAX]; + /* + * Certain values must be directly accessible. This array contains items that are directly + * accessed when checking if they've been seen or not. + */ + struct ExposedItemSeen exposed_item_seen_list[2]; + + bool analyzer_initialized; +} EngineAnalysisCtx; + +const DetectEngineAnalyzerItems analyzer_items[] = { + /* request keywords */ + { 0, false, false, true, "http_uri", "http uri" }, + { 0, false, false, false, "http_raw_uri", "http raw uri" }, + { 0, false, true, false, "http_method", "http method" }, + { 0, false, false, false, "http_request_line", "http request line" }, + { 0, false, false, false, "http_client_body", "http client body" }, + { 0, false, false, true, "http_header", "http header" }, + { 0, false, false, false, "http_raw_header", "http raw header" }, + { 0, false, false, true, "http_cookie", "http cookie" }, + { 0, false, false, false, "http_user_agent", "http user agent" }, + { 0, false, false, false, "http_host", "http host" }, + { 0, false, false, false, "http_raw_host", "http raw host" }, + { 0, false, false, false, "http_accept_enc", "http accept enc" }, + { 0, false, false, false, "http_referer", "http referer" }, + { 0, false, false, false, "http_content_type", "http content type" }, + { 0, false, false, false, "http_header_names", "http header names" }, + + /* response keywords not listed above */ + { 0, false, false, false, "http_stat_msg", "http stat msg" }, + { 0, false, false, false, "http_stat_code", "http stat code" }, + { 0, false, true, false, "file_data", "http server body" }, + + /* missing request keywords */ + { 0, false, false, false, "http_request_line", "http request line" }, + { 0, false, false, false, "http_accept", "http accept" }, + { 0, false, false, false, "http_accept_lang", "http accept lang" }, + { 0, false, false, false, "http_connection", "http connection" }, + { 0, false, false, false, "http_content_len", "http content len" }, + { 0, false, false, false, "http_protocol", "http protocol" }, + { 0, false, false, false, "http_start", "http start" }, + + /* missing response keywords; some of the missing are listed above*/ + { 0, false, false, false, "http_response_line", "http response line" }, + { 0, false, false, false, "http.server", "http server" }, + { 0, false, false, false, "http.location", "http location" }, +}; + +static void FpPatternStatsAdd(FpPatternStats *fp, int list, uint16_t patlen) +{ + if (list < 0 || list >= DETECT_SM_LIST_MAX) + return; + + FpPatternStats *f = &fp[list]; + + if (f->min == 0) + f->min = patlen; + else if (patlen < f->min) + f->min = patlen; + + if (patlen > f->max) + f->max = patlen; + + f->cnt++; + f->tot += patlen; +} + +void EngineAnalysisFP(const DetectEngineCtx *de_ctx, const Signature *s, char *line) +{ + int fast_pattern_set = 0; + int fast_pattern_only_set = 0; + int fast_pattern_chop_set = 0; + const DetectContentData *fp_cd = NULL; + const SigMatch *mpm_sm = s->init_data->mpm_sm; + const int mpm_sm_list = s->init_data->mpm_sm_list; + + if (mpm_sm != NULL) { + fp_cd = (DetectContentData *)mpm_sm->ctx; + if (fp_cd->flags & DETECT_CONTENT_FAST_PATTERN) { + fast_pattern_set = 1; + if (fp_cd->flags & DETECT_CONTENT_FAST_PATTERN_ONLY) { + fast_pattern_only_set = 1; + } else if (fp_cd->flags & DETECT_CONTENT_FAST_PATTERN_CHOP) { + fast_pattern_chop_set = 1; + } + } + } + + FILE *fp = de_ctx->ea->rule_engine_analysis_fp; + fprintf(fp, "== Sid: %u ==\n", s->id); + fprintf(fp, "%s\n", line); + + fprintf(fp, " Fast Pattern analysis:\n"); + if (s->init_data->prefilter_sm != NULL) { + fprintf(fp, " Prefilter on: %s\n", + sigmatch_table[s->init_data->prefilter_sm->type].name); + fprintf(fp, "\n"); + return; + } + + if (fp_cd == NULL) { + fprintf(fp, " No content present\n"); + fprintf(fp, "\n"); + return; + } + + fprintf(fp, " Fast pattern matcher: "); + int list_type = mpm_sm_list; + if (list_type == DETECT_SM_LIST_PMATCH) + fprintf(fp, "content\n"); + else { + const char *desc = DetectEngineBufferTypeGetDescriptionById(de_ctx, list_type); + const char *name = DetectEngineBufferTypeGetNameById(de_ctx, list_type); + if (desc && name) { + fprintf(fp, "%s (%s)\n", desc, name); + } + } + + int flags_set = 0; + fprintf(fp, " Flags:"); + if (fp_cd->flags & DETECT_CONTENT_OFFSET) { + fprintf(fp, " Offset"); + flags_set = 1; + } if (fp_cd->flags & DETECT_CONTENT_DEPTH) { + fprintf(fp, " Depth"); + flags_set = 1; + } + if (fp_cd->flags & DETECT_CONTENT_WITHIN) { + fprintf(fp, " Within"); + flags_set = 1; + } + if (fp_cd->flags & DETECT_CONTENT_DISTANCE) { + fprintf(fp, " Distance"); + flags_set = 1; + } + if (fp_cd->flags & DETECT_CONTENT_NOCASE) { + fprintf(fp, " Nocase"); + flags_set = 1; + } + if (fp_cd->flags & DETECT_CONTENT_NEGATED) { + fprintf(fp, " Negated"); + flags_set = 1; + } + if (flags_set == 0) + fprintf(fp, " None"); + fprintf(fp, "\n"); + + fprintf(fp, " Fast pattern set: %s\n", fast_pattern_set ? "yes" : "no"); + fprintf(fp, " Fast pattern only set: %s\n", fast_pattern_only_set ? "yes" : "no"); + fprintf(fp, " Fast pattern chop set: %s\n", fast_pattern_chop_set ? "yes" : "no"); + if (fast_pattern_chop_set) { + fprintf(fp, " Fast pattern offset, length: %u, %u\n", fp_cd->fp_chop_offset, + fp_cd->fp_chop_len); + } + + uint16_t patlen = fp_cd->content_len; + uint8_t *pat = SCMalloc(fp_cd->content_len + 1); + if (unlikely(pat == NULL)) { + FatalError("Error allocating memory"); + } + memcpy(pat, fp_cd->content, fp_cd->content_len); + pat[fp_cd->content_len] = '\0'; + fprintf(fp, " Original content: "); + PrintRawUriFp(fp, pat, patlen); + fprintf(fp, "\n"); + + if (fast_pattern_chop_set) { + SCFree(pat); + patlen = fp_cd->fp_chop_len; + pat = SCMalloc(fp_cd->fp_chop_len + 1); + if (unlikely(pat == NULL)) { + exit(EXIT_FAILURE); + } + memcpy(pat, fp_cd->content + fp_cd->fp_chop_offset, fp_cd->fp_chop_len); + pat[fp_cd->fp_chop_len] = '\0'; + fprintf(fp, " Final content: "); + PrintRawUriFp(fp, pat, patlen); + fprintf(fp, "\n"); + + FpPatternStatsAdd(&de_ctx->ea->fp_pattern_stats[0], list_type, patlen); + } else { + fprintf(fp, " Final content: "); + PrintRawUriFp(fp, pat, patlen); + fprintf(fp, "\n"); + + FpPatternStatsAdd(&de_ctx->ea->fp_pattern_stats[0], list_type, patlen); + } + SCFree(pat); + + fprintf(fp, "\n"); + return; +} + +/** + * \brief Sets up the fast pattern analyzer according to the config. + * + * \retval 1 If rule analyzer successfully enabled. + * \retval 0 If not enabled. + */ +static int SetupFPAnalyzer(DetectEngineCtx *de_ctx) +{ + int fp_engine_analysis_set = 0; + + if ((ConfGetBool("engine-analysis.rules-fast-pattern", + &fp_engine_analysis_set)) == 0) { + return false; + } + + if (fp_engine_analysis_set == 0) + return false; + + const char *log_dir = ConfigGetLogDirectory(); + char *log_path = SCMalloc(PATH_MAX); + if (log_path == NULL) { + FatalError("Unable to allocate scratch memory for rule filename"); + } + snprintf(log_path, PATH_MAX, "%s/%s%s", log_dir, + de_ctx->ea->file_prefix ? de_ctx->ea->file_prefix : "", "rules_fast_pattern.txt"); + + FILE *fp = fopen(log_path, "w"); + if (fp == NULL) { + SCLogError("failed to open %s: %s", log_path, strerror(errno)); + SCFree(log_path); + return false; + } + + de_ctx->ea->fp_engine_analysis_fp = fp; + + SCLogInfo("Engine-Analysis for fast_pattern printed to file - %s", + log_path); + SCFree(log_path); + + struct timeval tval; + gettimeofday(&tval, NULL); + struct tm local_tm; + struct tm *tms = SCLocalTime(tval.tv_sec, &local_tm); + fprintf(fp, "----------------------------------------------" + "---------------------\n"); + fprintf(fp, + "Date: %" PRId32 "/%" PRId32 "/%04d -- " + "%02d:%02d:%02d\n", + tms->tm_mday, tms->tm_mon + 1, tms->tm_year + 1900, tms->tm_hour, tms->tm_min, + tms->tm_sec); + fprintf(fp, "----------------------------------------------" + "---------------------\n"); + + memset(&de_ctx->ea->fp_pattern_stats[0], 0, sizeof(de_ctx->ea->fp_pattern_stats)); + return true; +} + +/** + * \brief Compiles regex for rule analysis + * \retval 1 if successful + * \retval 0 if on error + */ +static bool PerCentEncodingSetup(EngineAnalysisCtx *ea_ctx) +{ +#define DETECT_PERCENT_ENCODING_REGEX "%[0-9|a-f|A-F]{2}" + int en; + PCRE2_SIZE eo = 0; + int opts = 0; // PCRE2_NEWLINE_ANY?? + + ea_ctx->percent_re = pcre2_compile((PCRE2_SPTR8)DETECT_PERCENT_ENCODING_REGEX, + PCRE2_ZERO_TERMINATED, opts, &en, &eo, NULL); + if (ea_ctx->percent_re == NULL) { + PCRE2_UCHAR errbuffer[256]; + pcre2_get_error_message(en, errbuffer, sizeof(errbuffer)); + SCLogError("Compile of \"%s\" failed at offset %d: %s", DETECT_PERCENT_ENCODING_REGEX, + (int)eo, errbuffer); + return false; + } + + return true; +} +/** + * \brief Sets up the rule analyzer according to the config + * \retval 1 if rule analyzer successfully enabled + * \retval 0 if not enabled + */ +static int SetupRuleAnalyzer(DetectEngineCtx *de_ctx) +{ + ConfNode *conf = ConfGetNode("engine-analysis"); + int enabled = 0; + if (conf != NULL) { + const char *value = ConfNodeLookupChildValue(conf, "rules"); + if (value && ConfValIsTrue(value)) { + enabled = 1; + } else if (value && strcasecmp(value, "warnings-only") == 0) { + enabled = 1; + rule_warnings_only = 1; + } + if (enabled) { + const char *log_dir; + log_dir = ConfigGetLogDirectory(); + char log_path[PATH_MAX]; + snprintf(log_path, sizeof(log_path), "%s/%s%s", log_dir, + de_ctx->ea->file_prefix ? de_ctx->ea->file_prefix : "", "rules_analysis.txt"); + de_ctx->ea->rule_engine_analysis_fp = fopen(log_path, "w"); + if (de_ctx->ea->rule_engine_analysis_fp == NULL) { + SCLogError("failed to open %s: %s", log_path, strerror(errno)); + return 0; + } + + SCLogInfo("Engine-Analysis for rules printed to file - %s", + log_path); + + struct timeval tval; + gettimeofday(&tval, NULL); + struct tm local_tm; + struct tm *tms = SCLocalTime(tval.tv_sec, &local_tm); + fprintf(de_ctx->ea->rule_engine_analysis_fp, + "----------------------------------------------" + "---------------------\n"); + fprintf(de_ctx->ea->rule_engine_analysis_fp, + "Date: %" PRId32 "/%" PRId32 "/%04d -- " + "%02d:%02d:%02d\n", + tms->tm_mday, tms->tm_mon + 1, tms->tm_year + 1900, tms->tm_hour, tms->tm_min, + tms->tm_sec); + fprintf(de_ctx->ea->rule_engine_analysis_fp, + "----------------------------------------------" + "---------------------\n"); + + /*compile regex's for rule analysis*/ + if (!PerCentEncodingSetup(de_ctx->ea)) { + fprintf(de_ctx->ea->rule_engine_analysis_fp, + "Error compiling regex; can't check for percent encoding in normalized " + "http content.\n"); + } + } + } + else { + SCLogInfo("Conf parameter \"engine-analysis.rules\" not found. " + "Defaulting to not printing the rules analysis report."); + } + if (!enabled) { + SCLogInfo("Engine-Analysis for rules disabled in conf file."); + return 0; + } + return 1; +} + +static void CleanupFPAnalyzer(DetectEngineCtx *de_ctx) +{ + FILE *fp = de_ctx->ea->rule_engine_analysis_fp; + fprintf(fp, "============\n" + "Summary:\n============\n"); + + for (int i = 0; i < DETECT_SM_LIST_MAX; i++) { + FpPatternStats *f = &de_ctx->ea->fp_pattern_stats[i]; + if (f->cnt == 0) + continue; + + fprintf(fp, + "%s, smallest pattern %u byte(s), longest pattern %u byte(s), number of patterns " + "%u, avg pattern len %.2f byte(s)\n", + DetectSigmatchListEnumToString(i), f->min, f->max, f->cnt, + (float)((double)f->tot / (float)f->cnt)); + } + + fclose(de_ctx->ea->rule_engine_analysis_fp); + de_ctx->ea->rule_engine_analysis_fp = NULL; + + return; +} + +static void CleanupRuleAnalyzer(DetectEngineCtx *de_ctx) +{ + if (de_ctx->ea->fp_engine_analysis_fp != NULL) { + fclose(de_ctx->ea->fp_engine_analysis_fp); + de_ctx->ea->fp_engine_analysis_fp = NULL; + } + if (de_ctx->ea->percent_re != NULL) { + pcre2_code_free(de_ctx->ea->percent_re); + } +} + +void SetupEngineAnalysis(DetectEngineCtx *de_ctx, bool *fp_analysis, bool *rule_analysis) +{ + *fp_analysis = false; + *rule_analysis = false; + + EngineAnalysisCtx *ea = SCCalloc(1, sizeof(EngineAnalysisCtx)); + if (ea == NULL) { + FatalError("Unable to allocate per-engine analysis context"); + } + + ea->file_prefix = NULL; + int cfg_prefix_len = strlen(de_ctx->config_prefix); + if (cfg_prefix_len > 0) { + /* length of prefix + NULL + "." */ + ea->file_prefix = SCCalloc(1, cfg_prefix_len + 1 + 1); + if (ea->file_prefix == NULL) { + FatalError("Unable to allocate per-engine analysis context name buffer"); + } + + snprintf(ea->file_prefix, cfg_prefix_len + 1 + 1, "%s.", de_ctx->config_prefix); + } + + de_ctx->ea = ea; + + *fp_analysis = SetupFPAnalyzer(de_ctx); + *rule_analysis = SetupRuleAnalyzer(de_ctx); + + if (!(*fp_analysis || *rule_analysis)) { + if (ea->file_prefix) + SCFree(ea->file_prefix); + if (ea->analyzer_items) + SCFree(ea->analyzer_items); + SCFree(ea); + } +} + +void CleanupEngineAnalysis(DetectEngineCtx *de_ctx) +{ + if (de_ctx->ea) { + CleanupRuleAnalyzer(de_ctx); + CleanupFPAnalyzer(de_ctx); + if (de_ctx->ea->file_prefix) + SCFree(de_ctx->ea->file_prefix); + if (de_ctx->ea->analyzer_items) + SCFree(de_ctx->ea->analyzer_items); + SCFree(de_ctx->ea); + de_ctx->ea = NULL; + } +} + +/** + * \brief Checks for % encoding in content. + * \param Pointer to content + * \retval number of matches if content has % encoding + * \retval 0 if it doesn't have % encoding + * \retval -1 on error + */ +static int PerCentEncodingMatch(EngineAnalysisCtx *ea_ctx, uint8_t *content, uint16_t content_len) +{ + int ret = 0; + + pcre2_match_data *match = pcre2_match_data_create_from_pattern(ea_ctx->percent_re, NULL); + ret = pcre2_match(ea_ctx->percent_re, (PCRE2_SPTR8)content, content_len, 0, 0, match, NULL); + if (ret == -1) { + return 0; + } else if (ret < -1) { + SCLogError("Error parsing content - %s; error code is %d", content, ret); + ret = -1; + } + pcre2_match_data_free(match); + return ret; +} + +static void EngineAnalysisRulesPrintFP(const DetectEngineCtx *de_ctx, const Signature *s) +{ + const DetectContentData *fp_cd = NULL; + const SigMatch *mpm_sm = s->init_data->mpm_sm; + const int mpm_sm_list = s->init_data->mpm_sm_list; + + if (mpm_sm != NULL) { + fp_cd = (DetectContentData *)mpm_sm->ctx; + } + + if (fp_cd == NULL) { + return; + } + + uint16_t patlen = fp_cd->content_len; + uint8_t *pat = SCMalloc(fp_cd->content_len + 1); + if (unlikely(pat == NULL)) { + FatalError("Error allocating memory"); + } + + EngineAnalysisCtx *ea_ctx = de_ctx->ea; + + memcpy(pat, fp_cd->content, fp_cd->content_len); + pat[fp_cd->content_len] = '\0'; + + if (fp_cd->flags & DETECT_CONTENT_FAST_PATTERN_CHOP) { + SCFree(pat); + patlen = fp_cd->fp_chop_len; + pat = SCMalloc(fp_cd->fp_chop_len + 1); + if (unlikely(pat == NULL)) { + exit(EXIT_FAILURE); + } + memcpy(pat, fp_cd->content + fp_cd->fp_chop_offset, fp_cd->fp_chop_len); + pat[fp_cd->fp_chop_len] = '\0'; + fprintf(ea_ctx->rule_engine_analysis_fp, " Fast Pattern \""); + PrintRawUriFp(ea_ctx->rule_engine_analysis_fp, pat, patlen); + } else { + fprintf(ea_ctx->rule_engine_analysis_fp, " Fast Pattern \""); + PrintRawUriFp(ea_ctx->rule_engine_analysis_fp, pat, patlen); + } + SCFree(pat); + + fprintf(ea_ctx->rule_engine_analysis_fp, "\" on \""); + + const int list_type = mpm_sm_list; + if (list_type == DETECT_SM_LIST_PMATCH) { + int payload = 0; + int stream = 0; + if (SignatureHasPacketContent(s)) + payload = 1; + if (SignatureHasStreamContent(s)) + stream = 1; + fprintf(ea_ctx->rule_engine_analysis_fp, "%s", + payload ? (stream ? "payload and reassembled stream" : "payload") + : "reassembled stream"); + } + else { + const char *desc = DetectEngineBufferTypeGetDescriptionById(de_ctx, list_type); + const char *name = DetectEngineBufferTypeGetNameById(de_ctx, list_type); + if (desc && name) { + fprintf(ea_ctx->rule_engine_analysis_fp, "%s (%s)", desc, name); + } else if (desc || name) { + fprintf(ea_ctx->rule_engine_analysis_fp, "%s", desc ? desc : name); + } + + } + + fprintf(ea_ctx->rule_engine_analysis_fp, "\" "); + const DetectBufferType *bt = DetectEngineBufferTypeGetById(de_ctx, list_type); + if (bt && bt->transforms.cnt) { + fprintf(ea_ctx->rule_engine_analysis_fp, "(with %d transform(s)) ", bt->transforms.cnt); + } + fprintf(ea_ctx->rule_engine_analysis_fp, "buffer.\n"); + + return; +} + +void EngineAnalysisRulesFailure(const DetectEngineCtx *de_ctx, char *line, char *file, int lineno) +{ + fprintf(de_ctx->ea->fp_engine_analysis_fp, "== Sid: UNKNOWN ==\n"); + fprintf(de_ctx->ea->fp_engine_analysis_fp, "%s\n", line); + fprintf(de_ctx->ea->fp_engine_analysis_fp, " FAILURE: invalid rule.\n"); + fprintf(de_ctx->ea->fp_engine_analysis_fp, " File: %s.\n", file); + fprintf(de_ctx->ea->fp_engine_analysis_fp, " Line: %d.\n", lineno); + fprintf(de_ctx->ea->fp_engine_analysis_fp, "\n"); +} + +typedef struct RuleAnalyzer { + JsonBuilder *js; /* document root */ + + JsonBuilder *js_warnings; + JsonBuilder *js_notes; +} RuleAnalyzer; + +static void ATTR_FMT_PRINTF(2, 3) AnalyzerNote(RuleAnalyzer *ctx, char *fmt, ...) +{ + va_list ap; + char str[1024]; + + va_start(ap, fmt); + vsnprintf(str, sizeof(str), fmt, ap); + va_end(ap); + + if (!ctx->js_notes) + ctx->js_notes = jb_new_array(); + if (ctx->js_notes) + jb_append_string(ctx->js_notes, str); +} + +static void ATTR_FMT_PRINTF(2, 3) AnalyzerWarning(RuleAnalyzer *ctx, char *fmt, ...) +{ + va_list ap; + char str[1024]; + + va_start(ap, fmt); + vsnprintf(str, sizeof(str), fmt, ap); + va_end(ap); + + if (!ctx->js_warnings) + ctx->js_warnings = jb_new_array(); + if (ctx->js_warnings) + jb_append_string(ctx->js_warnings, str); +} + +#define CHECK(pat) if (strlen((pat)) <= len && memcmp((pat), buf, MIN(len, strlen((pat)))) == 0) return true; + +static bool LooksLikeHTTPMethod(const uint8_t *buf, uint16_t len) +{ + CHECK("GET /"); + CHECK("POST /"); + CHECK("HEAD /"); + CHECK("PUT /"); + return false; +} + +static bool LooksLikeHTTPUA(const uint8_t *buf, uint16_t len) +{ + CHECK("User-Agent: "); + CHECK("\nUser-Agent: "); + return false; +} + +static void DumpContent(JsonBuilder *js, const DetectContentData *cd) +{ + char pattern_str[1024] = ""; + DetectContentPatternPrettyPrint(cd, pattern_str, sizeof(pattern_str)); + + jb_set_string(js, "pattern", pattern_str); + jb_set_uint(js, "length", cd->content_len); + jb_set_bool(js, "nocase", cd->flags & DETECT_CONTENT_NOCASE); + jb_set_bool(js, "negated", cd->flags & DETECT_CONTENT_NEGATED); + jb_set_bool(js, "starts_with", cd->flags & DETECT_CONTENT_STARTS_WITH); + jb_set_bool(js, "ends_with", cd->flags & DETECT_CONTENT_ENDS_WITH); + jb_set_bool(js, "is_mpm", cd->flags & DETECT_CONTENT_MPM); + jb_set_bool(js, "no_double_inspect", cd->flags & DETECT_CONTENT_NO_DOUBLE_INSPECTION_REQUIRED); + if (cd->flags & DETECT_CONTENT_OFFSET) { + jb_set_uint(js, "offset", cd->offset); + } + if (cd->flags & DETECT_CONTENT_DEPTH) { + jb_set_uint(js, "depth", cd->depth); + } + if (cd->flags & DETECT_CONTENT_DISTANCE) { + jb_set_int(js, "distance", cd->distance); + } + if (cd->flags & DETECT_CONTENT_WITHIN) { + jb_set_int(js, "within", cd->within); + } + jb_set_bool(js, "fast_pattern", cd->flags & DETECT_CONTENT_FAST_PATTERN); + jb_set_bool(js, "relative_next", cd->flags & DETECT_CONTENT_RELATIVE_NEXT); +} + +static void DumpPcre(JsonBuilder *js, const DetectPcreData *cd) +{ + jb_set_bool(js, "relative", cd->flags & DETECT_PCRE_RELATIVE); + jb_set_bool(js, "relative_next", cd->flags & DETECT_PCRE_RELATIVE_NEXT); + jb_set_bool(js, "nocase", cd->flags & DETECT_PCRE_CASELESS); + jb_set_bool(js, "negated", cd->flags & DETECT_PCRE_NEGATE); +} + +static void DumpMatches(RuleAnalyzer *ctx, JsonBuilder *js, const SigMatchData *smd) +{ + if (smd == NULL) + return; + + jb_open_array(js, "matches"); + do { + jb_start_object(js); + const char *mname = sigmatch_table[smd->type].name; + jb_set_string(js, "name", mname); + + switch (smd->type) { + case DETECT_CONTENT: { + const DetectContentData *cd = (const DetectContentData *)smd->ctx; + + jb_open_object(js, "content"); + DumpContent(js, cd); + if (cd->flags & DETECT_CONTENT_FAST_PATTERN_ONLY) { + AnalyzerNote(ctx, (char *)"'fast_pattern:only' option is silently ignored and " + "is interpreted as regular 'fast_pattern'"); + } + if (LooksLikeHTTPMethod(cd->content, cd->content_len)) { + AnalyzerWarning(ctx, + (char *)"pattern looks like it inspects HTTP, use http.request_line or " + "http.method and http.uri instead for improved performance"); + } + if (LooksLikeHTTPUA(cd->content, cd->content_len)) { + AnalyzerWarning(ctx, + (char *)"pattern looks like it inspects HTTP, use http.user_agent " + "or http.header for improved performance"); + } + if (cd->flags & DETECT_CONTENT_WITHIN2DEPTH) { + AnalyzerNote(ctx, (char *)"'within' option for pattern w/o previous content " + "was converted to 'depth'"); + } + if (cd->flags & DETECT_CONTENT_DISTANCE2OFFSET) { + AnalyzerNote(ctx, (char *)"'distance' option for pattern w/o previous content " + "was converted to 'offset'"); + } + jb_close(js); + break; + } + case DETECT_PCRE: { + const DetectPcreData *cd = (const DetectPcreData *)smd->ctx; + + jb_open_object(js, "pcre"); + DumpPcre(js, cd); + jb_close(js); + if (cd->flags & DETECT_PCRE_RAWBYTES) { + AnalyzerNote(ctx, + (char *)"'/B' (rawbytes) option is a no-op and is silently ignored"); + } + break; + } + case DETECT_BYTEJUMP: { + const DetectBytejumpData *cd = (const DetectBytejumpData *)smd->ctx; + + jb_open_object(js, "byte_jump"); + jb_set_uint(js, "nbytes", cd->nbytes); + jb_set_int(js, "offset", cd->offset); + jb_set_uint(js, "multiplier", cd->multiplier); + jb_set_int(js, "post_offset", cd->post_offset); + switch (cd->base) { + case DETECT_BYTEJUMP_BASE_UNSET: + jb_set_string(js, "base", "unset"); + break; + case DETECT_BYTEJUMP_BASE_OCT: + jb_set_string(js, "base", "oct"); + break; + case DETECT_BYTEJUMP_BASE_DEC: + jb_set_string(js, "base", "dec"); + break; + case DETECT_BYTEJUMP_BASE_HEX: + jb_set_string(js, "base", "hex"); + break; + } + jb_open_array(js, "flags"); + if (cd->flags & DETECT_BYTEJUMP_BEGIN) + jb_append_string(js, "from_beginning"); + if (cd->flags & DETECT_BYTEJUMP_LITTLE) + jb_append_string(js, "little_endian"); + if (cd->flags & DETECT_BYTEJUMP_BIG) + jb_append_string(js, "big_endian"); + if (cd->flags & DETECT_BYTEJUMP_STRING) + jb_append_string(js, "string"); + if (cd->flags & DETECT_BYTEJUMP_RELATIVE) + jb_append_string(js, "relative"); + if (cd->flags & DETECT_BYTEJUMP_ALIGN) + jb_append_string(js, "align"); + if (cd->flags & DETECT_BYTEJUMP_DCE) + jb_append_string(js, "dce"); + if (cd->flags & DETECT_BYTEJUMP_OFFSET_BE) + jb_append_string(js, "offset_be"); + if (cd->flags & DETECT_BYTEJUMP_END) + jb_append_string(js, "from_end"); + jb_close(js); + jb_close(js); + break; + } + case DETECT_BYTETEST: { + const DetectBytetestData *cd = (const DetectBytetestData *)smd->ctx; + + jb_open_object(js, "byte_test"); + jb_set_uint(js, "nbytes", cd->nbytes); + jb_set_int(js, "offset", cd->offset); + switch (cd->base) { + case DETECT_BYTETEST_BASE_UNSET: + jb_set_string(js, "base", "unset"); + break; + case DETECT_BYTETEST_BASE_OCT: + jb_set_string(js, "base", "oct"); + break; + case DETECT_BYTETEST_BASE_DEC: + jb_set_string(js, "base", "dec"); + break; + case DETECT_BYTETEST_BASE_HEX: + jb_set_string(js, "base", "hex"); + break; + } + jb_open_array(js, "flags"); + if (cd->flags & DETECT_BYTETEST_LITTLE) + jb_append_string(js, "little_endian"); + if (cd->flags & DETECT_BYTETEST_BIG) + jb_append_string(js, "big_endian"); + if (cd->flags & DETECT_BYTETEST_STRING) + jb_append_string(js, "string"); + if (cd->flags & DETECT_BYTETEST_RELATIVE) + jb_append_string(js, "relative"); + if (cd->flags & DETECT_BYTETEST_DCE) + jb_append_string(js, "dce"); + jb_close(js); + jb_close(js); + break; + } + case DETECT_IPOPTS: { + const DetectIpOptsData *cd = (const DetectIpOptsData *)smd->ctx; + + jb_open_object(js, "ipopts"); + const char *flag = IpOptsFlagToString(cd->ipopt); + jb_set_string(js, "option", flag); + jb_close(js); + break; + } + } + jb_close(js); + + if (smd->is_last) + break; + smd++; + } while (1); + jb_close(js); +} + +SCMutex g_rules_analyzer_write_m = SCMUTEX_INITIALIZER; +void EngineAnalysisRules2(const DetectEngineCtx *de_ctx, const Signature *s) +{ + SCEnter(); + + RuleAnalyzer ctx = { NULL, NULL, NULL }; + + ctx.js = jb_new_object(); + if (ctx.js == NULL) + SCReturn; + + jb_set_string(ctx.js, "raw", s->sig_str); + jb_set_uint(ctx.js, "id", s->id); + jb_set_uint(ctx.js, "gid", s->gid); + jb_set_uint(ctx.js, "rev", s->rev); + jb_set_string(ctx.js, "msg", s->msg); + + const char *alproto = AppProtoToString(s->alproto); + jb_set_string(ctx.js, "app_proto", alproto); + + jb_open_array(ctx.js, "requirements"); + if (s->mask & SIG_MASK_REQUIRE_PAYLOAD) { + jb_append_string(ctx.js, "payload"); + } + if (s->mask & SIG_MASK_REQUIRE_NO_PAYLOAD) { + jb_append_string(ctx.js, "no_payload"); + } + if (s->mask & SIG_MASK_REQUIRE_FLOW) { + jb_append_string(ctx.js, "flow"); + } + if (s->mask & SIG_MASK_REQUIRE_FLAGS_INITDEINIT) { + jb_append_string(ctx.js, "tcp_flags_init_deinit"); + } + if (s->mask & SIG_MASK_REQUIRE_FLAGS_UNUSUAL) { + jb_append_string(ctx.js, "tcp_flags_unusual"); + } + if (s->mask & SIG_MASK_REQUIRE_DCERPC) { + jb_append_string(ctx.js, "dcerpc"); + } + if (s->mask & SIG_MASK_REQUIRE_ENGINE_EVENT) { + jb_append_string(ctx.js, "engine_event"); + } + jb_close(ctx.js); + + switch (s->type) { + case SIG_TYPE_NOT_SET: + jb_set_string(ctx.js, "type", "unset"); + break; + case SIG_TYPE_IPONLY: + jb_set_string(ctx.js, "type", "ip_only"); + break; + case SIG_TYPE_LIKE_IPONLY: + jb_set_string(ctx.js, "type", "like_ip_only"); + break; + case SIG_TYPE_PDONLY: + jb_set_string(ctx.js, "type", "pd_only"); + break; + case SIG_TYPE_DEONLY: + jb_set_string(ctx.js, "type", "de_only"); + break; + case SIG_TYPE_PKT: + jb_set_string(ctx.js, "type", "pkt"); + break; + case SIG_TYPE_PKT_STREAM: + jb_set_string(ctx.js, "type", "pkt_stream"); + break; + case SIG_TYPE_STREAM: + jb_set_string(ctx.js, "type", "stream"); + break; + case SIG_TYPE_APPLAYER: + jb_set_string(ctx.js, "type", "app_layer"); + break; + case SIG_TYPE_APP_TX: + jb_set_string(ctx.js, "type", "app_tx"); + break; + case SIG_TYPE_MAX: + jb_set_string(ctx.js, "type", "error"); + break; + } + + jb_open_array(ctx.js, "flags"); + if (s->flags & SIG_FLAG_SRC_ANY) { + jb_append_string(ctx.js, "src_any"); + } + if (s->flags & SIG_FLAG_DST_ANY) { + jb_append_string(ctx.js, "dst_any"); + } + if (s->flags & SIG_FLAG_SP_ANY) { + jb_append_string(ctx.js, "sp_any"); + } + if (s->flags & SIG_FLAG_DP_ANY) { + jb_append_string(ctx.js, "dp_any"); + } + if (s->flags & SIG_FLAG_NOALERT) { + jb_append_string(ctx.js, "noalert"); + } + if (s->flags & SIG_FLAG_DSIZE) { + jb_append_string(ctx.js, "dsize"); + } + if (s->flags & SIG_FLAG_APPLAYER) { + jb_append_string(ctx.js, "applayer"); + } + if (s->flags & SIG_FLAG_REQUIRE_PACKET) { + jb_append_string(ctx.js, "need_packet"); + } + if (s->flags & SIG_FLAG_REQUIRE_STREAM) { + jb_append_string(ctx.js, "need_stream"); + } + if (s->flags & SIG_FLAG_MPM_NEG) { + jb_append_string(ctx.js, "negated_mpm"); + } + if (s->flags & SIG_FLAG_FLUSH) { + jb_append_string(ctx.js, "flush"); + } + if (s->flags & SIG_FLAG_REQUIRE_FLOWVAR) { + jb_append_string(ctx.js, "need_flowvar"); + } + if (s->flags & SIG_FLAG_FILESTORE) { + jb_append_string(ctx.js, "filestore"); + } + if (s->flags & SIG_FLAG_TOSERVER) { + jb_append_string(ctx.js, "toserver"); + } + if (s->flags & SIG_FLAG_TOCLIENT) { + jb_append_string(ctx.js, "toclient"); + } + if (s->flags & SIG_FLAG_TLSSTORE) { + jb_append_string(ctx.js, "tlsstore"); + } + if (s->flags & SIG_FLAG_BYPASS) { + jb_append_string(ctx.js, "bypass"); + } + if (s->flags & SIG_FLAG_PREFILTER) { + jb_append_string(ctx.js, "prefilter"); + } + if (s->flags & SIG_FLAG_SRC_IS_TARGET) { + jb_append_string(ctx.js, "src_is_target"); + } + if (s->flags & SIG_FLAG_DEST_IS_TARGET) { + jb_append_string(ctx.js, "dst_is_target"); + } + jb_close(ctx.js); + + const DetectEnginePktInspectionEngine *pkt_mpm = NULL; + const DetectEngineAppInspectionEngine *app_mpm = NULL; + + jb_open_array(ctx.js, "pkt_engines"); + const DetectEnginePktInspectionEngine *pkt = s->pkt_inspect; + for ( ; pkt != NULL; pkt = pkt->next) { + const char *name = DetectEngineBufferTypeGetNameById(de_ctx, pkt->sm_list); + if (name == NULL) { + switch (pkt->sm_list) { + case DETECT_SM_LIST_PMATCH: + name = "payload"; + break; + case DETECT_SM_LIST_MATCH: + name = "packet"; + break; + default: + name = "unknown"; + break; + } + } + jb_start_object(ctx.js); + jb_set_string(ctx.js, "name", name); + jb_set_bool(ctx.js, "is_mpm", pkt->mpm); + if (pkt->v1.transforms != NULL) { + jb_open_array(ctx.js, "transforms"); + for (int t = 0; t < pkt->v1.transforms->cnt; t++) { + jb_start_object(ctx.js); + jb_set_string(ctx.js, "name", + sigmatch_table[pkt->v1.transforms->transforms[t].transform].name); + jb_close(ctx.js); + } + jb_close(ctx.js); + } + DumpMatches(&ctx, ctx.js, pkt->smd); + jb_close(ctx.js); + if (pkt->mpm) { + pkt_mpm = pkt; + } + } + jb_close(ctx.js); + jb_open_array(ctx.js, "frame_engines"); + const DetectEngineFrameInspectionEngine *frame = s->frame_inspect; + for (; frame != NULL; frame = frame->next) { + const char *name = DetectEngineBufferTypeGetNameById(de_ctx, frame->sm_list); + jb_start_object(ctx.js); + jb_set_string(ctx.js, "name", name); + jb_set_bool(ctx.js, "is_mpm", frame->mpm); + if (frame->v1.transforms != NULL) { + jb_open_array(ctx.js, "transforms"); + for (int t = 0; t < frame->v1.transforms->cnt; t++) { + jb_start_object(ctx.js); + jb_set_string(ctx.js, "name", + sigmatch_table[frame->v1.transforms->transforms[t].transform].name); + jb_close(ctx.js); + } + jb_close(ctx.js); + } + DumpMatches(&ctx, ctx.js, frame->smd); + jb_close(ctx.js); + } + jb_close(ctx.js); + + if (s->init_data->init_flags & SIG_FLAG_INIT_STATE_MATCH) { + bool has_stream = false; + bool has_client_body_mpm = false; + bool has_file_data_mpm = false; + + jb_open_array(ctx.js, "engines"); + const DetectEngineAppInspectionEngine *app = s->app_inspect; + for ( ; app != NULL; app = app->next) { + const char *name = DetectEngineBufferTypeGetNameById(de_ctx, app->sm_list); + if (name == NULL) { + switch (app->sm_list) { + case DETECT_SM_LIST_PMATCH: + name = "stream"; + break; + default: + name = "unknown"; + break; + } + } + + if (app->sm_list == DETECT_SM_LIST_PMATCH && !app->mpm) { + has_stream = true; + } else if (app->mpm && strcmp(name, "http_client_body") == 0) { + has_client_body_mpm = true; + } else if (app->mpm && strcmp(name, "file_data") == 0) { + has_file_data_mpm = true; + } + + jb_start_object(ctx.js); + jb_set_string(ctx.js, "name", name); + const char *direction = app->dir == 0 ? "toserver" : "toclient"; + jb_set_string(ctx.js, "direction", direction); + jb_set_bool(ctx.js, "is_mpm", app->mpm); + jb_set_string(ctx.js, "app_proto", AppProtoToString(app->alproto)); + jb_set_uint(ctx.js, "progress", app->progress); + + if (app->v2.transforms != NULL) { + jb_open_array(ctx.js, "transforms"); + for (int t = 0; t < app->v2.transforms->cnt; t++) { + jb_start_object(ctx.js); + jb_set_string(ctx.js, "name", + sigmatch_table[app->v2.transforms->transforms[t].transform].name); + jb_close(ctx.js); + } + jb_close(ctx.js); + } + DumpMatches(&ctx, ctx.js, app->smd); + jb_close(ctx.js); + if (app->mpm) { + app_mpm = app; + } + } + jb_close(ctx.js); + + if (has_stream && has_client_body_mpm) + AnalyzerNote(&ctx, (char *)"mpm in http_client_body combined with stream match leads to stream buffering"); + if (has_stream && has_file_data_mpm) + AnalyzerNote(&ctx, (char *)"mpm in file_data combined with stream match leads to stream buffering"); + } + + jb_open_object(ctx.js, "lists"); + for (int i = 0; i < DETECT_SM_LIST_MAX; i++) { + if (s->sm_arrays[i] != NULL) { + jb_open_object(ctx.js, DetectListToHumanString(i)); + DumpMatches(&ctx, ctx.js, s->sm_arrays[i]); + jb_close(ctx.js); + } + } + jb_close(ctx.js); + + if (pkt_mpm || app_mpm) { + jb_open_object(ctx.js, "mpm"); + + int mpm_list = pkt_mpm ? DETECT_SM_LIST_PMATCH : app_mpm->sm_list; + const char *name; + if (mpm_list < DETECT_SM_LIST_DYNAMIC_START) + name = DetectListToHumanString(mpm_list); + else + name = DetectEngineBufferTypeGetNameById(de_ctx, mpm_list); + jb_set_string(ctx.js, "buffer", name); + + SigMatchData *smd = pkt_mpm ? pkt_mpm->smd : app_mpm->smd; + if (smd == NULL && mpm_list == DETECT_SM_LIST_PMATCH) { + smd = s->sm_arrays[mpm_list]; + } + do { + switch (smd->type) { + case DETECT_CONTENT: { + const DetectContentData *cd = (const DetectContentData *)smd->ctx; + if (cd->flags & DETECT_CONTENT_MPM) { + DumpContent(ctx.js, cd); + } + break; + } + } + + if (smd->is_last) + break; + smd++; + } while (1); + jb_close(ctx.js); + } else if (s->init_data->prefilter_sm) { + jb_open_object(ctx.js, "prefilter"); + int prefilter_list = SigMatchListSMBelongsTo(s, s->init_data->prefilter_sm); + const char *name; + if (prefilter_list < DETECT_SM_LIST_DYNAMIC_START) + name = DetectListToHumanString(prefilter_list); + else + name = DetectEngineBufferTypeGetNameById(de_ctx, prefilter_list); + jb_set_string(ctx.js, "buffer", name); + const char *mname = sigmatch_table[s->init_data->prefilter_sm->type].name; + jb_set_string(ctx.js, "name", mname); + jb_close(ctx.js); + } + + if (ctx.js_warnings) { + jb_close(ctx.js_warnings); + jb_set_object(ctx.js, "warnings", ctx.js_warnings); + jb_free(ctx.js_warnings); + ctx.js_warnings = NULL; + } + if (ctx.js_notes) { + jb_close(ctx.js_notes); + jb_set_object(ctx.js, "notes", ctx.js_notes); + jb_free(ctx.js_notes); + ctx.js_notes = NULL; + } + jb_close(ctx.js); + + const char *filename = "rules.json"; + const char *log_dir = ConfigGetLogDirectory(); + char json_path[PATH_MAX] = ""; + snprintf(json_path, sizeof(json_path), "%s/%s%s", log_dir, + de_ctx->ea->file_prefix ? de_ctx->ea->file_prefix : "", filename); + + SCMutexLock(&g_rules_analyzer_write_m); + FILE *fp = fopen(json_path, "a"); + if (fp != NULL) { + fwrite(jb_ptr(ctx.js), jb_len(ctx.js), 1, fp); + fprintf(fp, "\n"); + fclose(fp); + } + SCMutexUnlock(&g_rules_analyzer_write_m); + jb_free(ctx.js); + SCReturn; +} + +void DumpPatterns(DetectEngineCtx *de_ctx) +{ + if (de_ctx->pattern_hash_table == NULL) + return; + + JsonBuilder *root_jb = jb_new_object(); + JsonBuilder *arrays[de_ctx->buffer_type_id]; + memset(&arrays, 0, sizeof(JsonBuilder *) * de_ctx->buffer_type_id); + + jb_open_array(root_jb, "buffers"); + + for (HashListTableBucket *htb = HashListTableGetListHead(de_ctx->pattern_hash_table); + htb != NULL; htb = HashListTableGetListNext(htb)) { + char str[1024] = ""; + DetectPatternTracker *p = HashListTableGetListData(htb); + DetectContentPatternPrettyPrint(p->cd, str, sizeof(str)); + + JsonBuilder *jb = arrays[p->sm_list]; + if (arrays[p->sm_list] == NULL) { + jb = arrays[p->sm_list] = jb_new_object(); + const char *name; + if (p->sm_list < DETECT_SM_LIST_DYNAMIC_START) + name = DetectListToHumanString(p->sm_list); + else + name = DetectEngineBufferTypeGetNameById(de_ctx, p->sm_list); + jb_set_string(jb, "name", name); + jb_set_uint(jb, "list_id", p->sm_list); + + jb_open_array(jb, "patterns"); + } + + jb_start_object(jb); + jb_set_string(jb, "pattern", str); + jb_set_uint(jb, "patlen", p->cd->content_len); + jb_set_uint(jb, "cnt", p->cnt); + jb_set_uint(jb, "mpm", p->mpm); + jb_open_object(jb, "flags"); + jb_set_bool(jb, "nocase", p->cd->flags & DETECT_CONTENT_NOCASE); + jb_set_bool(jb, "negated", p->cd->flags & DETECT_CONTENT_NEGATED); + jb_set_bool(jb, "depth", p->cd->flags & DETECT_CONTENT_DEPTH); + jb_set_bool(jb, "offset", p->cd->flags & DETECT_CONTENT_OFFSET); + jb_set_bool(jb, "endswith", p->cd->flags & DETECT_CONTENT_ENDS_WITH); + jb_close(jb); + jb_close(jb); + } + + for (uint32_t i = 0; i < de_ctx->buffer_type_id; i++) { + JsonBuilder *jb = arrays[i]; + if (jb == NULL) + continue; + + jb_close(jb); // array + jb_close(jb); // object + + jb_append_object(root_jb, jb); + jb_free(jb); + } + jb_close(root_jb); + jb_close(root_jb); + + const char *filename = "patterns.json"; + const char *log_dir = ConfigGetLogDirectory(); + char json_path[PATH_MAX] = ""; + snprintf(json_path, sizeof(json_path), "%s/%s%s", log_dir, + de_ctx->ea->file_prefix ? de_ctx->ea->file_prefix : "", filename); + + SCMutexLock(&g_rules_analyzer_write_m); + FILE *fp = fopen(json_path, "a"); + if (fp != NULL) { + fwrite(jb_ptr(root_jb), jb_len(root_jb), 1, fp); + fprintf(fp, "\n"); + fclose(fp); + } + SCMutexUnlock(&g_rules_analyzer_write_m); + jb_free(root_jb); + + HashListTableFree(de_ctx->pattern_hash_table); + de_ctx->pattern_hash_table = NULL; +} + +static void EngineAnalysisItemsReset(EngineAnalysisCtx *ea_ctx) +{ + for (size_t i = 0; i < ARRAY_SIZE(analyzer_items); i++) { + ea_ctx->analyzer_items[i].item_seen = false; + } +} + +static void EngineAnalysisItemsInit(EngineAnalysisCtx *ea_ctx) +{ + if (ea_ctx->analyzer_initialized) { + EngineAnalysisItemsReset(ea_ctx); + return; + } + + ea_ctx->exposed_item_seen_list[0].bufname = "http_method"; + ea_ctx->exposed_item_seen_list[1].bufname = "file_data"; + ea_ctx->analyzer_items = SCCalloc(1, sizeof(analyzer_items)); + if (!ea_ctx->analyzer_items) { + FatalError("Unable to allocate analysis scratch pad"); + } + memset(ea_ctx->analyzer_item_map, -1, sizeof(ea_ctx->analyzer_item_map)); + + for (size_t i = 0; i < ARRAY_SIZE(analyzer_items); i++) { + ea_ctx->analyzer_items[i] = analyzer_items[i]; + DetectEngineAnalyzerItems *analyzer_item = &ea_ctx->analyzer_items[i]; + + int item_id = DetectBufferTypeGetByName(analyzer_item->item_name); + DEBUG_VALIDATE_BUG_ON(item_id < 0 || item_id > UINT16_MAX); + analyzer_item->item_id = (uint16_t)item_id; + if (analyzer_item->item_id == -1) { + /* Mismatch between the analyzer_items array and what's supported */ + FatalError("unable to initialize engine-analysis table: detect buffer \"%s\" not " + "recognized.", + analyzer_item->item_name); + } + analyzer_item->item_seen = false; + + if (analyzer_item->export_item_seen) { + for (size_t k = 0; k < ARRAY_SIZE(ea_ctx->exposed_item_seen_list); k++) { + if (0 == + strcmp(ea_ctx->exposed_item_seen_list[k].bufname, analyzer_item->item_name)) + ea_ctx->exposed_item_seen_list[k].item_seen_ptr = &analyzer_item->item_seen; + } + } + ea_ctx->analyzer_item_map[analyzer_item->item_id] = (int16_t)i; + } + + ea_ctx->analyzer_initialized = true; +} + +/** + * \brief Prints analysis of loaded rules. + * + * Warns if potential rule issues are detected. For example, + * warns if a rule uses a construct that may perform poorly, + * e.g. pcre without content or with http_method content only; + * warns if a rule uses a construct that may not be consistent with intent, + * e.g. client side ports only, http and content without any http_* modifiers, etc. + * + * \param s Pointer to the signature. + */ +void EngineAnalysisRules(const DetectEngineCtx *de_ctx, + const Signature *s, const char *line) +{ + uint32_t rule_bidirectional = 0; + uint32_t rule_pcre = 0; + uint32_t rule_pcre_http = 0; + uint32_t rule_content = 0; + uint32_t rule_flow = 0; + uint32_t rule_flags = 0; + uint32_t rule_flow_toserver = 0; + uint32_t rule_flow_toclient = 0; + uint32_t rule_flow_nostream = 0; + uint32_t rule_ipv4_only = 0; + uint32_t rule_ipv6_only = 0; + uint32_t rule_flowbits = 0; + uint32_t rule_flowint = 0; + uint32_t rule_content_http = 0; + uint32_t rule_content_offset_depth = 0; + int32_t list_id = 0; + uint32_t rule_warning = 0; + uint32_t stream_buf = 0; + uint32_t packet_buf = 0; + uint32_t file_store = 0; + uint32_t warn_pcre_no_content = 0; + uint32_t warn_pcre_http_content = 0; + uint32_t warn_pcre_http = 0; + uint32_t warn_content_http_content = 0; + uint32_t warn_content_http = 0; + uint32_t warn_tcp_no_flow = 0; + uint32_t warn_client_ports = 0; + uint32_t warn_direction = 0; + uint32_t warn_method_toclient = 0; + uint32_t warn_method_serverbody = 0; + uint32_t warn_pcre_method = 0; + uint32_t warn_encoding_norm_http_buf = 0; + uint32_t warn_file_store_not_present = 0; + uint32_t warn_offset_depth_pkt_stream = 0; + uint32_t warn_offset_depth_alproto = 0; + uint32_t warn_non_alproto_fp_for_alproto_sig = 0; + uint32_t warn_no_direction = 0; + uint32_t warn_both_direction = 0; + + EngineAnalysisItemsInit(de_ctx->ea); + + bool *http_method_item_seen_ptr = de_ctx->ea->exposed_item_seen_list[0].item_seen_ptr; + bool *http_server_body_item_seen_ptr = de_ctx->ea->exposed_item_seen_list[1].item_seen_ptr; + + if (s->init_data->init_flags & SIG_FLAG_INIT_BIDIREC) { + rule_bidirectional = 1; + } + + if (s->flags & SIG_FLAG_REQUIRE_PACKET) { + packet_buf += 1; + } + if (s->flags & SIG_FLAG_FILESTORE) { + file_store += 1; + } + if (s->flags & SIG_FLAG_REQUIRE_STREAM) { + stream_buf += 1; + } + + if (s->proto.flags & DETECT_PROTO_IPV4) { + rule_ipv4_only += 1; + } + if (s->proto.flags & DETECT_PROTO_IPV6) { + rule_ipv6_only += 1; + } + + for (list_id = 0; list_id < DETECT_SM_LIST_MAX; list_id++) { + SigMatch *sm = NULL; + for (sm = s->init_data->smlists[list_id]; sm != NULL; sm = sm->next) { + int16_t item_slot = de_ctx->ea->analyzer_item_map[list_id]; + if (sm->type == DETECT_PCRE) { + if (item_slot == -1) { + rule_pcre++; + continue; + } + + rule_pcre_http++; + de_ctx->ea->analyzer_items[item_slot].item_seen = true; + } else if (sm->type == DETECT_CONTENT) { + if (item_slot == -1) { + rule_content++; + if (list_id == DETECT_SM_LIST_PMATCH) { + DetectContentData *cd = (DetectContentData *)sm->ctx; + if (cd->flags & (DETECT_CONTENT_OFFSET | DETECT_CONTENT_DEPTH)) { + rule_content_offset_depth++; + } + } + continue; + } + + rule_content_http++; + de_ctx->ea->analyzer_items[item_slot].item_seen = true; + + if (de_ctx->ea->analyzer_items[item_slot].check_encoding_match) { + DetectContentData *cd = (DetectContentData *)sm->ctx; + if (cd != NULL && + PerCentEncodingMatch(de_ctx->ea, cd->content, cd->content_len) > 0) { + warn_encoding_norm_http_buf += 1; + } + } + } + else if (sm->type == DETECT_FLOW) { + rule_flow += 1; + if ((s->flags & SIG_FLAG_TOSERVER) && !(s->flags & SIG_FLAG_TOCLIENT)) { + rule_flow_toserver = 1; + } + else if ((s->flags & SIG_FLAG_TOCLIENT) && !(s->flags & SIG_FLAG_TOSERVER)) { + rule_flow_toclient = 1; + } + DetectFlowData *fd = (DetectFlowData *)sm->ctx; + if (fd != NULL) { + if (fd->flags & DETECT_FLOW_FLAG_NOSTREAM) + rule_flow_nostream = 1; + } + } + else if (sm->type == DETECT_FLOWBITS) { + if (list_id == DETECT_SM_LIST_MATCH) { + rule_flowbits += 1; + } + } + else if (sm->type == DETECT_FLOWINT) { + if (list_id == DETECT_SM_LIST_MATCH) { + rule_flowint += 1; + } + } + else if (sm->type == DETECT_FLAGS) { + DetectFlagsData *fd = (DetectFlagsData *)sm->ctx; + if (fd != NULL) { + rule_flags = 1; + } + } + } /* for (sm = s->init_data->smlists[list_id]; sm != NULL; sm = sm->next) */ + + } /* for ( ; list_id < DETECT_SM_LIST_MAX; list_id++) */ + + if (file_store && !RequiresFeature("output::file-store")) { + rule_warning += 1; + warn_file_store_not_present = 1; + } + + if (rule_pcre > 0 && rule_content == 0 && rule_content_http == 0) { + rule_warning += 1; + warn_pcre_no_content = 1; + } + + if (rule_content_http > 0 && rule_pcre > 0 && rule_pcre_http == 0) { + rule_warning += 1; + warn_pcre_http_content = 1; + } else if (s->alproto == ALPROTO_HTTP1 && rule_pcre > 0 && rule_pcre_http == 0) { + rule_warning += 1; + warn_pcre_http = 1; + } + + if (rule_content > 0 && rule_content_http > 0) { + rule_warning += 1; + warn_content_http_content = 1; + } + if (s->alproto == ALPROTO_HTTP1 && rule_content > 0 && rule_content_http == 0) { + rule_warning += 1; + warn_content_http = 1; + } + if (rule_content == 1) { + //todo: warning if content is weak, separate warning for pcre + weak content + } + if (rule_flow == 0 && rule_flags == 0 && !(s->proto.flags & DETECT_PROTO_ANY) && + DetectProtoContainsProto(&s->proto, IPPROTO_TCP) && + (rule_content || rule_content_http || rule_pcre || rule_pcre_http || rule_flowbits || + rule_flowint)) { + rule_warning += 1; + warn_tcp_no_flow = 1; + } + if (rule_flow && !rule_bidirectional && (rule_flow_toserver || rule_flow_toclient) + && !((s->flags & SIG_FLAG_SP_ANY) && (s->flags & SIG_FLAG_DP_ANY))) { + if (((s->flags & SIG_FLAG_TOSERVER) && !(s->flags & SIG_FLAG_SP_ANY) && (s->flags & SIG_FLAG_DP_ANY)) + || ((s->flags & SIG_FLAG_TOCLIENT) && !(s->flags & SIG_FLAG_DP_ANY) && (s->flags & SIG_FLAG_SP_ANY))) { + rule_warning += 1; + warn_client_ports = 1; + } + } + if (rule_flow && rule_bidirectional && (rule_flow_toserver || rule_flow_toclient)) { + rule_warning += 1; + warn_direction = 1; + } + + if (*http_method_item_seen_ptr) { + if (rule_flow && rule_flow_toclient) { + rule_warning += 1; + warn_method_toclient = 1; + } + if (*http_server_body_item_seen_ptr) { + rule_warning += 1; + warn_method_serverbody = 1; + } + if (rule_content == 0 && rule_content_http == 0 && (rule_pcre > 0 || rule_pcre_http > 0)) { + rule_warning += 1; + warn_pcre_method = 1; + } + } + if (rule_content_offset_depth > 0 && stream_buf && packet_buf) { + rule_warning += 1; + warn_offset_depth_pkt_stream = 1; + } + if (rule_content_offset_depth > 0 && !stream_buf && packet_buf && s->alproto != ALPROTO_UNKNOWN) { + rule_warning += 1; + warn_offset_depth_alproto = 1; + } + if (s->init_data->mpm_sm != NULL && s->alproto == ALPROTO_HTTP1 && + s->init_data->mpm_sm_list == DETECT_SM_LIST_PMATCH) { + rule_warning += 1; + warn_non_alproto_fp_for_alproto_sig = 1; + } + + if ((s->flags & (SIG_FLAG_TOSERVER|SIG_FLAG_TOCLIENT)) == 0) { + warn_no_direction += 1; + rule_warning += 1; + } + + /* No warning about direction for ICMP protos */ + if (!(DetectProtoContainsProto(&s->proto, IPPROTO_ICMPV6) && DetectProtoContainsProto(&s->proto, IPPROTO_ICMP))) { + if ((s->flags & (SIG_FLAG_TOSERVER|SIG_FLAG_TOCLIENT)) == (SIG_FLAG_TOSERVER|SIG_FLAG_TOCLIENT)) { + warn_both_direction += 1; + rule_warning += 1; + } + } + + if (!rule_warnings_only || (rule_warnings_only && rule_warning > 0)) { + FILE *fp = de_ctx->ea->rule_engine_analysis_fp; + fprintf(fp, "== Sid: %u ==\n", s->id); + fprintf(fp, "%s\n", line); + + switch (s->type) { + case SIG_TYPE_NOT_SET: + break; + case SIG_TYPE_IPONLY: + fprintf(fp, " Rule is ip only.\n"); + break; + case SIG_TYPE_LIKE_IPONLY: + fprintf(fp, " Rule is like ip only.\n"); + break; + case SIG_TYPE_PDONLY: + fprintf(fp, " Rule is PD only.\n"); + break; + case SIG_TYPE_DEONLY: + fprintf(fp, " Rule is DE only.\n"); + break; + case SIG_TYPE_PKT: + fprintf(fp, " Rule is packet inspecting.\n"); + break; + case SIG_TYPE_PKT_STREAM: + fprintf(fp, " Rule is packet and stream inspecting.\n"); + break; + case SIG_TYPE_STREAM: + fprintf(fp, " Rule is stream inspecting.\n"); + break; + case SIG_TYPE_APPLAYER: + fprintf(fp, " Rule is app-layer inspecting.\n"); + break; + case SIG_TYPE_APP_TX: + fprintf(fp, " Rule is App-layer TX inspecting.\n"); + break; + case SIG_TYPE_MAX: + break; + } + if (rule_ipv6_only) + fprintf(fp, " Rule is IPv6 only.\n"); + if (rule_ipv4_only) + fprintf(fp, " Rule is IPv4 only.\n"); + if (packet_buf) + fprintf(fp, " Rule matches on packets.\n"); + if (!rule_flow_nostream && stream_buf && + (rule_flow || rule_flowbits || rule_flowint || rule_content || rule_pcre)) { + fprintf(fp, " Rule matches on reassembled stream.\n"); + } + for(size_t i = 0; i < ARRAY_SIZE(analyzer_items); i++) { + DetectEngineAnalyzerItems *ai = &de_ctx->ea->analyzer_items[i]; + if (ai->item_seen) { + fprintf(fp, " Rule matches on %s buffer.\n", ai->display_name); + } + } + if (s->alproto != ALPROTO_UNKNOWN) { + fprintf(fp, " App layer protocol is %s.\n", AppProtoToString(s->alproto)); + } + if (rule_content || rule_content_http || rule_pcre || rule_pcre_http) { + fprintf(fp, + " Rule contains %u content options, %u http content options, %u pcre " + "options, and %u pcre options with http modifiers.\n", + rule_content, rule_content_http, rule_pcre, rule_pcre_http); + } + + /* print fast pattern info */ + if (s->init_data->prefilter_sm) { + fprintf(fp, " Prefilter on: %s.\n", + sigmatch_table[s->init_data->prefilter_sm->type].name); + } else { + EngineAnalysisRulesPrintFP(de_ctx, s); + } + + /* this is where the warnings start */ + if (warn_pcre_no_content /*rule_pcre > 0 && rule_content == 0 && rule_content_http == 0*/) { + fprintf(fp, " Warning: Rule uses pcre without a content option present.\n" + " -Consider adding a content to improve performance of this " + "rule.\n"); + } + if (warn_pcre_http_content /*rule_content_http > 0 && rule_pcre > 0 && rule_pcre_http == 0*/) { + fprintf(fp, " Warning: Rule uses content options with http_* and pcre options " + "without http modifiers.\n" + " -Consider adding http pcre modifier.\n"); + } + else if (warn_pcre_http /*s->alproto == ALPROTO_HTTP1 && rule_pcre > 0 && rule_pcre_http == 0*/) { + fprintf(fp, " Warning: Rule app layer protocol is http, but pcre options do not " + "have http modifiers.\n" + " -Consider adding http pcre modifiers.\n"); + } + if (warn_content_http_content /*rule_content > 0 && rule_content_http > 0*/) { + fprintf(fp, + " Warning: Rule contains content with http_* and content without http_*.\n" + " -Consider adding http content modifiers.\n"); + } + if (warn_content_http /*s->alproto == ALPROTO_HTTP1 && rule_content > 0 && rule_content_http == 0*/) { + fprintf(fp, " Warning: Rule app layer protocol is http, but content options do not " + "have http_* modifiers.\n" + " -Consider adding http content modifiers.\n"); + } + if (rule_content == 1) { + //todo: warning if content is weak, separate warning for pcre + weak content + } + if (warn_encoding_norm_http_buf) { + fprintf(fp, " Warning: Rule may contain percent encoded content for a normalized " + "http buffer match.\n"); + } + if (warn_tcp_no_flow /*rule_flow == 0 && rule_flags == 0 + && !(s->proto.flags & DETECT_PROTO_ANY) && DetectProtoContainsProto(&s->proto, IPPROTO_TCP)*/) { + fprintf(fp, " Warning: TCP rule without a flow or flags option.\n" + " -Consider adding flow or flags to improve performance of " + "this rule.\n"); + } + if (warn_client_ports /*rule_flow && !rule_bidirectional && (rule_flow_toserver || rule_flow_toclient) + && !((s->flags & SIG_FLAG_SP_ANY) && (s->flags & SIG_FLAG_DP_ANY))) + if (((s->flags & SIG_FLAG_TOSERVER) && !(s->flags & SIG_FLAG_SP_ANY) && (s->flags & SIG_FLAG_DP_ANY)) + || ((s->flags & SIG_FLAG_TOCLIENT) && !(s->flags & SIG_FLAG_DP_ANY) && (s->flags & SIG_FLAG_SP_ANY))*/) { + fprintf(fp, + " Warning: Rule contains ports or port variables only on the client side.\n" + " -Flow direction possibly inconsistent with rule.\n"); + } + if (warn_direction /*rule_flow && rule_bidirectional && (rule_flow_toserver || rule_flow_toclient)*/) { + fprintf(fp, " Warning: Rule is bidirectional and has a flow option with a specific " + "direction.\n"); + } + if (warn_method_toclient /*http_method_buf && rule_flow && rule_flow_toclient*/) { + fprintf(fp, " Warning: Rule uses content or pcre for http_method with " + "flow:to_client or from_server\n"); + } + if (warn_method_serverbody /*http_method_buf && http_server_body_buf*/) { + fprintf(fp, " Warning: Rule uses content or pcre for http_method with content or " + "pcre for http_server_body.\n"); + } + if (warn_pcre_method /*http_method_buf && rule_content == 0 && rule_content_http == 0 + && (rule_pcre > 0 || rule_pcre_http > 0)*/) { + fprintf(fp, " Warning: Rule uses pcre with only a http_method content; possible " + "performance issue.\n"); + } + if (warn_offset_depth_pkt_stream) { + fprintf(fp, " Warning: Rule has depth" + "/offset with raw content keywords. Please note the " + "offset/depth will be checked against both packet " + "payloads and stream. If you meant to have the offset/" + "depth checked against just the payload, you can update " + "the signature as \"alert tcp-pkt...\"\n"); + } + if (warn_offset_depth_alproto) { + fprintf(fp, + " Warning: Rule has " + "offset/depth set along with a match on a specific " + "app layer protocol - %d. This can lead to FNs if we " + "have a offset/depth content match on a packet payload " + "before we can detect the app layer protocol for the " + "flow.\n", + s->alproto); + } + if (warn_non_alproto_fp_for_alproto_sig) { + fprintf(fp, " Warning: Rule app layer " + "protocol is http, but the fast_pattern is set on the raw " + "stream. Consider adding fast_pattern over a http " + "buffer for increased performance."); + } + if (warn_no_direction) { + fprintf(fp, " Warning: Rule has no direction indicator.\n"); + } + if (warn_both_direction) { + fprintf(fp, " Warning: Rule is inspecting both the request and the response.\n"); + } + if (warn_file_store_not_present) { + fprintf(fp, " Warning: Rule requires file-store but the output file-store is not " + "enabled.\n"); + } + if (rule_warning == 0) { + fprintf(fp, " No warnings for this rule.\n"); + } + fprintf(fp, "\n"); + } + return; +} |