/* pmsnare.c
 *
 * this detects logs sent by Snare and cleans them up so that they can be processed by the normal parser
 *
 * there are two variations of this, if the client is set to 'syslog' mode it sends
 *
 * <pri>timestamp<sp>hostname<sp>tag<tab>otherstuff
 *
 * if the client is not set to syslog it sends
 *
 * hostname<tab>tag<tab>otherstuff
 *
 * The tabs can be represented in different ways. This module will auto-detect the tab representation based on
 * the global config settings, but they can be overridden for each instance in the config file if needed.
 *
 * ToDo, take advantage of items in the message itself to set more friendly information
 * where the normal parser will find it by re-writing more of the message
 *
 * Interesting information includes:
 *
 * in the case of windows snare messages:
 *   the system hostname is field 12
 *   the severity is field 3 (criticality ranging form 0 to 4)
 *   the source of the log is field 4 and may be able to be mapped to facility
 *
 *
 * created 2010-12-13 by David Lang based on pmlastmsg
 * Modified 2017-05-29 by Shane Lawrence.
 *
 * 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 "rsyslog.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include "conf.h"
#include "syslogd-types.h"
#include "template.h"
#include "msg.h"
#include "module-template.h"
#include "glbl.h"
#include "errmsg.h"
#include "parser.h"
#include "datetime.h"
#include "unicode-helper.h"
#include "rsconf.h"

MODULE_TYPE_PARSER
MODULE_TYPE_NOKEEP;
PARSER_NAME("rsyslog.snare")
MODULE_CNFNAME("pmsnare")

/* internal structures
 */
DEF_PMOD_STATIC_DATA;
DEFobjCurrIf(glbl) DEFobjCurrIf(parser) DEFobjCurrIf(datetime)


    /* static data */
    static int bParseHOSTNAMEandTAG; /* cache for the equally-named global param - performance enhancement */

/* Keep a list of parser instances so we can apply global settings after config is loaded. */
typedef struct modInstances_s {
    instanceConf_t *root;
    instanceConf_t *tail;
} modInstances_t;
static modInstances_t *modInstances = NULL;

struct modConfData_s {
    rsconf_t *pConf; /* our overall config object */
};
static modConfData_t *modConf = NULL;

/* Per-instance config settings. */
static struct cnfparamdescr parserpdescr[] = {{"parser.controlcharacterescapeprefix", eCmdHdlrGetChar, 0},
                                              {"parser.escapecontrolcharactersonreceive", eCmdHdlrBinary, 0},
                                              {"parser.escapecontrolcharactertab", eCmdHdlrBinary, 0},
                                              {"parser.escapecontrolcharacterscstyle", eCmdHdlrBinary, 0}};
static struct cnfparamblk parserpblk = {CNFPARAMBLK_VERSION, sizeof(parserpdescr) / sizeof(struct cnfparamdescr),
                                        parserpdescr};
enum { PMSNARE_TAB_REPR_SIZE = 5 };

#define PMSNARE_SET_TAB_REPR(dst, literal)                                                                         \
    do {                                                                                                           \
        RS_STATIC_ASSERT(sizeof(literal) <= PMSNARE_TAB_REPR_SIZE, "pmsnare tabRepresentation literal too large"); \
        memset((dst), 0, PMSNARE_TAB_REPR_SIZE);                                                                   \
        memcpy((dst), (literal), sizeof(literal));                                                                 \
    } while (0)

struct instanceConf_s {
    int bEscapeCCOnRcv;
    int bEscapeTab;
    int bParserEscapeCCCStyle;
    uchar cCCEscapeChar;
    int tabLength;
    char tabRepresentation[PMSNARE_TAB_REPR_SIZE];
    struct instanceConf_s *next;
};

static int hasPrefixWithFollowingTab(const uchar *const p,
                                     const int len,
                                     const char *const prefix,
                                     const int prefixLen,
                                     const instanceConf_t *const pInst) {
    return len >= prefixLen + pInst->tabLength && strncasecmp((const char *)p, prefix, prefixLen) == 0 &&
           strncasecmp((const char *)(p + prefixLen), pInst->tabRepresentation, pInst->tabLength) == 0;
}

/* Creates the instance and adds it to the list of instances. */
static rsRetVal createInstance(instanceConf_t **pinst) {
    instanceConf_t *inst;
    DEFiRet;
    CHKmalloc(inst = malloc(sizeof(instanceConf_t)));
    inst->next = NULL;
    *pinst = inst;

    /*  Add to list of instances. */
    if (modInstances == NULL) {
        CHKmalloc(modInstances = malloc(sizeof(modInstances_t)));
        modInstances->tail = modInstances->root = NULL;
    }
    if (modInstances->tail == NULL) {
        modInstances->tail = modInstances->root = inst;
    } else {
        modInstances->tail->next = inst;
        modInstances->tail = inst;
    }

finalize_it:
    RETiRet;
}

BEGINnewParserInst
    struct cnfparamvals *pvals = NULL;
    int i;
    CODESTARTnewParserInst;
    DBGPRINTF("newParserInst (pmsnare)\n");
    inst = NULL;
    CHKiRet(createInstance(&inst));

    /* Mark these as unset so we know if they should be overridden later. */
    inst->bEscapeCCOnRcv = -1;
    inst->bEscapeTab = -1;
    inst->bParserEscapeCCCStyle = -1;
    inst->cCCEscapeChar = '\0';

    /* If using the old config, just use global settings for each instance. */
    if (lst == NULL) FINALIZE;

    /* If using the new config, process module settings for this instance. */
    if ((pvals = nvlstGetParams(lst, &parserpblk, NULL)) == NULL) {
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    if (Debug) {
        dbgprintf("pmsnare: parser param blk:\n");
        cnfparamsPrint(&parserpblk, pvals);
    }

    for (i = 0; i < parserpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(parserpblk.descr[i].name, "parser.escapecontrolcharactersonreceive")) {
            inst->bEscapeCCOnRcv = pvals[i].val.d.n;
        } else if (!strcmp(parserpblk.descr[i].name, "parser.escapecontrolcharactertab")) {
            inst->bEscapeTab = pvals[i].val.d.n;
        } else if (!strcmp(parserpblk.descr[i].name, "parser.escapecontrolcharacterscstyle")) {
            inst->bParserEscapeCCCStyle = pvals[i].val.d.n;
        } else if (!strcmp(parserpblk.descr[i].name, "parser.controlcharacterescapeprefix")) {
            {
                char *cstr;
                CHKmalloc(cstr = es_str2cstr(pvals[i].val.d.estr, NULL));
                inst->cCCEscapeChar = (uchar)*cstr;
                free(cstr);
            }
        } else {
            dbgprintf("pmsnare: program error, non-handled param '%s'\n", parserpblk.descr[i].name);
        }
    }

finalize_it:
    CODE_STD_FINALIZERnewParserInst if (lst != NULL) cnfparamvalsDestruct(pvals, &parserpblk);
    if (iRet != RS_RET_OK) free(inst);
ENDnewParserInst

BEGINfreeParserInst
    CODESTARTfreeParserInst;
    dbgprintf("pmsnare: free parser instance %p\n", pInst);
ENDfreeParserInst

BEGINcheckParserInst
    CODESTARTcheckParserInst;
ENDcheckParserInst

BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
    if (eFeat == sFEATUREAutomaticSanitazion) iRet = RS_RET_OK;
    if (eFeat == sFEATUREAutomaticPRIParsing) iRet = RS_RET_OK;
ENDisCompatibleWithFeature

/* Interface with the global config. */
BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    modConf = pModConf;
    pModConf->pConf = pConf;
ENDbeginCnfLoad

BEGINsetModCnf
    CODESTARTsetModCnf;
    /* Could use module-globals here, but not global globals. */
    (void)lst;
ENDsetModCnf

BEGINendCnfLoad
    instanceConf_t *inst;
    CODESTARTendCnfLoad;
    dbgprintf("pmsnare: Begin endCnfLoad\n");
    /* Loop through each parser instance and apply global settings to any option that hasn't been overridden.
     * This can't be done any earlier because the config wasn't fully loaded until now. */
    for (inst = modInstances->root; inst != NULL; inst = inst->next) {
        if (inst->bEscapeCCOnRcv == -1)
            inst->bEscapeCCOnRcv = glbl.GetParserEscapeControlCharactersOnReceive(modConf->pConf);
        if (inst->bEscapeTab == -1) inst->bEscapeTab = glbl.GetParserEscapeControlCharacterTab(modConf->pConf);
        if (inst->bParserEscapeCCCStyle == -1)
            inst->bParserEscapeCCCStyle = glbl.GetParserEscapeControlCharactersCStyle(modConf->pConf);
        if (inst->cCCEscapeChar == '\0')
            inst->cCCEscapeChar = glbl.GetParserControlCharacterEscapePrefix(modConf->pConf);

        /* Determine tab representation. Possible options:
         *		"#011"	escape on, escapetabs on, no change to prefix (default)
         *		"?011"	prefix changed in config
         *		"\\t"	C style
         *		'\t' 	escape turned off
         */
        if (inst->bEscapeCCOnRcv && inst->bEscapeTab) {
            if (inst->bParserEscapeCCCStyle) {
                PMSNARE_SET_TAB_REPR(inst->tabRepresentation, "\\t");
            } else {
                PMSNARE_SET_TAB_REPR(inst->tabRepresentation, "#011");
                inst->tabRepresentation[0] = inst->cCCEscapeChar;
            }
        } else {
            PMSNARE_SET_TAB_REPR(inst->tabRepresentation, "\t");
        }
        inst->tabLength = strlen(inst->tabRepresentation);
        /* TODO: This debug message would be more useful if it told which Snare instance! */
        dbgprintf("pmsnare: Snare parser will treat '%s' as tab.\n", inst->tabRepresentation);
    }

    assert(pModConf == modConf);
ENDendCnfLoad

BEGINcheckCnf
    CODESTARTcheckCnf;
ENDcheckCnf

BEGINactivateCnf
    CODESTARTactivateCnf;
ENDactivateCnf

BEGINfreeCnf
    CODESTARTfreeCnf;
    free(modInstances);
    modInstances = NULL;
ENDfreeCnf

BEGINparse2
    uchar *p2parse;
    const uchar *tagStart;
    int lenMsg;
    int tagLen;
    int snaremessage; /* 0 means not a snare message, otherwise it's the index of the tab after the tag  */

    CODESTARTparse2;
    dbgprintf("Message will now be parsed by fix Snare parser.\n");
    assert(pMsg != NULL);
    assert(pMsg->pszRawMsg != NULL);

    /* check if this message is of the type we handle in this (very limited) parser
     *
     * - Find out if the first separator is a tab.
     * - If it is, see if the second word is one of our expected tags.
     *   - If so, flag as Snare and replace the first tab with space so that
     *     hostname and syslog tag are going to be parsed properly
     *   - Else not a snare message, abort.
     * - Else assume valid 3164 timestamp, move over to the syslog tag.
     * - See if syslog header is followed by tab and one of our expected tags.
     *   - If so, flag as Snare.
     * - See if either type flagged as Snare.
     *   - If so, replace the tab with a space so that it will be parsed properly.
     */

    snaremessage = 0;
    /* note: offAfterPRI is already the number of PRI chars (do not add one!) */
    lenMsg = pMsg->iLenRawMsg - pMsg->offAfterPRI;
    /* point to start of text, after PRI */
    p2parse = pMsg->pszRawMsg + pMsg->offAfterPRI;
    dbgprintf("pmsnare: msg to look at: [%d]'%s'\n", lenMsg, p2parse);
    if ((unsigned)lenMsg < 30) {
        /* too short, can not be "our" message */
        dbgprintf("pmsnare: Message is too short to be Snare!\n");
        ABORT_FINALIZE(RS_RET_COULD_NOT_PARSE);
    }

    /* Find the first separator and check if it's a tab. */
    while (lenMsg && *p2parse != ' ' && *p2parse != '\t' && *p2parse != pInst->tabRepresentation[0]) {
        --lenMsg;
        ++p2parse;
    }
    if ((lenMsg > pInst->tabLength) &&
        (strncasecmp((char *)p2parse, pInst->tabRepresentation, pInst->tabLength) == 0)) {
        dbgprintf("pmsnare: tab separated message\n");
        dbgprintf("pmsnare: tab [%d]'%s'	msg at the first separator: [%d]'%s'\n", pInst->tabLength,
                  pInst->tabRepresentation, lenMsg, p2parse);

        /* Look for the Snare tag plus the following separator we rewrite below. */
        tagStart = p2parse + pInst->tabLength;
        tagLen = lenMsg - pInst->tabLength;
        if (hasPrefixWithFollowingTab(tagStart, tagLen, "MSWinEventLog", 13, pInst)) {
            dbgprintf("Found a non-syslog Windows Snare message.\n");
            snaremessage = p2parse - pMsg->pszRawMsg + pInst->tabLength + 13;
        } else if (hasPrefixWithFollowingTab(tagStart, tagLen, "LinuxKAudit", 11, pInst)) {
            dbgprintf("Found a non-syslog Linux Snare message.\n");
            snaremessage = p2parse - pMsg->pszRawMsg + pInst->tabLength + 11;
        } else {
            /* Tab-separated but no Snare tag-> can't be Snare! */
            ABORT_FINALIZE(RS_RET_COULD_NOT_PARSE);
        }

        /* This is a non-syslog Snare message. Example:
         * other.lab.home	MSWinEventLog	1	Security	606129	Wed May 17 02:25:10 2017
         */

        /* Remove the tab between the hostname and Snare tag. */
        *p2parse = ' ';
        p2parse++;
        lenMsg--;
        lenMsg -= (pInst->tabLength - 1); /* size of tab goes from tabLength to 1, so shorten
                          the message by the difference */
        memmove(p2parse, p2parse + (pInst->tabLength - 1), lenMsg);
        /* move the message portion up to overwrite the tab */
        *(p2parse + lenMsg) = '\0';
        pMsg->iLenRawMsg -= (pInst->tabLength - 1);
        pMsg->iLenMSG -= (pInst->tabLength - 1);
        snaremessage -= (pInst->tabLength - 1);
    } else {
        /* The first separator is not a tab. Look for a syslog Snare message. Example:
         * <14>May 17 02:25:10 syslog.lab.home MSWinEventLog     1    Security  606129
            Wed May 17 02:25:10 2017
         */

        /* go back to the beginning of the message */
        lenMsg = pMsg->iLenRawMsg - pMsg->offAfterPRI;
        /* offAfterPRI is already the number of PRI chars (do not add one!) */
        p2parse = pMsg->pszRawMsg + pMsg->offAfterPRI;

        /* skip over timestamp and space (15 chars + space). */
        lenMsg -= 16;
        p2parse += 16;
        /* skip over what should be the hostname and space */
        while (lenMsg && *p2parse != ' ') {
            --lenMsg;
            ++p2parse;
        }
        if (lenMsg) {
            --lenMsg;
            ++p2parse;
        }
        dbgprintf("pmsnare: tab [%d]'%s'	msg after the timestamp and hostname: [%d]'%s'\n", pInst->tabLength,
                  pInst->tabRepresentation, lenMsg, p2parse);

        /* Look for the Snare tag. */
        if (hasPrefixWithFollowingTab(p2parse, lenMsg, "MSWinEventLog", 13, pInst)) {
            dbgprintf("Found a syslog Windows Snare message.\n");
            snaremessage = p2parse - pMsg->pszRawMsg + 13;
        } else if (hasPrefixWithFollowingTab(p2parse, lenMsg, "LinuxKAudit", 11, pInst)) {
            dbgprintf("pmsnare: Found a syslog Linux Snare message.\n");
            snaremessage = p2parse - pMsg->pszRawMsg + 11;
        }
    }

    if (snaremessage) {
        /* Skip to the end of the tag. */
        p2parse = pMsg->pszRawMsg + snaremessage;
        lenMsg = pMsg->iLenRawMsg - snaremessage;

        /* Remove the tab after the tag. */
        if (lenMsg < pInst->tabLength) {
            ABORT_FINALIZE(RS_RET_COULD_NOT_PARSE);
        }
        *p2parse = ' ';
        p2parse++;
        lenMsg--;
        lenMsg -= (pInst->tabLength - 1); /* size of tab goes from tabLength to 1, so shorten
                          the message by the difference */
        memmove(p2parse, p2parse + (pInst->tabLength - 1), lenMsg);
        /* move the message portion up to overwrite the tab */
        *(p2parse + lenMsg) = '\0';
        pMsg->iLenRawMsg -= (pInst->tabLength - 1);
        pMsg->iLenMSG -= (pInst->tabLength - 1);

        DBGPRINTF("pmsnare: new message: [%d]'%s'\n", lenMsg, pMsg->pszRawMsg + pMsg->offAfterPRI);
    }

    ABORT_FINALIZE(RS_RET_COULD_NOT_PARSE);

finalize_it:
ENDparse2

BEGINmodExit
    CODESTARTmodExit;
    /* release what we no longer need */
    objRelease(glbl, CORE_COMPONENT);
    objRelease(parser, CORE_COMPONENT);
    objRelease(datetime, CORE_COMPONENT);
ENDmodExit

BEGINqueryEtryPt
    CODESTARTqueryEtryPt;
    CODEqueryEtryPt_STD_MOD_QUERIES;
    CODEqueryEtryPt_STD_CONF2_QUERIES;
    CODEqueryEtryPt_STD_CONF2_setModCnf_QUERIES;
    CODEqueryEtryPt_STD_PMOD2_QUERIES;
    CODEqueryEtryPt_IsCompatibleWithFeature_IF_OMOD_QUERIES;
ENDqueryEtryPt

BEGINmodInit()
    CODESTARTmodInit;
    *ipIFVersProvided = CURR_MOD_IF_VERSION; /* we only support the current interface specification */
    CODEmodInit_QueryRegCFSLineHdlr CHKiRet(objUse(glbl, CORE_COMPONENT));
    CHKiRet(objUse(parser, CORE_COMPONENT));
    CHKiRet(objUse(datetime, CORE_COMPONENT));

    DBGPRINTF("snare parser init called, compiled with version %s\n", VERSION);
    bParseHOSTNAMEandTAG = glbl.GetParseHOSTNAMEandTAG(loadConf);
    /* cache value, is set only during rsyslogd option processing */
ENDmodInit

/* vim:set ai:
 */
