From 9b153c877ccdb586c4d2167b679aac86a3098bf4 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Thu, 23 Nov 2023 14:44:52 +0100 Subject: [PATCH] SFTP publisher --- .gitignore | 6 +- src/publishers/managers/publishers_manager.py | 50 +++++- src/publishers/publishers/__init__.py | 16 +- src/publishers/publishers/sftp_publisher.py | 154 ++++++++++++++++++ 4 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/publishers/publishers/sftp_publisher.py diff --git a/.gitignore b/.gitignore index 3f374b125..3ae7e713f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,11 +22,15 @@ build/ .env.local .env.*.local src/.env -*.pem +src/publishers/crypto +!src/publishers/crypto/READEME.md *.key *.log *.crt *.asc +*.pub +*.pem +*.p12 local/ # settings of editors diff --git a/src/publishers/managers/publishers_manager.py b/src/publishers/managers/publishers_manager.py index df6a1fd38..133f6c8b2 100644 --- a/src/publishers/managers/publishers_manager.py +++ b/src/publishers/managers/publishers_manager.py @@ -1,4 +1,30 @@ +""" +This module provides functionality for managing publishers in Taranis-NG. + +The publishers manager is responsible for registering publishers, retrieving information about registered publishers, +and publishing data using the appropriate publisher based on the input. + +The module defines the following functions: +- initialize(): Initializes the publishers by registering them. +- register_publisher(publisher): Registers a publisher. +- get_registered_publishers_info(): Retrieves information about the registered publishers. +- publish(publisher_input_json): Publishes the given input using the appropriate publisher. + +The module also imports the following classes: +- FTPPublisher: Represents an FTP publisher. +- SFTPPublisher: Represents an SFTP publisher. +- EMAILPublisher: Represents an email publisher. +- TWITTERPublisher: Represents a Twitter publisher. +- WORDPRESSPublisher: Represents a WordPress publisher. +- MISPPublisher: Represents a MISP publisher. + +The module imports the following modules: +- PublisherInputSchema: Provides a schema for validating the input JSON for publishers. +- log_manager: Provides functionality for logging critical messages. +""" + from publishers.ftp_publisher import FTPPublisher +from publishers.sftp_publisher import SFTPPublisher from publishers.email_publisher import EMAILPublisher from publishers.twitter_publisher import TWITTERPublisher from publishers.wordpress_publisher import WORDPRESSPublisher @@ -10,7 +36,9 @@ def initialize(): + """Initialize the publishers by registering them.""" register_publisher(FTPPublisher()) + register_publisher(SFTPPublisher()) register_publisher(EMAILPublisher()) register_publisher(TWITTERPublisher()) register_publisher(WORDPRESSPublisher()) @@ -18,20 +46,40 @@ def initialize(): def register_publisher(publisher): + """ + Register a publisher. + + Args: + publisher: The publisher object to register. + """ publishers[publisher.type] = publisher def get_registered_publishers_info(): + """ + Retrieve information about the registered publishers. + + Returns: + A list of dictionaries containing information about each registered publisher. + """ publishers_info = [] for key in publishers: publishers_info.append(publishers[key].get_info()) log_manager.log_critical(publishers_info) - return publishers_info def publish(publisher_input_json): + """ + Publish the given input using the appropriate publisher. + + Args: + publisher_input_json: The JSON input for the publisher. + + Raises: + ValidationError: If the input JSON is invalid. + """ publisher_input_schema = PublisherInputSchema() publisher_input = publisher_input_schema.load(publisher_input_json) publishers[publisher_input.type].publish(publisher_input) diff --git a/src/publishers/publishers/__init__.py b/src/publishers/publishers/__init__.py index ea4b86df6..79bd71b72 100644 --- a/src/publishers/publishers/__init__.py +++ b/src/publishers/publishers/__init__.py @@ -1 +1,15 @@ -__all__ = ['base_publisher', 'ftp_publisher', 'email_publisher', 'twitter_publisher', 'wordpress_publisher'] +""" +This module contains the publishers package. + +The publishers package provides various classes for publishing data to different platforms, including FTP, SFTP, email, +Twitter, and WordPress. + +Classes: +- base_publisher: The base class for all publishers. +- ftp_publisher: A publisher for FTP. +- sftp_publisher: A publisher for SFTP. +- email_publisher: A publisher for email. +- twitter_publisher: A publisher for Twitter. +- wordpress_publisher: A publisher for WordPress. +""" +__all__ = ["base_publisher", "ftp_publisher", "sftp_publisher", "email_publisher", "twitter_publisher", "wordpress_publisher"] diff --git a/src/publishers/publishers/sftp_publisher.py b/src/publishers/publishers/sftp_publisher.py new file mode 100644 index 000000000..b789e85a4 --- /dev/null +++ b/src/publishers/publishers/sftp_publisher.py @@ -0,0 +1,154 @@ +"""Publisher for SFTP server.""" +from datetime import datetime +from base64 import b64decode +import paramiko +import mimetypes +from io import BytesIO +import os + +from managers import log_manager +from .base_publisher import BasePublisher +from shared.schema.parameter import Parameter, ParameterType + + +class SFTPPublisher(BasePublisher): + """SFTP Publisher class. + + This class represents a publisher that publishes data to an SFTP server. + + Attributes: + type (str): The type of the publisher. + name (str): The name of the publisher. + description (str): The description of the publisher. + parameters (list): The list of parameters required for the publisher. + + Methods: + publish(publisher_input): Publishes data to the SFTP server. + + """ + + type = "SFTP_PUBLISHER" + name = "SFTP Publisher" + description = "Publisher for publishing to SFTP server" + + parameters = [ + Parameter(0, "SFTP_URL", "SFTP URL", "SFTP server URL", ParameterType.STRING), + Parameter(0, "PORT", "SSH port", "Port remote machine is using for SSH (default 22)", ParameterType.STRING), + Parameter(0, "SSH_KEY", "SSH key", "Private key which should be used for SSH connection", ParameterType.STRING), + Parameter(0, "SSH_KEY_PASSWORD", "SSH key password", "Password for the SSH private key", ParameterType.STRING), + Parameter(0, "USERNAME", "Username", "Username for SFTP", ParameterType.STRING), + Parameter(0, "PASSWORD", "Password", "Password for SFTP", ParameterType.STRING), + Parameter( + 0, + "PATH", + "Remote path", + "Either absolute or relative path where the file should be saved on the remote machine", + ParameterType.STRING, + ), + Parameter( + 0, + "FILENAME", + "Filename", + "Custom mame of the transported file without extension (default file_%d-%m-%Y_%H:%M)", + ParameterType.STRING, + ), + Parameter(0, "COMMAND", "Command", "Command to be executed on the remote machine", ParameterType.STRING), + ] + parameters.extend(BasePublisher.parameters) + + def publish(self, publisher_input): + """Publish to SFTP server. + + This method publishes the data to the SFTP server based on the provided input. + + Args: + publisher_input (PublisherInput): The input data for the publisher. + + Raises: + Exception: If there is an error during the publishing process. + + """ + url = publisher_input.parameter_values_map["SFTP_URL"] + port = publisher_input.parameter_values_map["PORT"] + username = publisher_input.parameter_values_map["USERNAME"] + password = publisher_input.parameter_values_map["PASSWORD"] + path = publisher_input.parameter_values_map["PATH"] + filename = publisher_input.parameter_values_map["FILENAME"] + command = publisher_input.parameter_values_map["COMMAND"] + ssh_key = publisher_input.parameter_values_map["SSH_KEY"] + ssh_key_password = publisher_input.parameter_values_map["SSH_KEY_PASSWORD"] + + now = datetime.now().strftime("%Y%m%d%H%M%S") + + def _get_key(key_path, ssh_key_password=None): + try: + ssh_key = paramiko.RSAKey(filename=key_path, password=ssh_key_password) + return ssh_key + except paramiko.ssh_exception.SSHException: + pass + try: + ssh_key = paramiko.Ed25519Key(filename=key_path, password=ssh_key_password) + return ssh_key + except paramiko.ssh_exception.SSHException: + pass + try: + ssh_key = paramiko.ECDSAKey(filename=key_path, password=ssh_key_password) + return ssh_key + except paramiko.ssh_exception.SSHException: + pass + try: + ssh_key = paramiko.DSSKey(filename=key_path, password=ssh_key_password) + return ssh_key + except paramiko.ssh_exception.SSHException as error: + log_manager.log_critical(f"Issue with SSH key {key_path}") + log_manager.log_debug(f"Error: {error}") + return None + + try: + # decide filename and extension + mime_type = publisher_input.mime_type[:] + file_extension = mimetypes.guess_extension(mime_type) + if filename: + filename = f"{filename}{file_extension}" + else: + filename = f"file_{now}{file_extension}" + full_path = os.path.join(path, filename) + + # fill file with data + data = publisher_input.data[:] + bytes_data = b64decode(data, validate=True) + file_object = BytesIO(bytes_data) + + # decide SFTP port + port = port if port else 22 + + # determine SSH key type + if ssh_key: + ssh_key = _get_key(ssh_key, ssh_key_password) + else: + ssh_key = None + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + if ssh_key: + ssh.connect(hostname=url, port=port, username=username, pkey=ssh_key) + else: + ssh.connect(hostname=url, port=port, username=username, password=password) + sftp = ssh.open_sftp() + log_manager.log_info(f"Successfully connected to {url} on port {port}") + try: + sftp.putfo(file_object, f"{full_path}", confirm=True) + log_manager.log_info(f"Successfully saved data to {full_path} on remote machine") + except Exception as error: + log_manager.log_critical(f"Failed to save data to {full_path} on remote machine") + log_manager.log_debug(f"Error: {error}") + if command: + try: + ssh.exec_command(command) + log_manager.log_info(f"Executed command {command} on remote machine") + except Exception as error: + log_manager.log_critical(f"Failed to execute command {command} on remote machine") + log_manager.log_debug(f"Error: {error}") + sftp.close() + except Exception as error: + BasePublisher.print_exception(self, error)