mirror of
https://github.com/domainaware/parsedmarc.git
synced 2026-09-17 19:37:59 +00:00
202 lines
7.7 KiB
Python
202 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import logging.handlers
|
|
import socket
|
|
import ssl
|
|
import time
|
|
|
|
from parsedmarc import (
|
|
parsed_aggregate_reports_to_csv_rows,
|
|
parsed_failure_reports_to_csv_rows,
|
|
parsed_smtp_tls_reports_to_csv_rows,
|
|
)
|
|
from parsedmarc.types import AggregateReport, FailureReport, SMTPTLSReport
|
|
|
|
|
|
class SyslogClient(object):
|
|
"""A client for Syslog"""
|
|
|
|
def __init__(
|
|
self,
|
|
server_name: str,
|
|
server_port: int,
|
|
protocol: str = "udp",
|
|
cafile_path: str | None = None,
|
|
certfile_path: str | None = None,
|
|
keyfile_path: str | None = 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 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)
|
|
|
|
# Create the appropriate syslog handler based on protocol
|
|
self.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(self.log_handler)
|
|
|
|
def _create_syslog_handler(
|
|
self,
|
|
server_name: str,
|
|
server_port: int,
|
|
protocol: str,
|
|
cafile_path: str | None,
|
|
certfile_path: str | None,
|
|
keyfile_path: str | None,
|
|
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
|
|
sock = getattr(handler, "socket", None)
|
|
if sock is not None:
|
|
sock.settimeout(timeout)
|
|
return handler
|
|
else:
|
|
# TLS protocol
|
|
# Create SSL context with secure defaults
|
|
ssl_context = ssl.create_default_context()
|
|
|
|
# Explicitly set minimum TLS version to 1.2 for security
|
|
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
|
|
# 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,
|
|
)
|
|
elif certfile_path or keyfile_path:
|
|
# Warn if only one of the two required parameters is provided
|
|
self.logger.warning(
|
|
"Both certfile_path and keyfile_path are required for "
|
|
"client certificate authentication. Client authentication "
|
|
"will not be used."
|
|
)
|
|
|
|
# Create TCP handler first
|
|
handler = logging.handlers.SysLogHandler(
|
|
address=(server_name, server_port),
|
|
socktype=socket.SOCK_STREAM,
|
|
)
|
|
|
|
# Wrap socket with TLS
|
|
sock = getattr(handler, "socket", None)
|
|
if sock is not None:
|
|
tls_sock = ssl_context.wrap_socket(
|
|
sock,
|
|
server_hostname=server_name,
|
|
)
|
|
tls_sock.settimeout(timeout)
|
|
setattr(handler, "socket", tls_sock)
|
|
|
|
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
|
|
# Only reachable when retry_attempts < 1, which would otherwise
|
|
# silently return None and break the caller's addHandler() call.
|
|
raise ValueError("retry_attempts must be at least 1")
|
|
else:
|
|
raise ValueError(
|
|
f"Invalid protocol '{protocol}'. Must be 'udp', 'tcp', or 'tls'."
|
|
)
|
|
|
|
def save_aggregate_report_to_syslog(
|
|
self, aggregate_reports: AggregateReport | list[AggregateReport]
|
|
):
|
|
rows = parsed_aggregate_reports_to_csv_rows(aggregate_reports)
|
|
for row in rows:
|
|
self.logger.info(json.dumps(row))
|
|
|
|
def save_failure_report_to_syslog(
|
|
self, failure_reports: FailureReport | list[FailureReport]
|
|
):
|
|
rows = parsed_failure_reports_to_csv_rows(failure_reports)
|
|
for row in rows:
|
|
self.logger.info(json.dumps(row))
|
|
|
|
def save_smtp_tls_report_to_syslog(
|
|
self, smtp_tls_reports: SMTPTLSReport | list[SMTPTLSReport]
|
|
):
|
|
rows = parsed_smtp_tls_reports_to_csv_rows(smtp_tls_reports)
|
|
for row in rows:
|
|
self.logger.info(json.dumps(row))
|
|
|
|
def close(self):
|
|
"""Remove and close the syslog handler, releasing its socket."""
|
|
self.logger.removeHandler(self.log_handler)
|
|
self.log_handler.close()
|
|
|
|
# Backward-compatible alias
|
|
save_forensic_report_to_syslog = save_failure_report_to_syslog
|