【作者主页】只道当时是寻常
【专栏介绍】Suricata入侵检测。专注网络、主机安全,欢迎关注与评论。
1. 概要
👋 Suricata 的引擎日志记录系统主要记录该引擎在启动、运行以及关闭期间应用程序的相关信息,如错误信息和其他诊断信息,但不包含 Suricata 自身生成的警报和事件。该系统设有多个日志级别,包括错误、警告、通知、信息、性能、配置和调试。
2. 配置格式
下面是Suricata配置文件中对于引擎日志默认配置:
logging:default-log-level: notice#default-log-format: "[%i] %t [%S] - (%f:%l) <%d> (%n) -- "default-output-filter:#stacktrace-on-signal: onoutputs:- console:enabled: yes# type: json- file:enabled: yeslevel: infofilename: suricata.log# format: "[%i - %m] %z %d: %S: %M"# type: json- syslog:enabled: nofacility: local5format: "[%i] <%d> -- "# type: json
-
default-log-level:此选项可以设置 Suricata 引擎日志输出级别。
-
日志等级主要分为erro、warning、notice、info、perf、config 和 debug。注意,只有当 Suricata 使用 --enable-debug 配置选项进行编译时,才会发出调试级别的日志记录。
-
注意,默认日志级别的值会被环境变量 “SC_LOG_LEVEL” 所覆盖。
-
-
default-log-format:自定义引擎日志输出格式。注意,默认日志级别的值会被环境变量 “SC_LOG_FORMAT” 所覆盖。日志格式详细介绍见3.4章节内容。
-
default-output-filter:自定义正则表达式,用于过滤引擎日志输出。
-
outputs:自定义Suricata 引擎日志输出类型。
-
console,输出到终端;file,输出到文件;syslog,输出到syslog。
-
enabled:(yes/no)开关选项,用于控制输出方式的开关。
-
level:用于控制日志输出等级,支持的等级为erro、warning、notice、info、perf、config 和 debug。
-
type:(regular/json)控制引擎日志输出格式。
-
format:引擎日志格式。
-
filename:引擎日志输出类型为 file 时有效,用于指定引擎日志文件名。
-
facility:引擎日志输出类型为 syslog 时有效,此字段能够对日志消息的来源进行分类,也就是说这里定义 Suricata 通过 syslog 发送的日志类型。Suricata 支持此字段配置值分别为 auth,authpriv,cron,daemon,ftp,kern,lpr,mail,news,security,syslog,user,uucp,local0,local1,local2,local3,local4,local5,local6,local7。
-
3. 源码解析
3.1 配置解析
Suricata 启动后,通过调用 SCLogLoadConfig 函数解析配置文件中引擎日志模块的相关信息。在该函数执行过程中,会依次读取、解析引擎日志的各类参数,并完成初始化操作,为后续引擎日志的输出提供配置支持。
引擎日志的配置信息存储在下面数据结构中:
typedef struct SCLogInitData_ {/* startup message */const char *startup_message;/* the log level */SCLogLevel global_log_level;/* the log format */const char *global_log_format;/* output filter */const char *op_filter;/* list of output interfaces to be used */SCLogOPIfaceCtx *op_ifaces;/* no of op ifaces */uint8_t op_ifaces_cnt;
} SCLogInitData;
SCLogLoadConfig 函数实现代码如下所示:
void SCLogLoadConfig(int daemon, int verbose, uint32_t userid, uint32_t groupid)
{ConfNode *outputs;SCLogInitData *sc_lid;int have_logging = 0;int max_level = 0;SCLogLevel min_level = 0;/* If verbose logging was requested, set the minimum as* SC_LOG_NOTICE plus the extra verbosity. */if (verbose) {min_level = SC_LOG_NOTICE + verbose;}outputs = ConfGetNode("logging.outputs");if (outputs == NULL) {SCLogDebug("No logging.output configuration section found.");return;}sc_lid = SCLogAllocLogInitData();if (sc_lid == NULL) {SCLogDebug("Could not allocate memory for log init data");return;}/* Get default log level and format. */const char *default_log_level_s = NULL;if (ConfGet("logging.default-log-level", &default_log_level_s) == 1) {SCLogLevel default_log_level =SCMapEnumNameToValue(default_log_level_s, sc_log_level_map);if (default_log_level == -1) {SCLogError("Invalid default log level: %s", default_log_level_s);exit(EXIT_FAILURE);}sc_lid->global_log_level = MAX(min_level, default_log_level);}else {sc_lid->global_log_level = MAX(min_level, SC_LOG_NOTICE);}if (ConfGet("logging.default-log-format", &sc_lid->global_log_format) != 1)sc_lid->global_log_format = SCLogGetDefaultLogFormat(sc_lid->global_log_level);(void)ConfGet("logging.default-output-filter", &sc_lid->op_filter);ConfNode *seq_node, *output;TAILQ_FOREACH(seq_node, &outputs->head, next) {SCLogLevel level = sc_lid->global_log_level;SCLogOPIfaceCtx *op_iface_ctx = NULL;const char *format;const char *level_s;output = ConfNodeLookupChild(seq_node, seq_node->val);if (output == NULL)continue;/* By default an output is enabled. */const char *enabled = ConfNodeLookupChildValue(output, "enabled");if (enabled != NULL && ConfValIsFalse(enabled))continue;SCLogOPType type = SC_LOG_OP_TYPE_REGULAR;const char *type_s = ConfNodeLookupChildValue(output, "type");if (type_s != NULL) {if (strcmp(type_s, "regular") == 0)type = SC_LOG_OP_TYPE_REGULAR;else if (strcmp(type_s, "json") == 0) {type = SC_LOG_OP_TYPE_JSON;}}format = ConfNodeLookupChildValue(output, "format");level_s = ConfNodeLookupChildValue(output, "level");if (level_s != NULL) {level = SCMapEnumNameToValue(level_s, sc_log_level_map);if (level == -1) {SCLogError("Invalid log level: %s", level_s);exit(EXIT_FAILURE);}max_level = MAX(max_level, level);}/* Increase the level of extra verbosity was requested. */level = MAX(min_level, level);if (strcmp(output->name, "console") == 0) {op_iface_ctx = SCLogInitConsoleOPIface(format, level, type);}else if (strcmp(output->name, "file") == 0) {if (format == NULL) {format = SC_LOG_DEF_FILE_FORMAT;}const char *filename = ConfNodeLookupChildValue(output, "filename");if (filename == NULL) {FatalError("Logging to file requires a filename");}char *path = NULL;if (!(PathIsAbsolute(filename))) {path = SCLogGetLogFilename(filename);} else {path = SCStrdup(filename);}if (path == NULL)FatalError("failed to setup output to file");have_logging = 1;op_iface_ctx = SCLogInitFileOPIface(path, userid, groupid, format, level, type);SCFree(path);}else if (strcmp(output->name, "syslog") == 0) {int facility = SC_LOG_DEF_SYSLOG_FACILITY;const char *facility_s = ConfNodeLookupChildValue(output,"facility");if (facility_s != NULL) {facility = SCMapEnumNameToValue(facility_s, SCSyslogGetFacilityMap());if (facility == -1) {SCLogWarning("Invalid syslog ""facility: \"%s\", now using \"%s\" as syslog ""facility",facility_s, SC_LOG_DEF_SYSLOG_FACILITY_STR);facility = SC_LOG_DEF_SYSLOG_FACILITY;}}SCLogDebug("Initializing syslog logging with format \"%s\"", format);have_logging = 1;op_iface_ctx = SCLogInitSyslogOPIface(facility, format, level, type);}else {SCLogWarning("invalid logging method: %s, ignoring", output->name);}if (op_iface_ctx != NULL) {SCLogAppendOPIfaceCtx(op_iface_ctx, sc_lid);}}if (daemon && (have_logging == 0)) {SCLogWarning("no logging compatible with daemon mode selected,"" suricata won't be able to log. Please update "" 'logging.outputs' in the YAML.");}/* Set the global log level to that of the max level used. */sc_lid->global_log_level = MAX(sc_lid->global_log_level, max_level);SCLogInitLogModule(sc_lid);SCLogDebug("sc_log_global_log_level: %d", sc_log_global_log_level);SCLogDebug("sc_lc->log_format: %s", sc_log_config->log_format);SCLogDebug("SCLogSetOPFilter: filter: %s", sc_log_config->op_filter);if (sc_lid != NULL)SCFree(sc_lid);
}
3.2 日志等级
在 Suricata 中,对于引擎告警日志默认日志等级为 notice。其支持的日志等级定义在 default_log_level_s 这个结构体中,其格式如下所示:
typedef struct SCEnumCharMap_ {const char *enum_name;int enum_value;
} SCEnumCharMap;SCEnumCharMap sc_log_level_map[] = {{ "Not set", SC_LOG_NOTSET },{ "None", SC_LOG_NONE },{ "Error", SC_LOG_ERROR },{ "Warning", SC_LOG_WARNING },{ "Notice", SC_LOG_NOTICE },{ "Info", SC_LOG_INFO },{ "Perf", SC_LOG_PERF },{ "Config", SC_LOG_CONFIG },{ "Debug", SC_LOG_DEBUG },{ NULL, -1 }
};
3.3 日志过滤
在 Suricata 中,引擎日志具备正则表达式过滤功能。SCLogMessageGetBuffer 函数承担着依据指定的日志格式和内容,生成最终日志消息字符串的任务。当该函数完成字符串的组装工作后,若系统配置了日志过滤规则,便会调用 pcre2_match 函数,将生成的日志消息字符串与过滤规则进行精确匹配。只有当字符串命中过滤规则时,相应的日志才会被输出;若未命中,则不会进行输出操作。(注意,如果字符串是JSON格式则过滤条件不生效)
SCLogMessageGetBuffer 函数进行正则匹配部分的实现如下所示:
/*** \brief Adds the global log_format to the outgoing buffer** \param log_level log_level of the message that has to be logged* \param msg Buffer containing the outgoing message* \param file File_name from where the message originated* \param function Function_name from where the message originated* \param line Line_no from where the messaged originated** \retval 0 on success; else a negative value on error*/
static SCError SCLogMessageGetBuffer(SCTime_t tval, int color, SCLogOPType type, char *buffer,size_t buffer_size, const char *log_format, const SCLogLevel log_level, const char *file,const unsigned int line, const char *function, const char *module, const char *message)
{if (type == SC_LOG_OP_TYPE_JSON)return SCLogMessageJSON(tval, buffer, buffer_size, log_level, file, line, function, module, message);... ... // 省略部分代码实现if (sc_log_config->op_filter_regex != NULL) {if (pcre2_match(sc_log_config->op_filter_regex, (PCRE2_SPTR8)buffer, strlen(buffer), 0, 0,sc_log_config->op_filter_regex_match, NULL) < 0) {return -1; // bit hacky, but just return !0}}return 0;
}
3.4 日志格式
在 Suricata 中,默认状态下,不同告警等级对应着差异化的告警格式。若未配置 default-log-format 日志格式,系统将通过 SCLogGetDefaultLogFormat 函数,依据具体的日志等级,从预定义的日志格式集合中选取适配的格式进行应用。SCLogGetDefaultLogFormat 函数实现如下所示:
/* The default log_format, if it is not supplied by the user */
#define SC_LOG_DEF_FILE_FORMAT "[%i - %m] %z %d: %S: %M"
#define SC_LOG_DEF_LOG_FORMAT_REL_NOTICE "%D: %S: %M"
#define SC_LOG_DEF_LOG_FORMAT_REL_INFO "%d: %S: %M"
#define SC_LOG_DEF_LOG_FORMAT_REL_CONFIG "[%i] %d: %S: %M"
#define SC_LOG_DEF_LOG_FORMAT_DEBUG "%d: %S: %M [%n:%f:%l]"static inline const char *SCLogGetDefaultLogFormat(const SCLogLevel lvl)
{const char *prog_ver = GetProgramVersion();if (strstr(prog_ver, "RELEASE") != NULL) {if (lvl <= SC_LOG_NOTICE)return SC_LOG_DEF_LOG_FORMAT_REL_NOTICE;else if (lvl <= SC_LOG_INFO)return SC_LOG_DEF_LOG_FORMAT_REL_INFO;else if (lvl <= SC_LOG_CONFIG)return SC_LOG_DEF_LOG_FORMAT_REL_CONFIG;}return SC_LOG_DEF_LOG_FORMAT_DEBUG;
}
日志格式定义中包含诸如 % D、% S、% M 等特殊字符,其具体含义如下:
/* The different log format specifiers supported by the API */
#define SC_LOG_FMT_TIME 'z' /* Timestamp in RFC3339 like format */
#define SC_LOG_FMT_TIME_LEGACY 't' /* Timestamp in legacy format */
#define SC_LOG_FMT_PID 'p' /* PID */
#define SC_LOG_FMT_TID 'i' /* Thread ID */
#define SC_LOG_FMT_TM 'm' /* Thread module name */
#define SC_LOG_FMT_LOG_LEVEL 'd' /* Log level */
#define SC_LOG_FMT_LOG_SLEVEL 'D' /* Log level */
#define SC_LOG_FMT_FILE_NAME 'f' /* File name */
#define SC_LOG_FMT_LINE 'l' /* Line number */
#define SC_LOG_FMT_FUNCTION 'n' /* Function */
#define SC_LOG_FMT_SUBSYSTEM 'S' /* Subsystem name */
#define SC_LOG_FMT_THREAD_NAME 'T' /* thread name */
#define SC_LOG_FMT_MESSAGE 'M' /* log message body *//* The log format prefix for the format specifiers */
#define SC_LOG_FMT_PREFIX '%'
%z | 符合 RFC3339 格式的时间戳 |
%t | 旧格式的时间戳 |
%p | 进程 ID(PID) |
%i | 线程 ID(TID) |
%m | 线程模块名称 |
%d | 日志级别 |
%D | 日志级别 |
%f | 文件名 |
%l | 行号 |
%n | 函数名 |
%S | 子系统名称 |
%T | 线程名称 |
%M | 日志消息主体 |
3.5 syslog-facility
在 Suricata 中,SCLogLoadConfig 函数负责解析日志输出类型。当检测到开启 console 输出时,会调用 SCLogInitConsoleOPIface 函数进行初始化;若开启 file 输出,则调用 SCLogInitFileOPIface 函数;而当 syslog 输出开启时,会调用 SCLogInitSyslogOPIface 函数。其中,console 和 file 输出类型的初始化函数,其功能正如我们所预期,主要是完成文件描述符的打开操作。接下来,我们着重探讨 syslog 输出模式。
Suricata在使用syslog作为输出时支持facility类型的配置,其所支持的类型存储在
sc_syslog_facility_map结构体中,其定义如下所示:
SCEnumCharMap sc_syslog_facility_map[] = {{ "auth", LOG_AUTH },{ "authpriv", LOG_AUTHPRIV },{ "cron", LOG_CRON },{ "daemon", LOG_DAEMON },{ "ftp", LOG_FTP },{ "kern", LOG_KERN },{ "lpr", LOG_LPR },{ "mail", LOG_MAIL },{ "news", LOG_NEWS },{ "security", LOG_AUTH },{ "syslog", LOG_SYSLOG },{ "user", LOG_USER },{ "uucp", LOG_UUCP },{ "local0", LOG_LOCAL0 },{ "local1", LOG_LOCAL1 },{ "local2", LOG_LOCAL2 },{ "local3", LOG_LOCAL3 },{ "local4", LOG_LOCAL4 },{ "local5", LOG_LOCAL5 },{ "local6", LOG_LOCAL6 },{ "local7", LOG_LOCAL7 },{ NULL, -1 }
};
在 SCLogInitSyslogOPIface 函数中通过调用openlog函数打开 syslog,并将facility 属性赋值。该函数实现如下所示:
/*** \brief Initializes the syslog output interface** \param facility The facility code for syslog* \param log_format Pointer to the log_format for this op interface, that* overrides the global_log_format* \param log_level Override of the global_log_level by this interface** \retval iface_ctx Pointer to the syslog output interface context created*/
static inline SCLogOPIfaceCtx *SCLogInitSyslogOPIface(int facility,const char *log_format,SCLogLevel log_level,SCLogOPType type)
{SCLogOPIfaceCtx *iface_ctx = SCLogAllocLogOPIfaceCtx();if ( iface_ctx == NULL) {FatalError("Fatal error encountered in SCLogInitSyslogOPIface. Exiting...");}iface_ctx->iface = SC_LOG_OP_IFACE_SYSLOG;iface_ctx->type = type;if (facility == -1)facility = SC_LOG_DEF_SYSLOG_FACILITY;iface_ctx->facility = facility;if (log_format != NULL &&(iface_ctx->log_format = SCStrdup(log_format)) == NULL) {printf("Error allocating memory\n");exit(EXIT_FAILURE);}iface_ctx->log_level = log_level;openlog(NULL, LOG_NDELAY, iface_ctx->facility);return iface_ctx;
}