Coverage Report

Created: 2025-07-23 07:29

/src/suricata7/src/output-json-email-common.c
Line
Count
Source (jump to first uncovered line)
1
/* Copyright (C) 2007-2015 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
 * \file
20
 *
21
 * \author Tom DeCanio <td@npulsetech.com>
22
 * \author Eric Leblond <eric@regit.org>
23
 *
24
 * Implements json common email logging portion of the engine.
25
 */
26
27
#include "suricata-common.h"
28
#include "detect.h"
29
#include "pkt-var.h"
30
#include "conf.h"
31
#include "suricata.h"
32
33
#include "threads.h"
34
#include "threadvars.h"
35
#include "tm-threads.h"
36
#include "tm-threads-common.h"
37
38
#include "util-print.h"
39
#include "util-unittest.h"
40
41
#include "util-debug.h"
42
#include "app-layer-parser.h"
43
#include "output.h"
44
#include "app-layer-smtp.h"
45
#include "app-layer.h"
46
#include "util-privs.h"
47
#include "util-buffer.h"
48
#include "util-byte.h"
49
50
#include "util-logopenfile.h"
51
52
#include "output-json.h"
53
#include "output-json-email-common.h"
54
55
#define LOG_EMAIL_DEFAULT       0
56
6.53k
#define LOG_EMAIL_EXTENDED      (1<<0)
57
0
#define LOG_EMAIL_ARRAY         (1<<1) /* require array handling */
58
0
#define LOG_EMAIL_COMMA         (1<<2) /* require array handling */
59
561
#define LOG_EMAIL_BODY_MD5      (1<<3)
60
561
#define LOG_EMAIL_SUBJECT_MD5   (1<<4)
61
62
struct {
63
    const char *config_field;
64
    const char *email_field;
65
    uint32_t flags;
66
} email_fields[] =  {
67
    { "reply_to", "reply-to", LOG_EMAIL_DEFAULT },
68
    { "bcc", "bcc", LOG_EMAIL_COMMA|LOG_EMAIL_EXTENDED },
69
    { "message_id", "message-id", LOG_EMAIL_EXTENDED },
70
    { "subject", "subject", LOG_EMAIL_EXTENDED },
71
    { "x_mailer", "x-mailer", LOG_EMAIL_EXTENDED },
72
    { "user_agent", "user-agent", LOG_EMAIL_EXTENDED },
73
    { "received", "received", LOG_EMAIL_ARRAY },
74
    { "x_originating_ip", "x-originating-ip", LOG_EMAIL_DEFAULT },
75
    { "in_reply_to",  "in-reply-to", LOG_EMAIL_DEFAULT },
76
    { "references",  "references", LOG_EMAIL_DEFAULT },
77
    { "importance",  "importance", LOG_EMAIL_DEFAULT },
78
    { "priority",  "priority", LOG_EMAIL_DEFAULT },
79
    { "sensitivity",  "sensitivity", LOG_EMAIL_DEFAULT },
80
    { "organization",  "organization", LOG_EMAIL_DEFAULT },
81
    { "content_md5",  "content-md5", LOG_EMAIL_DEFAULT },
82
    { "date", "date", LOG_EMAIL_DEFAULT },
83
    { NULL, NULL, LOG_EMAIL_DEFAULT},
84
};
85
86
static inline char *SkipWhiteSpaceTill(char *p, char *savep)
87
1.46k
{
88
1.46k
    char *sp = p;
89
1.46k
    if (unlikely(p == NULL)) {
90
0
        return NULL;
91
0
    }
92
1.46k
    while (((*sp == '\t') || (*sp == ' ')) && (sp < savep)) {
93
0
        sp++;
94
0
    }
95
1.46k
    return sp;
96
1.46k
}
97
98
static bool EveEmailJsonArrayFromCommaList(JsonBuilder *js, const uint8_t *val, size_t len)
99
1.47k
{
100
1.47k
    bool has_not_empty_field = false;
101
1.47k
    size_t start = 0;
102
1.47k
    int state = 0;
103
104
35.3k
    for (size_t i = 0; i < len; i++) {
105
33.8k
        switch (state) {
106
1.47k
            case 0:
107
1.47k
                if (val[i] == ' ' || val[i] == '\t') {
108
                    // skip leading space
109
0
                    start += 1;
110
1.47k
                } else if (val[i] == '"') {
111
                    // quoted state
112
0
                    state = 2;
113
1.47k
                } else {
114
                    // field
115
1.47k
                    state = 1;
116
1.47k
                }
117
1.47k
                break;
118
32.4k
            case 1: // field
119
32.4k
                if (val[i] == ',') {
120
3
                    if (i > start) {
121
3
                        jb_append_string_from_bytes(js, val + start, i - start);
122
3
                        has_not_empty_field = true;
123
3
                    }
124
3
                    start = i + 1;
125
3
                    state = 0;
126
32.4k
                } else if (val[i] == '"') {
127
                    // quoted
128
0
                    state = 2;
129
0
                }
130
32.4k
                break;
131
0
            case 2: // quoted
132
0
                if (val[i] == '"') {
133
                    // out of quotes, back to field
134
0
                    state = 1;
135
0
                }
136
33.8k
        }
137
33.8k
    }
138
1.47k
    if (len > start) {
139
1.47k
        jb_append_string_from_bytes(js, val + start, len - start);
140
1.47k
        has_not_empty_field = true;
141
1.47k
    }
142
1.47k
    return has_not_empty_field;
143
1.47k
}
144
145
static void EveEmailLogJSONMd5(OutputJsonEmailCtx *email_ctx, JsonBuilder *js, SMTPTransaction *tx)
146
561
{
147
561
    if (email_ctx->flags & LOG_EMAIL_SUBJECT_MD5) {
148
0
        MimeDecEntity *entity = tx->msg_tail;
149
0
        if (entity == NULL) {
150
0
            return;
151
0
        }
152
0
        MimeDecField *field = MimeDecFindField(entity, "subject");
153
0
        if (field != NULL) {
154
0
            char smd5[SC_MD5_HEX_LEN + 1];
155
0
            SCMd5HashBufferToHex((uint8_t *)field->value, field->value_len, smd5, sizeof(smd5));
156
0
            jb_set_string(js, "subject_md5", smd5);
157
0
        }
158
0
    }
159
160
561
    if (email_ctx->flags & LOG_EMAIL_BODY_MD5) {
161
0
        MimeDecParseState *mime_state = tx->mime_state;
162
0
        if (mime_state && mime_state->has_md5 && (mime_state->state_flag == PARSE_DONE)) {
163
0
            jb_set_hex(js, "body_md5", mime_state->md5, (uint32_t)sizeof(mime_state->md5));
164
0
        }
165
0
    }
166
561
}
167
168
static int JsonEmailAddToJsonArray(const uint8_t *val, size_t len, void *data)
169
0
{
170
0
    JsonBuilder *ajs = data;
171
172
0
    if (ajs == NULL)
173
0
        return 0;
174
0
    jb_append_string_from_bytes(ajs, val, (uint32_t)len);
175
0
    return 1;
176
0
}
177
178
static void EveEmailLogJSONCustom(OutputJsonEmailCtx *email_ctx, JsonBuilder *js, SMTPTransaction *tx)
179
0
{
180
0
    int f = 0;
181
0
    JsonBuilderMark mark = { 0, 0, 0 };
182
0
    MimeDecField *field;
183
0
    MimeDecEntity *entity = tx->msg_tail;
184
0
    if (entity == NULL) {
185
0
        return;
186
0
    }
187
188
0
    while(email_fields[f].config_field) {
189
0
        if (((email_ctx->fields & (1ULL<<f)) != 0)
190
0
              ||
191
0
              ((email_ctx->flags & LOG_EMAIL_EXTENDED) && (email_fields[f].flags & LOG_EMAIL_EXTENDED))
192
0
           ) {
193
0
            if (email_fields[f].flags & LOG_EMAIL_ARRAY) {
194
0
                jb_get_mark(js, &mark);
195
0
                jb_open_array(js, email_fields[f].config_field);
196
0
                int found = MimeDecFindFieldsForEach(entity, email_fields[f].email_field, JsonEmailAddToJsonArray, js);
197
0
                if (found > 0) {
198
0
                    jb_close(js);
199
0
                } else {
200
0
                    jb_restore_mark(js, &mark);
201
0
                }
202
0
            } else if (email_fields[f].flags & LOG_EMAIL_COMMA) {
203
0
                field = MimeDecFindField(entity, email_fields[f].email_field);
204
0
                if (field) {
205
0
                    jb_get_mark(js, &mark);
206
0
                    jb_open_array(js, email_fields[f].config_field);
207
0
                    if (EveEmailJsonArrayFromCommaList(js, field->value, field->value_len)) {
208
0
                        jb_close(js);
209
0
                    } else {
210
0
                        jb_restore_mark(js, &mark);
211
0
                    }
212
0
                }
213
0
            } else {
214
0
                field = MimeDecFindField(entity, email_fields[f].email_field);
215
0
                if (field != NULL) {
216
0
                    jb_set_string_from_bytes(
217
0
                            js, email_fields[f].config_field, field->value, field->value_len);
218
0
                }
219
0
            }
220
221
0
        }
222
0
        f++;
223
0
    }
224
0
}
225
226
/* JSON format logging */
227
static bool EveEmailLogJsonData(const Flow *f, void *state, void *vtx, uint64_t tx_id, JsonBuilder *sjs)
228
2.51k
{
229
2.51k
    SMTPState *smtp_state;
230
2.51k
    MimeDecParseState *mime_state;
231
2.51k
    MimeDecEntity *entity;
232
2.51k
    JsonBuilderMark mark = { 0, 0, 0 };
233
234
    /* check if we have SMTP state or not */
235
2.51k
    AppProto proto = FlowGetAppProtocol(f);
236
2.51k
    switch (proto) {
237
2.51k
        case ALPROTO_SMTP:
238
2.51k
            smtp_state = (SMTPState *)state;
239
2.51k
            if (smtp_state == NULL) {
240
0
                SCLogDebug("no smtp state, so no request logging");
241
0
                jb_free(sjs);
242
0
                SCReturnPtr(NULL, "JsonBuilder");
243
0
            }
244
2.51k
            SMTPTransaction *tx = vtx;
245
2.51k
            mime_state = tx->mime_state;
246
2.51k
            entity = tx->msg_tail;
247
2.51k
            SCLogDebug("lets go mime_state %p, entity %p, state_flag %u", mime_state, entity, mime_state ? mime_state->state_flag : 0);
248
2.51k
            break;
249
0
        default:
250
            /* don't know how we got here */
251
0
            SCReturnBool(false);
252
2.51k
    }
253
2.51k
    if ((mime_state != NULL)) {
254
2.05k
        if (entity == NULL) {
255
0
            SCReturnBool(false);
256
0
        }
257
258
2.05k
        jb_set_string(sjs, "status", MimeDecParseStateGetStatus(mime_state));
259
260
2.05k
        MimeDecField *field;
261
262
        /* From: */
263
2.05k
        field = MimeDecFindField(entity, "from");
264
2.05k
        if (field != NULL) {
265
1.46k
            char *s = BytesToString((uint8_t *)field->value,
266
1.46k
                                    (size_t)field->value_len);
267
1.46k
            if (likely(s != NULL)) {
268
                //printf("From: \"%s\"\n", s);
269
1.46k
                char * sp = SkipWhiteSpaceTill(s, s + strlen(s));
270
1.46k
                jb_set_string(sjs, "from", sp);
271
1.46k
                SCFree(s);
272
1.46k
            }
273
1.46k
        }
274
275
        /* To: */
276
2.05k
        field = MimeDecFindField(entity, "to");
277
2.05k
        if (field != NULL) {
278
1.47k
            jb_get_mark(sjs, &mark);
279
1.47k
            jb_open_array(sjs, "to");
280
1.47k
            if (EveEmailJsonArrayFromCommaList(sjs, field->value, field->value_len)) {
281
1.47k
                jb_close(sjs);
282
1.47k
            } else {
283
0
                jb_restore_mark(sjs, &mark);
284
0
            }
285
1.47k
        }
286
287
        /* Cc: */
288
2.05k
        field = MimeDecFindField(entity, "cc");
289
2.05k
        if (field != NULL) {
290
0
            jb_get_mark(sjs, &mark);
291
0
            jb_open_array(sjs, "cc");
292
0
            if (EveEmailJsonArrayFromCommaList(sjs, field->value, field->value_len)) {
293
0
                jb_close(sjs);
294
0
            } else {
295
0
                jb_restore_mark(sjs, &mark);
296
0
            }
297
0
        }
298
299
2.05k
        if (mime_state->stack == NULL || mime_state->stack->top == NULL || mime_state->stack->top->data == NULL) {
300
0
            SCReturnBool(false);
301
0
        }
302
303
2.05k
        entity = (MimeDecEntity *)mime_state->stack->top->data;
304
2.05k
        int attach_cnt = 0;
305
2.05k
        int url_cnt = 0;
306
2.05k
        JsonBuilder *js_attach = jb_new_array();
307
2.05k
        JsonBuilder *js_url = jb_new_array();
308
2.05k
        if (entity->url_list != NULL) {
309
0
            MimeDecUrl *url;
310
0
            bool has_ipv6_url = false;
311
0
            bool has_ipv4_url = false;
312
0
            bool has_exe_url = false;
313
0
            for (url = entity->url_list; url != NULL; url = url->next) {
314
0
                jb_append_string_from_bytes(js_url, url->url, url->url_len);
315
0
                if (url->url_flags & URL_IS_EXE)
316
0
                    has_exe_url = true;
317
0
                if (url->url_flags & URL_IS_IP6)
318
0
                    has_ipv6_url = true;
319
0
                if (url->url_flags & URL_IS_IP4)
320
0
                    has_ipv6_url = true;
321
0
                url_cnt += 1;
322
0
            }
323
0
            jb_set_bool(sjs, "has_ipv6_url", has_ipv6_url);
324
0
            jb_set_bool(sjs, "has_ipv4_url", has_ipv4_url);
325
0
            jb_set_bool(sjs, "has_exe_url", has_exe_url);
326
0
        }
327
2.05k
        for (entity = entity->child; entity != NULL; entity = entity->next) {
328
0
            if (entity->ctnt_flags & CTNT_IS_ATTACHMENT) {
329
0
                jb_append_string_from_bytes(js_attach, entity->filename, entity->filename_len);
330
0
                attach_cnt += 1;
331
0
            }
332
0
            if (entity->url_list != NULL) {
333
0
                MimeDecUrl *url;
334
0
                for (url = entity->url_list; url != NULL; url = url->next) {
335
0
                    jb_append_string_from_bytes(js_url, url->url, url->url_len);
336
0
                    url_cnt += 1;
337
0
                }
338
0
            }
339
0
        }
340
2.05k
        if (attach_cnt > 0) {
341
0
            jb_close(js_attach);
342
0
            jb_set_object(sjs, "attachment", js_attach);
343
0
        }
344
2.05k
        jb_free(js_attach);
345
2.05k
        if (url_cnt > 0) {
346
0
            jb_close(js_url);
347
0
            jb_set_object(sjs, "url", js_url);
348
0
        }
349
2.05k
        jb_free(js_url);
350
2.05k
        SCReturnBool(true);
351
2.05k
    }
352
353
2.51k
    SCReturnBool(false);
354
2.51k
}
355
356
/* JSON format logging */
357
TmEcode EveEmailLogJson(JsonEmailLogThread *aft, JsonBuilder *js, const Packet *p, Flow *f, void *state, void *vtx, uint64_t tx_id)
358
8.72k
{
359
8.72k
    OutputJsonEmailCtx *email_ctx = aft->emaillog_ctx;
360
8.72k
    SMTPTransaction *tx = (SMTPTransaction *) vtx;
361
8.72k
    JsonBuilderMark mark = { 0, 0, 0 };
362
363
8.72k
    jb_get_mark(js, &mark);
364
8.72k
    jb_open_object(js, "email");
365
8.72k
    if (!EveEmailLogJsonData(f, state, vtx, tx_id, js)) {
366
2.19k
        jb_restore_mark(js, &mark);
367
2.19k
        SCReturnInt(TM_ECODE_FAILED);
368
2.19k
    }
369
370
6.53k
    if ((email_ctx->flags & LOG_EMAIL_EXTENDED) || (email_ctx->fields != 0))
371
0
        EveEmailLogJSONCustom(email_ctx, js, tx);
372
373
6.53k
    if (!g_disable_hashing) {
374
6.53k
        EveEmailLogJSONMd5(email_ctx, js, tx);
375
6.53k
    }
376
377
6.53k
    jb_close(js);
378
6.53k
    SCReturnInt(TM_ECODE_OK);
379
8.72k
}
380
381
bool EveEmailAddMetadata(const Flow *f, uint32_t tx_id, JsonBuilder *js)
382
3.86k
{
383
3.86k
    SMTPState *smtp_state = (SMTPState *)FlowGetAppState(f);
384
3.86k
    if (smtp_state) {
385
3.86k
        SMTPTransaction *tx = AppLayerParserGetTx(IPPROTO_TCP, ALPROTO_SMTP, smtp_state, tx_id);
386
3.86k
        if (tx) {
387
3.83k
            return EveEmailLogJsonData(f, smtp_state, tx, tx_id, js);
388
3.83k
        }
389
3.86k
    }
390
391
28
    return false;
392
3.86k
}
393
394
void OutputEmailInitConf(ConfNode *conf, OutputJsonEmailCtx *email_ctx)
395
2
{
396
2
    if (conf) {
397
2
        const char *extended = ConfNodeLookupChildValue(conf, "extended");
398
399
2
        if (extended != NULL) {
400
2
            if (ConfValIsTrue(extended)) {
401
2
                email_ctx->flags = LOG_EMAIL_EXTENDED;
402
2
            }
403
2
        }
404
405
2
        email_ctx->fields  = 0;
406
2
        ConfNode *custom;
407
2
        if ((custom = ConfNodeLookupChild(conf, "custom")) != NULL) {
408
0
            ConfNode *field;
409
0
            TAILQ_FOREACH (field, &custom->head, next) {
410
0
                int f = 0;
411
0
                while (email_fields[f].config_field) {
412
0
                    if ((strcmp(email_fields[f].config_field, field->val) == 0) ||
413
0
                            (strcasecmp(email_fields[f].email_field, field->val) == 0)) {
414
0
                        email_ctx->fields |= (1ULL << f);
415
0
                        break;
416
0
                    }
417
0
                    f++;
418
0
                }
419
0
            }
420
0
        }
421
422
2
        email_ctx->flags  = 0;
423
2
        ConfNode *md5_conf;
424
2
        if ((md5_conf = ConfNodeLookupChild(conf, "md5")) != NULL) {
425
0
            ConfNode *field;
426
0
            TAILQ_FOREACH (field, &md5_conf->head, next) {
427
0
                if (strcmp("body", field->val) == 0) {
428
0
                    SCLogInfo("Going to log the md5 sum of email body");
429
0
                    email_ctx->flags |= LOG_EMAIL_BODY_MD5;
430
0
                }
431
0
                if (strcmp("subject", field->val) == 0) {
432
0
                    SCLogInfo("Going to log the md5 sum of email subject");
433
0
                    email_ctx->flags |= LOG_EMAIL_SUBJECT_MD5;
434
0
                }
435
0
            }
436
0
        }
437
2
    }
438
2
    return;
439
2
}