/
githubmirror
/
rsyslog
Обзор
Документация
Войти
/
githubmirror
/
rsyslog
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
plugins/mmleefparse/mmleefparse.c
656 строк
21 KB
Rainer Gerhards
mmleefparse: check config string allocations
08 май 2026, 10:10
Не верифицирован
08 май 2026, 10:10
161f8a1
Код
Авторство
О чём код?
/* mmleefparse.c * This is a message modification module that parses LEEF events and * exposes them as structured data inside the message object. * * Copyright (C) 2025 by Rainer Gerhards and Adiscon GmbH. * * This file is part of rsyslog. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * -or- * see COPYING.ASL20 in the source distribution * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "config.h" #include <stdio.h> #include <stdarg.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <ctype.h> #include <json.h> #include "rsyslog.h" #include "conf.h" #include "syslogd-types.h" #include "template.h" #include "module-template.h" #include "errmsg.h" #include "cfsysline.h" #include "parserif.h" #include "dirty.h" MODULE_TYPE_OUTPUT; MODULE_TYPE_NOKEEP; MODULE_CNFNAME("mmleefparse") static rsRetVal resetConfigVariables(uchar __attribute__((unused)) * pp, void __attribute__((unused)) * pVal); /* static data */ /* internal structures */ DEF_OMOD_STATIC_DATA; typedef struct _instanceData { sbool bUseRawMsg; /**< use %rawmsg% instead of %msg% */ sbool bSearchCookie; /**< search for the cookie within a window */ char *cookie; uchar *container; int lenCookie; char delimiter; int searchWindow; /**< maximum number of characters to search */ } instanceData; typedef struct wrkrInstanceData { instanceData *pData; } wrkrInstanceData_t; struct modConfData_s { rsconf_t *pConf; /* our overall config object */ }; static modConfData_t *loadModConf = NULL; /* modConf ptr to use for the current load process */ static modConfData_t *runModConf = NULL; /* modConf ptr to use for the current exec process */ /* tables for interfacing with the v6 config system */ /* action (instance) parameters */ static struct cnfparamdescr actpdescr[] = {{"cookie", eCmdHdlrString, 0}, {"container", eCmdHdlrString, 0}, {"userawmsg", eCmdHdlrBinary, 0}, {"delimiter", eCmdHdlrString, 0}, {"searchcookie", eCmdHdlrBinary, 0}, {"searchwindow", eCmdHdlrInt, 0}}; static struct cnfparamblk actpblk = {CNFPARAMBLK_VERSION, sizeof(actpdescr) / sizeof(struct cnfparamdescr), actpdescr}; BEGINbeginCnfLoad CODESTARTbeginCnfLoad; loadModConf = pModConf; pModConf->pConf = pConf; ENDbeginCnfLoad BEGINendCnfLoad CODESTARTendCnfLoad; ENDendCnfLoad BEGINcheckCnf CODESTARTcheckCnf; ENDcheckCnf BEGINactivateCnf CODESTARTactivateCnf; runModConf = pModConf; ENDactivateCnf BEGINfreeCnf CODESTARTfreeCnf; ENDfreeCnf BEGINcreateInstance CODESTARTcreateInstance; CHKmalloc(pData->container = (uchar *)strdup("!")); CHKmalloc(pData->cookie = strdup("LEEF:")); pData->lenCookie = strlen(pData->cookie); pData->delimiter = '\t'; pData->bUseRawMsg = 1; pData->bSearchCookie = 1; pData->searchWindow = 64; finalize_it: ENDcreateInstance BEGINcreateWrkrInstance CODESTARTcreateWrkrInstance; ENDcreateWrkrInstance BEGINisCompatibleWithFeature CODESTARTisCompatibleWithFeature; ENDisCompatibleWithFeature BEGINfreeInstance CODESTARTfreeInstance; free(pData->cookie); free(pData->container); ENDfreeInstance BEGINfreeWrkrInstance CODESTARTfreeWrkrInstance; ENDfreeWrkrInstance BEGINdbgPrintInstInfo CODESTARTdbgPrintInstInfo; DBGPRINTF("mmleefparse\n"); ENDdbgPrintInstInfo BEGINtryResume CODESTARTtryResume; ENDtryResume /** * Locate the next delimiter-separated segment in the payload. * * @param start pointer to the beginning of the segment * @param delim delimiter that terminates the segment * @param[out] len optional length of the segment (excludes delimiter) * @param[out] hasDelim optional flag set when a delimiter was found * @retval pointer to the delimiter or the terminating NUL character */ static inline const char *find_next_segment(const char *start, char delim, size_t *len, sbool *hasDelim) { const char *p = start; sbool escaped = 0; while (*p != '\0') { if (!escaped && *p == '\\') { escaped = 1; ++p; continue; } if (!escaped && *p == delim) { if (len != NULL) *len = (size_t)(p - start); if (hasDelim != NULL) *hasDelim = 1; return p; } escaped = 0; ++p; } if (len != NULL) *len = (size_t)(p - start); if (hasDelim != NULL) *hasDelim = 0; return p; } /** * Find the first occurrence of a character that is not escaped with '\\'. * * @param start input buffer * @param len number of bytes to inspect * @param ch character to locate * @retval pointer to the matching character or NULL if not found */ static inline const char *find_unescaped_char(const char *start, size_t len, char ch) { const char *p = start; sbool escaped = 0; const char *end = start + len; while (p < end) { if (!escaped && *p == '\\') { escaped = 1; ++p; continue; } if (!escaped && *p == ch) return p; escaped = 0; ++p; } return NULL; } /** * Duplicate a LEEF segment while resolving escape sequences. * * @param start segment start pointer * @param len segment length * @param delim delimiter used for the current parsing context * @param[out] out newly allocated C string with unescaped content * @retval RS_RET_OK on success */ static rsRetVal leef_unescape_copy(const char *start, size_t len, char delim, char **out) { char *buf; size_t i; size_t w = 0; DEFiRet; CHKmalloc(buf = (char *)malloc(len + 1)); for (i = 0; i < len; ++i) { if (start[i] == '\\' && (i + 1) < len) { char next = start[i + 1]; if (next == '\\' || next == '=' || next == delim || next == '|') { buf[w++] = next; ++i; continue; } } buf[w++] = start[i]; } buf[w] = '\0'; *out = buf; finalize_it: RETiRet; } /** * Parse the header portion of a LEEF payload. * * @param payload pointer to the LEEF header (right after the cookie) * @param[out] protocolVersion extracted protocol version component * @param[out] vendor extracted vendor name * @param[out] product extracted product identifier * @param[out] productVersion extracted product version * @param[out] eventID extracted event identifier * @param[out] extensionStart pointer positioned at the key/value extension block * @retval RS_RET_OK on success */ static rsRetVal parse_leef_header(const char *payload, char **protocolVersion, char **vendor, char **product, char **productVersion, char **eventID, const char **extensionStart) { const char *cursor = payload; const char *segmentEnd; size_t len = 0; sbool hasDelim = 0; DEFiRet; /* protocol version */ segmentEnd = find_next_segment(cursor, '|', &len, &hasDelim); if (!hasDelim) ABORT_FINALIZE(RS_RET_NO_CEE_MSG); CHKiRet(leef_unescape_copy(cursor, len, '|', protocolVersion)); cursor = segmentEnd + 1; /* vendor */ segmentEnd = find_next_segment(cursor, '|', &len, &hasDelim); if (!hasDelim) ABORT_FINALIZE(RS_RET_NO_CEE_MSG); CHKiRet(leef_unescape_copy(cursor, len, '|', vendor)); cursor = segmentEnd + 1; /* product */ segmentEnd = find_next_segment(cursor, '|', &len, &hasDelim); if (!hasDelim) ABORT_FINALIZE(RS_RET_NO_CEE_MSG); CHKiRet(leef_unescape_copy(cursor, len, '|', product)); cursor = segmentEnd + 1; /* product version */ segmentEnd = find_next_segment(cursor, '|', &len, &hasDelim); if (!hasDelim && len == 0) ABORT_FINALIZE(RS_RET_NO_CEE_MSG); CHKiRet(leef_unescape_copy(cursor, len, '|', productVersion)); cursor = hasDelim ? segmentEnd + 1 : segmentEnd; /* event id */ segmentEnd = find_next_segment(cursor, '|', &len, &hasDelim); CHKiRet(leef_unescape_copy(cursor, len, '|', eventID)); cursor = hasDelim ? segmentEnd + 1 : segmentEnd; if (extensionStart != NULL) *extensionStart = cursor; finalize_it: RETiRet; } /** * Parse the key/value extension block of a LEEF message. * * @param pData module instance data (provides delimiter configuration) * @param payload pointer to the start of the extension block * @param fields JSON object that receives the parsed key/value pairs * @retval RS_RET_OK on success */ static rsRetVal parse_leef_extensions(const instanceData *pData, const char *payload, struct json_object *fields) { const char *cursor = payload; const char *segmentEnd; size_t len = 0; sbool hasDelim = 0; char delim = pData->delimiter; DEFiRet; if (cursor == NULL || *cursor == '\0') RETiRet; while (*cursor != '\0') { segmentEnd = find_next_segment(cursor, delim, &len, &hasDelim); if (len == 0 && !hasDelim) break; if (len == 0) { cursor = hasDelim ? segmentEnd + 1 : segmentEnd; continue; } const char *eq = find_unescaped_char(cursor, len, '='); char *key = NULL; char *value = NULL; if (eq == NULL) { CHKiRet(leef_unescape_copy(cursor, len, delim, &key)); value = strdup(""); if (value == NULL) { free(key); ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY); } } else { size_t keyLen = (size_t)(eq - cursor); size_t valueLen = len - keyLen - 1; CHKiRet(leef_unescape_copy(cursor, keyLen, delim, &key)); CHKiRet(leef_unescape_copy(eq + 1, valueLen, delim, &value)); } if (key != NULL && value != NULL) { struct json_object *jval = json_object_new_string(value); if (jval == NULL) { free(key); free(value); ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY); } json_object_object_add(fields, key, jval); } free(key); free(value); cursor = hasDelim ? segmentEnd + 1 : segmentEnd; } finalize_it: RETiRet; } /** * Convert a LEEF payload into structured JSON fields and attach them to a message. * * @param pWrkrData worker-specific instance data * @param pMsg message being processed * @param payload pointer to the payload section following the cookie * @retval RS_RET_OK on success */ static ATTR_NONNULL() rsRetVal processLEEF(wrkrInstanceData_t *pWrkrData, smsg_t *pMsg, const char *payload) { char *protocolVersion = NULL; char *vendor = NULL; char *product = NULL; char *productVersion = NULL; char *eventID = NULL; const char *extensionStart = NULL; struct json_object *root = NULL; struct json_object *header = NULL; struct json_object *fields = NULL; sbool added = 0; DEFiRet; CHKiRet( parse_leef_header(payload, &protocolVersion, &vendor, &product, &productVersion, &eventID, &extensionStart)); root = json_object_new_object(); if (root == NULL) ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY); header = json_object_new_object(); if (header == NULL) { json_object_put(root); ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY); } fields = json_object_new_object(); if (fields == NULL) { json_object_put(header); json_object_put(root); ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY); } json_object_object_add(root, "header", header); json_object_object_add(root, "fields", fields); json_object_object_add(header, "protocolVersion", json_object_new_string(protocolVersion)); json_object_object_add(header, "vendor", json_object_new_string(vendor)); json_object_object_add(header, "product", json_object_new_string(product)); json_object_object_add(header, "productVersion", json_object_new_string(productVersion)); json_object_object_add(header, "eventID", json_object_new_string(eventID)); CHKiRet(parse_leef_extensions(pWrkrData->pData, extensionStart, fields)); msgAddJSON(pMsg, pWrkrData->pData->container, root, 0, 0); added = 1; finalize_it: if (!added && root != NULL) json_object_put(root); free(protocolVersion); free(vendor); free(product); free(productVersion); free(eventID); RETiRet; } BEGINdoAction_NoStrings smsg_t **ppMsg = (smsg_t **)pMsgData; smsg_t *pMsg = ppMsg[0]; uchar *buf; uchar *bufBase; rs_size_t len = 0; int bSuccess = 0; instanceData *pData; CODESTARTdoAction; pData = pWrkrData->pData; if (pData->bUseRawMsg) { getRawMsg(pMsg, &bufBase, &len); buf = bufBase; } else { bufBase = getMSG(pMsg); buf = bufBase; } while (*buf && isspace(*buf)) ++buf; if (pData->bUseRawMsg && buf > bufBase) { rs_size_t consumed = (rs_size_t)(buf - bufBase); if (len > consumed) len -= consumed; else len = 0; } if (pData->bSearchCookie) { /* * Raw syslog transports often prepend a PRI and timestamp before the * LEEF cookie. We scan a limited window from the trimmed start to * locate the cookie without risking large linear passes over the full * message. */ size_t available; if (pData->bUseRawMsg) available = (size_t)len; else available = strlen((char *)buf); size_t window = (size_t)pData->searchWindow; if (window > available) window = available; if (window >= (size_t)pData->lenCookie) { size_t maxOffset = window - (size_t)pData->lenCookie; /* Sequential search keeps complexity minimal for small windows. */ for (size_t offset = 0; offset <= maxOffset; ++offset) { if (memcmp(buf + offset, pData->cookie, (size_t)pData->lenCookie) == 0) { buf += offset; if (pData->bUseRawMsg) { if (len >= (rs_size_t)(offset)) len -= (rs_size_t)offset; else len = 0; } break; } } } } sbool haveCookie = 0; if (pData->bUseRawMsg) { if (len >= (rs_size_t)pData->lenCookie && memcmp(buf, pData->cookie, (size_t)pData->lenCookie) == 0) { haveCookie = 1; } } else { if (strncmp((char *)buf, pData->cookie, pData->lenCookie) == 0) haveCookie = 1; } if (!haveCookie) { DBGPRINTF("mmleefparse: no LEEF cookie: '%s'\n", buf); ABORT_FINALIZE(RS_RET_NO_CEE_MSG); } buf += pData->lenCookie; CHKiRet(processLEEF(pWrkrData, pMsg, (char *)buf)); bSuccess = 1; finalize_it: if (iRet == RS_RET_NO_CEE_MSG) { iRet = RS_RET_OK; } MsgSetParseSuccess(pMsg, bSuccess); ENDdoAction static inline void setInstParamDefaults(instanceData *pData) { pData->bUseRawMsg = 1; pData->delimiter = '\t'; pData->bSearchCookie = 1; pData->searchWindow = 64; } BEGINnewActInst struct cnfparamvals *pvals; int i; CODESTARTnewActInst; DBGPRINTF("newActInst (mmleefparse)\n"); if ((pvals = nvlstGetParams(lst, &actpblk, NULL)) == NULL) { ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS); } CODE_STD_STRING_REQUESTnewActInst(1); CHKiRet(OMSRsetEntry(*ppOMSR, 0, NULL, OMSR_TPL_AS_MSG)); CHKiRet(createInstance(&pData)); setInstParamDefaults(pData); for (i = 0; i < actpblk.nParams; ++i) { if (!pvals[i].bUsed) continue; if (!strcmp(actpblk.descr[i].name, "cookie")) { free(pData->cookie); CHKmalloc(pData->cookie = es_str2cstr(pvals[i].val.d.estr, NULL)); } else if (!strcmp(actpblk.descr[i].name, "container")) { free(pData->container); size_t lenvar = es_strlen(pvals[i].val.d.estr); CHKmalloc(pData->container = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL)); if (pData->container[0] == '$') { memmove(pData->container, pData->container + 1, lenvar); --lenvar; } if ((lenvar == 0) || (!(pData->container[0] == '!' || pData->container[0] == '.' || pData->container[0] == '/'))) { parser_errmsg( "mmleefparse: invalid container name '%s', name must start with either '$!', '$.', or '$/'", pData->container); ABORT_FINALIZE(RS_RET_INVALID_VAR); } } else if (!strcmp(actpblk.descr[i].name, "userawmsg")) { pData->bUseRawMsg = (int)pvals[i].val.d.n; } else if (!strcmp(actpblk.descr[i].name, "delimiter")) { char *tmp; CHKmalloc(tmp = es_str2cstr(pvals[i].val.d.estr, NULL)); if (tmp[0] == '\0') { free(tmp); parser_errmsg("mmleefparse: delimiter must not be empty"); ABORT_FINALIZE(RS_RET_INVALID_VALUE); } if (tmp[0] == '\\') { if (tmp[1] == 't' && tmp[2] == '\0') pData->delimiter = '\t'; else if (tmp[1] == 'n' && tmp[2] == '\0') pData->delimiter = '\n'; else if (tmp[1] == '\\' && tmp[2] == '\0') pData->delimiter = '\\'; else { parser_errmsg("mmleefparse: unsupported escape sequence '%s' for delimiter", tmp); free(tmp); ABORT_FINALIZE(RS_RET_INVALID_VALUE); } } else { if (tmp[1] != '\0') { parser_errmsg("mmleefparse: delimiter must be a single character"); free(tmp); ABORT_FINALIZE(RS_RET_INVALID_VALUE); } pData->delimiter = tmp[0]; } free(tmp); } else if (!strcmp(actpblk.descr[i].name, "searchcookie")) { pData->bSearchCookie = (int)pvals[i].val.d.n; } else if (!strcmp(actpblk.descr[i].name, "searchwindow")) { int window = (int)pvals[i].val.d.n; if (window <= 0) { parser_errmsg("mmleefparse: searchWindow must be greater than zero"); ABORT_FINALIZE(RS_RET_INVALID_VALUE); } pData->searchWindow = window; } else { dbgprintf("mmleefparse: program error, non-handled param '%s'\n", actpblk.descr[i].name); } } if (pData->container == NULL) CHKmalloc(pData->container = (uchar *)strdup("!")); if (pData->cookie == NULL) CHKmalloc(pData->cookie = strdup("LEEF:")); pData->lenCookie = strlen(pData->cookie); CODE_STD_FINALIZERnewActInst; cnfparamvalsDestruct(pvals, &actpblk); ENDnewActInst BEGINparseSelectorAct CODESTARTparseSelectorAct; CODE_STD_STRING_REQUESTparseSelectorAct(1) if (strncmp((char *)p, ":mmleefparse:", sizeof(":mmleefparse:") - 1)) { ABORT_FINALIZE(RS_RET_CONFLINE_UNPROCESSED); } p += sizeof(":mmleefparse:") - 1; CHKiRet(createInstance(&pData)); if (*(p - 1) == ';') --p; iRet = cflineParseTemplateName(&p, *ppOMSR, 0, OMSR_TPL_AS_MSG, (uchar *)"RSYSLOG_FileFormat"); CODE_STD_FINALIZERparseSelectorAct ENDparseSelectorAct BEGINmodExit CODESTARTmodExit; ENDmodExit BEGINqueryEtryPt CODESTARTqueryEtryPt; CODEqueryEtryPt_STD_OMOD_QUERIES; CODEqueryEtryPt_STD_OMOD8_QUERIES; CODEqueryEtryPt_STD_CONF2_OMOD_QUERIES; CODEqueryEtryPt_STD_CONF2_QUERIES; ENDqueryEtryPt static rsRetVal resetConfigVariables(uchar __attribute__((unused)) * pp, void __attribute__((unused)) * pVal) { DEFiRet; RETiRet; } BEGINmodInit() rsRetVal localRet; rsRetVal (*pomsrGetSupportedTplOpts)(unsigned long *pOpts); unsigned long opts; int bMsgPassingSupported; CODESTARTmodInit; *ipIFVersProvided = CURR_MOD_IF_VERSION; CODEmodInit_QueryRegCFSLineHdlr DBGPRINTF("mmleefparse: module compiled with rsyslog version %s.\n", VERSION); bMsgPassingSupported = 0; localRet = pHostQueryEtryPt((uchar *)"OMSRgetSupportedTplOpts", &pomsrGetSupportedTplOpts); if (localRet == RS_RET_OK) { CHKiRet((*pomsrGetSupportedTplOpts)(&opts)); if (opts & OMSR_TPL_AS_MSG) bMsgPassingSupported = 1; } else if (localRet != RS_RET_ENTRY_POINT_NOT_FOUND) { ABORT_FINALIZE(localRet); } if (!bMsgPassingSupported) { DBGPRINTF("mmleefparse: msg-passing is not supported by rsyslog core, can not continue.\n"); ABORT_FINALIZE(RS_RET_NO_MSG_PASSING); } CHKiRet(omsdRegCFSLineHdlr((uchar *)"resetconfigvariables", 1, eCmdHdlrCustomHandler, resetConfigVariables, NULL, STD_LOADABLE_MODULE_ID)); ENDmodInit