Syslog v2
A Syslog server enables automatically opening incidents from Syslog clients. This integration supports filtering logs to convert to incidents, or alternatively converting all logs.
Analytics & SIEM · Syslog
Details
| ID | Syslog v2 |
|---|---|
| Provider | Open Source |
| Category | Analytics & SIEM |
| From Version | 6.0.0 |
| Docker Image | demisto/syslog:1.0.0.10133006 |
| Supported Modules | Agentix XSIAM |
README
A Syslog server enables opening incidents automatically from Syslog clients. This integration supports converting to incidents, filtered logs or all logs.
This integration is a long-running integration. For more information about long-running integrations, see the Cortex XSOAR 8 Cloud, Cortex XSOAR 8 On-prem or Cortex XSIAM documentation.
This integration was integrated and tested with RFC3164 and RFC5424 formats of Syslog.
Notes
- Important: Supported log formats: RFC3164, RFC5424, RFC6587 (with RFC3164 or RFC5424)
- Important: Do not use an engine group for this integration. It can cause the integration to run on a different engine, and the Syslog server may send logs to an IP for which Syslog is not configured.
- The integration does not support encrypted private keys.
Configure Syslog v2 in Cortex
| Parameter | Description | Required |
|---|---|---|
| Port mapping | The listening port to receive Syslog message on (<port> or <host port>:<docker port>). Port 6514 is the default when using TLS. |
True |
| Certificate | Required for TLS | False |
| Private Key | Required for TLS | False |
| Message Regex Filter For Incidents Creation | Creates an incident in Cortex XSOAR for every received log message that matches this regex. | False |
Troubleshooting
To receive incidents, the Syslog engine listens on a configured port that needs to be available for external in-coming traffic. There may be cases that docker is configured not to expose the port for external in-comming traffic. In this case, you can use host networking and not the docker based networking. Enable host networking usage by adding the following server configuration (Settings > About > Troubleshooting > Add Server Configuration):
- Key:
python.pass.extra.keys.demisto/syslog - Value:
--network=host
If listening on a port less than 1024 and running with the Docker Hardening configuration, you may need to disable the “run with non-root internal user” setting for the Syslog integration to listen on the host networking on a lower port. From more information, see: Run Docker with Non Root Internal Users (Cortex XSOAR 6.13) or Docker hardening guide (Cortex XSOAR 6.13) or Docker hardening guide (Cortex XSOAR 8 Cloud) or Docker hardening guide (Cortex XSOAR 8.7 On-prem) You can disable this setting by adding the following server configuration:
- Key:
docker.run.internal.asuser.ignore - Value:
demisto/syslog
If the integration is running via an engine, you need to add this setting to the engine configuration either via the d1.conf file or in the Server Settings->Engines-> Edit Configuration.
Configuration parameters
creds_certificate— Certificatecertificate— Certificateprivate_key— Private Keymessage_regex— Message Regex Filter For Incidents Creation.longRunningPort— Port mapping (required)longRunning— Long Running Instance
Commands (1)
-
get-mapping-fieldsReturns the list of fields for an incident type. This command should be used for debugging.
from dataclasses import dataclass from tempfile import NamedTemporaryFile from collections.abc import Callable import urllib3 import syslogmp from gevent.server import StreamServer from syslog_rfc5424_parser import SyslogMessage, ParseError from CommonServerPython import * # noqa # pylint: disable=unused-wildcard-import from CommonServerUserPython import * # noqa # Disable insecure warnings urllib3.disable_warnings() # pylint: disable=no-member """ CONSTANTS """ MAX_SAMPLES = 10 BUF_SIZE = 1024 MESSAGE_REGEX: Optional[str] = None MAX_PORT: int = 65535 @dataclass class SyslogMessageExtract: app_name: Optional[str] facility: str host_name: Optional[str] msg: str msg_id: Optional[str] process_id: Optional[str] sd: dict severity: str timestamp: str version: Optional[int] occurred: Optional[str] def parse_no_length_limit(data: bytes) -> syslogmp.parser.Message: """ Parse a syslog message with no length limit. """ parser = syslogmp.parser._Parser(b"") parser.stream = syslogmp.parser.Stream(data) priority_value = parser._parse_pri_part() timestamp, hostname = parser._parse_header_part() message = parser._parse_msg_part() return syslogmp.parser.Message( facility=priority_value.facility, severity=priority_value.severity, timestamp=timestamp, hostname=hostname, message=message, ) def parse_rfc_3164_format(log_message: bytes) -> Optional[SyslogMessageExtract]: """ Receives a log message which is in RFC 3164 format. Parses it into SyslogMessageExtract data class object Args: log_message (bytes): Syslog message. Returns: (Optional[SyslogMessageExtract]): Extraction data class """ try: syslog_message: syslogmp.Message = parse_no_length_limit(log_message) except syslogmp.parser.MessageFormatError as e: demisto.debug(f"Could not parse the log message, got MessageFormatError. Error was: {e}") return None return SyslogMessageExtract( app_name=None, facility=syslog_message.facility.name, host_name=syslog_message.hostname, msg=syslog_message.message.decode("utf-8"), msg_id=None, process_id=None, sd={}, severity=syslog_message.severity.name, timestamp=syslog_message.timestamp.isoformat(), version=None, # Because RF-3164 doesn't return localized date, can't determine the localized time it occurred. occurred=None, ) def parse_rfc_5424_format(log_message: bytes) -> Optional[SyslogMessageExtract]: """ Receives a log message which is in RFC 5424 format. Parses it into SyslogMessageExtract data class object Args: log_message (bytes): Syslog message. Returns: (Optional[SyslogMessageExtract]): Extraction data class """ try: syslog_message: SyslogMessage = SyslogMessage.parse(log_message.decode("utf-8")) except ParseError as e: demisto.debug(f"Could not parse the log message, got ParseError. Error was: {e}") return None return SyslogMessageExtract( app_name=syslog_message.appname, facility=syslog_message.facility.name, host_name=syslog_message.hostname, msg=syslog_message.msg, msg_id=syslog_message.msgid, process_id=syslog_message.procid, sd=syslog_message.sd, severity=syslog_message.severity.name, timestamp=syslog_message.timestamp, version=syslog_message.version, occurred=syslog_message.timestamp, ) def parse_rfc_6587_format(log_message: bytes) -> Optional[SyslogMessageExtract]: """ Receives a log message which is in RFC 6587 format. Parses it into SyslogMessageExtract data class object Args: log_message (bytes): Syslog message. Returns: (SyslogMessageExtract): Extraction data class """ log_message = log_message.decode("utf-8") split_msg: List[str] = log_message.split(" ") if not log_message or not log_message[0].isdigit() or not len(split_msg) > 1: return None try: log_message = " ".join(split_msg[1:]) encoded_msg = log_message.encode() for format_func in format_funcs: # if it is RFC6587 itself, continue if format_func == parse_rfc_6587_format: continue extracted_message = format_func(encoded_msg) if extracted_message: return extracted_message except ValueError as e: demisto.debug(f"Could not parse the log message, got ValueError. Error was: {e}") return None return None format_funcs: List[Callable[[bytes], Optional[SyslogMessageExtract]]] = [ parse_rfc_3164_format, parse_rfc_5424_format, parse_rfc_6587_format, ] def fetch_samples() -> None: """ Retrieves samples from context. """ demisto.incidents(get_integration_context().get("samples")) def create_incident_from_syslog_message(extracted_message: SyslogMessageExtract, incident_type: Optional[str]) -> dict: """ Creates incident from the extracted Syslog message. Args: extracted_message (SyslogMessageExtract): Syslog message extraction details. incident_type (Optional[str]): The incident type Returns: (dict): Incident. """ return { "name": f"Syslog from [{extracted_message.host_name}][{extracted_message.timestamp}]", "rawJSON": json.dumps(vars(extracted_message)), "occurred": extracted_message.occurred, "type": incident_type, "details": "\n".join([f"{k}: {v}" for k, v in vars(extracted_message).items() if v]), } def update_integration_context_samples(incident: dict, max_samples: int = MAX_SAMPLES) -> None: """ Updates the integration context samples with the newly created incident. If the size of the samples has reached `MAX_SAMPLES`, will pop out the latest sample. Args: incident (dict): The newly created incident. max_samples (int): Max samples size. Returns: (None): Modifies the integration context samples field. """ ctx = get_integration_context() updated_samples_list: List[Dict] = [incident] + ctx.get("samples", []) if len(updated_samples_list) > max_samples: updated_samples_list.pop() ctx["samples"] = updated_samples_list set_integration_context(ctx) def log_message_passes_filter(log_message: SyslogMessageExtract, message_regex: Optional[str]) -> bool: """ Given log message extraction and a possible message regex, checks if the message passes the filters: 1) Message regex is None, therefore no filter was asked to be made. 2) Message regex is not None: Filter the Syslog message if regex does not exist in the message, if regex exists in the Syslog message, do not filter. Args: log_message (SyslogMessageExtract): The extracted details of a Syslog message. message_regex (Optional[str]): Message regex to match if exists. Returns: (bool): True if the message shouldn't be filtered, false if the message should be filtered. """ if not message_regex: return True regexp = re.compile(message_regex) return bool(regexp.search(log_message.msg)) def perform_long_running_loop(socket_data: bytes): """ Performs one loop of a long running execution. - Gets data from socket. - Parses the Syslog message data. - If the Syslog message data passes filter, creates a new incident. - Saves the incident in integration context for samples. Args: socket_data (bytes): Retrieved socket data. Returns: (None): Creates incident in Cortex XSOAR platform. """ incident_type: Optional[str] = demisto.params().get("incident_type", "") extracted_message: Optional[SyslogMessageExtract] = None for format_func in format_funcs: extracted_message = format_func(socket_data) if extracted_message: demisto.debug(f"Succeeded in parsing the message with {format_func}") break if not extracted_message: raise DemistoException(f'Could not parse the following message: {socket_data.decode("utf-8")}') if log_message_passes_filter(extracted_message, MESSAGE_REGEX): incident: dict = create_incident_from_syslog_message(extracted_message, incident_type) update_integration_context_samples(incident) demisto.createIncidents([incident]) def perform_long_running_execution(sock: Any, address: tuple) -> None: """ The long running execution loop. Gets input, and performs a while True loop and logs any error that happens. Stops when there is no more data to read. Args: sock: Socket. address(tuple): Address. Not used inside loop so marked as underscore. Returns: (None): Reads data, calls that creates incidents from inputted data. """ demisto.debug("Starting long running execution") file_obj = sock.makefile(mode="rb") try: while True: try: line = file_obj.readline() if not line: demisto.info(f"Disconnected from {address}") break perform_long_running_loop(line.strip()) except Exception as e: demisto.error(traceback.format_exc()) # print the traceback demisto.error(f"Error occurred during long running loop. Error was: {e}") finally: demisto.debug("Finished reading message") finally: file_obj.close() def prepare_globals_and_create_server( port: int, message_regex: Optional[str], certificate: Optional[str], private_key: Optional[str] ) -> StreamServer: """ Prepares global environments of LOG_FORMAT, MESSAGE_REGEX and creates the server to listen to Syslog messages. Args: port (int): Port message_regex (Optional[str]): Regex. Will create incident only if Syslog message matches this regex. certificate (Optional[str]): Certificate. For SSL connection. private_key (Optional[str]): Private key. For SSL connection. Returns: (StreamServer): Server to listen to Syslog messages. """ global MESSAGE_REGEX MESSAGE_REGEX = message_regex if certificate and private_key: certificate_file = NamedTemporaryFile(delete=False) certificate_path = certificate_file.name certificate_file.write(bytes(certificate, "utf-8")) certificate_file.close() private_key_file = NamedTemporaryFile(delete=False) private_key_path = private_key_file.name private_key_file.write(bytes(private_key, "utf-8")) private_key_file.close() server = StreamServer( ("0.0.0.0", port), perform_long_running_execution, keyfile=private_key_path, certfile=certificate_path ) demisto.debug("Starting HTTPS Server") else: server = StreamServer(("0.0.0.0", port), perform_long_running_execution) demisto.debug("Starting HTTP Server") return server def get_mapping_fields() -> Dict[str, str]: return { "app_name": "Application Name", "facility": "Facility", "host_name": "Host Name", "msg": "Message", "msg_id": "Message ID", "process_id": "Process ID", "sd": "Structured Data", "severity": "Severity", "timestamp": "Timestamp", "version": "Syslog Version", "occurred": "Occurred Time", } """ MAIN FUNCTION """ def main() -> None: params = demisto.params() command = demisto.command() message_regex: Optional[str] = params.get("message_regex") certificate = replace_spaces_in_credential(params.get("creds_certificate", {}).get("identifier")) or params.get("certificate") private_key = replace_spaces_in_credential(params.get("creds_certificate", {}).get("password", "")) or params.get( "private_key" ) port: Union[Optional[str], int] = params.get("longRunningPort") try: port = int(params.get("longRunningPort")) except (ValueError, TypeError): raise DemistoException("Please select an engine and insert a valid listen port.") if port < 0 or port > MAX_PORT: raise DemistoException(f"Given port: {port} is not valid and must be between 0-{MAX_PORT}") demisto.debug(f"Command being called is {demisto.command()}") try: if command == "test-module": try: prepare_globals_and_create_server(port, message_regex, certificate, private_key) except OSError as e: if "Address already in use" in str(e): raise DemistoException( f"Given port: {port} is already in use. Please either change port or " f"make sure to close the connection in the server using that port." ) raise e return_results("ok") elif command == "fetch-incidents": # The integration fetches incidents in the long-running-execution command. Fetch incidents is called # only when "Pull From Instance" is clicked in create new classifier section in Cortex XSOAR. # The fetch incidents returns samples of incidents generated by the long-running-execution. fetch_samples() elif command == "long-running-execution": server: StreamServer = prepare_globals_and_create_server(port, message_regex, certificate, private_key) server.serve_forever() elif command == "get-mapping-fields": return_results(get_mapping_fields()) else: raise NotImplementedError(f"""Command '{command}' is not implemented.""") # Log exceptions and return errors except Exception as e: return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()