|
| 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