pull/15802/merge
Jeff Lucovsky 12 hours ago committed by GitHub
commit 4a59eba35b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -548,6 +548,13 @@
AC_DEFINE([PROFILE_RULES],[1],[Enable performance profiling for rules])
])
# detection-engine inspection tracing
AC_ARG_ENABLE(detect-trace,
AS_HELP_STRING([--enable-detect-trace], [Enable human-readable tracing of the content-inspection engine (developer aid; runtime toggle via SURICATA_DETECT_TRACE)]),[enable_detect_trace=$enableval],[enable_detect_trace=no])
AS_IF([test "x$enable_detect_trace" = "xyes"], [
AC_DEFINE([DETECT_TRACE],[1],[Enable content-inspection engine tracing])
])
# enable support for IPFW
AC_ARG_ENABLE(ipfw,
AS_HELP_STRING([--enable-ipfw], [Enable FreeBSD IPFW support for inline IDP]),[enable_ipfw=$enableval],[enable_ipfw=no])
@ -2641,6 +2648,7 @@ SURICATA_BUILD_CONF="Suricata Configuration:
Profiling enabled: ${enable_profiling}
Profiling locks enabled: ${enable_profiling_locks}
Profiling rules enabled: ${enable_profiling_rules}
Detection tracing enabled: ${enable_detect_trace}
Plugin support (experimental): ${plugin_support}
DPDK Bond PMD: ${enable_dpdk_bond_pmd}

@ -0,0 +1,107 @@
Detection Inspection Tracing
============================
The content-inspection engine (``DetectEngineContentInspection``) can emit a
human-readable trace of every keyword it evaluates against a buffer. For each
inspected keyword it prints the signature being evaluated, the keyword and its
parameters, and -- on a match or a definitive no-match -- a hexdump of the
buffer windowed around the current inspection offset, with the bytes the
keyword matched called out in the dump. It is a developer aid for answering
"why did (or didn't) this rule match this traffic?".
The facility is intended for interactive debugging with a single pcap and a
small rule set. It writes directly to ``stdout`` and is not meant for
production traffic.
Enabling
--------
Tracing is compiled in only when Suricata is configured with::
./configure --enable-detect-trace
This defines the ``DETECT_TRACE`` preprocessor macro. When the macro is not
defined the trace call sites expand to nothing, so a normal build carries no
overhead at all.
Even in a trace-enabled build the output stays off until it is switched on at
runtime through an environment variable::
SURICATA_DETECT_TRACE=1 suricata -S rules.rules -r input.pcap -k none -l ./log
When the variable is unset, each trace call is guarded by a single predictable
branch, so a trace-enabled build can be used normally with negligible cost.
Output
------
Each inspected keyword produces a block such as::
------------------------------------------------------------------------------
TRACE (4) MSG: trace demo deep body
TRACE (4) Mode: state AppProto: http_any Buffer: file_data Match Type: content Recursion: 1/3000
TRACE (4) Inspecting content:"</html>" (len 7)
TRACE (4) -> MATCH! <- (content)
TRACE (4) post-detect offset: 4259
TRACE buffer window +4000..+4259 of 4259 (match +4252..+4259, cursor +4259 past end):
00000FA0 3E 20 6C 6F 77 3C 62 72 20 2F 3E 3C 65 6D 3E 50 > low<br /><em>P
...
00001080 0D 0A 0D 0A 09 09 3C 2F 64 69 76 3E 0D 0A 0D 0A ......</ div>....
>00001090 09 3C 2F 62 6F 64 79 3E 0D 0A 0D 0A 3C 2F 68 74 .</body> ....</ht
>000010A0 6D 6C 3E ml>
Reading the dump
~~~~~~~~~~~~~~~~
Row offsets are absolute within the inspected buffer, so they can be compared
directly against the ``post-detect offset`` and against the ``offset``,
``depth`` and ``distance`` values printed for the keyword. Only a window around
the current offset is dumped -- 256 bytes on either side -- so that a large
stream or body buffer does not flood the terminal; the header line gives the
window bounds and the buffer's full length.
Two things are called out in the dump. The bytes the keyword just matched are
shown as a span, and the offset the next keyword will start from is shown as a
single cursor byte. They are distinct because a content match leaves the
inspection offset just *past* the bytes it matched, so ``match +4252..+4259``
and ``cursor +4259`` in the example above describe the seven bytes of
``</html>`` and the position after them. A match span is only reported for a
positive, non-negated ``content``; every other keyword, and every no-match,
leaves the offset as a bare cursor with no span to show. Rows containing either
marker carry a ``>`` in the left gutter.
When ``stdout`` is a terminal the span and the cursor are colored, in both the
hex and the ascii column. Otherwise -- output redirected to a file or a pager,
or ``NO_COLOR`` set in the environment -- the whole trace is emitted without
escape sequences and the markers appear on a row of their own beneath the hex
bytes, with ``^`` under the matched span and ``*`` under the cursor::
TRACE buffer window +0..+27 of 27 (cursor +10) [^ match, * cursor]:
>00000000 2F 66 69 6C 65 32 70 63 61 70 2F 62 6F 64 79 74 /file2pc ap/bodyt
**
00000010 65 78 74 74 65 73 74 2E 74 78 74 exttest. txt
The ``Buffer`` name is only shown when Suricata is additionally built with
``--enable-profiling`` (the current inspection buffer id is only tracked on the
thread context in profiling builds); otherwise it is reported as ``(null)``.
Per-keyword parameter detail is printed for ``content``, ``pcre``, ``isdataat``,
``byte_test``, ``byte_jump`` and ``base64_decode``. Other keywords are still
traced by name via the ``Match Type`` field.
Implementation
--------------
All of the tracing lives in ``src/detect-engine-inspect-trace.{c,h}`` and is
invoked from ``DetectEngineContentInspection`` through the ``DETECT_TRACE_*``
macros, which are no-ops unless ``DETECT_TRACE`` is defined. To keep the trace
readable, ``content`` values are rendered from the parsed ``DetectContentData``
(no signature re-parsing), and the original ``pcre`` pattern text is retained on
the ``DetectParseRegex`` only in trace builds.
The dump is written by ``DetectTraceHexdump()`` rather than by the shared
``PrintRawDataFp()`` helper, since it needs absolute row offsets and per-byte
markers that the shared dumper does not provide. Color is decided once in
``DetectTraceInit()``: the ANSI sequences are held in file-scope pointers that
stay empty strings unless ``stdout`` is a tty and ``NO_COLOR`` is unset, so the
same format strings produce both forms of output.

@ -12,6 +12,14 @@ Stream
stream/inspection_raw_data
Detection
---------
.. toctree::
:maxdepth: 2
detect_trace
Defrag
------

@ -134,6 +134,7 @@ noinst_HEADERS = \
detect-engine-buffer.h \
detect-engine-build.h \
detect-engine-content-inspection.h \
detect-engine-inspect-trace.h \
detect-engine-event.h \
detect-engine-file.h \
detect-engine-frame.h \
@ -710,6 +711,7 @@ libsuricata_c_a_SOURCES = \
detect-engine-buffer.c \
detect-engine-build.c \
detect-engine-content-inspection.c \
detect-engine-inspect-trace.c \
detect-engine-event.c \
detect-engine-file.c \
detect-engine-frame.c \

@ -1,4 +1,4 @@
/* Copyright (C) 2020-2024 Open Information Security Foundation
/* Copyright (C) 2020-2026 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
@ -27,12 +27,6 @@
/* Arbitrary maximum buffer size for decoded base64 data. */
#define BASE64_DECODE_MAX 65535
typedef struct DetectBase64Decode_ {
uint16_t bytes;
uint32_t offset;
uint8_t relative;
} DetectBase64Decode;
static const char decode_pattern[] = "\\s*(bytes\\s+(\\d+),?)?"
"\\s*(offset\\s+(\\d+),?)?"
"\\s*(\\w+)?";

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2022 Open Information Security Foundation
/* Copyright (C) 2015-2026 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
@ -18,6 +18,12 @@
#ifndef SURICATA_DETECT_BASE64_DECODE_H
#define SURICATA_DETECT_BASE64_DECODE_H
typedef struct DetectBase64Decode_ {
uint16_t bytes;
uint32_t offset;
uint8_t relative;
} DetectBase64Decode;
void DetectBase64DecodeRegister(void);
int DetectBase64DecodeDoMatch(DetectEngineThreadCtx *, const Signature *,
const SigMatchData *, const uint8_t *, uint32_t);

@ -1,4 +1,4 @@
/* Copyright (C) 2007-2023 Open Information Security Foundation
/* Copyright (C) 2007-2026 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
@ -44,6 +44,7 @@
#include "detect-entropy.h"
#include "detect-replace.h"
#include "detect-engine-content-inspection.h"
#include "detect-engine-inspect-trace.h"
#include "detect-uricontent.h"
#include "detect-urilen.h"
#include "detect-engine-uint.h"
@ -124,6 +125,9 @@ static int DetectEngineContentInspectionInternal(DetectEngineThreadCtx *det_ctx,
SCReturnInt(0);
}
DETECT_TRACE_KEYWORD(
det_ctx, s, smd, p, inspection_mode, ctx->recursion.count, ctx->recursion.limit);
if (smd->type == DETECT_CONTENT) {
const DetectContentData *cd = (const DetectContentData *)smd->ctx;
SCLogDebug("inspecting content %"PRIu32" buffer_len %"PRIu32, cd->id, buffer_len);
@ -702,6 +706,7 @@ static int DetectEngineContentInspectionInternal(DetectEngineThreadCtx *det_ctx,
DETECT_CI_FLAGS_SINGLE, DETECT_ENGINE_CONTENT_INSPECTION_MODE_STATE);
if (r == 1) {
/* Base64 is a terminal list. */
DETECT_TRACE_RESULT(s, smd, buffer, buffer_len, det_ctx->buffer_offset, 1);
goto final_match;
}
}
@ -722,16 +727,19 @@ static int DetectEngineContentInspectionInternal(DetectEngineThreadCtx *det_ctx,
}
no_match:
DETECT_TRACE_RESULT(s, smd, buffer, buffer_len, det_ctx->buffer_offset, 0);
KEYWORD_PROFILING_END(det_ctx, smd->type, 0);
SCReturnInt(0);
no_match_discontinue:
DETECT_TRACE_RESULT(s, smd, buffer, buffer_len, det_ctx->buffer_offset, -1);
KEYWORD_PROFILING_END(det_ctx, smd->type, 0);
SCReturnInt(-1);
match:
/* this sigmatch matched, inspect the next one. If it was the last,
* the buffer portion of the signature matched. */
DETECT_TRACE_RESULT(s, smd, buffer, buffer_len, det_ctx->buffer_offset, 1);
if (!smd->is_last) {
KEYWORD_PROFILING_END(det_ctx, smd->type, 1);
int r = DetectEngineContentInspectionInternal(det_ctx, ctx, s, smd + 1, p, f, buffer,

@ -0,0 +1,446 @@
/* Copyright (C) 2026 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 Jeff Lucovsky <jlucovsky@oisf.net>
*
* See detect-engine-inspect-trace.h for an overview.
*/
#include "suricata-common.h"
#include "detect-engine-inspect-trace.h"
#ifdef DETECT_TRACE
#include "decode.h"
#include "detect.h"
#include "detect-parse.h"
#include "detect-engine.h"
#include "detect-content.h"
#include "detect-pcre.h"
#include "detect-isdataat.h"
#include "detect-bytetest.h"
#include "detect-bytejump.h"
#include "detect-urilen.h"
#include "detect-base64-decode.h"
#include "app-layer-protos.h"
#include "util-print.h"
/* ANSI colors used to make the trace readable on a terminal. They are empty
* strings unless DetectTraceInit() finds an interactive stdout, so the same
* format strings serve both the colored and the plain output. */
static const char *c_rst = "";
static const char *c_red = "";
static const char *c_grn = "";
static const char *c_blu = "";
static const char *c_mag = "";
static const char *c_cyn = "";
/* the bytes a keyword just matched, and the byte the next keyword starts at */
static const char *c_match = "";
static const char *c_cursor = "";
bool g_detect_trace_enabled = false;
static bool g_detect_trace_color = false;
void DetectTraceInit(void)
{
g_detect_trace_enabled = (getenv("SURICATA_DETECT_TRACE") != NULL);
if (!g_detect_trace_enabled)
return;
/* Escape sequences only help on a terminal: they are noise in a file and
* they would swamp a hexdump. NO_COLOR (https://no-color.org/) turns them
* off even there. */
const char *no_color = getenv("NO_COLOR");
#ifndef OS_WIN32
g_detect_trace_color = isatty(fileno(stdout)) && (no_color == NULL || no_color[0] == '\0');
#endif
if (g_detect_trace_color) {
c_rst = "\033[0m";
c_red = "\033[31m";
c_grn = "\033[32m";
c_blu = "\033[34m";
c_mag = "\033[35m";
c_cyn = "\033[36m";
/* explicit foreground and background so the markers render the same
* whatever the terminal's own palette is */
c_match = "\033[30;102m";
c_cursor = "\033[30;103m";
}
SCLogNotice("detection-engine inspection tracing enabled "
"(SURICATA_DETECT_TRACE set); output goes to stdout");
}
static const char *DetectTraceModeName(enum DetectContentInspectionType mode)
{
switch (mode) {
case DETECT_ENGINE_CONTENT_INSPECTION_MODE_PAYLOAD:
return "payload";
case DETECT_ENGINE_CONTENT_INSPECTION_MODE_HEADER:
return "header";
case DETECT_ENGINE_CONTENT_INSPECTION_MODE_STREAM:
return "stream";
case DETECT_ENGINE_CONTENT_INSPECTION_MODE_FRAME:
return "frame";
case DETECT_ENGINE_CONTENT_INSPECTION_MODE_STATE:
return "state";
}
return "unknown";
}
/** \brief how a byte of the dump is called out, if at all */
enum DetectTraceMark {
TRACE_MARK_NONE = 0,
TRACE_MARK_MATCH,
TRACE_MARK_CURSOR,
};
/** \brief width of the gutter and offset column preceding the hex bytes */
#define TRACE_DUMP_PREFIX 11
/** \brief classify byte \p i of the buffer against the marked region */
static enum DetectTraceMark DetectTraceByteMark(
uint32_t i, uint32_t hl_start, uint32_t hl_end, uint32_t cursor, bool has_cursor)
{
if (has_cursor && i == cursor)
return TRACE_MARK_CURSOR;
if (i >= hl_start && i < hl_end)
return TRACE_MARK_MATCH;
return TRACE_MARK_NONE;
}
static const char *DetectTraceMarkColor(enum DetectTraceMark mark)
{
switch (mark) {
case TRACE_MARK_MATCH:
return c_match;
case TRACE_MARK_CURSOR:
return c_cursor;
case TRACE_MARK_NONE:
break;
}
return "";
}
/**
* \brief Whether the highlight should run through the separator after byte \p i.
*
* It continues only into a neighbour on the same row carrying the same mark, so
* a span reads as one bar instead of as detached pairs, without the background
* bleeding past the end of the row.
*/
static bool DetectTraceMarkJoins(uint32_t i, uint32_t rowend, enum DetectTraceMark mark,
uint32_t hl_start, uint32_t hl_end, uint32_t cursor, bool has_cursor)
{
if (mark == TRACE_MARK_NONE || i + 1 >= rowend)
return false;
return DetectTraceByteMark(i + 1, hl_start, hl_end, cursor, has_cursor) == mark;
}
/** \brief the caret glyph standing in for a color when there is no terminal */
static char DetectTraceMarkGlyph(enum DetectTraceMark mark)
{
switch (mark) {
case TRACE_MARK_MATCH:
return '^';
case TRACE_MARK_CURSOR:
return '*';
case TRACE_MARK_NONE:
break;
}
return ' ';
}
/**
* \brief Hexdump of \p buf around the current inspection \p cursor.
*
* Large payload/stream buffers are windowed so the trace doesn't flood the
* terminal. Row offsets are absolute within the buffer, so they can be read
* against the offsets the rest of the trace prints without any arithmetic.
*
* \p match_len is the number of bytes the keyword just consumed. Those bytes
* end at \p cursor rather than starting there, so they are highlighted as the
* span [cursor - match_len, cursor) and \p cursor itself is marked separately
* as the point the next keyword starts from.
*/
static void DetectTraceHexdump(
const uint8_t *buf, uint32_t len, uint32_t cursor, uint32_t match_len)
{
const uint32_t window = 256; /* bytes of context on each side of the cursor */
uint32_t start = (cursor > window) ? cursor - window : 0;
start &= ~0xFU; /* align rows to 16 so the columns stay in fixed positions */
uint32_t end = (cursor + window < len) ? cursor + window : len;
if (start >= end) {
start = 0;
end = len;
}
const bool span = (match_len > 0 && match_len <= cursor);
const uint32_t hl_start = span ? cursor - match_len : 0;
const uint32_t hl_end = span ? cursor : 0; /* exclusive */
const bool has_cursor = (cursor < len);
printf("TRACE buffer window +%u..+%u of %u (", start, end, len);
if (span)
printf("match +%u..+%u, ", hl_start, hl_end);
printf("cursor +%u%s)", cursor, has_cursor ? "" : " past end");
if (!g_detect_trace_color)
printf(" [^ match, * cursor]");
printf(":\n");
for (uint32_t u = start; u < end; u += 16) {
const uint32_t rowend = (u + 16 < end) ? u + 16 : end;
bool marked = false;
for (uint32_t i = u; i < rowend; i++) {
if (DetectTraceByteMark(i, hl_start, hl_end, cursor, has_cursor) != TRACE_MARK_NONE) {
marked = true;
break;
}
}
printf("%c%08X ", marked ? '>' : ' ', u);
uint32_t ch;
for (ch = 0; u + ch < rowend; ch++) {
const enum DetectTraceMark mark =
DetectTraceByteMark(u + ch, hl_start, hl_end, cursor, has_cursor);
const bool joins = DetectTraceMarkJoins(
u + ch, rowend, mark, hl_start, hl_end, cursor, has_cursor);
const char *esc = (mark == TRACE_MARK_NONE) ? "" : DetectTraceMarkColor(mark);
printf("%s%02X%s ", esc, buf[u + ch], (mark != TRACE_MARK_NONE && !joins) ? c_rst : "");
if (ch == 7)
putchar(' ');
if (joins)
printf("%s", c_rst);
}
/* pad a short final row so the ascii column stays where it belongs */
for (uint32_t pad = ch; pad < 16; pad++) {
printf(" ");
if (pad == 7)
putchar(' ');
}
printf(" ");
for (ch = 0; u + ch < rowend; ch++) {
const uint8_t c = buf[u + ch];
const char pc = isprint(c) ? (char)c : '.';
const enum DetectTraceMark mark =
DetectTraceByteMark(u + ch, hl_start, hl_end, cursor, has_cursor);
const bool joins = DetectTraceMarkJoins(
u + ch, rowend, mark, hl_start, hl_end, cursor, has_cursor);
const char *esc = (mark == TRACE_MARK_NONE) ? "" : DetectTraceMarkColor(mark);
printf("%s%c%s", esc, pc, (mark != TRACE_MARK_NONE && !joins) ? c_rst : "");
if (ch == 7)
putchar(' ');
if (joins)
printf("%s", c_rst);
}
putchar('\n');
/* with no color to carry it, the marked bytes get a row of their own */
if (!g_detect_trace_color && marked) {
for (int i = 0; i < TRACE_DUMP_PREFIX; i++)
putchar(' ');
for (ch = 0; u + ch < rowend; ch++) {
const char glyph = DetectTraceMarkGlyph(
DetectTraceByteMark(u + ch, hl_start, hl_end, cursor, has_cursor));
printf("%c%c ", glyph, glyph);
if (ch == 7)
putchar(' ');
}
putchar('\n');
}
}
}
/**
* \brief Number of bytes the keyword consumed, ending at the post-detect offset.
*
* A positive, non-negated content match is the only case that moves
* det_ctx->buffer_offset to the end of the bytes it matched (match_offset in
* DetectEngineContentInspectionInternal()); every other keyword and every
* no-match leaves the offset as a bare cursor with no span to show.
*/
static uint32_t DetectTraceMatchLen(const SigMatchData *smd, int result)
{
if (result != 1 || smd->type != DETECT_CONTENT)
return 0;
const DetectContentData *cd = (const DetectContentData *)smd->ctx;
if (cd->flags & DETECT_CONTENT_NEGATED)
return 0;
return cd->content_len;
}
/** \brief append " name:value" for a set flag, into a fixed buffer */
static void DetectTraceAppend(char *dst, size_t dstlen, const char *text)
{
strlcat(dst, text, dstlen);
}
/** \brief render the keyword-specific parameters into \p out */
static void DetectTraceKeywordDetail(const SigMatchData *smd, char *out, size_t outlen)
{
out[0] = '\0';
switch (smd->type) {
case DETECT_CONTENT: {
const DetectContentData *cd = (const DetectContentData *)smd->ctx;
char content[1024] = "";
uint32_t o = 0;
PrintRawUriBuf(content, &o, sizeof(content), cd->content, cd->content_len);
char mods[256] = "";
char tmp[64];
if (cd->flags & DETECT_CONTENT_OFFSET) {
snprintf(tmp, sizeof(tmp), " offset:%u", cd->offset);
DetectTraceAppend(mods, sizeof(mods), tmp);
}
if (cd->flags & DETECT_CONTENT_DEPTH) {
snprintf(tmp, sizeof(tmp), " depth:%u", cd->depth);
DetectTraceAppend(mods, sizeof(mods), tmp);
}
if (cd->flags & DETECT_CONTENT_DISTANCE) {
snprintf(tmp, sizeof(tmp), " distance:%d", cd->distance);
DetectTraceAppend(mods, sizeof(mods), tmp);
}
if (cd->flags & DETECT_CONTENT_WITHIN) {
snprintf(tmp, sizeof(tmp), " within:%d", cd->within);
DetectTraceAppend(mods, sizeof(mods), tmp);
}
if (cd->flags & DETECT_CONTENT_STARTS_WITH)
DetectTraceAppend(mods, sizeof(mods), " startswith");
if (cd->flags & DETECT_CONTENT_ENDS_WITH)
DetectTraceAppend(mods, sizeof(mods), " endswith");
if (cd->flags & DETECT_CONTENT_FAST_PATTERN)
DetectTraceAppend(mods, sizeof(mods), " fast_pattern");
if (cd->flags & DETECT_CONTENT_RAWBYTES)
DetectTraceAppend(mods, sizeof(mods), " rawbytes");
snprintf(out, outlen, "content:%s\"%s%s%s\"%s (len %u)",
(cd->flags & DETECT_CONTENT_NEGATED) ? "!" : "", c_grn, content, c_rst, mods,
cd->content_len);
break;
}
case DETECT_PCRE: {
const DetectPcreData *pe = (const DetectPcreData *)smd->ctx;
char mods[64] = "";
if (pe->flags & DETECT_PCRE_RELATIVE)
DetectTraceAppend(mods, sizeof(mods), " relative");
if (pe->flags & DETECT_PCRE_CASELESS)
DetectTraceAppend(mods, sizeof(mods), " nocase");
if (pe->flags & DETECT_PCRE_RAWBYTES)
DetectTraceAppend(mods, sizeof(mods), " rawbytes");
snprintf(out, outlen, "pcre:%s\"%s%s%s\"%s",
(pe->flags & DETECT_PCRE_NEGATE) ? "!" : "", c_grn,
pe->parse_regex.regexstr ? pe->parse_regex.regexstr : "(unknown)", c_rst, mods);
break;
}
case DETECT_ISDATAAT: {
const DetectIsdataatData *id = (const DetectIsdataatData *)smd->ctx;
snprintf(out, outlen, "isdataat:%s%s%u%s%s", (id->flags & ISDATAAT_NEGATED) ? "!" : "",
c_grn, id->dataat, c_rst, (id->flags & ISDATAAT_RELATIVE) ? " relative" : "");
break;
}
case DETECT_BYTETEST: {
const DetectBytetestData *btd = (const DetectBytetestData *)smd->ctx;
snprintf(out, outlen,
"byte_test:%sbytes %u, op %u, value %" PRIu64 ", offset %d, bitmask 0x%x%s",
c_grn, btd->nbytes, btd->op, btd->value, btd->offset, btd->bitmask, c_rst);
break;
}
case DETECT_BYTEJUMP: {
const DetectBytejumpData *bjd = (const DetectBytejumpData *)smd->ctx;
snprintf(out, outlen, "byte_jump:%sbytes %u, offset %d, post_offset %d%s", c_grn,
bjd->nbytes, bjd->offset, bjd->post_offset, c_rst);
break;
}
case DETECT_BASE64_DECODE: {
const DetectBase64Decode *bd = (const DetectBase64Decode *)smd->ctx;
snprintf(out, outlen, "base64_decode:%sbytes %u, offset %u, relative %s%s", c_grn,
bd->bytes, bd->offset, bd->relative ? "true" : "false", c_rst);
break;
}
default:
/* no dedicated formatter; the keyword name is shown in the header */
break;
}
}
void DetectTraceKeyword(const DetectEngineThreadCtx *det_ctx, const Signature *s,
const SigMatchData *smd, const Packet *p, enum DetectContentInspectionType inspection_mode,
uint32_t recursion, uint32_t recursion_limit)
{
char detail[1400];
DetectTraceKeywordDetail(smd, detail, sizeof(detail));
/* The current buffer/list id is only tracked on det_ctx when profiling is
* compiled in; without it we simply omit the buffer name. */
const char *bufname = NULL;
#ifdef PROFILING
if (det_ctx->de_ctx != NULL) {
bufname = DetectEngineBufferTypeGetNameById(det_ctx->de_ctx, det_ctx->keyword_perf_list);
}
#endif
flockfile(stdout);
for (int i = 0; i < 78; i++)
putchar('-');
printf("\nTRACE (%u) MSG: %s\n", s->id, s->msg ? s->msg : "");
printf("TRACE (%u)", s->id);
if (p != NULL)
printf(" Packet: %" PRIu64, PcapPacketCntGet(p));
printf(" Mode: %s%s%s", c_red, DetectTraceModeName(inspection_mode), c_rst);
if (s->alproto != ALPROTO_UNKNOWN)
printf(" AppProto: %s%s%s", c_red, AppProtoToString(s->alproto), c_rst);
printf(" Buffer: %s%s%s", c_mag, bufname ? bufname : "(null)", c_rst);
printf(" Match Type: %s%s%s", c_blu, sigmatch_table[smd->type].name, c_rst);
printf(" Recursion: %s%u/%u%s\n", c_grn, recursion, recursion_limit, c_rst);
if (detail[0] != '\0')
printf("TRACE (%u) Inspecting %s\n", s->id, detail);
fflush(stdout);
funlockfile(stdout);
}
void DetectTraceResult(const Signature *s, const SigMatchData *smd, const uint8_t *buffer,
uint32_t buffer_len, uint32_t offset, int result)
{
flockfile(stdout);
if (result == 1) {
printf("TRACE (%u) -> %sMATCH!%s <- (%s)\n", s->id, c_cyn, c_rst,
sigmatch_table[smd->type].name);
} else {
printf("TRACE (%u) -> %sNO MATCH!%s <-%s (%s)\n", s->id, c_red, c_rst,
(result == -1) ? " [discontinue]" : "", sigmatch_table[smd->type].name);
}
printf("TRACE (%u) post-detect offset: %s%u%s\n", s->id, c_grn, offset, c_rst);
if (buffer != NULL)
DetectTraceHexdump(buffer, buffer_len, offset, DetectTraceMatchLen(smd, result));
fflush(stdout);
funlockfile(stdout);
}
#endif /* DETECT_TRACE */

@ -0,0 +1,93 @@
/* Copyright (C) 2026 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 Jeff Lucovsky <jlucovsky@oisf.net>
*
* Optional, human-readable tracing of the content-inspection engine. Prints, per
* inspected keyword, the rule being evaluated, the keyword parameters and a
* hexdump of the buffer around the current inspection offset, followed by the
* match/no-match outcome. The dump calls out the bytes the keyword matched and
* the offset the next keyword starts from, with color on an interactive stdout
* and with a caret row otherwise.
*
* The whole facility is compiled in only when configured with
* --enable-detect-trace (which defines DETECT_TRACE). Even then it stays silent
* unless enabled at runtime via the SURICATA_DETECT_TRACE environment variable,
* so a trace-enabled build pays nothing more than a single predictable branch
* per keyword when tracing is off.
*/
#ifndef SURICATA_DETECT_ENGINE_INSPECT_TRACE_H
#define SURICATA_DETECT_ENGINE_INSPECT_TRACE_H
#include "detect.h"
#include "detect-engine-content-inspection.h"
#ifdef DETECT_TRACE
/** \brief runtime toggle; set once from the environment by DetectTraceInit() */
extern bool g_detect_trace_enabled;
/** \brief read the SURICATA_DETECT_TRACE environment variable once at startup */
void DetectTraceInit(void);
/** \internal implementation of DETECT_TRACE_KEYWORD() */
void DetectTraceKeyword(const DetectEngineThreadCtx *det_ctx, const Signature *s,
const SigMatchData *smd, const Packet *p, enum DetectContentInspectionType inspection_mode,
uint32_t recursion, uint32_t recursion_limit);
/** \internal implementation of DETECT_TRACE_RESULT() */
void DetectTraceResult(const Signature *s, const SigMatchData *smd, const uint8_t *buffer,
uint32_t buffer_len, uint32_t offset, int result);
/**
* \brief Trace the keyword about to be inspected (rule, parameters, buffer).
*/
#define DETECT_TRACE_KEYWORD(det_ctx, s, smd, p, mode, rec, lim) \
do { \
if (unlikely(g_detect_trace_enabled)) \
DetectTraceKeyword((det_ctx), (s), (smd), (p), (mode), (rec), (lim)); \
} while (0)
/**
* \brief Trace the outcome of the current keyword. \p result is 1 for a match,
* 0 for no match and -1 for no match with "discontinue".
*/
#define DETECT_TRACE_RESULT(s, smd, buffer, buffer_len, offset, result) \
do { \
if (unlikely(g_detect_trace_enabled)) \
DetectTraceResult((s), (smd), (buffer), (buffer_len), (offset), (result)); \
} while (0)
#else /* !DETECT_TRACE */
static inline void DetectTraceInit(void)
{
}
#define DETECT_TRACE_KEYWORD(det_ctx, s, smd, p, mode, rec, lim) \
do { \
} while (0)
#define DETECT_TRACE_RESULT(s, smd, buffer, buffer_len, offset, result) \
do { \
} while (0)
#endif /* DETECT_TRACE */
#endif /* SURICATA_DETECT_ENGINE_INSPECT_TRACE_H */

@ -1,4 +1,4 @@
/* Copyright (C) 2007-2020 Open Information Security Foundation
/* Copyright (C) 2007-2026 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
@ -95,6 +95,9 @@ typedef struct DetectParseRegex {
pcre2_code *regex;
pcre2_match_context *context;
struct DetectParseRegex *next;
#ifdef DETECT_TRACE
char *regexstr; /**< source string, kept for inspection tracing only */
#endif
} DetectParseRegex;
DetectParseRegex *DetectSetupPCRE2(const char *parse_str, int opts);

@ -742,6 +742,15 @@ static DetectPcreData *DetectPcreParse (DetectEngineCtx *de_ctx,
goto error;
}
#ifdef DETECT_TRACE
/* keep the pattern text so the inspection tracer can display it */
pd->parse_regex.regexstr = SCStrdup(re);
if (pd->parse_regex.regexstr == NULL) {
SCLogError("failed to store regex string for tracing");
goto error;
}
#endif
#ifdef PCRE2_HAVE_JIT
if (pcre2_use_jit) {
ret = pcre2_jit_compile(pd->parse_regex.regex, PCRE2_JIT_COMPLETE);
@ -1080,6 +1089,10 @@ static void DetectPcreFree(DetectEngineCtx *de_ctx, void *ptr)
return;
DetectPcreData *pd = (DetectPcreData *)ptr;
#ifdef DETECT_TRACE
if (pd->parse_regex.regexstr != NULL)
SCFree(pd->parse_regex.regexstr);
#endif
DetectParseFreeRegex(&pd->parse_regex);
DetectUnregisterThreadCtxFuncs(de_ctx, pd, "pcre");

@ -53,6 +53,7 @@
#include "detect.h"
#include "detect-parse.h"
#include "detect-engine.h"
#include "detect-engine-inspect-trace.h"
#include "detect-engine-address.h"
#include "detect-engine-alert.h"
#include "detect-engine-port.h"
@ -2821,6 +2822,10 @@ static void SetupUserMode(SCInstance *suri)
*/
int PostConfLoadedSetup(SCInstance *suri)
{
/* pick up the detection-engine inspection trace toggle (no-op unless built
* with --enable-detect-trace) */
DetectTraceInit();
int cnf_firewall_enabled = 0;
if (SCConfGetBool("firewall.enabled", &cnf_firewall_enabled) == 1) {
if (cnf_firewall_enabled == 1) {

Loading…
Cancel
Save