Coverage Report

Created: 2025-07-23 07:29

/src/suricata7/src/output-json-ftp.c
Line
Count
Source (jump to first uncovered line)
1
/* Copyright (C) 2017-2021 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 Jeff Lucovsky <jeff@lucovsky.org>
22
 *
23
 * Implement JSON/eve logging app-layer FTP.
24
 */
25
26
#include "suricata-common.h"
27
#include "detect.h"
28
#include "pkt-var.h"
29
#include "conf.h"
30
31
#include "threads.h"
32
#include "threadvars.h"
33
#include "tm-threads.h"
34
35
#include "util-unittest.h"
36
#include "util-buffer.h"
37
#include "util-debug.h"
38
#include "util-mem.h"
39
40
#include "output.h"
41
#include "output-json.h"
42
43
#include "app-layer.h"
44
#include "app-layer-parser.h"
45
46
#include "app-layer-ftp.h"
47
#include "output-json-ftp.h"
48
49
static void EveFTPLogCommand(FTPTransaction *tx, JsonBuilder *jb)
50
21.7k
{
51
    /* Preallocate array objects to simplify failure case */
52
21.7k
    JsonBuilder *js_resplist = NULL;
53
21.7k
    if (!TAILQ_EMPTY(&tx->response_list)) {
54
13.5k
        js_resplist = jb_new_array();
55
56
13.5k
        if (unlikely(js_resplist == NULL)) {
57
0
            return;
58
0
        }
59
13.5k
    }
60
21.7k
    jb_set_string(jb, "command", tx->command_descriptor->command_name);
61
21.7k
    uint32_t min_length = tx->command_descriptor->command_length + 1; /* command + space */
62
21.7k
    if (tx->request_length > min_length) {
63
11.4k
        jb_set_string_from_bytes(jb,
64
11.4k
                "command_data",
65
11.4k
                (const uint8_t *)tx->request + min_length,
66
11.4k
                tx->request_length - min_length - 1);
67
11.4k
        if (tx->request_truncated) {
68
226
            JB_SET_TRUE(jb, "command_truncated");
69
11.2k
        } else {
70
11.2k
            JB_SET_FALSE(jb, "command_truncated");
71
11.2k
        }
72
11.4k
    }
73
74
21.7k
    bool reply_truncated = false;
75
76
21.7k
    if (!TAILQ_EMPTY(&tx->response_list)) {
77
13.5k
        int resp_cnt = 0;
78
13.5k
        FTPString *response;
79
13.5k
        bool is_cc_array_open = false;
80
68.2k
        TAILQ_FOREACH(response, &tx->response_list, next) {
81
            /* handle multiple lines within the response, \r\n delimited */
82
68.2k
            uint8_t *where = response->str;
83
68.2k
            uint16_t length = 0;
84
68.2k
            uint16_t pos;
85
68.2k
            if (response->len > 0 && response->len <= UINT16_MAX) {
86
66.4k
                length = (uint16_t)response->len - 1;
87
66.4k
            } else if (response->len > UINT16_MAX) {
88
0
                length = UINT16_MAX;
89
0
            }
90
68.2k
            if (!reply_truncated && response->truncated) {
91
370
                reply_truncated = true;
92
370
            }
93
132k
            while ((pos = JsonGetNextLineFromBuffer((const char *)where, length)) != UINT16_MAX) {
94
64.2k
                uint16_t offset = 0;
95
                /* Try to find a completion code for this line */
96
64.2k
                if (pos >= 3)  {
97
                    /* Gather the completion code if present */
98
54.8k
                    if (isdigit(where[0]) && isdigit(where[1]) && isdigit(where[2])) {
99
16.0k
                        if (!is_cc_array_open) {
100
513
                            jb_open_array(jb, "completion_code");
101
513
                            is_cc_array_open = true;
102
513
                        }
103
16.0k
                        jb_append_string_from_bytes(jb, (const uint8_t *)where, 3);
104
16.0k
                        offset = 4;
105
16.0k
                    }
106
54.8k
                }
107
                /* move past 3 character completion code */
108
64.2k
                if (pos >= offset) {
109
63.8k
                    jb_append_string_from_bytes(js_resplist, (const uint8_t *)where + offset, pos - offset);
110
63.8k
                    resp_cnt++;
111
63.8k
                }
112
113
64.2k
                where += pos;
114
64.2k
                length -= pos;
115
64.2k
            }
116
68.2k
        }
117
118
13.5k
        if (is_cc_array_open) {
119
513
            jb_close(jb);
120
513
        }
121
13.5k
        if (resp_cnt) {
122
12.7k
            jb_close(js_resplist);
123
12.7k
            jb_set_object(jb, "reply", js_resplist);
124
12.7k
        }
125
13.5k
        jb_free(js_resplist);
126
13.5k
    }
127
128
21.7k
    if (tx->dyn_port) {
129
98
        jb_set_uint(jb, "dynamic_port", tx->dyn_port);
130
98
    }
131
132
21.7k
    if (tx->command_descriptor->command == FTP_COMMAND_PORT ||
133
21.7k
        tx->command_descriptor->command == FTP_COMMAND_EPRT) {
134
6.19k
        if (tx->active) {
135
31
            JB_SET_STRING(jb, "mode", "active");
136
6.16k
        } else {
137
6.16k
            JB_SET_STRING(jb, "mode", "passive");
138
6.16k
        }
139
6.19k
    }
140
141
21.7k
    if (tx->done) {
142
16.4k
        JB_SET_STRING(jb, "reply_received", "yes");
143
16.4k
    } else {
144
5.29k
        JB_SET_STRING(jb, "reply_received", "no");
145
5.29k
    }
146
147
21.7k
    if (reply_truncated) {
148
370
        JB_SET_TRUE(jb, "reply_truncated");
149
21.3k
    } else {
150
21.3k
        JB_SET_FALSE(jb, "reply_truncated");
151
21.3k
    }
152
21.7k
}
153
154
155
static int JsonFTPLogger(ThreadVars *tv, void *thread_data,
156
    const Packet *p, Flow *f, void *state, void *vtx, uint64_t tx_id)
157
21.7k
{
158
21.7k
    SCEnter();
159
21.7k
    OutputJsonThreadCtx *thread = thread_data;
160
161
21.7k
    const char *event_type;
162
21.7k
    if (f->alproto == ALPROTO_FTPDATA) {
163
0
        event_type = "ftp_data";
164
21.7k
    } else {
165
21.7k
        event_type = "ftp";
166
21.7k
    }
167
21.7k
    FTPTransaction *tx = vtx;
168
169
21.7k
    JsonBuilder *jb =
170
21.7k
            CreateEveHeaderWithTxId(p, LOG_DIR_FLOW, event_type, NULL, tx_id, thread->ctx);
171
21.7k
    if (likely(jb)) {
172
21.7k
        jb_open_object(jb, event_type);
173
21.7k
        if (f->alproto == ALPROTO_FTPDATA) {
174
0
            EveFTPDataAddMetadata(f, jb);
175
21.7k
        } else {
176
21.7k
            EveFTPLogCommand(tx, jb);
177
21.7k
        }
178
179
21.7k
        if (!jb_close(jb)) {
180
0
            goto fail;
181
0
        }
182
183
21.7k
        OutputJsonBuilderBuffer(jb, thread);
184
185
21.7k
        jb_free(jb);
186
21.7k
    }
187
21.7k
    return TM_ECODE_OK;
188
189
0
fail:
190
0
    jb_free(jb);
191
0
    return TM_ECODE_FAILED;
192
21.7k
}
193
194
static OutputInitResult OutputFTPLogInitSub(ConfNode *conf,
195
    OutputCtx *parent_ctx)
196
4
{
197
4
    AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_FTP);
198
4
    AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_FTPDATA);
199
4
    return OutputJsonLogInitSub(conf, parent_ctx);
200
4
}
201
202
void JsonFTPLogRegister(void)
203
33
{
204
    /* Register as an eve sub-module. */
205
33
    OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonFTPLog", "eve-log.ftp",
206
33
            OutputFTPLogInitSub, ALPROTO_FTP, JsonFTPLogger, JsonLogThreadInit, JsonLogThreadDeinit,
207
33
            NULL);
208
33
    OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonFTPLog", "eve-log.ftp",
209
33
            OutputFTPLogInitSub, ALPROTO_FTPDATA, JsonFTPLogger, JsonLogThreadInit,
210
33
            JsonLogThreadDeinit, NULL);
211
212
33
    SCLogDebug("FTP JSON logger registered.");
213
33
}