Skip to content

Commit a5ba063

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 a5ba063

4 files changed

Lines changed: 262 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: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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 "detect.h"
28+
#include "suricata-common.h"
29+
#include "suricata-plugin.h"
30+
31+
#include "decode-tcp.h"
32+
#include "detect-engine-helper.h"
33+
#include "detect-parse.h"
34+
#include "flow-callbacks.h"
35+
#include "flow-storage.h"
36+
#include "output-eve.h"
37+
#include "util-debug.h"
38+
39+
static FlowStorageId flow_storage_id = { .id = -1 };
40+
static int ja4t_stub_keyword_id = -1;
41+
42+
/* Per-thread context structure */
43+
struct Ja4tThreadContext {
44+
uint64_t packet_count;
45+
};
46+
47+
/* Per-flow context structure */
48+
struct Ja4tFlowContext {
49+
};
50+
51+
/* Detection keyword data structure */
52+
typedef struct DetectJa4tStubData_ {
53+
uint32_t value;
54+
bool negated;
55+
} DetectJa4tStubData;
56+
57+
/* Free flow storage */
58+
static void FlowStorageFree(void *ptr)
59+
{
60+
SCLogDebug("Free'ing JA4T stub flow storage");
61+
struct Ja4tFlowContext *ctx = ptr;
62+
SCFree(ctx);
63+
}
64+
65+
/* Flow initialization callback - called when a new flow is created.
66+
* Only processes SYN-only packets (SYN set, ACK not set). */
67+
static void OnFlowInit(ThreadVars *tv, Flow *f, const Packet *p, void *_data)
68+
{
69+
/* Only process TCP SYN-only packets (not SYN-ACK) */
70+
if (!PacketIsTCP(p)) {
71+
return;
72+
}
73+
74+
const TCPHdr *tcph = PacketGetTCP(p);
75+
if (tcph == NULL) {
76+
return;
77+
}
78+
79+
/* Only process client SYN packets (without ACK) */
80+
if (!TCP_ISSET_FLAG_SYN(p) || TCP_ISSET_FLAG_ACK(p)) {
81+
return;
82+
}
83+
84+
/* Do we already have JA4T data? */
85+
if (FlowGetStorageById(f, flow_storage_id) != NULL) {
86+
/* We do, just return. */
87+
return;
88+
}
89+
90+
struct Ja4tFlowContext *flowctx = SCCalloc(1, sizeof(*flowctx));
91+
if (flowctx == NULL) {
92+
FatalError("Failed to allocate JA4T stub flow context");
93+
}
94+
95+
FlowSetStorageById(f, flow_storage_id, flowctx);
96+
97+
SCLogDebug("JA4T stub: Flow initialized for SYN-only packet");
98+
}
99+
100+
/* Detection keyword packet match callback */
101+
static int DetectJa4tStubPacketMatch(
102+
DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx)
103+
{
104+
const Flow *f = p->flow;
105+
struct Ja4tFlowContext *flowctx = FlowGetStorageById(f, flow_storage_id);
106+
const DetectJa4tStubData *data = (const DetectJa4tStubData *)ctx;
107+
108+
SCEnter();
109+
110+
if (f == NULL) {
111+
SCLogDebug("packet %" PRIu64 ": no flow", PcapPacketCntGet(p));
112+
SCReturnInt(0);
113+
}
114+
115+
if (flowctx == NULL) {
116+
SCReturnInt(0);
117+
}
118+
119+
/* Never match... */
120+
bool r = false;
121+
122+
if (r) {
123+
SCLogDebug("JA4T stub keyword match on packet_count >= %u", data->value);
124+
SCReturnInt(1);
125+
}
126+
127+
SCReturnInt(0);
128+
}
129+
130+
/* Setup detection keyword */
131+
static int DetectJa4tStubSetup(DetectEngineCtx *de_ctx, Signature *s, const char *arg)
132+
{
133+
// JA4T only works with TCP packets, reject UDP and other protocols
134+
if (!(DetectProtoContainsProto(&s->proto, IPPROTO_TCP))) {
135+
SCLogError("ja4t.hash keyword can only be used with TCP based rules");
136+
return -1;
137+
}
138+
139+
s->flags |= SIG_FLAG_REQUIRE_PACKET;
140+
141+
/* TODO */
142+
143+
return 0;
144+
}
145+
146+
/* Free detection keyword data */
147+
static void DetectJa4tStubFree(DetectEngineCtx *de_ctx, void *ptr)
148+
{
149+
SCFree(ptr);
150+
}
151+
152+
/* EVE output callback - adds data to EVE JSON output */
153+
static void EveCallback(ThreadVars *tv, const Packet *p, Flow *f, SCJsonBuilder *jb, void *data)
154+
{
155+
/* EVE callback requires a flow */
156+
if (f == NULL) {
157+
return;
158+
}
159+
160+
struct Ja4tFlowContext *flowctx = FlowGetStorageById(f, flow_storage_id);
161+
162+
if (flowctx == NULL) {
163+
return;
164+
}
165+
166+
SCLogDebug("JA4T stub EVE callback: tv=%p, p=%p, f=%p", tv, p, f);
167+
168+
/* Open a JA4T stub object in the EVE output */
169+
SCJbOpenObject(jb, "ja4t_stub");
170+
171+
/* Nothing added. */
172+
173+
/* Close the JA4T stub object */
174+
SCJbClose(jb);
175+
}
176+
177+
/* Initialize detection keyword */
178+
static void Ja4tStubInitKeyword(void)
179+
{
180+
ja4t_stub_keyword_id = SCDetectHelperNewKeywordId();
181+
SCLogDebug("Registered new ja4t-stub keyword with ID %" PRIu32, ja4t_stub_keyword_id);
182+
sigmatch_table[ja4t_stub_keyword_id].name = "ja4t-stub";
183+
sigmatch_table[ja4t_stub_keyword_id].desc = "match on JA4T stub flow statistics";
184+
sigmatch_table[ja4t_stub_keyword_id].url = "/rules/ja4t-stub.html";
185+
sigmatch_table[ja4t_stub_keyword_id].Match = DetectJa4tStubPacketMatch;
186+
sigmatch_table[ja4t_stub_keyword_id].Setup = DetectJa4tStubSetup;
187+
sigmatch_table[ja4t_stub_keyword_id].Free = DetectJa4tStubFree;
188+
sigmatch_table[ja4t_stub_keyword_id].flags =
189+
(SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER);
190+
}
191+
192+
/* Plugin initialization */
193+
static void Ja4tStubInit(void)
194+
{
195+
SCLogNotice("Initializing JA4T stub plugin - example of Suricata callback APIs");
196+
197+
/* Register flow storage for per-flow data */
198+
flow_storage_id = FlowStorageRegister("ja4t_stub", sizeof(void *), NULL, FlowStorageFree);
199+
if (flow_storage_id.id < 0) {
200+
FatalError("Failed to register JA4T stub flow storage");
201+
}
202+
203+
/* Register flow lifecycle callbacks */
204+
SCFlowRegisterInitCallback(OnFlowInit, NULL);
205+
206+
/* Register an EVE callback for JSON output */
207+
SCEveRegisterCallback(EveCallback, NULL);
208+
209+
/* Register detection keyword */
210+
Ja4tStubInitKeyword();
211+
212+
SCLogNotice("JA4T stub plugin initialized successfully");
213+
}
214+
215+
/* Plugin registration structure */
216+
const SCPlugin PluginRegistration = {
217+
.version = SC_API_VERSION,
218+
.suricata_version = SC_PACKAGE_VERSION,
219+
.name = "ja4t-stub",
220+
.plugin_version = "0.1.0",
221+
.license = "GPLv2",
222+
.author = "FooBar",
223+
.Init = Ja4tStubInit,
224+
};
225+
226+
/* Entry point for plugin loading */
227+
const SCPlugin *SCPluginRegister(void)
228+
{
229+
return &PluginRegistration;
230+
}

0 commit comments

Comments
 (0)