/* 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;
}
