-Snort 3 can log IPS events with some meta data and dump packets. The Data
+Snort 3 can log IPS events with some meta data and dump packets. Advanced
Logging feature extends that ability to log protocol-specific data, sniffing
traffic alongside with normal inspection.
** `fields` - data fields to log (if a field is not supported it will be ignored)
Configuration from different bindings do not interfere. Among other
-things it allows tenants to get independent data logging configurations.
+things it allows tenants to get independent logging configurations.
extractor =
{
{ service = 'http', on_events = 'eot', fields = 'ts, uri' },
{ service = 'conn', on_events = 'eof', fields = 'ts, uid, service' },
{ service = 'dns', on_events = 'response', fields = 'ts, uid, query, answers' }
+ { service = 'weird', on_events = 'builtin', fields = 'ts, msg, gid, sid' }
+ { service = 'notice', on_events = 'ips_logging', fields = 'msg, sid, refs' }
}
}
** `response`
* connection (conn)
** `eof` (end of flow)
+* internal built-in checks which failed (weird)
+ ** 'builtin' (internally-detected infraction is queued for further processing)
+* triggered IPS rule, whether built-in or text or SO (notice)
+ ** `ips_logging` (matched rules sent to IPS logging)
Common fields available for every service:
For TCP orig_bytes and resp_bytes are calculated using first seen sequence number and next expected sequence number.
These are reset during TCP flow restart. For this case only bytes seen following the restart will be reported.
+Fields supported for 'weird' and 'notice' logs:
+
+* `sid` - unique signature number of the rule
+* `gid` - component ID which generated the event
+* `msg` - rule message
+* `proto` - transport protocol
+* `source` - assigned inspector
+
+'notice' events for text rules also get the following fields:
+
+* `action` - action of triggered event
+* `refs` - references mentioned in a rule
+* `rev` - particular revision number of the rule
+
==== Example
Adding the following lines to a default snort configuration (which supports FTP
p->dsize = dsize;
}
- IpsRuleEvent data_event(event, p);
+ IpsRuleEvent data_event(event);
DataBus::publish(DetectionEngine::get_pub_id(), DetectionEventIds::IPS_LOGGING, data_event, p->flow);
OutputSet* idx = head ? head->LogList : nullptr;
if ( !otn )
return 0;
+ const Packet* p = get_current_packet();
+ if ( p )
+ {
+ IpsQueuingEvent data_event(otn->sigInfo);
+ DataBus::publish(get_pub_id(), DetectionEventIds::BUILTIN, data_event, p->flow);
+ }
+
SF_EVENTQ* pq = get_event_queue();
EventNode* en = (EventNode*)sfeventq_event_alloc(pq);
extractor_conn.cc
extractor_csv_logger.cc
extractor_csv_logger.h
+ extractor_detection.cc
+ extractor_detection.h
extractor_dns.cc
extractor_enums.h
extractor_flow_data.cc
extractor_json_logger.h
extractor_logger.cc
extractor_logger.h
+ extractor_null_conn.h
extractor_service.cc
extractor_service.h
extractors.cc
extractors.h
- extractor_null_conn.h
)
add_library(extractor OBJECT ${FILE_LIST})
static const Parameter extractor_proto_params[] =
{
- { "service", Parameter::PT_ENUM, "http | ftp | conn | dns", nullptr,
+ { "service", Parameter::PT_ENUM, "http | ftp | conn | dns | weird | notice", nullptr,
"service to extract from" },
{ "tenant_id", Parameter::PT_INT, "0:max32", "0",
struct ExtractorStats
{
- PegCount total_event;
+ PegCount total_events;
};
class ExtractorReloadSwapper;
if (flow->pkt_type < PktType::IP or flow->pkt_type > PktType::ICMP or !filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
logger->open_record();
log(nts_fields, &event, flow);
record.append(to_string(sec * 1000000 + usec));
}
+void CsvExtractorLogger::add_array_separator()
+{
+ record.push_back(' ');
+}
+
+void CsvExtractorLogger::add_field(const char*, const std::vector<const char*>& v)
+{
+ record.push_back(delimiter);
+
+ if (v.empty())
+ {
+ if (!isprint(delimiter))
+ record.append("-");
+ return;
+ }
+
+ if (v[0] && v[0][0])
+ add_escaped(v[0], strlen(v[0]));
+
+ for (size_t i = 1; i < v.size(); ++i)
+ {
+ add_array_separator();
+ if (v[i] && v[i][0])
+ add_escaped(v[i], strlen(v[i]));
+ }
+}
+
+void CsvExtractorLogger::add_field(const char*, const std::vector<uint64_t>& v)
+{
+ record.push_back(delimiter);
+
+ if (v.empty())
+ {
+ if (!isprint(delimiter))
+ record.append("-");
+ return;
+ }
+
+ record.append(to_string(v[0]));
+ for (size_t i = 1; i < v.size(); ++i)
+ {
+ add_array_separator();
+ record.append(to_string(v[i]));
+ }
+}
+
+void CsvExtractorLogger::add_field(const char*, const std::vector<bool>& v)
+{
+ record.push_back(delimiter);
+
+ if (v.empty())
+ {
+ if (!isprint(delimiter))
+ record.append("-");
+ return;
+ }
+
+ record.append(v[0] ? "true" : "false");
+ for (size_t i = 1; i < v.size(); ++i)
+ {
+ add_array_separator();
+ record.append(v[i] ? "true" : "false");
+ }
+}
+
#ifdef UNIT_TEST
+#include <vector>
+
#include "catch/snort_catch.h"
class CsvExtractorLoggerHelper : public CsvExtractorLogger
add_escaped(input, i_len);
CHECK(record == expected);
}
+
+ void check(const std::vector<bool>& v, const std::string& expected)
+ {
+ record.clear();
+ add_field(nullptr, v);
+ auto data = record.substr(1);
+ CHECK(data == expected);
+ }
+
+ void check(const std::vector<uint64_t>& v, const std::string& expected)
+ {
+ record.clear();
+ add_field(nullptr, v);
+ auto data = record.substr(1);
+ CHECK(data == expected);
+ }
+
+ void check(const std::vector<const char*>& v, const std::string& expected)
+ {
+ record.clear();
+ add_field(nullptr, v);
+ auto data = record.substr(1);
+ CHECK(data == expected);
+ }
};
class CsvExtractorLoggerTest
void check_tsv(const char* input, size_t i_len, const std::string& expected)
{ tsv.check(input, i_len, expected); }
+ void check_csv_vec(const std::vector<bool>& v, const std::string& expected)
+ { csv.check(v, expected); }
+
+ void check_csv_vec(const std::vector<uint64_t>& v, const std::string& expected)
+ { csv.check(v, expected); }
+
+ void check_csv_vec(const std::vector<const char*>& v, const std::string& expected)
+ { csv.check(v, expected); }
+
+ void check_tsv_vec(const std::vector<bool>& v, const std::string& expected)
+ { tsv.check(v, expected); }
+
+ void check_tsv_vec(const std::vector<uint64_t>& v, const std::string& expected)
+ { tsv.check(v, expected); }
+
+ void check_tsv_vec(const std::vector<const char*>& v, const std::string& expected)
+ { tsv.check(v, expected); }
+
private:
CsvExtractorLoggerHelper csv{','};
CsvExtractorLoggerHelper tsv{'\t'};
check_tsv(input, strlen(input), " ");
}
+TEST_CASE_METHOD(CsvExtractorLoggerTest, "csv/tsv vector bool: empty", "[extractor]")
+{
+ const std::vector<bool> bool_vec = {};
+ check_csv_vec(bool_vec, "");
+ check_tsv_vec(bool_vec, "-");
+}
+
+TEST_CASE_METHOD(CsvExtractorLoggerTest, "csv/tsv vector bool: 3 items", "[extractor]")
+{
+ const std::vector<bool> bool_vec = {true, false, true};
+ check_csv_vec(bool_vec, "true false true");
+ check_tsv_vec(bool_vec, "true false true");
+}
+
+TEST_CASE_METHOD(CsvExtractorLoggerTest, "csv/tsv vector uint64_t: empty", "[extractor]")
+{
+ const std::vector<uint64_t> unum_vec = {};
+ check_csv_vec(unum_vec, "");
+ check_tsv_vec(unum_vec, "-");
+}
+
+TEST_CASE_METHOD(CsvExtractorLoggerTest, "csv/tsv vector uint64_t: 3 items", "[extractor]")
+{
+ const std::vector<uint64_t> unum_vec = {1,2,3};
+ check_csv_vec(unum_vec, "1 2 3");
+ check_tsv_vec(unum_vec, "1 2 3");
+}
+
+TEST_CASE_METHOD(CsvExtractorLoggerTest, "csv/tsv vector str: empty", "[extractor]")
+{
+ const std::vector<const char*> char_vec = {};
+ check_csv_vec(char_vec, "");
+ check_tsv_vec(char_vec, "-");
+}
+
+TEST_CASE_METHOD(CsvExtractorLoggerTest, "csv/tsv vector str: 3 items", "[extractor]")
+{
+ const std::vector<const char*> char_vec = {"exe", "pdf", "txt"};
+ check_csv_vec(char_vec, "exe pdf txt");
+ check_tsv_vec(char_vec, "exe pdf txt");
+}
+
#endif
void add_field(const char*, struct timeval) override;
void add_field(const char*, const snort::SfIp&) override;
void add_field(const char*, bool) override;
+
+ void add_field(const char*, const std::vector<const char*>&) override;
+ void add_field(const char*, const std::vector<uint64_t>&) override;
+ void add_field(const char*, const std::vector<bool>&) override;
+
void open_record() override;
void close_record(const snort::Connector::ID&) override;
protected:
+ void add_array_separator();
void add_escaped(const char*, size_t);
void ts_snort(const struct timeval&);
void ts_snort_yy(const struct timeval&);
--- /dev/null
+//--------------------------------------------------------------------------
+// Copyright (C) 2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation. You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//--------------------------------------------------------------------------
+// extractor_detection.cc author Anna Norokh <anorokh@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "extractor_detection.h"
+
+#include "profiler/profiler.h"
+#include "pub_sub/detection_events.h"
+
+#include "extractor.h"
+
+using namespace snort;
+using namespace std;
+
+namespace builtin
+{
+static const char* get_msg(const DataEvent* event, const Flow*)
+{
+ return ((const IpsQueuingEvent*)event)->get_stripped_msg().c_str();
+}
+
+static const char* get_proto(const DataEvent*, const Flow*)
+{
+ const Packet* p = ExtractorEvent::get_packet();
+
+ if (p != nullptr)
+ return p->get_type();
+
+ return nullptr;
+}
+
+static const char* get_source(const DataEvent*, const Flow* flow)
+{
+ if (flow->gadget)
+ return flow->gadget->get_name();
+
+ return "";
+}
+
+static uint64_t get_sid(const DataEvent* event, const Flow*)
+{
+ return (uint64_t)((const IpsQueuingEvent*)event)->get_sid();
+}
+
+static uint64_t get_gid(const DataEvent* event, const Flow*)
+{
+ return (uint64_t)((const IpsQueuingEvent*)event)->get_gid();
+}
+
+static const map<string, ExtractorEvent::BufGetFn> sub_buf_getters =
+{
+ {"msg", get_msg},
+ {"source", get_source},
+ {"proto", get_proto},
+};
+
+static const map<string, ExtractorEvent::NumGetFn> gid_sid_getters =
+{
+ {"gid", get_gid},
+ {"sid", get_sid},
+};
+}
+
+THREAD_LOCAL const snort::Connector::ID* BuiltinExtractor::log_id = nullptr;
+
+BuiltinExtractor::BuiltinExtractor(Extractor& i, uint32_t t, const vector<string>& fields)
+ : ExtractorEvent(ServiceType::IPS_BUILTIN, i, t)
+{
+ for (const auto& f : fields)
+ {
+ if (append(nts_fields, nts_getters, f))
+ continue;
+ if (append(sip_fields, sip_getters, f))
+ continue;
+ if (append(num_fields, num_getters, f))
+ continue;
+ if (append(num_fields, builtin::gid_sid_getters, f))
+ continue;
+ if (append(buf_fields, builtin::sub_buf_getters, f))
+ continue;
+ }
+
+ DataBus::subscribe_global(de_pub_key, DetectionEventIds::BUILTIN, new IpsBuiltin(*this, S_NAME), i.get_snort_config());
+}
+
+void BuiltinExtractor::internal_tinit(const snort::Connector::ID* service_id)
+{ log_id = service_id; }
+
+void BuiltinExtractor::handle(DataEvent& event, Flow* flow)
+{
+ // cppcheck-suppress unreadVariable
+ Profile profile(extractor_perf_stats);
+
+ if (!filter(flow))
+ return;
+
+ extractor_stats.total_events++;
+
+ logger->open_record();
+ log(nts_fields, &event, flow);
+ log(sip_fields, &event, flow);
+ log(num_fields, &event, flow);
+ log(buf_fields, &event, flow);
+ logger->close_record(*log_id);
+}
+
+namespace ips
+{
+static const char* get_msg(const DataEvent* event, const Flow*)
+{
+ return ((const IpsRuleEvent*)event)->get_stripped_msg().c_str();
+}
+
+static const char* get_action(const DataEvent* event, const Flow*)
+{
+ return ((const IpsRuleEvent*)event)->get_action();
+}
+
+static const vector<const char*>& get_refs(const DataEvent* event, const Flow*)
+{
+ return ((const IpsRuleEvent*)event)->get_references();
+}
+
+static const char* get_proto(const DataEvent*, const Flow*)
+{
+ const Packet* p = ExtractorEvent::get_packet();
+
+ if (p != nullptr)
+ return p->get_type();
+
+ return nullptr;
+}
+
+static const char* get_source(const DataEvent*, const Flow* flow)
+{
+ if (flow->gadget)
+ return flow->gadget->get_name();
+
+ return "";
+}
+
+static uint64_t get_sid(const DataEvent* event, const Flow*)
+{
+ return (uint64_t)((const IpsRuleEvent*)event)->get_sid();
+}
+
+static uint64_t get_gid(const DataEvent* event, const Flow*)
+{
+ return (uint64_t)((const IpsRuleEvent*)event)->get_gid();
+}
+
+static uint64_t get_rev(const DataEvent* event, const Flow*)
+{
+ return (uint64_t)((const IpsRuleEvent*)event)->get_rev();
+}
+
+static const map<string, ExtractorEvent::BufGetFn> sub_buf_getters =
+{
+ {"msg", get_msg},
+ {"action", get_action},
+ {"source", get_source},
+ {"proto", get_proto},
+};
+
+static const map<string, ExtractorEvent::NumGetFn> gid_sid_rev_getters =
+{
+ {"gid", get_gid},
+ {"sid", get_sid},
+ {"rev", get_rev},
+};
+
+static const map<string, IpsUserExtractor::VecGetFn> vec_getters =
+{
+ {"refs", get_refs}
+};
+}
+
+THREAD_LOCAL const snort::Connector::ID* IpsUserExtractor::log_id = nullptr;
+
+IpsUserExtractor::IpsUserExtractor(Extractor& i, uint32_t t, const vector<string>& fields)
+ : ExtractorEvent(ServiceType::IPS_USER, i, t)
+{
+ for (const auto& f : fields)
+ {
+ if (append(nts_fields, nts_getters, f))
+ continue;
+ if (append(sip_fields, sip_getters, f))
+ continue;
+ if (append(num_fields, num_getters, f))
+ continue;
+ if (append(num_fields, ips::gid_sid_rev_getters, f))
+ continue;
+ if (append(buf_fields, ips::sub_buf_getters, f))
+ continue;
+ if (append(vec_fields, ips::vec_getters, f))
+ continue;
+ }
+
+ DataBus::subscribe_global(de_pub_key, DetectionEventIds::IPS_LOGGING, new IpsUser(*this, S_NAME), i.get_snort_config());
+}
+
+void IpsUserExtractor::internal_tinit(const snort::Connector::ID* service_id)
+{ log_id = service_id; }
+
+void IpsUserExtractor::handle(DataEvent& event, Flow* flow)
+{
+ // cppcheck-suppress unreadVariable
+ Profile profile(extractor_perf_stats);
+
+ if (!filter(flow))
+ return;
+
+ extractor_stats.total_events++;
+
+ logger->open_record();
+ log(nts_fields, &event, flow);
+ log(sip_fields, &event, flow);
+ log(num_fields, &event, flow);
+ log(buf_fields, &event, flow);
+ log(vec_fields, &event, flow);
+ logger->close_record(*log_id);
+}
+
+vector<const char*> IpsUserExtractor::get_field_names() const
+{
+ vector<const char*> res = ExtractorEvent::get_field_names();
+
+ for (const auto& f : vec_fields)
+ res.push_back(f.name);
+
+ return res;
+}
+
--- /dev/null
+//--------------------------------------------------------------------------
+// Copyright (C) 2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation. You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//--------------------------------------------------------------------------
+// extractor_detection.h author Anna Norokh <anorokh@cisco.com>
+
+#ifndef EXTRACTOR_RULES_H
+#define EXTRACTOR_RULES_H
+
+#include "extractors.h"
+
+class BuiltinExtractor : public ExtractorEvent
+{
+public:
+ BuiltinExtractor(Extractor&, uint32_t tenant, const std::vector<std::string>& fields);
+
+ void handle(DataEvent&, Flow*);
+
+private:
+ using IpsBuiltin = Handler<BuiltinExtractor>;
+
+ void internal_tinit(const snort::Connector::ID*) override;
+
+ static THREAD_LOCAL const snort::Connector::ID* log_id;
+};
+
+class IpsUserExtractor : public ExtractorEvent
+{
+public:
+ using VecGetFn = const std::vector<const char*>& (*) (const DataEvent*, const Flow*);
+ using VecField = DataField<const std::vector<const char*>&, const DataEvent*, const Flow*>;
+
+ IpsUserExtractor(Extractor&, uint32_t tenant, const std::vector<std::string>& fields);
+
+ std::vector<const char*> get_field_names() const override;
+ void handle(DataEvent&, Flow*);
+
+private:
+ using IpsUser = Handler<IpsUserExtractor>;
+
+ void internal_tinit(const snort::Connector::ID*) override;
+
+ std::vector<VecField> vec_fields;
+ static THREAD_LOCAL const snort::Connector::ID* log_id;
+};
+
+#endif
if (!filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
logger->open_record();
log(nts_fields, &event, flow);
FTP,
CONN,
DNS,
+ IPS_BUILTIN,
+ IPS_USER,
ANY,
MAX
};
return "conn";
case DNS:
return "dns";
+ case IPS_BUILTIN:
+ return "weird";
+ case IPS_USER:
+ return "notice";
case ANY: // fallthrough
case MAX: // fallthrough
default:
if (!filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
logger->open_record();
log(nts_fields, &event, flow);
if (!filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
logger->open_record();
log(nts_fields, &event, flow);
if (!owner.filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
auto fd = ExtractorFlowData::get<FtpExtractorFlowData>(flow);
if (!owner.filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
auto fd = ExtractorFlowData::get<FtpExtractorFlowData>(flow);
if (!filter(flow))
return;
- extractor_stats.total_event++;
+ extractor_stats.total_events++;
logger->open_record();
log(nts_fields, &event, flow);
js.uput(f, sec * 1000000 + usec);
}
+
+void JsonExtractorLogger::add_field(const char* f, const std::vector<const char*>& v)
+{
+ if (v.empty())
+ return;
+
+ js.open_array(f);
+ for (const auto& val : v)
+ js.put(nullptr, val);
+
+ js.close_array();
+}
+
+void JsonExtractorLogger::add_field(const char* f, const std::vector<uint64_t>& v)
+{
+ if (v.empty())
+ return;
+
+ js.open_array(f);
+ for (const auto unum : v)
+ js.put(nullptr, unum);
+
+ js.close_array();
+}
+
+void JsonExtractorLogger::add_field(const char* f, const std::vector<bool>& v)
+{
+ if (v.empty())
+ return;
+
+ js.open_array(f);
+ for (bool b : v)
+ b ? js.put_true(nullptr) : js.put_false(nullptr);
+
+ js.close_array();
+}
+
+#ifdef UNIT_TEST
+
+#include <vector>
+
+#include "catch/snort_catch.h"
+
+class JsonExtractorLoggerTest : public JsonExtractorLogger
+{
+public:
+ JsonExtractorLoggerTest() : JsonExtractorLogger(nullptr, TimeType::MAX) { }
+
+ void check(const char* f, const std::vector<bool>& v, const std::string& expected)
+ {
+ oss.str(std::string());
+ add_field(f, v);
+ CHECK(oss.str() == expected);
+ }
+
+ void check(const char* f, const std::vector<uint64_t>& v, const std::string& expected)
+ {
+ oss.str(std::string());
+ add_field(f, v);
+ CHECK(oss.str() == expected);
+ }
+
+ void check(const char* f, const std::vector<const char*>& v, const std::string& expected)
+ {
+ oss.str(std::string());
+ add_field(f, v);
+ CHECK(oss.str() == expected);
+ }
+};
+
+TEST_CASE_METHOD(JsonExtractorLoggerTest, "json vector bool: empty", "[extractor]")
+{
+ const std::vector<bool> bool_vec = {};
+ check("bool", bool_vec, "");
+}
+
+TEST_CASE_METHOD(JsonExtractorLoggerTest, "json vector bool: 3 items", "[extractor]")
+{
+ const std::vector<bool> bool_vec = {true, false, true};
+ check("bool", bool_vec, "\"bool\": [ true, false, true ]\n");
+}
+
+TEST_CASE_METHOD(JsonExtractorLoggerTest, "json vector uint64_t: empty", "[extractor]")
+{
+ const std::vector<uint64_t> num_vec = {};
+ check("num", num_vec, "");
+}
+
+TEST_CASE_METHOD(JsonExtractorLoggerTest, "json vector uint64_t: 3 items", "[extractor]")
+{
+ const std::vector<uint64_t> num_vec = {1,2,3};
+ check("num", num_vec, "\"num\": [ 1, 2, 3 ]\n");
+}
+
+TEST_CASE_METHOD(JsonExtractorLoggerTest, "json vector str: empty", "[extractor]")
+{
+ const std::vector<const char*> char_vec = {};
+ check("str", char_vec, "");
+}
+
+TEST_CASE_METHOD(JsonExtractorLoggerTest, "json vector str: 3 items", "[extractor]")
+{
+ const std::vector<const char*> num_vec = {"exe", "pdf", "txt"};
+ check("str", num_vec, "\"str\": [ \"exe\", \"pdf\", \"txt\" ]\n");
+}
+
+#endif
void add_field(const char*, struct timeval) override;
void add_field(const char*, const snort::SfIp&) override;
void add_field(const char*, bool) override;
+
+ void add_field(const char*, const std::vector<const char*>&) override;
+ void add_field(const char*, const std::vector<uint64_t>&) override;
+ void add_field(const char*, const std::vector<bool>&) override;
+
void open_record() override;
void close_record(const snort::Connector::ID&) override;
+protected:
+ std::ostringstream oss;
+
private:
void ts_snort(const char*, const struct timeval&);
void ts_snort_yy(const char*, const struct timeval&);
void ts_sec(const char*, const struct timeval&);
void ts_usec(const char*, const struct timeval&);
- std::ostringstream oss;
snort::JsonStream js;
void (JsonExtractorLogger::*add_ts)(const char*, const struct timeval&);
};
virtual void add_field(const char*, const snort::SfIp&) {}
virtual void add_field(const char*, bool) {}
+ virtual void add_field(const char*, const std::vector<const char*>&) {}
+ virtual void add_field(const char*, const std::vector<uint64_t>&) {}
+ virtual void add_field(const char*, const std::vector<bool>&) {}
+
const snort::Connector::ID get_id(const char* service_name) const
{ return output_conn->get_id(service_name); }
#include "extractor.h"
#include "extractor_conn.h"
+#include "extractor_detection.h"
#include "extractor_dns.h"
#include "extractor_ftp.h"
#include "extractor_http.h"
srv = new DnsExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins);
break;
+ case ServiceType::IPS_BUILTIN:
+ srv = new BuiltinExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins);
+ break;
+
+ case ServiceType::IPS_USER:
+ srv = new IpsUserExtractorService(cfg.tenant_id, cfg.fields, cfg.on_events, cfg.service, ins);
+ break;
+
case ServiceType::ANY: // fallthrough
default:
ErrorMessage("Extractor: '%s' service is not supported\n", cfg.service.c_str());
validate_fields(DnsExtractorService::blueprint, cfg.fields);
break;
+ case ServiceType::IPS_BUILTIN:
+ validate_fields(BuiltinExtractorService::blueprint, cfg.fields);
+ validate_events(BuiltinExtractorService::blueprint, cfg.on_events);
+ break;
+
+ case ServiceType::IPS_USER:
+ validate_fields(IpsUserExtractorService::blueprint, cfg.fields);
+ validate_events(IpsUserExtractorService::blueprint, cfg.on_events);
+ break;
+
case ServiceType::ANY: // fallthrough
default:
ParseError("'%s' service is not supported", cfg.service.c_str());
const snort::Connector::ID& DnsExtractorService::get_log_id()
{ return log_id; }
+//-------------------------------------------------------------------------
+// IpsUserExtractorService
+//-------------------------------------------------------------------------
+
+const ServiceBlueprint IpsUserExtractorService::blueprint =
+{
+ // events
+ {
+ "ips_logging",
+ },
+ // fields
+ {
+ "action",
+ "sid",
+ "gid",
+ "rev",
+ "msg",
+ "refs",
+ "proto",
+ "source",
+ },
+};
+
+THREAD_LOCAL Connector::ID IpsUserExtractorService::log_id;
+
+IpsUserExtractorService::IpsUserExtractorService(uint32_t tenant, const std::vector<std::string>& srv_fields,
+ const std::vector<std::string>& srv_events, ServiceType s_type, Extractor& ins)
+ : ExtractorService(tenant, srv_fields, srv_events, blueprint, s_type, ins)
+{
+ for (const auto& event : get_events())
+ {
+ if (!strcmp("ips_logging", event.c_str()))
+ handlers.push_back(new IpsUserExtractor(ins, tenant_id, get_fields()));
+ }
+}
+
+const snort::Connector::ID& IpsUserExtractorService::internal_tinit()
+{ return log_id = logger->get_id(type.c_str()); }
+
+const snort::Connector::ID& IpsUserExtractorService::get_log_id()
+{ return log_id; }
+
+//-------------------------------------------------------------------------
+// BuiltinExtractorService
+//-------------------------------------------------------------------------
+
+const ServiceBlueprint BuiltinExtractorService::blueprint =
+{
+ // events
+ {
+ "builtin",
+ },
+ // fields
+ {
+ "sid",
+ "gid",
+ "msg",
+ "proto",
+ "source",
+ },
+};
+
+THREAD_LOCAL Connector::ID BuiltinExtractorService::log_id;
+
+BuiltinExtractorService::BuiltinExtractorService(uint32_t tenant, const std::vector<std::string>& srv_fields,
+ const std::vector<std::string>& srv_events, ServiceType s_type, Extractor& ins)
+ : ExtractorService(tenant, srv_fields, srv_events, blueprint, s_type, ins)
+{
+ for (const auto& event : get_events())
+ {
+ if (!strcmp("builtin", event.c_str()))
+ handlers.push_back(new BuiltinExtractor(ins, tenant_id, get_fields()));
+ }
+}
+
+const snort::Connector::ID& BuiltinExtractorService::internal_tinit()
+{ return log_id = logger->get_id(type.c_str()); }
+
+const snort::Connector::ID& BuiltinExtractorService::get_log_id()
+{ return log_id; }
+
//-------------------------------------------------------------------------
// Unit Tests
//-------------------------------------------------------------------------
ServiceType ftp = ServiceType::FTP;
ServiceType conn = ServiceType::CONN;
ServiceType dns = ServiceType::DNS;
+ ServiceType weird = ServiceType::IPS_BUILTIN;
+ ServiceType notice = ServiceType::IPS_USER;
ServiceType any = ServiceType::ANY;
ServiceType max = ServiceType::MAX;
CHECK_FALSE(strcmp("ftp", ftp.c_str()));
CHECK_FALSE(strcmp("conn", conn.c_str()));
CHECK_FALSE(strcmp("dns", dns.c_str()));
+ CHECK_FALSE(strcmp("weird", weird.c_str()));
+ CHECK_FALSE(strcmp("notice", notice.c_str()));
CHECK_FALSE(strcmp("(not set)", any.c_str()));
CHECK_FALSE(strcmp("(not set)", max.c_str()));
}
static THREAD_LOCAL snort::Connector::ID log_id;
};
+class BuiltinExtractorService : public ExtractorService
+{
+public:
+ static const ServiceBlueprint blueprint;
+
+ BuiltinExtractorService(uint32_t tenant, const std::vector<std::string>& fields,
+ const std::vector<std::string>& events, ServiceType, Extractor&);
+
+private:
+ const snort::Connector::ID& internal_tinit() override;
+ const snort::Connector::ID& get_log_id() override;
+
+ static THREAD_LOCAL snort::Connector::ID log_id;
+};
+
+class IpsUserExtractorService : public ExtractorService
+{
+public:
+ static const ServiceBlueprint blueprint;
+
+ IpsUserExtractorService(uint32_t tenant, const std::vector<std::string>& fields,
+ const std::vector<std::string>& events, ServiceType, Extractor&);
+
+private:
+ const snort::Connector::ID& internal_tinit() override;
+ const snort::Connector::ID& get_log_id() override;
+
+ static THREAD_LOCAL snort::Connector::ID log_id;
+};
+
#endif
${PUB_SUB_INCLUDES}
cip_events.cc
http_events.cc
+ detection_events.cc
dns_events.cc
http_request_body_event.cc
http_body_event.cc
--- /dev/null
+//--------------------------------------------------------------------------
+// Copyright (C) 2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation. You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//--------------------------------------------------------------------------
+// detection_events.cc author Anna Norokh <anorokh@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <cstdio>
+#include <cstring>
+#include <string>
+#include <vector>
+
+#include "detection_events.h"
+
+using namespace snort;
+
+static const char* merge_with_colon(const char* first, const char* second)
+{
+ size_t length = std::strlen(first) + std::strlen(second) + 2;
+ char* result = new char[length];
+
+ std::sprintf(result, "%s:%s", first, second);
+
+ return result;
+}
+
+static std::string strip_msg(const char* msg)
+{
+ std::string str(msg);
+ if (str.front() == '"' and str.back() == '"')
+ str = str.substr(1, str.length() - 2);
+
+ return str;
+}
+
+const std::vector<const char*>& IpsRuleEvent::get_references() const
+{
+ if (!references.empty())
+ return references;
+
+ unsigned idx = 0;
+ const char* name = nullptr;
+ const char* id = nullptr;
+ const char* url = nullptr;
+
+ while (get_reference(idx++, name, id, url))
+ {
+ if (url and *url)
+ references.push_back(merge_with_colon(url, id));
+ else
+ references.push_back(merge_with_colon(name, id));
+ }
+
+ return references;
+}
+
+const std::string& IpsRuleEvent::get_stripped_msg() const
+{
+ if (stripped_msg.empty())
+ stripped_msg = strip_msg(get_msg());
+
+ return stripped_msg;
+}
+
+const std::string& IpsQueuingEvent::get_stripped_msg() const
+{
+ if (stripped_msg.empty())
+ stripped_msg = strip_msg(get_msg());
+
+ return stripped_msg;
+}
#include "events/event.h"
#include "framework/data_bus.h"
+#include "protocols/packet.h"
namespace snort
{
enum : unsigned
{
IPS_LOGGING,
+ BUILTIN,
MAX
};
};
class IpsRuleEvent : public DataEvent, public Event
{
public:
- IpsRuleEvent(const Event& e, const Packet* p) : Event(e), p(p) {}
+ IpsRuleEvent(const Event& e) : Event(e) {}
+ ~IpsRuleEvent() override
+ {
+ for (const char* ref : references)
+ delete[] ref;
+ }
+
+ const std::string& get_stripped_msg() const;
+
+ const std::vector<const char*>& get_references() const;
+
+protected:
+ mutable std::vector<const char*> references;
+
+private:
+ mutable std::string stripped_msg;
+};
+
+class IpsQueuingEvent : public DataEvent, public Event
+{
+public:
+ IpsQueuingEvent(const SigInfo& sig_info) : Event(0, 0, sig_info, nullptr, "") {}
- const snort::Packet* get_packet() const override
- { return p; }
+ const std::string& get_stripped_msg() const;
private:
- const Packet* p;
+ mutable std::string stripped_msg;
};
}
../ftp_events.h
$<TARGET_OBJECTS:extr_cpputest_deps>
)
+add_cpputest( pub_sub_detection_events_test
+ SOURCES
+ ../detection_events.cc
+)
--- /dev/null
+//--------------------------------------------------------------------------
+// Copyright (C) 2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation. You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//--------------------------------------------------------------------------
+// http_transaction_test.cc author Anna Norokh <anorokh@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "detection/signature.h"
+#include "pub_sub/detection_events.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+using namespace snort;
+
+static SigInfo s_dummy;
+Event::Event() : sig_info(s_dummy) { }
+Event::Event(unsigned int, unsigned int, SigInfo const&, char const**, char const*) : sig_info(s_dummy) { }
+bool Event::get_reference(unsigned int, const char*&, const char*&, const char*&) const
+{
+ return true;
+}
+const char* Event::get_msg() const
+{
+ return "\"mock message\"";
+}
+
+class MockIpsRuleEvent : public IpsRuleEvent
+{
+public:
+ MockIpsRuleEvent() : IpsRuleEvent({}) {}
+ void mock_references()
+ {
+ const char* url = new char[strlen("https://example.com") + 1];
+ strcpy(const_cast<char*>(url), "https://example.com");
+ references.push_back(url);
+ }
+};
+class MockIpsQueuingEvent : public IpsQueuingEvent
+{
+public:
+ MockIpsQueuingEvent() : IpsQueuingEvent(s_dummy) {}
+};
+
+TEST_GROUP(detection_events_test)
+{
+ MockIpsRuleEvent* mock_ips_rule_event = new MockIpsRuleEvent();
+
+ void teardown() override
+ {
+ delete mock_ips_rule_event;
+ }
+};
+
+TEST(detection_events_test, get_references_is_not_empty)
+{
+ mock_ips_rule_event->mock_references();
+
+ auto vec = mock_ips_rule_event->get_references();
+
+ CHECK_FALSE(vec.empty());
+ CHECK(1 == vec.size());
+ CHECK(strcmp("https://example.com", vec[0]) == 0);
+}
+
+TEST(detection_events_test, get_ips_rule_message_is_not_empty)
+{
+ // first call initialize the message
+ auto stripped_msg1 = mock_ips_rule_event->get_stripped_msg();
+ CHECK("mock message" == stripped_msg1);
+
+ // check that we got cached message
+ auto stripped_msg2 = mock_ips_rule_event->get_stripped_msg();
+ CHECK("mock message" == stripped_msg2);
+}
+
+TEST(detection_events_test, get_ips_queued_message_is_not_empty)
+{
+ MockIpsQueuingEvent* ips_queuing_event = new MockIpsQueuingEvent();
+
+ // first call initialize the message
+ auto stripped_msg1 = ips_queuing_event->get_stripped_msg();
+ CHECK("mock message" == stripped_msg1);
+
+ // check that we got cached message
+ auto stripped_msg2 = ips_queuing_event->get_stripped_msg();
+ CHECK("mock message" == stripped_msg2);
+
+ delete ips_queuing_event;
+}
+
+int main(int argc, char** argv)
+{
+ return CommandLineTestRunner::RunAllTests(argc, argv);
+}