diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/cyberark/pas/plugins/event_source | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cyberark/pas/plugins/event_source')
-rw-r--r-- | ansible_collections/cyberark/pas/plugins/event_source/cyberark-eda-json-v1.0.xsl | 175 | ||||
-rw-r--r-- | ansible_collections/cyberark/pas/plugins/event_source/syslog.py | 173 |
2 files changed, 348 insertions, 0 deletions
diff --git a/ansible_collections/cyberark/pas/plugins/event_source/cyberark-eda-json-v1.0.xsl b/ansible_collections/cyberark/pas/plugins/event_source/cyberark-eda-json-v1.0.xsl new file mode 100644 index 000000000..d9ee36467 --- /dev/null +++ b/ansible_collections/cyberark/pas/plugins/event_source/cyberark-eda-json-v1.0.xsl @@ -0,0 +1,175 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:import href="./Syslog/RFC5424Changes.xsl"/> +<xsl:output method='text' version='1.0' encoding='UTF-8' indent='no'/> + +<!-- version control variables --> +<xsl:variable name="format" select="'cyberark-eda-json'"/> +<xsl:variable name="version" select="'1.0'"/> + +<!-- configuration --> +<xsl:variable name="include_raw" select="0"/> <!-- save a "raw" key with the original XML --> + +<!-- main object with header info --> +<xsl:template match="/"> + <xsl:apply-imports/> + <xsl:text>{"format":"</xsl:text><xsl:value-of select="$format"/> + <xsl:text>","version":"</xsl:text><xsl:value-of select="$version"/> + <xsl:text>"</xsl:text> + <xsl:choose> + <xsl:when test="$include_raw=1"> + <xsl:text>,"raw":</xsl:text> + <xsl:call-template name="json-string"> + <xsl:with-param name="text"> + <xsl:apply-templates select="*" mode="raw"/> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + </xsl:choose> + <xsl:text>,</xsl:text> + <xsl:apply-templates select="*" mode="object"/> + <!-- this text below includes the terminating newline --> + <xsl:text>}
</xsl:text> +</xsl:template> + +<xsl:template match="*" mode="raw"> + <xsl:value-of select="concat('<', name())" /> + <xsl:for-each select="@*"> + <xsl:value-of select="concat(' ', name(), '="', ., '"')"/> + </xsl:for-each> + <xsl:text>></xsl:text> + <xsl:apply-templates mode="raw"/> + <xsl:value-of select="concat('</', name(), '>')" /> + </xsl:template> + +<!-- serialize objects --> +<xsl:template match="*" mode="object"> + <xsl:text>"</xsl:text> + <xsl:value-of select="name()"/><xsl:text>":</xsl:text><xsl:call-template name="value"> + <xsl:with-param name="parent" select="1"/> + </xsl:call-template> +</xsl:template> + +<!-- serialize array elements --> +<xsl:template match="*" mode="array"> + <xsl:call-template name="value"/> +</xsl:template> + +<!-- value of node serializer --> +<xsl:template name="value"> + <xsl:param name="parent"/> + <xsl:variable name="childName" select="name(*[1])"/> + <xsl:choose> + <xsl:when test="not(*|@*)"> + <xsl:choose> + <xsl:when test="$parent=1"> + <xsl:call-template name="json-string"> + <xsl:with-param name="text" select="."/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:call-template name="json-string"> + <xsl:with-param name="text" select="name()"/> + </xsl:call-template> + <xsl:text>:</xsl:text> + <xsl:call-template name="json-string"> + <xsl:with-param name="text" select="."/> + </xsl:call-template> + </xsl:otherwise> + </xsl:choose> + </xsl:when> + <xsl:when test="count(*[name()=$childName]) > 1"> + <xsl:text>{</xsl:text> + <xsl:call-template name="json-string"> + <xsl:with-param name="text" select="$childName"/> + </xsl:call-template> + <xsl:text>:[</xsl:text> + <xsl:apply-templates select="*" mode="array"/> + <xsl:text>]}</xsl:text> + </xsl:when> + <xsl:otherwise> + <xsl:text>{</xsl:text> + <xsl:apply-templates select="@*" mode="attrs"/> + <xsl:if test='count(@*)>0 and count(*)>0'>,</xsl:if> + <xsl:apply-templates select="*" mode="object"/> + <xsl:text>}</xsl:text> + </xsl:otherwise> + </xsl:choose> + <xsl:if test="following-sibling::*"><xsl:text>,</xsl:text></xsl:if> +</xsl:template> + +<!-- serialize attributes --> +<xsl:template match="@*" mode="attrs"> + <xsl:call-template name="json-string"> + <xsl:with-param name="text" select="name()"/> + </xsl:call-template> + <xsl:text>:</xsl:text> + <xsl:call-template name="json-string"> + <xsl:with-param name="text" select="."/> + </xsl:call-template> + <xsl:if test="position()!=last()"><xsl:text>,</xsl:text></xsl:if> +</xsl:template> + +<!-- json-string converts a text to a quoted and escaped JSON string --> +<xsl:template name="json-string"> + <xsl:param name="text"/> + <xsl:variable name="tmp"> + <xsl:call-template name="string-replace"> + <xsl:with-param name="string" select="$text"/> + <xsl:with-param name="from" select="'\'"/> + <xsl:with-param name="to" select="'\\'"/> + </xsl:call-template> + </xsl:variable> + <xsl:variable name="tmp2"> + <xsl:call-template name="string-replace"> + <xsl:with-param name="string" select="$tmp"/> + <xsl:with-param name="from" select="'
'"/> + <xsl:with-param name="to" select="'\n'"/> + </xsl:call-template> + </xsl:variable> + <xsl:variable name="tmp3"> + <xsl:call-template name="string-replace"> + <xsl:with-param name="string" select="$tmp2"/> + <xsl:with-param name="from" select="'
'"/> + <xsl:with-param name="to" select="'\r'"/> + </xsl:call-template> + </xsl:variable> + <xsl:variable name="tmp4"> + <xsl:call-template name="string-replace"> + <xsl:with-param name="string" select="$tmp3"/> + <xsl:with-param name="from" select="'	'"/> + <xsl:with-param name="to" select="'\t'"/> + </xsl:call-template> + </xsl:variable> + <xsl:text>"</xsl:text> + <xsl:call-template name="string-replace"> + <xsl:with-param name="string" select="$tmp4"/> + <xsl:with-param name="from" select="'"'"/> + <xsl:with-param name="to" select="'\"'"/> + </xsl:call-template> + <xsl:text>"</xsl:text> +</xsl:template> + +<!-- replace all occurences of the character(s) `from' + by the string `to' in the string `string'.--> +<xsl:template name="string-replace"> + <xsl:param name="string"/> + <xsl:param name="from"/> + <xsl:param name="to"/> + <xsl:choose> + <xsl:when test="contains($string,$from)"> + <xsl:value-of select="substring-before($string,$from)"/> + <xsl:value-of select="$to"/> + <xsl:call-template name="string-replace"> + <xsl:with-param name="string" select="substring-after($string,$from)"/> + <xsl:with-param name="from" select="$from"/> + <xsl:with-param name="to" select="$to"/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$string"/> + </xsl:otherwise> + </xsl:choose> +</xsl:template> + +</xsl:stylesheet> diff --git a/ansible_collections/cyberark/pas/plugins/event_source/syslog.py b/ansible_collections/cyberark/pas/plugins/event_source/syslog.py new file mode 100644 index 000000000..2fbe1916c --- /dev/null +++ b/ansible_collections/cyberark/pas/plugins/event_source/syslog.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) + + +__metaclass__ = type + +""" +syslog.py + +An ansible-rulebook event source module for receiving events via a syslog. + +Arguments: + host: The hostname to listen to. Set to 0.0.0.0 to listen on all + interfaces. Defaults to 127.0.0.1 + port: The TCP port to listen to. Defaults to 1514 + +""" + +import asyncio +import json +import logging +import socketserver +from typing import Any, Dict +import re + +def parse(str_input): + """ + Parse a string in CEF format and return a dict with the header values + and the extension data. + """ + + logger = logging.getLogger() + # Create the empty dict we'll return later + values = dict() + + # This regex separates the string into the CEF header and the extension + # data. Once we do this, it's easier to use other regexes to parse each + # part. + header_re = r'((CEF:\d+)([^=\\]+\|){,7})(.*)' + + res = re.search(header_re, str_input) + + if res: + header = res.group(1) + extension = res.group(4) + + # Split the header on the "|" char. Uses a negative lookbehind + # assertion to ensure we don't accidentally split on escaped chars, + # though. + spl = re.split(r'(?<!\\)\|', header) + + # If the input entry had any blanks in the required headers, that's wrong + # and we should return. Note we explicitly don't check the last item in the + # split list becuase the header ends in a '|' which means the last item + # will always be an empty string (it doesn't exist, but the delimiter does). + if "" in spl[0:-1]: + logger.warning(f'Blank field(s) in CEF header. Is it valid CEF format?') + return None + + # Since these values are set by their position in the header, it's + # easy to know which is which. + values["DeviceVendor"] = spl[1] + values["DeviceProduct"] = spl[2] + values["DeviceVersion"] = spl[3] + values["DeviceEventClassID"] = spl[4] + values["Name"] = spl[5] + values["DeviceName"] = spl[5] + if len(spl) > 6: + values["Severity"] = spl[6] + values["DeviceSeverity"] = spl[6] + + # The first value is actually the CEF version, formatted like + # "CEF:#". Ignore anything before that (like a date from a syslog message). + # We then split on the colon and use the second value as the + # version number. + cef_start = spl[0].find('CEF') + if cef_start == -1: + return None + (cef, version) = spl[0][cef_start:].split(':') + values["CEFVersion"] = version + + # The ugly, gnarly regex here finds a single key=value pair, + # taking into account multiple whitespaces, escaped '=' and '|' + # chars. It returns an iterator of tuples. + spl = re.findall(r'([^=\s]+)=((?:[\\]=|[^=])+)(?:\s|$)', extension) + for i in spl: + # Split the tuples and put them into the dictionary + values[i[0]] = i[1] + + # Process custom field labels + for key in list(values.keys()): + # If the key string ends with Label, replace it in the appropriate + # custom field + if key[-5:] == "Label": + customlabel = key[:-5] + # Find the corresponding customfield and replace with the label + for customfield in list(values.keys()): + if customfield == customlabel: + values[values[key]] = values[customfield] + del values[customfield] + del values[key] + else: + # return None if our regex had now output + # logger.warning('Could not parse record. Is it valid CEF format?') + return None + + # Now we're done! + logger.debug('Returning values: ' + str(values)) + return values + + +class SyslogProtocol(asyncio.DatagramProtocol): + def __init__(self, edaQueue): + super().__init__() + self.edaQueue = edaQueue + def connection_made(self, transport) -> "Used by asyncio": + self.transport = transport + + def datagram_received(self, data, addr): + asyncio.get_event_loop().create_task(self.datagram_received_async( data, addr)) + + async def datagram_received_async(self, indata, addr) -> "Main entrypoint for processing message": + # Syslog event data received, and processed for EDA + logger = logging.getLogger() + rcvdata = indata.decode() + logger.info(f"Received Syslog message: {rcvdata}") + data = parse(rcvdata) + + if data is None: + # if not CEF, we will try JSON load of the text from first curly brace + try: + value = rcvdata[rcvdata.index("{"):len(rcvdata)] + #logger.info("value after encoding:%s", value1) + data = json.loads(value) + #logger.info("json:%s", data) + except json.decoder.JSONDecodeError as jerror: + logger.error(jerror) + data = rcvdata + except UnicodeError as e: + logger.error(e) + + if data: + #logger.info("json data:%s", data) + queue = self.edaQueue + await queue.put({"cyberark": data}) + +async def main(queue: asyncio.Queue, args: Dict[str, Any]): + logger = logging.getLogger() + + loop = asyncio.get_event_loop() + host = args.get("host") or '0.0.0.0' + port = args.get("port") or 1514 + transport, protocol = await asyncio.get_running_loop().create_datagram_endpoint( + lambda: SyslogProtocol(queue), + local_addr=((host, port))) + logger.info(f"Starting cyberark.pas.syslog [Host={host}, port={port}]") + try: + while True: + await asyncio.sleep(3600) # Serve for 1 hour. + finally: + transport.close() + + +if __name__ == "__main__": + + class MockQueue: + async def put(self, event): + pass #print(event) + + asyncio.run(main(MockQueue(), {})) |