Skip to content

Commit b3017a4

Browse files
committed
plugins: add ja4t-stub example plugin
The plugin is enabled with --enable-ja4t-stub and has no dependencies on external libraries.
1 parent 81572cb commit b3017a4

4 files changed

Lines changed: 371 additions & 0 deletions

File tree

configure.ac

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2326,6 +2326,20 @@ fi
23262326
fi
23272327
AC_SUBST([ndpi_comment])
23282328

2329+
# JA4T stub plugin (no library dependencies)
2330+
AC_ARG_ENABLE(ja4t-stub,
2331+
AS_HELP_STRING([--enable-ja4t-stub], [Enable JA4T stub plugin]),
2332+
[enable_ja4t_stub=$enableval],[enable_ja4t_stub=no])
2333+
2334+
if test "x$enable_ja4t_stub" = "xyes"; then
2335+
AM_CONDITIONAL([BUILD_JA4T_STUB], [true])
2336+
ja4t_stub_comment=""
2337+
else
2338+
AM_CONDITIONAL([BUILD_JA4T_STUB], [false])
2339+
ja4t_stub_comment="#"
2340+
fi
2341+
AC_SUBST([ja4t_stub_comment])
2342+
23292343
AC_ARG_ENABLE(warnings,
23302344
AS_HELP_STRING([--enable-warnings], [Enable supported C compiler warnings]),[enable_warnings=$enableval],[enable_warnings=no])
23312345
AS_IF([test "x$enable_warnings" = "xyes"], [
@@ -2577,6 +2591,7 @@ AC_CONFIG_FILES(plugins/Makefile)
25772591
AC_CONFIG_FILES(plugins/pfring/Makefile)
25782592
AC_CONFIG_FILES(plugins/napatech/Makefile)
25792593
AC_CONFIG_FILES(plugins/ndpi/Makefile)
2594+
AC_CONFIG_FILES(plugins/ja4t-stub/Makefile)
25802595

25812596
AC_OUTPUT
25822597

@@ -2635,6 +2650,7 @@ SURICATA_BUILD_CONF="Suricata Configuration:
26352650

26362651
Plugins:
26372652
nDPI: ${enable_ndpi}
2653+
JA4T stub: ${enable_ja4t_stub}
26382654

26392655
Development settings:
26402656
Coccinelle / spatch: ${enable_coccinelle}

plugins/Makefile.am

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ endif
1111
if BUILD_NDPI
1212
SUBDIRS += ndpi
1313
endif
14+
15+
if BUILD_JA4T_STUB
16+
SUBDIRS += ja4t-stub
17+
endif

plugins/ja4t-stub/Makefile.am

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
pkglib_LTLIBRARIES = ja4t-stub.la
2+
3+
ja4t_stub_la_LDFLAGS = -module -avoid-version -shared
4+
5+
# Only required to find these headers when building plugins from the
6+
# source directory.
7+
ja4t_stub_la_CFLAGS = -I../../rust/gen -I../../rust/dist
8+
9+
ja4t_stub_la_SOURCES = ja4t-stub.c
10+
11+
install-exec-hook:
12+
cd $(DESTDIR)$(pkglibdir) && $(RM) $(pkglib_LTLIBRARIES)

plugins/ja4t-stub/ja4t-stub.c

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/* Copyright (C) 2026 Open Information Security Foundation
2+
*
3+
* You can copy, redistribute or modify this Program under the terms of
4+
* the GNU General Public License version 2 as published by the Free
5+
* Software Foundation.
6+
*
7+
* This program is distributed in the hope that it will be useful,
8+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
* GNU General Public License for more details.
11+
*
12+
* You should have received a copy of the GNU General Public License
13+
* version 2 along with this program; if not, write to the Free Software
14+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15+
* 02110-1301, USA.
16+
*/
17+
18+
/*
19+
* This is a stub/example plugin demonstrating Suricata callback APIs.
20+
* It doesn't perform any real analysis but shows how to hook into:
21+
* - Thread lifecycle callbacks
22+
* - Flow lifecycle callbacks
23+
* - EVE output callbacks
24+
* - Detection/keyword registration
25+
*/
26+
27+
#include "suricata-common.h"
28+
#include "suricata-plugin.h"
29+
30+
#include "detect-engine-helper.h"
31+
#include "detect-parse.h"
32+
#include "flow-callbacks.h"
33+
#include "flow-storage.h"
34+
#include "output-eve.h"
35+
#include "thread-callbacks.h"
36+
#include "thread-storage.h"
37+
#include "util-debug.h"
38+
39+
static ThreadStorageId thread_storage_id = { .id = -1 };
40+
static FlowStorageId flow_storage_id = { .id = -1 };
41+
static int ja4t_stub_keyword_id = -1;
42+
43+
/* Per-thread context structure */
44+
struct Ja4tThreadContext {
45+
uint64_t packet_count;
46+
};
47+
48+
/* Per-flow context structure */
49+
struct Ja4tFlowContext {
50+
uint64_t packets;
51+
uint64_t bytes;
52+
};
53+
54+
/* Detection keyword data structure */
55+
typedef struct DetectJa4tStubData_ {
56+
uint32_t value;
57+
bool negated;
58+
} DetectJa4tStubData;
59+
60+
/* Free thread storage */
61+
static void ThreadStorageFree(void *ptr)
62+
{
63+
SCLogDebug("Free'ing JA4T stub thread storage");
64+
struct Ja4tThreadContext *context = ptr;
65+
SCFree(context);
66+
}
67+
68+
/* Free flow storage */
69+
static void FlowStorageFree(void *ptr)
70+
{
71+
SCLogDebug("Free'ing JA4T stub flow storage");
72+
struct Ja4tFlowContext *ctx = ptr;
73+
SCFree(ctx);
74+
}
75+
76+
/* Flow initialization callback - called when a new flow is created */
77+
static void OnFlowInit(ThreadVars *tv, Flow *f, const Packet *p, void *_data)
78+
{
79+
struct Ja4tFlowContext *flowctx = SCCalloc(1, sizeof(*flowctx));
80+
if (flowctx == NULL) {
81+
FatalError("Failed to allocate JA4T stub flow context");
82+
}
83+
84+
flowctx->packets = 0;
85+
flowctx->bytes = 0;
86+
FlowSetStorageById(f, flow_storage_id, flowctx);
87+
88+
SCLogDebug("JA4T stub: Flow initialized");
89+
}
90+
91+
/* Flow update callback - called for each packet in a flow */
92+
static void OnFlowUpdate(ThreadVars *tv, Flow *f, Packet *p, void *_data)
93+
{
94+
struct Ja4tFlowContext *flowctx = FlowGetStorageById(f, flow_storage_id);
95+
96+
if (flowctx == NULL) {
97+
return;
98+
}
99+
100+
/* Update flow statistics */
101+
flowctx->packets++;
102+
flowctx->bytes += GET_PKT_LEN(p);
103+
104+
SCLogDebug("JA4T stub: Flow update - packets=%" PRIu64 ", bytes=%" PRIu64, flowctx->packets,
105+
flowctx->bytes);
106+
}
107+
108+
/* Flow finish callback - called when a flow is destroyed */
109+
static void OnFlowFinish(ThreadVars *tv, Flow *f, void *_data)
110+
{
111+
struct Ja4tFlowContext *flowctx = FlowGetStorageById(f, flow_storage_id);
112+
113+
if (flowctx != NULL) {
114+
SCLogDebug("JA4T stub: Flow finished - total packets=%" PRIu64 ", total bytes=%" PRIu64,
115+
flowctx->packets, flowctx->bytes);
116+
}
117+
}
118+
119+
/* Thread initialization callback - called when a thread starts */
120+
static void OnThreadInit(ThreadVars *tv, void *_data)
121+
{
122+
struct Ja4tThreadContext *context = SCCalloc(1, sizeof(*context));
123+
if (context == NULL) {
124+
FatalError("Failed to allocate JA4T stub thread context");
125+
}
126+
127+
context->packet_count = 0;
128+
ThreadSetStorageById(tv, thread_storage_id, context);
129+
130+
SCLogDebug("JA4T stub: Thread initialized");
131+
}
132+
133+
/* Detection keyword packet match callback */
134+
static int DetectJa4tStubPacketMatch(
135+
DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx)
136+
{
137+
const Flow *f = p->flow;
138+
struct Ja4tFlowContext *flowctx = FlowGetStorageById(f, flow_storage_id);
139+
const DetectJa4tStubData *data = (const DetectJa4tStubData *)ctx;
140+
141+
SCEnter();
142+
143+
if (f == NULL) {
144+
SCLogDebug("packet %" PRIu64 ": no flow", PcapPacketCntGet(p));
145+
SCReturnInt(0);
146+
}
147+
148+
if (flowctx == NULL) {
149+
SCReturnInt(0);
150+
}
151+
152+
/* Example matching logic: match if packet count exceeds threshold */
153+
bool r = (flowctx->packets >= data->value);
154+
r = r ^ data->negated;
155+
156+
if (r) {
157+
SCLogDebug("JA4T stub keyword match on packet_count >= %u", data->value);
158+
SCReturnInt(1);
159+
}
160+
161+
SCReturnInt(0);
162+
}
163+
164+
/* Parse detection keyword arguments */
165+
static DetectJa4tStubData *DetectJa4tStubParse(const char *arg, bool negate)
166+
{
167+
DetectJa4tStubData *data;
168+
char *endptr;
169+
unsigned long val;
170+
171+
/* Parse numeric argument */
172+
val = strtoul(arg, &endptr, 10);
173+
if (*endptr != '\0' || endptr == arg) {
174+
SCLogError("invalid ja4t-stub value '%s'", arg);
175+
return NULL;
176+
}
177+
178+
data = SCMalloc(sizeof(DetectJa4tStubData));
179+
if (unlikely(data == NULL))
180+
return NULL;
181+
182+
data->value = (uint32_t)val;
183+
data->negated = negate;
184+
185+
return data;
186+
}
187+
188+
/* Check for conflicts in detection keyword usage */
189+
static bool Ja4tStubDataHasConflicts(const DetectJa4tStubData *us, const DetectJa4tStubData *them)
190+
{
191+
/* Check for mix of negated and non-negated */
192+
if (them->negated ^ us->negated)
193+
return true;
194+
195+
/* Check for multiple non-negated */
196+
if (!us->negated)
197+
return true;
198+
199+
/* Check for duplicate */
200+
if (us->value == them->value)
201+
return true;
202+
203+
return false;
204+
}
205+
206+
/* Setup detection keyword */
207+
static int DetectJa4tStubSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg)
208+
{
209+
DetectJa4tStubData *data = DetectJa4tStubParse(arg, s->init_data->negated);
210+
if (data == NULL)
211+
goto error;
212+
213+
SigMatch *tsm = s->init_data->smlists[DETECT_SM_LIST_MATCH];
214+
for (; tsm != NULL; tsm = tsm->next) {
215+
if (tsm->type == ja4t_stub_keyword_id) {
216+
const DetectJa4tStubData *them = (const DetectJa4tStubData *)tsm->ctx;
217+
218+
if (Ja4tStubDataHasConflicts(data, them)) {
219+
SCLogError("can't mix positive ja4t-stub match with negated");
220+
goto error;
221+
}
222+
}
223+
}
224+
225+
if (SCSigMatchAppendSMToList(de_ctx, s, ja4t_stub_keyword_id, (SigMatchCtx *)data,
226+
DETECT_SM_LIST_MATCH) == NULL) {
227+
goto error;
228+
}
229+
return 0;
230+
231+
error:
232+
if (data != NULL)
233+
SCFree(data);
234+
return -1;
235+
}
236+
237+
/* Free detection keyword data */
238+
static void DetectJa4tStubFree(DetectEngineCtx *de_ctx, void *ptr)
239+
{
240+
SCFree(ptr);
241+
}
242+
243+
/* EVE output callback - adds data to EVE JSON output */
244+
static void EveCallback(ThreadVars *tv, const Packet *p, Flow *f, SCJsonBuilder *jb, void *data)
245+
{
246+
/* EVE callback requires a flow */
247+
if (f == NULL) {
248+
return;
249+
}
250+
251+
struct Ja4tFlowContext *flowctx = FlowGetStorageById(f, flow_storage_id);
252+
253+
if (flowctx == NULL) {
254+
return;
255+
}
256+
257+
SCLogDebug("JA4T stub EVE callback: tv=%p, p=%p, f=%p", tv, p, f);
258+
259+
/* Open a JA4T stub object in the EVE output */
260+
SCJbOpenObject(jb, "ja4t_stub");
261+
262+
/* Add flow statistics */
263+
SCJbSetUint(jb, "packet_count", flowctx->packets);
264+
SCJbSetUint(jb, "byte_count", flowctx->bytes);
265+
266+
/* Close the JA4T stub object */
267+
SCJbClose(jb);
268+
}
269+
270+
/* Initialize detection keyword */
271+
static void Ja4tStubInitKeyword(void)
272+
{
273+
/* SCSigTableAppLiteElmt and SCDetectHelperKeywordRegister don't yet
274+
* support all the fields required to register the keyword,
275+
* missing the (packet) Match callback,
276+
* so we'll just register with an empty keyword specifier to get
277+
* the ID, then fill in the ID. */
278+
ja4t_stub_keyword_id = SCDetectHelperNewKeywordId();
279+
SCLogDebug("Registered new ja4t-stub keyword with ID %" PRIu32, ja4t_stub_keyword_id);
280+
281+
sigmatch_table[ja4t_stub_keyword_id].name = "ja4t-stub";
282+
sigmatch_table[ja4t_stub_keyword_id].desc = "match on JA4T stub flow statistics";
283+
sigmatch_table[ja4t_stub_keyword_id].url = "/rules/ja4t-stub.html";
284+
sigmatch_table[ja4t_stub_keyword_id].Match = DetectJa4tStubPacketMatch;
285+
sigmatch_table[ja4t_stub_keyword_id].Setup = DetectJa4tStubSetup;
286+
sigmatch_table[ja4t_stub_keyword_id].Free = DetectJa4tStubFree;
287+
sigmatch_table[ja4t_stub_keyword_id].flags =
288+
(SIGMATCH_QUOTES_OPTIONAL | SIGMATCH_HANDLE_NEGATION);
289+
}
290+
291+
/* Plugin initialization */
292+
static void Ja4tStubInit(void)
293+
{
294+
SCLogNotice("Initializing JA4T stub plugin - example of Suricata callback APIs");
295+
296+
/* Register thread storage for per-thread data */
297+
thread_storage_id = ThreadStorageRegister("ja4t_stub", sizeof(void *), NULL, ThreadStorageFree);
298+
if (thread_storage_id.id < 0) {
299+
FatalError("Failed to register JA4T stub thread storage");
300+
}
301+
302+
/* Register flow storage for per-flow data */
303+
flow_storage_id = FlowStorageRegister("ja4t_stub", sizeof(void *), NULL, FlowStorageFree);
304+
if (flow_storage_id.id < 0) {
305+
FatalError("Failed to register JA4T stub flow storage");
306+
}
307+
308+
/* Register flow lifecycle callbacks */
309+
SCFlowRegisterInitCallback(OnFlowInit, NULL);
310+
SCFlowRegisterUpdateCallback(OnFlowUpdate, NULL);
311+
SCFlowRegisterFinishCallback(OnFlowFinish, NULL);
312+
313+
/* Register thread init callback */
314+
SCThreadRegisterInitCallback(OnThreadInit, NULL);
315+
316+
/* Register an EVE callback for JSON output */
317+
SCEveRegisterCallback(EveCallback, NULL);
318+
319+
/* Register detection keyword */
320+
Ja4tStubInitKeyword();
321+
322+
SCLogNotice("JA4T stub plugin initialized successfully");
323+
}
324+
325+
/* Plugin registration structure */
326+
const SCPlugin PluginRegistration = {
327+
.version = SC_API_VERSION,
328+
.suricata_version = SC_PACKAGE_VERSION,
329+
.name = "ja4t-stub",
330+
.author = "Open Information Security Foundation",
331+
.license = "GPLv2",
332+
.Init = Ja4tStubInit,
333+
};
334+
335+
/* Entry point for plugin loading */
336+
const SCPlugin *SCPluginRegister(void)
337+
{
338+
return &PluginRegistration;
339+
}

0 commit comments

Comments
 (0)