diff --git a/doc/userguide/capture-hardware/pcap-file.rst b/doc/userguide/capture-hardware/pcap-file.rst index f87d259b74..d5ed628dde 100644 --- a/doc/userguide/capture-hardware/pcap-file.rst +++ b/doc/userguide/capture-hardware/pcap-file.rst @@ -15,6 +15,8 @@ Configuration checksum-checks: auto # buffer-size: 128 KiB # tenant-id: none + # Applies to file and directory. Options: false (no deletion), true (always delete), + # "non-alerts" (delete only files with no alerts) # delete-when-done: false # recursive: false # continuous: false @@ -85,9 +87,22 @@ Other options **delete-when-done** -- If ``true``, Suricata deletes the PCAP file after processing. -- The command-line option is - :ref:`--pcap-file-delete ` +Controls when PCAP files are deleted after processing. Three values are supported: + +- ``false`` (default): Files are never deleted +- ``true``: Files are always deleted after processing +- ``"non-alerts"``: Files are deleted only if they didn't generate any alerts + +.. note:: + + The command-line option :ref:`--pcap-file-delete ` + overrides this configuration and forces "always delete" mode (``true``). + +.. warning:: + + When using ``"non-alerts"`` mode, file deletion is deferred until thread + cleanup to ensure alert counts are finalized. This may delay deletion + compared to other modes. **BPF filter** diff --git a/doc/userguide/partials/options.rst b/doc/userguide/partials/options.rst index 170677bff2..c11992e516 100644 --- a/doc/userguide/partials/options.rst +++ b/doc/userguide/partials/options.rst @@ -80,10 +80,23 @@ .. option:: --pcap-file-delete - Used with the -r option to indicate that the mode should delete pcap files - after they have been processed. This is useful with pcap-file-continuous to - continuously feed files to a directory and have them cleaned up when done. If - this option is not set, pcap files will not be deleted after processing. + Used with the -r option to force deletion of pcap files after they have been + processed. This is useful with pcap-file-continuous to continuously feed files + to a directory and have them cleaned up when done. + + **command-line vs Configuration**: This command-line option overrides the + ``pcap-file.delete-when-done`` configuration option in ``suricata.yaml`` and + forces "always delete" mode (equivalent to ``delete-when-done: true``). + + **For more control**, use the ``pcap-file.delete-when-done`` configuration + option instead, which supports three values: + + - ``false`` (default): No files are deleted + - ``true``: All files are deleted after processing + - ``"non-alerts"``: Only files that generated no alerts are deleted + + If neither ``--pcap-file-delete`` nor ``delete-when-done`` is configured, + pcap files will not be deleted after processing. .. _cmdline-option-pcap-file-buffer-size: diff --git a/src/Makefile.am b/src/Makefile.am index f170992c6e..161061a2c1 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -51,6 +51,7 @@ noinst_HEADERS = \ app-layer.h \ bindgen.h \ build-info.h \ + capture-hooks.h \ conf-yaml-loader.h \ conf.h \ counters.h \ @@ -636,6 +637,7 @@ libsuricata_c_a_SOURCES = \ app-layer-ssl.c \ app-layer-tftp.c \ app-layer.c \ + capture-hooks.c \ conf-yaml-loader.c \ conf.c \ counters.c \ diff --git a/src/capture-hooks.c b/src/capture-hooks.c new file mode 100644 index 0000000000..92c8c69144 --- /dev/null +++ b/src/capture-hooks.c @@ -0,0 +1,65 @@ +/* Copyright (C) 2007-2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * Lightweight indirection layer for capture-related callbacks. + * + * This module lets the capture implementation register small hooks that the + * generic engine can invoke without hard dependencies. Two hooks are used: + * - on-alerts: invoked when a packet produced alerts so capture can update + * per-input stats (e.g., deciding if a pcap should be deleted or kept). + * - on-pseudo-created: invoked when the engine creates pseudo packets (e.g., + * flow timeout or shutdown flush). This allows capture to retain references + * or track alert outcomes tied to those pseudo packets. + */ + +#include "suricata-common.h" +#include "capture-hooks.h" + +static CaptureOnPacketWithAlertsHook g_on_alerts_hook = NULL; +static CaptureOnPseudoPacketCreatedHook g_on_pseudo_created_hook = NULL; + +void CaptureHooksSet( + CaptureOnPacketWithAlertsHook OnAlerts, CaptureOnPseudoPacketCreatedHook OnPseudoCreated) +{ + /* Allow re-setting the same hooks (e.g. during pcap file reload), but BUG_ON + * if overwriting with different ones without clearing. */ + if (g_on_alerts_hook != NULL) { + BUG_ON(g_on_alerts_hook != OnAlerts); + } + if (g_on_pseudo_created_hook != NULL) { + BUG_ON(g_on_pseudo_created_hook != OnPseudoCreated); + } + + g_on_alerts_hook = OnAlerts; + g_on_pseudo_created_hook = OnPseudoCreated; +} + +void CaptureHooksOnPacketWithAlerts(const Packet *p) +{ + if (g_on_alerts_hook != NULL) { + g_on_alerts_hook(p); + } +} + +void CaptureHooksOnPseudoPacketCreated(Packet *p) +{ + if (g_on_pseudo_created_hook != NULL) { + g_on_pseudo_created_hook(p); + } +} diff --git a/src/capture-hooks.h b/src/capture-hooks.h new file mode 100644 index 0000000000..350912a48a --- /dev/null +++ b/src/capture-hooks.h @@ -0,0 +1,43 @@ +/* Copyright (C) 2007-2026 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file capture-hooks.h + * Small hook interface for capture modules to react to events in the + * generic engine without creating circular dependencies. + */ + +#ifndef SURICATA_CAPTURE_HOOKS_H +#define SURICATA_CAPTURE_HOOKS_H + +#include "suricata-common.h" + +struct Packet_; +typedef struct Packet_ Packet; + +typedef void (*CaptureOnPacketWithAlertsHook)(const Packet *p); +typedef void (*CaptureOnPseudoPacketCreatedHook)(Packet *p); + +/* Register/clear hooks (called by capture implementations) */ +void CaptureHooksSet( + CaptureOnPacketWithAlertsHook OnAlerts, CaptureOnPseudoPacketCreatedHook OnPseudoCreated); + +/* Invoke hooks (called from generic code, safe if unset) */ +void CaptureHooksOnPacketWithAlerts(const Packet *p); +void CaptureHooksOnPseudoPacketCreated(Packet *p); + +#endif /* SURICATA_CAPTURE_HOOKS_H */ diff --git a/src/detect-engine-alert.c b/src/detect-engine-alert.c index 4b2466d899..15929a8eb3 100644 --- a/src/detect-engine-alert.c +++ b/src/detect-engine-alert.c @@ -37,6 +37,7 @@ #include "util-validate.h" #include "action-globals.h" +#include "capture-hooks.h" /** tag signature we use for tag alerts */ static Signature g_tag_signature; @@ -582,6 +583,12 @@ static inline void PacketAlertFinalizeProcessQueue( p->flags |= PKT_FIRST_ALERTS; } } + + /* Notify capture layer about packets with real alerts (not pass-only), + * so capture impl can update per-capture context (e.g. pcap-file alert counts). */ + if (alerted) { + CaptureHooksOnPacketWithAlerts(p); + } } /** diff --git a/src/runmode-unittests.c b/src/runmode-unittests.c index 11565c0d75..c749315aab 100644 --- a/src/runmode-unittests.c +++ b/src/runmode-unittests.c @@ -116,6 +116,7 @@ #include "decode-vntag.h" #include "decode-vxlan.h" #include "decode-pppoe.h" +#include "source-pcap-file-helper.h" #include "output-json-stats.h" @@ -212,6 +213,7 @@ static void RegisterUnittests(void) StreamingBufferRegisterTests(); MacSetRegisterTests(); FlowRateRegisterTests(); + SourcePcapFileHelperRegisterTests(); #ifdef OS_WIN32 Win32SyscallRegisterTests(); #endif diff --git a/src/source-pcap-file-helper.c b/src/source-pcap-file-helper.c index 8283ce9e85..0f3aac98f3 100644 --- a/src/source-pcap-file-helper.c +++ b/src/source-pcap-file-helper.c @@ -28,34 +28,77 @@ #include "util-datalink.h" #include "util-checksum.h" #include "util-profiling.h" -#include "source-pcap-file.h" #include "util-exception-policy.h" +#include "conf-yaml-loader.h" +#include "capture-hooks.h" +#include "threads.h" extern uint32_t max_pending_packets; extern PcapFileGlobalVars pcap_g; +static PcapFileFileVars *pcap_current_pfv = NULL; + static void PcapFileCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt); +static void PcapFileReleasePacket(Packet *p) +{ + PcapFileFileVars *pfv = p->pcap_v.pfv; + if (pfv != NULL) { + PcapFileFinalizePacket(pfv); + } + + PacketFreeOrRelease(p); +} + +void PcapFileReleasePseudoPacket(Packet *p) +{ + PcapFileFileVars *pfv = p->pcap_v.pfv; + /* Alerts are counted in PacketAlertFinalize via PcapFileAddAlertCount, so + * avoid double-counting here. Decrement refcount if we held one. */ + if (pfv != NULL) { + uint32_t prev = SC_ATOMIC_SUB(pfv->ref_cnt, 1); + if (prev == 1 && pfv->cleanup_requested) { + CleanupPcapFileFileVars(pfv); + } + } + PacketFreeOrRelease(p); +} + void CleanupPcapFileFileVars(PcapFileFileVars *pfv) { - if (pfv != NULL) { - if (pfv->pcap_handle != NULL) { - pcap_close(pfv->pcap_handle); - pfv->pcap_handle = NULL; - } - if (pfv->filename != NULL) { - if (pfv->shared != NULL && pfv->shared->should_delete) { - SCLogDebug("Deleting pcap file %s", pfv->filename); - if (unlink(pfv->filename) != 0) { - SCLogWarning("Failed to delete %s: %s", pfv->filename, strerror(errno)); - } - } - SCFree(pfv->filename); - pfv->filename = NULL; - } - pfv->shared = NULL; - SCFree(pfv); + if (pfv == NULL) { + return; } + + /* If there are still packets in flight, defer ALL cleanup actions, including + * the deletion decision, until the last packet completes. */ + if (SC_ATOMIC_GET(pfv->ref_cnt) != 0) { + pfv->cleanup_requested = true; + return; + } + + /* No packets in flight anymore: it's now safe to close, decide, and delete. */ + if (pfv->pcap_handle != NULL) { + pcap_close(pfv->pcap_handle); + pfv->pcap_handle = NULL; + } + + if (pfv->filename != NULL) { + if (PcapFileShouldDeletePcapFile(pfv)) { + SCLogDebug("Deleting pcap file %s", pfv->filename); + if (unlink(pfv->filename) != 0) { + SCLogWarning("Failed to delete %s: %s", pfv->filename, strerror(errno)); + } + } + SCFree(pfv->filename); + pfv->filename = NULL; + } + + pfv->shared = NULL; + if (pcap_current_pfv == pfv) { + pcap_current_pfv = NULL; + } + SCFree(pfv); } void PcapFileCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt) @@ -74,6 +117,10 @@ void PcapFileCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt) if (unlikely(p == NULL)) { SCReturn; } + SC_ATOMIC_ADD(ptv->ref_cnt, 1); + SCLogDebug("pcap-file: got packet, pfv=%p filename=%s ref_cnt now=%u p=%p", (void *)ptv, + ptv->filename, SC_ATOMIC_GET(ptv->ref_cnt), (void *)p); + PACKET_PROFILING_TMM_START(p, TMM_RECEIVEPCAPFILE); PKT_SET_SRC(p, PKT_SRC_WIRE); @@ -83,9 +130,12 @@ void PcapFileCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt) p->pcap_v.pcap_cnt = ++pcap_g.cnt; p->pcap_v.tenant_id = ptv->shared->tenant_id; + p->pcap_v.pfv = ptv; ptv->shared->pkts++; ptv->shared->bytes += h->caplen; + p->ReleasePacket = PcapFileReleasePacket; + if (unlikely(PacketCopyData(p, pkt, h->caplen))) { TmqhOutputPacketpool(ptv->shared->tv, p); PACKET_PROFILING_TMM_END(p, TMM_RECEIVEPCAPFILE); @@ -120,6 +170,16 @@ const char *PcapFileGetFilename(void) return pcap_filename; } +void PcapFileSetCurrentPfv(PcapFileFileVars *pfv) +{ + pcap_current_pfv = pfv; +} + +PcapFileFileVars *PcapFileGetCurrentPfv(void) +{ + return pcap_current_pfv; +} + /** * \brief Main PCAP file reading Loop function */ @@ -127,6 +187,7 @@ TmEcode PcapFileDispatch(PcapFileFileVars *ptv) { SCEnter(); + PcapFileSetCurrentPfv(ptv); /* initialize all the thread's initial timestamp */ if (likely(ptv->first_pkt_hdr != NULL)) { TmThreadsInitThreadsTimestamp(SCTIME_FROM_TIMEVAL(&ptv->first_pkt_ts)); @@ -236,6 +297,14 @@ TmEcode InitPcapFile(PcapFileFileVars *pfv) pcap_freecode(&pfv->filter); } + SC_ATOMIC_INIT(pfv->alerts_count); + SC_ATOMIC_SET(pfv->alerts_count, 0); + + SC_ATOMIC_INIT(pfv->ref_cnt); + SC_ATOMIC_SET(pfv->ref_cnt, 0); + + pfv->cleanup_requested = false; + pfv->datalink = pcap_datalink(pfv->pcap_handle); SCLogDebug("datalink %" PRId32 "", pfv->datalink); DatalinkSetGlobalType(pfv->datalink); @@ -285,3 +354,687 @@ TmEcode ValidateLinkType(int datalink, DecoderFunc *DecoderFn) SCReturnInt(TM_ECODE_OK); } + +bool PcapFileShouldDeletePcapFile(PcapFileFileVars *pfv) +{ + if (pfv == NULL || pfv->shared == NULL) { + return false; + } + + if (pfv->shared->delete_mode == PCAP_FILE_DELETE_NONE) { + return false; + } + + if (pfv->shared->delete_mode == PCAP_FILE_DELETE_ALWAYS) { + return true; + } + + /* PCAP_FILE_DELETE_NON_ALERTS mode */ + uint64_t file_alerts = SC_ATOMIC_GET(pfv->alerts_count); + + if (file_alerts != 0) { + SCLogDebug("Skipping deletion of %s due to %" PRIu64 " alert(s) generated.", pfv->filename, + file_alerts); + return false; + } + + SCLogDebug("pcap-file: will delete %s (no alerts counted)", pfv->filename); + + return true; +} + +void PcapFileFinalizePacket(PcapFileFileVars *pfv) +{ + if (pfv != NULL) { + /* decrease ref count as packet is done */ + uint32_t prev = SC_ATOMIC_SUB(pfv->ref_cnt, 1); + SCLogDebug("pcap-file: packet done pfv=%p filename=%s ref_cnt was=%u now=%u", (void *)pfv, + pfv->filename, prev, prev - 1); + if (prev == 1) { + if (pfv->cleanup_requested) { + CleanupPcapFileFileVars(pfv); + } + } + } +} + +void PcapFileAddAlertCount(PcapFileFileVars *pfv, uint16_t alert_count) +{ + if (pfv != NULL && alert_count > 0) { + SC_ATOMIC_ADD(pfv->alerts_count, alert_count); + } +} + +static void PcapCaptureOnPacketWithAlerts(const Packet *p) +{ + PcapFileFileVars *pfv = p->pcap_v.pfv; + if (pfv == NULL) { + pfv = PcapFileGetCurrentPfv(); + } + if (pfv != NULL) { + /* alerts.cnt is uint16_t; count alerts for delete-on-non-alerts logic */ + PcapFileAddAlertCount(pfv, p->alerts.cnt); + } +} + +static void PcapCaptureOnPseudoPacketCreated(Packet *p) +{ + /* For pseudo packets created by generic layers, associate with current pfv + * and ensure refcount held so deletion defers. */ + if (p->pcap_v.pfv == NULL) { + PcapFileFileVars *pfv = PcapFileGetCurrentPfv(); + if (pfv != NULL) { + p->pcap_v.pfv = pfv; + p->ReleasePacket = PcapFileReleasePseudoPacket; + SC_ATOMIC_ADD(pfv->ref_cnt, 1); + } + } +} + +void PcapFileInstallCaptureHooks(void) +{ + CaptureHooksSet(PcapCaptureOnPacketWithAlerts, PcapCaptureOnPseudoPacketCreated); +} + +PcapFileDeleteMode PcapFileParseDeleteMode(void) +{ + PcapFileDeleteMode delete_mode = PCAP_FILE_DELETE_NONE; + const char *delete_when_done_str = NULL; + + if (SCConfGet("pcap-file.delete-when-done", &delete_when_done_str) == 1) { + if (strcmp(delete_when_done_str, "non-alerts") == 0) { + delete_mode = PCAP_FILE_DELETE_NON_ALERTS; + } else { + int delete_always = 0; + if (SCConfGetBool("pcap-file.delete-when-done", &delete_always) == 1) { + if (delete_always == 1) { + delete_mode = PCAP_FILE_DELETE_ALWAYS; + } + } + } + } + + return delete_mode; +} + +#ifdef UNITTESTS +#include "util-unittest-helper.h" +/** + * \test Tests that the PcapFileShouldDeletePcapFile function correctly applies the + * delete mode configuration. + */ +static int SourcePcapFileHelperTest01(void) +{ + PcapFileSharedVars shared; + memset(&shared, 0, sizeof(shared)); + shared.delete_mode = PCAP_FILE_DELETE_ALWAYS; + + PcapFileFileVars pfv; + memset(&pfv, 0, sizeof(pfv)); + pfv.shared = &shared; + pfv.filename = SCStrdup("test.pcap"); + SC_ATOMIC_INIT(pfv.alerts_count); + SC_ATOMIC_SET(pfv.alerts_count, 0); + + /* Test case 1: Always delete mode */ + int result1 = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF_NOT(result1); + + /* Test case 2: Non-alerts mode with no alerts */ + shared.delete_mode = PCAP_FILE_DELETE_NON_ALERTS; + int result2 = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF_NOT(result2); + + /* Test case 3: Non-alerts mode with alerts */ + SC_ATOMIC_ADD(pfv.alerts_count, 1); + int result3 = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF(result3); + + /* Test case 4: Always delete mode with alerts */ + shared.delete_mode = PCAP_FILE_DELETE_ALWAYS; + int result4 = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF_NOT(result4); + + /* Test case 5: No delete mode */ + shared.delete_mode = PCAP_FILE_DELETE_NONE; + int result5 = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF(result5); + + SCFree(pfv.filename); + + PASS; +} + +/** + * \test Test PcapFileFinalizePacket function with reference counting + */ +static int SourcePcapFileHelperTest02(void) +{ + PcapFileFileVars pfv; + memset(&pfv, 0, sizeof(pfv)); + SC_ATOMIC_INIT(pfv.alerts_count); + SC_ATOMIC_SET(pfv.alerts_count, 0); + SC_ATOMIC_INIT(pfv.ref_cnt); + SC_ATOMIC_SET(pfv.ref_cnt, 0); + pfv.cleanup_requested = false; + + /* Test adding alerts with reference counting */ + SC_ATOMIC_ADD(pfv.ref_cnt, 1); /* simulate packet in flight */ + PcapFileAddAlertCount(&pfv, 5); + PcapFileFinalizePacket(&pfv); + uint64_t count = SC_ATOMIC_GET(pfv.alerts_count); + FAIL_IF_NOT(count == 5); + FAIL_IF_NOT(SC_ATOMIC_GET(pfv.ref_cnt) == 0); /* should be decremented */ + + /* Test adding more alerts */ + SC_ATOMIC_ADD(pfv.ref_cnt, 1); + PcapFileAddAlertCount(&pfv, 3); + PcapFileFinalizePacket(&pfv); + count = SC_ATOMIC_GET(pfv.alerts_count); + FAIL_IF_NOT(count == 8); + + /* Test with zero alerts (should not increment count) */ + SC_ATOMIC_ADD(pfv.ref_cnt, 1); + PcapFileFinalizePacket(&pfv); + count = SC_ATOMIC_GET(pfv.alerts_count); + FAIL_IF_NOT(count == 8); + + /* Test with NULL pfv (should not crash) */ + PcapFileFinalizePacket(NULL); + + PASS; +} + +/* Mock for configuration testing */ +static int SetupYamlConf(const char *conf_string) +{ + SCConfCreateContextBackup(); + SCConfInit(); + + return SCConfYamlLoadString(conf_string, strlen(conf_string)); +} + +static void CleanupYamlConf(void) +{ + SCConfDeInit(); + SCConfRestoreContextBackup(); +} + +/** + * \test Test PcapFileParseDeleteMode with all configuration combinations + */ +static int SourcePcapFileHelperTest03(void) +{ + /* Test 1: No configuration (should default to NONE) */ + SCConfCreateContextBackup(); + SCConfInit(); + + PcapFileDeleteMode result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_NONE); + + SCConfDeInit(); + SCConfRestoreContextBackup(); + + /* Test 2: "false" configuration */ + const char *conf_false = "%YAML 1.1\n" + "---\n" + "pcap-file:\n" + " delete-when-done: false\n"; + + SetupYamlConf(conf_false); + result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_NONE); + CleanupYamlConf(); + + /* Test 3: "true" configuration */ + const char *conf_true = "%YAML 1.1\n" + "---\n" + "pcap-file:\n" + " delete-when-done: true\n"; + + SetupYamlConf(conf_true); + result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); + CleanupYamlConf(); + + /* Test 4: "non-alerts" configuration */ + const char *conf_non_alerts = "%YAML 1.1\n" + "---\n" + "pcap-file:\n" + " delete-when-done: \"non-alerts\"\n"; + + SetupYamlConf(conf_non_alerts); + result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_NON_ALERTS); + CleanupYamlConf(); + + /* Test 5: Invalid configuration (should default to NONE) */ + const char *conf_invalid = "%YAML 1.1\n" + "---\n" + "pcap-file:\n" + " delete-when-done: \"invalid-value\"\n"; + + SetupYamlConf(conf_invalid); + result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_NONE); + CleanupYamlConf(); + + PASS; +} + +/** + * \test pfv is NULL. + */ +static int SourcePcapFileHelperTest04(void) +{ + int rc = PcapFileShouldDeletePcapFile(NULL); + FAIL_IF(rc); + PASS; +} + +/** + * \test pfv->shared is NULL. + */ +static int SourcePcapFileHelperTest05(void) +{ + PcapFileFileVars pfv; + memset(&pfv, 0, sizeof(pfv)); + + int rc = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF(rc); + PASS; +} + +/** + * \test Test cleanup with reference counting and deferred deletion + */ +static int SourcePcapFileHelperTest06(void) +{ + PcapFileFileVars pfv; + memset(&pfv, 0, sizeof(pfv)); + SC_ATOMIC_INIT(pfv.alerts_count); + SC_ATOMIC_SET(pfv.alerts_count, 0); + SC_ATOMIC_INIT(pfv.ref_cnt); + SC_ATOMIC_SET(pfv.ref_cnt, 2); /* simulate 2 packets in flight */ + pfv.cleanup_requested = false; + + /* Simulate first packet completion - should not cleanup yet */ + SC_ATOMIC_SUB(pfv.ref_cnt, 1); + FAIL_IF_NOT(SC_ATOMIC_GET(pfv.ref_cnt) == 1); + + /* Request cleanup while packets are still in flight */ + pfv.cleanup_requested = true; + + /* Simulate second packet completion - should trigger cleanup */ + if (SC_ATOMIC_SUB(pfv.ref_cnt, 1) == 1) { + FAIL_IF_NOT(pfv.cleanup_requested); /* cleanup should have been requested */ + /* In real code, CleanupPcapFileFileVars would be called here */ + } + + PASS; +} + +/** + * \test Test edge cases and error conditions + */ +static int SourcePcapFileHelperTest07(void) +{ + /* Test 1: PcapFileShouldDeletePcapFile with very high alert count */ + PcapFileSharedVars shared; + memset(&shared, 0, sizeof(shared)); + shared.delete_mode = PCAP_FILE_DELETE_NON_ALERTS; + + PcapFileFileVars pfv; + memset(&pfv, 0, sizeof(pfv)); + pfv.shared = &shared; + pfv.filename = SCStrdup("test.pcap"); + SC_ATOMIC_INIT(pfv.alerts_count); + SC_ATOMIC_SET(pfv.alerts_count, UINT64_MAX); /* max value */ + + int result = PcapFileShouldDeletePcapFile(&pfv); + FAIL_IF(result); /* should not delete with max alerts */ + + /* Test 2: PcapFileFinalizePacket with max alert count */ + SC_ATOMIC_INIT(pfv.ref_cnt); + SC_ATOMIC_ADD(pfv.ref_cnt, 1); + PcapFileAddAlertCount(&pfv, UINT16_MAX); /* max uint16_t */ + PcapFileFinalizePacket(&pfv); + /* Should not overflow or crash */ + + SCFree(pfv.filename); + PASS; +} + +/** + * \test Test command-line --pcap-file-delete override behavior + */ +static int SourcePcapFileHelperTest08(void) +{ + /* Test 1: Command line overrides YAML "false" */ + const char *conf_false = "%YAML 1.1\n" + "---\n" + "pcap-file:\n" + " delete-when-done: false\n"; + + SetupYamlConf(conf_false); + + /* Simulate --pcap-file-delete command line option */ + int set_result = SCConfSetFinal("pcap-file.delete-when-done", "true"); + FAIL_IF_NOT(set_result == 1); + + PcapFileDeleteMode result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); /* Should override YAML false */ + CleanupYamlConf(); + + /* Test 2: Command line overrides YAML "non-alerts" */ + const char *conf_non_alerts = "%YAML 1.1\n" + "---\n" + "pcap-file:\n" + " delete-when-done: \"non-alerts\"\n"; + + SetupYamlConf(conf_non_alerts); + + /* Simulate --pcap-file-delete command line option */ + set_result = SCConfSetFinal("pcap-file.delete-when-done", "true"); + FAIL_IF_NOT(set_result == 1); + + result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); /* Should override YAML "non-alerts" */ + CleanupYamlConf(); + + /* Test 3: Command line overrides no YAML config */ + SCConfCreateContextBackup(); + SCConfInit(); + + /* Simulate --pcap-file-delete command line option with no YAML config */ + set_result = SCConfSetFinal("pcap-file.delete-when-done", "true"); + FAIL_IF_NOT(set_result == 1); + + result = PcapFileParseDeleteMode(); + FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); /* Should set to always delete */ + + SCConfDeInit(); + SCConfRestoreContextBackup(); + + PASS; +} + +/** + * \test Test that cleanup defers while packets are in flight and that a file + * with alerts is not deleted in NON_ALERTS mode. + */ +static int SourcePcapFileHelperTest09(void) +{ + PcapFileSharedVars *shared = SCCalloc(1, sizeof(*shared)); + FAIL_IF_NULL(shared); + shared->delete_mode = PCAP_FILE_DELETE_NON_ALERTS; + + PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv)); + FAIL_IF_NULL(pfv); + pfv->shared = shared; + pfv->filename = SCStrdup("unit_del_test.pcap"); + FAIL_IF_NULL(pfv->filename); + + SC_ATOMIC_INIT(pfv->alerts_count); + SC_ATOMIC_SET(pfv->alerts_count, 0); + SC_ATOMIC_INIT(pfv->ref_cnt); + SC_ATOMIC_SET(pfv->ref_cnt, 2); /* two packets in flight */ + pfv->cleanup_requested = false; + + /* Request cleanup while packets still in flight: should defer. */ + CleanupPcapFileFileVars(pfv); + FAIL_IF_NOT(pfv->cleanup_requested); + FAIL_IF_NULL(pfv->filename); /* not freed yet */ + + /* First packet completes and generates an alert. */ + PcapFileAddAlertCount(pfv, 1); + PcapFileFinalizePacket(pfv); + FAIL_IF_NOT(SC_ATOMIC_GET(pfv->alerts_count) == 1); + + /* Second (last) packet completes: triggers final cleanup. */ + PcapFileFinalizePacket(pfv); + + /* pfv memory is freed at this point; only free shared. */ + SCFree(shared); + + PASS; +} + +/** + * \test Cover unlink-on-ALWAYS branch (ref_cnt == 0) and deferred deletion when ref_cnt > 0 + */ +static int SourcePcapFileHelperTest10(void) +{ + /* Create a temporary file that we expect to be deleted. */ + const char *tmpname = "suri_ut_delete_always.pcap"; + const uint8_t dummy[] = { 0x00 }; + int rc = TestHelperBufferToFile(tmpname, dummy, sizeof(dummy)); + FAIL_IF_NOT(rc >= 0); + + /* Case 1: delete ALWAYS with no packets in flight -> file unlinked immediately */ + PcapFileSharedVars *shared1 = SCCalloc(1, sizeof(*shared1)); + FAIL_IF_NULL(shared1); + shared1->delete_mode = PCAP_FILE_DELETE_ALWAYS; + + PcapFileFileVars *pfv1 = SCCalloc(1, sizeof(*pfv1)); + FAIL_IF_NULL(pfv1); + pfv1->shared = shared1; + pfv1->filename = SCStrdup(tmpname); + FAIL_IF_NULL(pfv1->filename); + + SC_ATOMIC_INIT(pfv1->alerts_count); + SC_ATOMIC_SET(pfv1->alerts_count, 0); + SC_ATOMIC_INIT(pfv1->ref_cnt); + SC_ATOMIC_SET(pfv1->ref_cnt, 0); + + /* Provide a closable handle to cover close path. */ + pfv1->pcap_handle = pcap_open_dead(DLT_EN10MB, 65535); + FAIL_IF_NULL(pfv1->pcap_handle); + + CleanupPcapFileFileVars(pfv1); + + /* File should be gone. */ + FILE *f = fopen(tmpname, "rb"); + FAIL_IF_NOT_NULL(f); + if (f != NULL) + fclose(f); + + /* Case 2: delete ALWAYS but ref_cnt > 0 -> defer until finalize. */ + /* Recreate the file. */ + rc = TestHelperBufferToFile(tmpname, dummy, sizeof(dummy)); + FAIL_IF_NOT(rc >= 0); + + PcapFileSharedVars *shared2 = SCCalloc(1, sizeof(*shared2)); + FAIL_IF_NULL(shared2); + shared2->delete_mode = PCAP_FILE_DELETE_ALWAYS; + + PcapFileFileVars *pfv2 = SCCalloc(1, sizeof(*pfv2)); + FAIL_IF_NULL(pfv2); + pfv2->shared = shared2; + pfv2->filename = SCStrdup(tmpname); + FAIL_IF_NULL(pfv2->filename); + + SC_ATOMIC_INIT(pfv2->alerts_count); + SC_ATOMIC_SET(pfv2->alerts_count, 0); + SC_ATOMIC_INIT(pfv2->ref_cnt); + SC_ATOMIC_SET(pfv2->ref_cnt, 1); + pfv2->pcap_handle = pcap_open_dead(DLT_EN10MB, 65535); + FAIL_IF_NULL(pfv2->pcap_handle); + + CleanupPcapFileFileVars(pfv2); + + /* Still exists now because deletion should be deferred. */ + f = fopen(tmpname, "rb"); + FAIL_IF_NULL(f); + if (f != NULL) + fclose(f); + + /* Finalize the last packet which should trigger final cleanup & unlink. */ + PcapFileFinalizePacket(pfv2); + + /* Now the file should be gone. */ + f = fopen(tmpname, "rb"); + FAIL_IF_NOT_NULL(f); + if (f != NULL) + fclose(f); + + SCFree(shared1); + SCFree(shared2); + + PASS; +} + +/** + * \test Test PcapFileReleasePseudoPacket refcount decrement without cleanup + */ +static int SourcePcapFileHelperTest11(void) +{ + /* Setup pfv with ref_cnt=2 so release does not trigger cleanup */ + PcapFileFileVars pfv; + memset(&pfv, 0, sizeof(pfv)); + SC_ATOMIC_INIT(pfv.ref_cnt); + SC_ATOMIC_SET(pfv.ref_cnt, 2); + pfv.cleanup_requested = false; + + /* Allocate a packet from the pool to allow safe release */ + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + p->pcap_v.pfv = &pfv; + + /* Call release and ensure ref count decremented by 1 */ + PcapFileReleasePseudoPacket(p); + FAIL_IF_NOT(SC_ATOMIC_GET(pfv.ref_cnt) == 1); + + PASS; +} + +/** + * \test Test adding alert count and that cleanup after refcnt reaches zero + * does not delete when alerts exist in NON_ALERTS mode. + */ +static int SourcePcapFileHelperTest12(void) +{ + PcapFileSharedVars shared; + memset(&shared, 0, sizeof(shared)); + shared.delete_mode = PCAP_FILE_DELETE_NON_ALERTS; + + PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv)); + FAIL_IF_NULL(pfv); + pfv->shared = &shared; + pfv->filename = SCStrdup("ut_non_alerts.pcap"); + FAIL_IF_NULL(pfv->filename); + + SC_ATOMIC_INIT(pfv->alerts_count); + SC_ATOMIC_SET(pfv->alerts_count, 0); + SC_ATOMIC_INIT(pfv->ref_cnt); + SC_ATOMIC_SET(pfv->ref_cnt, 1); + pfv->cleanup_requested = true; + + /* Simulate pseudo alert */ + PcapFileAddAlertCount(pfv, 2); + FAIL_IF_NOT(SC_ATOMIC_GET(pfv->alerts_count) == 2); + + /* Simulate last ref release triggering cleanup; file shouldn't be deleted + * due to alerts > 0. We cannot check unlink here; rely on return value. */ + if (SC_ATOMIC_SUB(pfv->ref_cnt, 1) == 1) { + CleanupPcapFileFileVars(pfv); + } + /* Success if no crash. */ + PASS; +} + +/** + * \test Test global current pfv pointer lifecycle + */ +static int SourcePcapFileHelperTest13(void) +{ + PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv)); + FAIL_IF_NULL(pfv); + pfv->filename = SCStrdup("ut_global_clear.pcap"); + FAIL_IF_NULL(pfv->filename); + + PcapFileSetCurrentPfv(pfv); + FAIL_IF_NOT(PcapFileGetCurrentPfv() == pfv); + + /* Cleanup should clear global reference when pointing to this pfv */ + CleanupPcapFileFileVars(pfv); + /* Global accessor must be NULL after cleanup. */ + FAIL_IF_NOT(PcapFileGetCurrentPfv() == NULL); + PASS; +} + +/** + * \test Exercise unlink failure branch in CleanupPcapFileFileVars + */ +static int SourcePcapFileHelperTest14(void) +{ + PcapFileSharedVars shared; + memset(&shared, 0, sizeof(shared)); + shared.delete_mode = PCAP_FILE_DELETE_ALWAYS; + + PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv)); + FAIL_IF_NULL(pfv); + pfv->shared = &shared; + pfv->filename = SCStrdup("does-not-exist-ut.pcap"); + FAIL_IF_NULL(pfv->filename); + + SC_ATOMIC_INIT(pfv->ref_cnt); + SC_ATOMIC_SET(pfv->ref_cnt, 0); + + /* Attempt cleanup; unlink should fail but must not crash */ + CleanupPcapFileFileVars(pfv); + PASS; +} + +/** + * \test Cover alerts hook fallback using current PFV + */ +static int SourcePcapFileHelperTest15(void) +{ + PcapFileInstallCaptureHooks(); + + PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv)); + FAIL_IF_NULL(pfv); + SC_ATOMIC_INIT(pfv->alerts_count); + SC_ATOMIC_SET(pfv->alerts_count, 0); + + /* Set current PFV to exercise fallback path */ + PcapFileSetCurrentPfv(pfv); + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + p->alerts.cnt = 3; /* simulate 3 alerts */ + p->pcap_v.pfv = NULL; /* force fallback */ + + /* Call hook: it should update pfv->alerts_count */ + CaptureHooksOnPacketWithAlerts(p); + FAIL_IF_NOT(SC_ATOMIC_GET(pfv->alerts_count) == 3); + + /* Cleanup */ + PacketFreeOrRelease(p); + CleanupPcapFileFileVars(pfv); + PASS; +} + +/** + * \brief Register unit tests for pcap file helper + */ +void SourcePcapFileHelperRegisterTests(void) +{ + UtRegisterTest("SourcePcapFileHelperTest01", SourcePcapFileHelperTest01); + UtRegisterTest("SourcePcapFileHelperTest02", SourcePcapFileHelperTest02); + UtRegisterTest("SourcePcapFileHelperTest03", SourcePcapFileHelperTest03); + UtRegisterTest("SourcePcapFileHelperTest04", SourcePcapFileHelperTest04); + UtRegisterTest("SourcePcapFileHelperTest05", SourcePcapFileHelperTest05); + UtRegisterTest("SourcePcapFileHelperTest06", SourcePcapFileHelperTest06); + UtRegisterTest("SourcePcapFileHelperTest07", SourcePcapFileHelperTest07); + UtRegisterTest("SourcePcapFileHelperTest08", SourcePcapFileHelperTest08); + UtRegisterTest("SourcePcapFileHelperTest09", SourcePcapFileHelperTest09); + UtRegisterTest("SourcePcapFileHelperTest10", SourcePcapFileHelperTest10); + UtRegisterTest("SourcePcapFileHelperTest11", SourcePcapFileHelperTest11); + UtRegisterTest("SourcePcapFileHelperTest12", SourcePcapFileHelperTest12); + UtRegisterTest("SourcePcapFileHelperTest13", SourcePcapFileHelperTest13); + UtRegisterTest("SourcePcapFileHelperTest14", SourcePcapFileHelperTest14); + UtRegisterTest("SourcePcapFileHelperTest15", SourcePcapFileHelperTest15); +} +#endif /* UNITTESTS */ diff --git a/src/source-pcap-file-helper.h b/src/source-pcap-file-helper.h index 2de7d16cc3..7d59d4fc75 100644 --- a/src/source-pcap-file-helper.h +++ b/src/source-pcap-file-helper.h @@ -23,10 +23,17 @@ #include "suricata-common.h" #include "tm-threads.h" +#include "util-atomic.h" #ifndef SURICATA_SOURCE_PCAP_FILE_HELPER_H #define SURICATA_SOURCE_PCAP_FILE_HELPER_H +typedef enum { + PCAP_FILE_DELETE_NONE = 0, + PCAP_FILE_DELETE_ALWAYS, + PCAP_FILE_DELETE_NON_ALERTS +} PcapFileDeleteMode; + typedef struct PcapFileGlobalVars_ { uint64_t cnt; /** packet counter */ ChecksumValidationMode conf_checksum_mode; @@ -46,7 +53,7 @@ typedef struct PcapFileSharedVars_ struct timespec last_processed; - bool should_delete; + PcapFileDeleteMode delete_mode; ThreadVars *tv; TmSlot *slot; @@ -76,6 +83,14 @@ typedef struct PcapFileFileVars_ PcapFileSharedVars *shared; + SC_ATOMIC_DECLARE(uint64_t, alerts_count); + /* Reference count for outstanding users (e.g., pseudo packets created + * during shutdown/timeout handling). Ensures the per-file state is not + * cleaned up or the file deleted before all dependent processing that may + * still raise alerts has completed. */ + SC_ATOMIC_DECLARE(uint32_t, ref_cnt); + bool cleanup_requested; + /* fields used to get the first packet's timestamp early, * so it can be used to setup the time subsys. */ const u_char *first_pkt_data; @@ -121,4 +136,23 @@ TmEcode ValidateLinkType(int datalink, DecoderFunc *decoder); const char *PcapFileGetFilename(void); +bool PcapFileShouldDeletePcapFile(PcapFileFileVars *pfv); + +void PcapFileFinalizePacket(PcapFileFileVars *pfv); + +PcapFileDeleteMode PcapFileParseDeleteMode(void); + +void PcapFileAddAlertCount(PcapFileFileVars *pfv, uint16_t alert_count); + +void PcapFileReleasePseudoPacket(Packet *p); + +void PcapFileSetCurrentPfv(PcapFileFileVars *pfv); +PcapFileFileVars *PcapFileGetCurrentPfv(void); + +void PcapFileInstallCaptureHooks(void); + +#ifdef UNITTESTS +void SourcePcapFileHelperRegisterTests(void); +#endif + #endif /* SURICATA_SOURCE_PCAP_FILE_HELPER_H */ diff --git a/src/source-pcap-file.c b/src/source-pcap-file.c index 75252bc3bb..fd15b2036e 100644 --- a/src/source-pcap-file.c +++ b/src/source-pcap-file.c @@ -33,6 +33,7 @@ #include "suricata.h" #include "conf.h" #include "util-misc.h" +#include "capture-hooks.h" extern uint32_t max_pending_packets; PcapFileGlobalVars pcap_g; @@ -148,6 +149,8 @@ void PcapFileGlobalInit(void) memset(&pcap_g, 0x00, sizeof(pcap_g)); SC_ATOMIC_INIT(pcap_g.invalid_checksums); + PcapFileInstallCaptureHooks(); + #if defined(HAVE_SETVBUF) && defined(OS_LINUX) pcap_g.read_buffer_size = PCAP_FILE_BUFFER_SIZE_DEFAULT; @@ -204,8 +207,11 @@ TmEcode ReceivePcapFileLoop(ThreadVars *tv, void *data, void *slot) if(ptv->is_directory == 0) { SCLogInfo("Starting file run for %s", ptv->behavior.file->filename); + /* Hold a reference for the duration of file processing, including + * post-dispatch flow draining during EngineStop, so deletion decision + * is deferred until all pseudo packets have been processed. */ + SC_ATOMIC_ADD(ptv->behavior.file->ref_cnt, 1); status = PcapFileDispatch(ptv->behavior.file); - CleanupPcapFileFromThreadVars(ptv, ptv->behavior.file); } else { SCLogInfo("Starting directory run for %s", ptv->behavior.directory->filename); PcapDirectoryDispatch(ptv->behavior.directory); @@ -215,6 +221,19 @@ TmEcode ReceivePcapFileLoop(ThreadVars *tv, void *data, void *slot) SCLogDebug("Pcap file loop complete with status %u", status); status = PcapFileExit(status, &ptv->shared.last_processed); + + /* Release the hold set before dispatch and trigger deferred cleanup + * if requested and this was the final reference. */ + if (ptv->is_directory == 0) { + /* Ensure current pfv global is set for any pseudo packets emitted + * during EngineStop/flow drain. */ + PcapFileSetCurrentPfv(ptv->behavior.file); + uint32_t prev = SC_ATOMIC_SUB(ptv->behavior.file->ref_cnt, 1); + if (prev == 1 && ptv->behavior.file->cleanup_requested) { + CleanupPcapFileFileVars(ptv->behavior.file); + ptv->behavior.file = NULL; + } + } SCReturnInt(status); } @@ -261,11 +280,7 @@ TmEcode ReceivePcapFileThreadInit(ThreadVars *tv, const void *initdata, void **d } } - int should_delete = 0; - ptv->shared.should_delete = false; - if (SCConfGetBool("pcap-file.delete-when-done", &should_delete) == 1) { - ptv->shared.should_delete = should_delete == 1; - } + ptv->shared.delete_mode = PcapFileParseDeleteMode(); DIR *directory = NULL; SCLogDebug("checking file or directory %s", (char*)initdata); @@ -422,6 +437,11 @@ TmEcode ReceivePcapFileThreadDeinit(ThreadVars *tv, void *data) SCEnter(); if(data != NULL) { PcapFileThreadVars *ptv = (PcapFileThreadVars *) data; + + if (!ptv->is_directory && ptv->behavior.file != NULL) { + CleanupPcapFileFromThreadVars(ptv, ptv->behavior.file); + } + CleanupPcapFileThreadVars(ptv); } SCReturnInt(TM_ECODE_OK); diff --git a/src/source-pcap.h b/src/source-pcap.h index 7fac8e7d9f..489b01f67c 100644 --- a/src/source-pcap.h +++ b/src/source-pcap.h @@ -24,6 +24,8 @@ #ifndef SURICATA_SOURCE_PCAP_H #define SURICATA_SOURCE_PCAP_H +typedef struct PcapFileFileVars_ PcapFileFileVars; + void TmModuleReceivePcapRegister (void); void TmModuleDecodePcapRegister (void); void PcapTranslateIPToDevice(char *pcap_dev, size_t len); @@ -35,6 +37,7 @@ void PcapTranslateIPToDevice(char *pcap_dev, size_t len); typedef struct PcapPacketVars_ { uint64_t pcap_cnt; uint32_t tenant_id; + PcapFileFileVars *pfv; } PcapPacketVars; /** needs to be able to contain Windows adapter id's, so diff --git a/src/tm-threads.c b/src/tm-threads.c index 5d3d577b19..647ba83598 100644 --- a/src/tm-threads.c +++ b/src/tm-threads.c @@ -35,6 +35,7 @@ #include "tm-queues.h" #include "tm-queuehandlers.h" #include "tm-threads.h" +#include "capture-hooks.h" #include "tmqh-packetpool.h" #include "threads.h" #include "util-affinity.h" @@ -46,6 +47,7 @@ #include "util-signal.h" #include "queue.h" #include "util-validate.h" +#include "source-pcap-file-helper.h" #ifdef PROFILE_LOCKING thread_local uint64_t mutex_lock_contention; @@ -1358,6 +1360,7 @@ again: if (p != NULL) { p->flags |= PKT_PSEUDO_STREAM_END; PKT_SET_SRC(p, PKT_SRC_SHUTDOWN_FLUSH); + CaptureHooksOnPseudoPacketCreated(p); PacketQueue *q = tv->stream_pq; SCMutexLock(&q->mutex_q); PacketEnqueue(q, p); @@ -1441,6 +1444,7 @@ again: if (p != NULL) { p->flags |= PKT_PSEUDO_STREAM_END; PKT_SET_SRC(p, PKT_SRC_SHUTDOWN_FLUSH); + CaptureHooksOnPseudoPacketCreated(p); PacketQueue *q = tv->stream_pq; SCMutexLock(&q->mutex_q); PacketEnqueue(q, p); diff --git a/suricata.yaml.in b/suricata.yaml.in index 3b29513f9d..950856befe 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -896,7 +896,9 @@ pcap-file: # buffer-size: 128 KiB # tenant-id: none # applies in multi-tenant environment with "direct" selector - # delete-when-done: false # applies to file and directory + # Applies to file and directory. Options: false (no deletion), true (always delete), + # "non-alerts" (delete only files with no alerts) + # delete-when-done: false # PCAP Directory Processing options # recursive: false