Coverage Report

Created: 2026-09-06 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/suricata8/src/output-filestore.c
Line
Count
Source
1
/* Copyright (C) 2018-2022 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
#include "suricata-common.h"
19
#include "output-filestore.h"
20
21
#include "stream-tcp.h"
22
23
#include "feature.h"
24
25
#include "output.h"
26
#include "output-json-file.h"
27
28
#include "util-conf.h"
29
#include "util-misc.h"
30
#include "util-path.h"
31
#include "util-print.h"
32
33
78
#define MODULE_NAME "OutputFilestore"
34
35
/* Create a filestore specific PATH_MAX that is less than the system
36
 * PATH_MAX to prevent newer gcc truncation warnings with snprint. */
37
#define SHA256_STRING_LEN    (SC_SHA256_LEN * 2)
38
#define LEAF_DIR_MAX_LEN 4
39
#define FILESTORE_PREFIX_MAX (PATH_MAX - SHA256_STRING_LEN - LEAF_DIR_MAX_LEN)
40
41
/* The default log directory, relative to the default log
42
 * directory. */
43
static const char *default_log_dir = "filestore";
44
45
/* Atomic counter of simultaneously open files. */
46
static SC_ATOMIC_DECLARE(uint32_t, filestore_open_file_cnt);
47
48
typedef struct OutputFilestoreCtx_ {
49
    char prefix[FILESTORE_PREFIX_MAX];
50
    char tmpdir[FILESTORE_PREFIX_MAX];
51
    bool fileinfo;
52
    HttpXFFCfg *xff_cfg;
53
} OutputFilestoreCtx;
54
55
typedef struct OutputFilestoreLogThread_ {
56
    OutputFilestoreCtx *ctx;
57
    uint16_t counter_max_hits;
58
    uint16_t fs_error_counter;
59
} OutputFilestoreLogThread;
60
61
enum WarnOnceTypes {
62
    WOT_OPEN,
63
    WOT_WRITE,
64
    WOT_UNLINK,
65
    WOT_RENAME,
66
    WOT_SNPRINTF,
67
68
    WOT_MAX,
69
};
70
71
/* For WARN_ONCE, a record of warnings that have already been
72
 * issued. */
73
static thread_local bool once_errs[WOT_MAX];
74
75
#define WARN_ONCE(wot_type, ...)                                                                   \
76
0
    do {                                                                                           \
77
0
        if (!once_errs[wot_type]) {                                                                \
78
0
            once_errs[wot_type] = true;                                                            \
79
0
            SCLogWarning(__VA_ARGS__);                                                             \
80
0
        }                                                                                          \
81
0
    } while (0)
82
83
static uint64_t OutputFilestoreOpenFilesCounter(void)
84
0
{
85
0
    return SC_ATOMIC_GET(filestore_open_file_cnt);
86
0
}
87
88
static uint32_t g_file_store_max_open_files = 0;
89
90
static void FileSetMaxOpenFiles(uint32_t count)
91
0
{
92
0
    g_file_store_max_open_files = count;
93
0
}
94
95
static uint32_t FileGetMaxOpenFiles(void)
96
238k
{
97
238k
    return g_file_store_max_open_files;
98
238k
}
99
100
/**
101
 * \brief Update the timestamps on a file to match those of another
102
 *     file.
103
 *
104
 * \param src_filename Filename to use as timestamp source.
105
 * \param filename Filename to apply timestamps to.
106
 */
107
static void OutputFilestoreUpdateFileTime(const char *src_filename,
108
        const char *filename)
109
106k
{
110
106k
    struct stat sb;
111
106k
    if (stat(src_filename, &sb) != 0) {
112
0
        SCLogDebug("Failed to stat %s: %s", filename, strerror(errno));
113
0
        return;
114
0
    }
115
106k
    struct utimbuf utimbuf = {
116
106k
        .actime = sb.st_atime,
117
106k
        .modtime = sb.st_mtime,
118
106k
    };
119
106k
    if (utime(filename, &utimbuf) != 0) {
120
0
        SCLogDebug("Failed to update file timestamps: %s: %s", filename,
121
0
                strerror(errno));
122
0
    }
123
106k
}
124
125
static void OutputFilestoreFinalizeFiles(ThreadVars *tv, const OutputFilestoreLogThread *oft,
126
        const OutputFilestoreCtx *ctx, const Packet *p, File *ff, void *tx, const uint64_t tx_id,
127
        uint8_t dir)
128
46
{
129
    /* Stringify the SHA256 which will be used in the final
130
     * filename. */
131
46
    char sha256string[(SC_SHA256_LEN * 2) + 1];
132
46
    PrintHexString(sha256string, sizeof(sha256string), ff->sha256,
133
46
            sizeof(ff->sha256));
134
135
46
    char tmp_filename[PATH_MAX] = "";
136
46
    snprintf(tmp_filename, sizeof(tmp_filename), "%s/file.%u", ctx->tmpdir,
137
46
            ff->file_store_id);
138
139
46
    char final_filename[PATH_MAX] = "";
140
46
    snprintf(final_filename, sizeof(final_filename), "%s/%c%c/%s",
141
46
            ctx->prefix, sha256string[0], sha256string[1], sha256string);
142
143
46
    if (SCPathExists(final_filename)) {
144
34
        OutputFilestoreUpdateFileTime(tmp_filename, final_filename);
145
34
        if (unlink(tmp_filename) != 0) {
146
0
            StatsIncr(tv, oft->fs_error_counter);
147
0
            WARN_ONCE(WOT_UNLINK, "Failed to remove temporary file %s: %s", tmp_filename,
148
0
                    strerror(errno));
149
0
        }
150
34
    } else if (rename(tmp_filename, final_filename) != 0) {
151
0
        StatsIncr(tv, oft->fs_error_counter);
152
0
        WARN_ONCE(WOT_RENAME, "Failed to rename %s to %s: %s", tmp_filename, final_filename,
153
0
                strerror(errno));
154
0
        if (unlink(tmp_filename) != 0) {
155
            /* Just increment, don't log as has_fs_errors would
156
             * already be set above. */
157
0
            StatsIncr(tv, oft->fs_error_counter);
158
0
        }
159
0
        return;
160
0
    }
161
162
46
    if (ctx->fileinfo) {
163
0
        char js_metadata_filename[PATH_MAX];
164
0
        if (snprintf(js_metadata_filename, sizeof(js_metadata_filename), "%s.%" PRIuMAX ".%u.json",
165
0
                    final_filename, (uintmax_t)SCTIME_SECS(p->ts),
166
0
                    ff->file_store_id) == (int)sizeof(js_metadata_filename)) {
167
0
            WARN_ONCE(WOT_SNPRINTF, "Failed to write file info record. Output filename truncated.");
168
0
        } else {
169
0
            SCJsonBuilder *js_fileinfo =
170
0
                    JsonBuildFileInfoRecord(p, ff, tx, tx_id, true, dir, ctx->xff_cfg, NULL);
171
0
            if (likely(js_fileinfo != NULL)) {
172
0
                SCJbClose(js_fileinfo);
173
0
                FILE *out = fopen(js_metadata_filename, "w");
174
0
                if (out != NULL) {
175
0
                    size_t js_len = SCJbLen(js_fileinfo);
176
0
                    fwrite(SCJbPtr(js_fileinfo), js_len, 1, out);
177
0
                    fclose(out);
178
0
                }
179
0
                SCJbFree(js_fileinfo);
180
0
            }
181
0
        }
182
0
    }
183
46
}
184
185
static int OutputFilestoreLogger(ThreadVars *tv, void *thread_data, const Packet *p, File *ff,
186
        void *tx, const uint64_t tx_id, const uint8_t *data, uint32_t data_len, uint8_t flags,
187
        uint8_t dir)
188
2.99k
{
189
2.99k
    SCEnter();
190
2.99k
    OutputFilestoreLogThread *aft = (OutputFilestoreLogThread *)thread_data;
191
2.99k
    OutputFilestoreCtx *ctx = aft->ctx;
192
2.99k
    char filename[PATH_MAX] = "";
193
2.99k
    int file_fd = -1;
194
195
2.99k
    SCLogDebug("ff %p, data %p, data_len %u", ff, data, data_len);
196
197
2.99k
    if (flags & OUTPUT_FILEDATA_FLAG_OPEN) {
198
160
        snprintf(filename, sizeof(filename), "%s/file.%u", ctx->tmpdir, ff->file_store_id);
199
160
        file_fd = open(filename, O_CREAT | O_TRUNC | O_NOFOLLOW | O_WRONLY,
200
160
                0644);
201
160
        if (file_fd == -1) {
202
0
            StatsIncr(tv, aft->fs_error_counter);
203
0
            SCLogWarning("Filestore (v2) failed to create %s: %s", filename, strerror(errno));
204
0
            return -1;
205
0
        }
206
207
160
        if (SC_ATOMIC_GET(filestore_open_file_cnt) < FileGetMaxOpenFiles()) {
208
0
            SC_ATOMIC_ADD(filestore_open_file_cnt, 1);
209
0
            ff->fd = file_fd;
210
160
        } else {
211
160
            if (FileGetMaxOpenFiles() > 0) {
212
0
                StatsIncr(tv, aft->counter_max_hits);
213
0
            }
214
160
            ff->fd = -1;
215
160
        }
216
    /* we can get called with a NULL ffd when we need to close */
217
2.83k
    } else if (data != NULL) {
218
1.46k
        if (ff->fd == -1) {
219
1.46k
            snprintf(filename, sizeof(filename), "%s/file.%u", ctx->tmpdir, ff->file_store_id);
220
1.46k
            file_fd = open(filename, O_APPEND | O_NOFOLLOW | O_WRONLY);
221
1.46k
            if (file_fd == -1) {
222
0
                StatsIncr(tv, aft->fs_error_counter);
223
0
                WARN_ONCE(WOT_OPEN, "Filestore (v2) failed to open file %s: %s", filename,
224
0
                        strerror(errno));
225
0
                return -1;
226
0
            }
227
1.46k
        } else {
228
0
            file_fd = ff->fd;
229
0
        }
230
1.46k
    }
231
232
2.99k
    if (file_fd != -1) {
233
1.62k
        ssize_t r = write(file_fd, (const void *)data, (size_t)data_len);
234
1.62k
        if (r == -1) {
235
0
            snprintf(filename, sizeof(filename), "%s/file.%u", ctx->tmpdir, ff->file_store_id);
236
0
            StatsIncr(tv, aft->fs_error_counter);
237
0
            WARN_ONCE(WOT_WRITE, "Filestore (v2) failed to write to %s: %s", filename,
238
0
                    strerror(errno));
239
0
            if (ff->fd != -1) {
240
0
                SC_ATOMIC_SUB(filestore_open_file_cnt, 1);
241
0
            }
242
0
            ff->fd = -1;
243
0
        }
244
1.62k
        if (ff->fd == -1) {
245
1.62k
            close(file_fd);
246
1.62k
        }
247
1.62k
    }
248
249
2.99k
    if (flags & OUTPUT_FILEDATA_FLAG_CLOSE) {
250
46
        if (ff->fd != -1) {
251
0
            close(ff->fd);
252
0
            ff->fd = -1;
253
0
            SC_ATOMIC_SUB(filestore_open_file_cnt, 1);
254
0
        }
255
46
        OutputFilestoreFinalizeFiles(tv, aft, ctx, p, ff, tx, tx_id, dir);
256
46
    }
257
258
2.99k
    return 0;
259
2.99k
}
260
261
static TmEcode OutputFilestoreLogThreadInit(ThreadVars *t, const void *initdata,
262
        void **data)
263
4
{
264
4
    OutputFilestoreLogThread *aft = SCCalloc(1, sizeof(OutputFilestoreLogThread));
265
4
    if (unlikely(aft == NULL))
266
0
        return TM_ECODE_FAILED;
267
268
4
    if (initdata == NULL) {
269
0
        SCLogDebug("Error getting context for LogFileStore. \"initdata\" argument NULL");
270
0
        SCFree(aft);
271
0
        return TM_ECODE_FAILED;
272
0
    }
273
274
4
    OutputFilestoreCtx *ctx = ((OutputCtx *)initdata)->data;
275
4
    aft->ctx = ctx;
276
277
4
    aft->counter_max_hits =
278
4
        StatsRegisterCounter("file_store.open_files_max_hit", t);
279
280
    /* File system type errors (open, write, rename) will only be
281
     * logged once. But this stat will be incremented for every
282
     * occurrence. */
283
4
    aft->fs_error_counter = StatsRegisterCounter("file_store.fs_errors", t);
284
285
4
    *data = (void *)aft;
286
4
    return TM_ECODE_OK;
287
4
}
288
289
static TmEcode OutputFilestoreLogThreadDeinit(ThreadVars *t, void *data)
290
0
{
291
0
    OutputFilestoreLogThread *aft = (OutputFilestoreLogThread *)data;
292
0
    if (aft == NULL) {
293
0
        return TM_ECODE_OK;
294
0
    }
295
296
    /* clear memory */
297
0
    memset(aft, 0, sizeof(OutputFilestoreLogThread));
298
299
0
    SCFree(aft);
300
0
    return TM_ECODE_OK;
301
0
}
302
303
static void OutputFilestoreLogDeInitCtx(OutputCtx *output_ctx)
304
0
{
305
0
    OutputFilestoreCtx *ctx = (OutputFilestoreCtx *)output_ctx->data;
306
0
    if (ctx->xff_cfg != NULL) {
307
0
        SCFree(ctx->xff_cfg);
308
0
    }
309
0
    SCFree(ctx);
310
0
    SCFree(output_ctx);
311
0
}
312
313
static void GetLogDirectory(const SCConfNode *conf, char *out, size_t out_size)
314
2
{
315
2
    const char *log_base_dir = SCConfNodeLookupChildValue(conf, "dir");
316
2
    if (log_base_dir == NULL) {
317
2
        SCLogConfig("Filestore (v2) default log directory %s", default_log_dir);
318
2
        log_base_dir = default_log_dir;
319
2
    }
320
2
    if (PathIsAbsolute(log_base_dir)) {
321
0
        strlcpy(out, log_base_dir, out_size);
322
2
    } else {
323
2
        const char *default_log_prefix = SCConfigGetLogDirectory();
324
2
        snprintf(out, out_size, "%s/%s", default_log_prefix, log_base_dir);
325
2
    }
326
2
}
327
328
static bool InitFilestoreDirectory(const char *dir)
329
2
{
330
2
    const uint8_t dir_count = 0xff;
331
332
2
    if (!SCPathExists(dir)) {
333
1
        SCLogInfo("Filestore (v2) creating directory %s", dir);
334
1
        if (SCCreateDirectoryTree(dir, true) != 0) {
335
0
            SCLogError("Filestore (v2) failed to create directory %s: %s", dir, strerror(errno));
336
0
            return false;
337
0
        }
338
1
    }
339
340
514
    for (int i = 0; i <= dir_count; i++) {
341
512
        char leaf[PATH_MAX];
342
512
        int n = snprintf(leaf, sizeof(leaf), "%s/%02x", dir, i);
343
512
        if (n < 0 || n >= PATH_MAX) {
344
0
            SCLogError("Filestore (v2) failed to create leaf directory: "
345
0
                       "path too long");
346
0
            return false;
347
0
        }
348
512
        if (!SCPathExists(leaf)) {
349
256
            SCLogInfo("Filestore (v2) creating directory %s", leaf);
350
256
            if (SCDefaultMkDir(leaf) != 0) {
351
0
                SCLogError(
352
0
                        "Filestore (v2) failed to create directory %s: %s", leaf, strerror(errno));
353
0
                return false;
354
0
            }
355
256
        }
356
512
    }
357
358
    /* Make sure the tmp directory exists. */
359
2
    char tmpdir[PATH_MAX];
360
2
    int n = snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", dir);
361
2
    if (n < 0 || n >= PATH_MAX) {
362
0
        SCLogError("Filestore (v2) failed to create tmp directory: path too long");
363
0
        return false;
364
0
    }
365
2
    if (!SCPathExists(tmpdir)) {
366
1
        SCLogInfo("Filestore (v2) creating directory %s", tmpdir);
367
1
        if (SCDefaultMkDir(tmpdir) != 0) {
368
0
            SCLogError("Filestore (v2) failed to create directory %s: %s", tmpdir, strerror(errno));
369
0
            return false;
370
0
        }
371
1
    }
372
373
2
    return true;
374
2
}
375
376
/** \brief Create a new http log OutputFilestoreCtx.
377
 *  \param conf Pointer to ConfNode containing this loggers configuration.
378
 *  \return NULL if failure, OutputFilestoreCtx* to the file_ctx if succesful
379
 * */
380
static OutputInitResult OutputFilestoreLogInitCtx(SCConfNode *conf)
381
2
{
382
2
    OutputInitResult result = { NULL, false };
383
384
2
    intmax_t version = 0;
385
2
    if (!SCConfGetChildValueInt(conf, "version", &version) || version < 2) {
386
0
        SCLogWarning("File-store v1 has been removed. Please update to file-store v2.");
387
0
        return result;
388
0
    }
389
390
2
    if (RunModeOutputFiledataEnabled()) {
391
0
        SCLogWarning("A file data logger is already enabled. Filestore (v2) "
392
0
                     "will not be enabled.");
393
0
        return result;
394
0
    }
395
396
2
    char log_directory[PATH_MAX] = "";
397
2
    GetLogDirectory(conf, log_directory, sizeof(log_directory));
398
2
    if (!InitFilestoreDirectory(log_directory)) {
399
0
        return result;
400
0
    }
401
402
2
    OutputFilestoreCtx *ctx = SCCalloc(1, sizeof(*ctx));
403
2
    if (unlikely(ctx == NULL)) {
404
0
        return result;
405
0
    }
406
407
2
    strlcpy(ctx->prefix, log_directory, sizeof(ctx->prefix));
408
2
    int written = snprintf(ctx->tmpdir, sizeof(ctx->tmpdir) - 1, "%s/tmp",
409
2
            log_directory);
410
2
    if (written == sizeof(ctx->tmpdir)) {
411
0
        SCLogError("File-store output directory overflow.");
412
0
        SCFree(ctx);
413
0
        return result;
414
0
    }
415
416
2
    ctx->xff_cfg = SCCalloc(1, sizeof(HttpXFFCfg));
417
2
    if (ctx->xff_cfg != NULL) {
418
2
        HttpXFFGetCfg(conf, ctx->xff_cfg);
419
2
    }
420
421
2
    OutputCtx *output_ctx = SCCalloc(1, sizeof(OutputCtx));
422
2
    if (unlikely(output_ctx == NULL)) {
423
0
        SCFree(ctx->xff_cfg);
424
0
        SCFree(ctx);
425
0
        return result;
426
0
    }
427
428
2
    output_ctx->data = ctx;
429
2
    output_ctx->DeInit = OutputFilestoreLogDeInitCtx;
430
431
2
    const char *write_fileinfo = SCConfNodeLookupChildValue(conf, "write-fileinfo");
432
2
    if (write_fileinfo != NULL && SCConfValIsTrue(write_fileinfo)) {
433
0
        SCLogConfig("Filestore (v2) will output fileinfo records.");
434
0
        ctx->fileinfo = true;
435
0
    }
436
437
2
    const char *force_filestore = SCConfNodeLookupChildValue(conf, "force-filestore");
438
2
    if (force_filestore != NULL && SCConfValIsTrue(force_filestore)) {
439
2
        FileForceFilestoreEnable();
440
2
        SCLogInfo("forcing filestore of all files");
441
2
    }
442
443
2
    const char *force_magic = SCConfNodeLookupChildValue(conf, "force-magic");
444
2
    if (force_magic != NULL && SCConfValIsTrue(force_magic)) {
445
0
        FileForceMagicEnable();
446
0
        SCLogConfig("Filestore (v2) forcing magic lookup for stored files");
447
0
    }
448
449
2
    FileForceHashParseCfg(conf);
450
451
    /* The new filestore requires SHA256. */
452
2
    FileForceSha256Enable();
453
454
2
    ProvidesFeature(FEATURE_OUTPUT_FILESTORE);
455
456
2
    const char *stream_depth_str = SCConfNodeLookupChildValue(conf, "stream-depth");
457
2
    if (stream_depth_str != NULL && strcmp(stream_depth_str, "no")) {
458
0
        uint32_t stream_depth = 0;
459
0
        if (ParseSizeStringU32(stream_depth_str,
460
0
                               &stream_depth) < 0) {
461
0
            SCLogError("Error parsing "
462
0
                       "file-store.stream-depth "
463
0
                       "from conf file - %s.  Killing engine",
464
0
                    stream_depth_str);
465
0
            exit(EXIT_FAILURE);
466
0
        }
467
0
        if (stream_depth) {
468
0
            if (stream_depth <= stream_config.reassembly_depth) {
469
0
                SCLogWarning("file-store.stream-depth value %" PRIu32 " has "
470
0
                             "no effect since it's less than stream.reassembly.depth "
471
0
                             "value.",
472
0
                        stream_depth);
473
0
            } else {
474
0
                FileReassemblyDepthEnable(stream_depth);
475
0
            }
476
0
        }
477
0
    }
478
479
2
    const char *file_count_str = SCConfNodeLookupChildValue(conf, "max-open-files");
480
2
    if (file_count_str != NULL) {
481
0
        uint32_t file_count = 0;
482
0
        if (ParseSizeStringU32(file_count_str,
483
0
                               &file_count) < 0) {
484
0
            SCLogError("Error parsing "
485
0
                       "file-store.max-open-files "
486
0
                       "from conf file - %s.  Killing engine",
487
0
                    file_count_str);
488
0
            exit(EXIT_FAILURE);
489
0
        } else {
490
0
            if (file_count != 0) {
491
0
                FileSetMaxOpenFiles(file_count);
492
0
                SCLogConfig("Filestore (v2) will keep a max of %d "
493
0
                        "simultaneously open files", file_count);
494
0
            }
495
0
        }
496
0
    }
497
498
2
    result.ctx = output_ctx;
499
2
    result.ok = true;
500
2
    SCReturnCT(result, "OutputInitResult");
501
2
}
502
503
void OutputFilestoreRegister(void)
504
78
{
505
78
    OutputRegisterFiledataModule(LOGGER_FILE_STORE, MODULE_NAME, "file-store",
506
78
            OutputFilestoreLogInitCtx, OutputFilestoreLogger, OutputFilestoreLogThreadInit,
507
78
            OutputFilestoreLogThreadDeinit);
508
509
78
    SC_ATOMIC_INIT(filestore_open_file_cnt);
510
78
    SC_ATOMIC_SET(filestore_open_file_cnt, 0);
511
78
}
512
513
void OutputFilestoreRegisterGlobalCounters(void)
514
78
{
515
78
    StatsRegisterGlobalCounter("file_store.open_files", OutputFilestoreOpenFilesCounter);
516
78
}