diff --git a/docs/source/usage.md b/docs/source/usage.md index 85eec61..6a18b5f 100644 --- a/docs/source/usage.md +++ b/docs/source/usage.md @@ -336,7 +336,59 @@ The full set of configuration options are: - `secret_access_key` - str: The secret access key (Optional) - `syslog` - `server` - str: The Syslog server name or IP address - - `port` - int: The UDP port to use (Default: `514`) + - `port` - int: The port to use (Default: `514`) + - `protocol` - str: The protocol to use: `udp`, `tcp`, or `tls` (Default: `udp`) + - `cafile_path` - str: Path to CA certificate file for TLS server verification (Optional) + - `certfile_path` - str: Path to client certificate file for TLS authentication (Optional) + - `keyfile_path` - str: Path to client private key file for TLS authentication (Optional) + - `timeout` - float: Connection timeout in seconds for TCP/TLS (Default: `5.0`) + - `retry_attempts` - int: Number of retry attempts for failed connections (Default: `3`) + - `retry_delay` - int: Delay in seconds between retry attempts (Default: `5`) + + **Example UDP configuration (default):** + + ```ini + [syslog] + server = syslog.example.com + port = 514 + ``` + + **Example TCP configuration:** + + ```ini + [syslog] + server = syslog.example.com + port = 6514 + protocol = tcp + timeout = 10.0 + retry_attempts = 5 + ``` + + **Example TLS configuration with server verification:** + + ```ini + [syslog] + server = syslog.example.com + port = 6514 + protocol = tls + cafile_path = /path/to/ca-cert.pem + timeout = 10.0 + ``` + + **Example TLS configuration with mutual authentication:** + + ```ini + [syslog] + server = syslog.example.com + port = 6514 + protocol = tls + cafile_path = /path/to/ca-cert.pem + certfile_path = /path/to/client-cert.pem + keyfile_path = /path/to/client-key.pem + timeout = 10.0 + retry_attempts = 3 + retry_delay = 5 + ``` - `gmail_api` - `credentials_file` - str: Path to file containing the credentials, None to disable (Default: `None`) diff --git a/parsedmarc/cli.py b/parsedmarc/cli.py index 692c3fd..ae30168 100644 --- a/parsedmarc/cli.py +++ b/parsedmarc/cli.py @@ -585,6 +585,45 @@ def _main(): dest="prettify_json", help="output JSON in a single line without indentation", ) + arg_parser.add_argument( + "--syslog-protocol", + choices=["udp", "tcp", "tls"], + default=None, + help="syslog protocol: udp, tcp, or tls (Default: udp)", + ) + arg_parser.add_argument( + "--syslog-cafile-path", + default=None, + help="path to CA certificate file for TLS server verification", + ) + arg_parser.add_argument( + "--syslog-certfile-path", + default=None, + help="path to client certificate file for TLS authentication", + ) + arg_parser.add_argument( + "--syslog-keyfile-path", + default=None, + help="path to client private key file for TLS authentication", + ) + arg_parser.add_argument( + "--syslog-timeout", + type=float, + default=None, + help="connection timeout in seconds for TCP/TLS (Default: 5.0)", + ) + arg_parser.add_argument( + "--syslog-retry-attempts", + type=int, + default=None, + help="number of retry attempts for failed connections (Default: 3)", + ) + arg_parser.add_argument( + "--syslog-retry-delay", + type=int, + default=None, + help="delay in seconds between retry attempts (Default: 5)", + ) arg_parser.add_argument("-v", "--version", action="version", version=__version__) aggregate_reports = [] @@ -697,6 +736,13 @@ def _main(): s3_secret_access_key=None, syslog_server=None, syslog_port=None, + syslog_protocol=args.syslog_protocol, + syslog_cafile_path=args.syslog_cafile_path, + syslog_certfile_path=args.syslog_certfile_path, + syslog_keyfile_path=args.syslog_keyfile_path, + syslog_timeout=args.syslog_timeout, + syslog_retry_attempts=args.syslog_retry_attempts, + syslog_retry_delay=args.syslog_retry_delay, gmail_api_credentials_file=None, gmail_api_token_file=None, gmail_api_include_spam_trash=False, @@ -1239,6 +1285,28 @@ def _main(): opts.syslog_port = syslog_config["port"] else: opts.syslog_port = 514 + if "protocol" in syslog_config: + opts.syslog_protocol = syslog_config["protocol"] + else: + opts.syslog_protocol = "udp" + if "cafile_path" in syslog_config: + opts.syslog_cafile_path = syslog_config["cafile_path"] + if "certfile_path" in syslog_config: + opts.syslog_certfile_path = syslog_config["certfile_path"] + if "keyfile_path" in syslog_config: + opts.syslog_keyfile_path = syslog_config["keyfile_path"] + if "timeout" in syslog_config: + opts.syslog_timeout = float(syslog_config["timeout"]) + else: + opts.syslog_timeout = 5.0 + if "retry_attempts" in syslog_config: + opts.syslog_retry_attempts = int(syslog_config["retry_attempts"]) + else: + opts.syslog_retry_attempts = 3 + if "retry_delay" in syslog_config: + opts.syslog_retry_delay = int(syslog_config["retry_delay"]) + else: + opts.syslog_retry_delay = 5 if "gmail_api" in config.sections(): gmail_api_config = config["gmail_api"] @@ -1436,6 +1504,13 @@ def _main(): syslog_client = syslog.SyslogClient( server_name=opts.syslog_server, server_port=int(opts.syslog_port), + protocol=opts.syslog_protocol or "udp", + cafile_path=opts.syslog_cafile_path, + certfile_path=opts.syslog_certfile_path, + keyfile_path=opts.syslog_keyfile_path, + timeout=opts.syslog_timeout if opts.syslog_timeout is not None else 5.0, + retry_attempts=opts.syslog_retry_attempts if opts.syslog_retry_attempts is not None else 3, + retry_delay=opts.syslog_retry_delay if opts.syslog_retry_delay is not None else 5, ) except Exception as error_: logger.error("Syslog Error: {0}".format(error_.__str__())) diff --git a/parsedmarc/syslog.py b/parsedmarc/syslog.py index c08e348..bedda86 100644 --- a/parsedmarc/syslog.py +++ b/parsedmarc/syslog.py @@ -6,7 +6,10 @@ from __future__ import annotations import json import logging import logging.handlers -from typing import Any +import socket +import ssl +import time +from typing import Any, Optional from parsedmarc import ( parsed_aggregate_reports_to_csv_rows, @@ -18,20 +21,140 @@ from parsedmarc import ( class SyslogClient(object): """A client for Syslog""" - def __init__(self, server_name: str, server_port: int): + def __init__( + self, + server_name: str, + server_port: int, + protocol: str = "udp", + cafile_path: Optional[str] = None, + certfile_path: Optional[str] = None, + keyfile_path: Optional[str] = None, + timeout: float = 5.0, + retry_attempts: int = 3, + retry_delay: int = 5, + ): """ Initializes the SyslogClient Args: server_name (str): The Syslog server - server_port (int): The Syslog UDP port + server_port (int): The Syslog port + protocol (str): The protocol to use: "udp", "tcp", or "tls" (Default: "udp") + cafile_path (str): Path to CA certificate file for TLS server verification (Optional) + certfile_path (str): Path to client certificate file for TLS authentication (Optional) + keyfile_path (str): Path to client private key file for TLS authentication (Optional) + timeout (float): Connection timeout in seconds for TCP/TLS (Default: 5.0) + retry_attempts (int): Number of retry attempts for failed connections (Default: 3) + retry_delay (int): Delay in seconds between retry attempts (Default: 5) """ self.server_name = server_name self.server_port = server_port + self.protocol = protocol.lower() + self.timeout = timeout + self.retry_attempts = retry_attempts + self.retry_delay = retry_delay + self.logger = logging.getLogger("parsedmarc_syslog") self.logger.setLevel(logging.INFO) - log_handler = logging.handlers.SysLogHandler(address=(server_name, server_port)) + + # Create the appropriate syslog handler based on protocol + log_handler = self._create_syslog_handler( + server_name, + server_port, + self.protocol, + cafile_path, + certfile_path, + keyfile_path, + timeout, + retry_attempts, + retry_delay, + ) + self.logger.addHandler(log_handler) + def _create_syslog_handler( + self, + server_name: str, + server_port: int, + protocol: str, + cafile_path: Optional[str], + certfile_path: Optional[str], + keyfile_path: Optional[str], + timeout: float, + retry_attempts: int, + retry_delay: int, + ) -> logging.handlers.SysLogHandler: + """ + Creates a SysLogHandler with the specified protocol and TLS settings + """ + if protocol == "udp": + # UDP protocol (default, backward compatible) + return logging.handlers.SysLogHandler( + address=(server_name, server_port), + socktype=socket.SOCK_DGRAM, + ) + elif protocol in ["tcp", "tls"]: + # TCP or TLS protocol with retry logic + for attempt in range(1, retry_attempts + 1): + try: + if protocol == "tcp": + # TCP without TLS + handler = logging.handlers.SysLogHandler( + address=(server_name, server_port), + socktype=socket.SOCK_STREAM, + ) + # Set timeout on the socket + if hasattr(handler, "socket") and handler.socket: + handler.socket.settimeout(timeout) + return handler + else: + # TLS protocol + # Create SSL context + ssl_context = ssl.create_default_context() + + # Configure server certificate verification + if cafile_path: + ssl_context.load_verify_locations(cafile=cafile_path) + + # Configure client certificate authentication + if certfile_path and keyfile_path: + ssl_context.load_cert_chain( + certfile=certfile_path, + keyfile=keyfile_path, + ) + + # Create TCP handler first + handler = logging.handlers.SysLogHandler( + address=(server_name, server_port), + socktype=socket.SOCK_STREAM, + ) + + # Wrap socket with TLS + if hasattr(handler, "socket") and handler.socket: + handler.socket = ssl_context.wrap_socket( + handler.socket, + server_hostname=server_name, + ) + handler.socket.settimeout(timeout) + + return handler + + except Exception as e: + if attempt < retry_attempts: + self.logger.warning( + f"Syslog connection attempt {attempt}/{retry_attempts} failed: {e}. " + f"Retrying in {retry_delay} seconds..." + ) + time.sleep(retry_delay) + else: + self.logger.error( + f"Syslog connection failed after {retry_attempts} attempts: {e}" + ) + raise + else: + raise ValueError( + f"Invalid protocol '{protocol}'. Must be 'udp', 'tcp', or 'tls'." + ) + def save_aggregate_report_to_syslog(self, aggregate_reports: list[dict[str, Any]]): rows = parsed_aggregate_reports_to_csv_rows(aggregate_reports) for row in rows: