diff --git a/.gitignore b/.gitignore index 68e1d41f4..c4afe0ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ src/.env *.key *.log *.crt +*.asc local/ # settings of editors diff --git a/docker/Dockerfile.publishers b/docker/Dockerfile.publishers index 50cd13b0a..6b0367b56 100644 --- a/docker/Dockerfile.publishers +++ b/docker/Dockerfile.publishers @@ -1,4 +1,4 @@ -FROM python:3.7-alpine3.14 AS build_shared +FROM python:3.9-alpine3.17 AS build_shared WORKDIR /build_shared/ @@ -8,7 +8,7 @@ RUN python -m build -FROM python:3.7-alpine3.14 AS production +FROM python:3.9-alpine3.17 AS production WORKDIR /app/ @@ -24,6 +24,10 @@ RUN pip install --no-cache-dir ./custom_packages/taranis_ng_shared-*.whl && rm - # install dependencies COPY ./src/publishers/requirements.txt /app/requirements.txt +RUN apk add --no-cache \ + swig\ + gnupg + RUN \ apk add --no-cache --virtual .build-deps build-base \ gcc \ @@ -32,6 +36,7 @@ RUN \ musl-dev \ python3-dev \ libffi-dev \ + openssl-dev \ rust && \ pip install --no-cache-dir -r /app/requirements.txt && \ apk --purge del .build-deps diff --git a/src/core/managers/publishers_manager.py b/src/core/managers/publishers_manager.py index 944ad78b0..374b9c188 100644 --- a/src/core/managers/publishers_manager.py +++ b/src/core/managers/publishers_manager.py @@ -1,3 +1,8 @@ +"""Manager for publishers. + +Returns: + _type_: _description_ +""" from model.publishers_node import PublishersNode from model.publisher import Publisher from model.publisher_preset import PublisherPreset @@ -7,6 +12,14 @@ def add_publishers_node(data): + """_summary_. + + Args: + data (_type_): _description_ + + Returns: + _type_: _description_ + """ node = PublishersNodeSchema.create(data) publishers_info, status_code = PublishersApi(node.api_url, node.api_key).get_publishers_info() if status_code == 200: @@ -17,6 +30,15 @@ def add_publishers_node(data): def update_publishers_node(node_id, data): + """_summary_. + + Args: + node_id (_type_): _description_ + data (_type_): _description_ + + Returns: + _type_: _description_ + """ node = PublishersNodeSchema.create(data) publishers_info, status_code = PublishersApi(node.api_url, node.api_key).get_publishers_info() if status_code == 200: @@ -27,20 +49,38 @@ def update_publishers_node(node_id, data): def add_publisher_preset(data): + """_summary_. + + Args: + data (_type_): _description_ + """ PublisherPreset.add_new(data) def publish(preset, data, message_title, message_body, recipients): + """_summary_. + + Args: + preset (_type_): _description_ + data (_type_): _description_ + message_title (_type_): _description_ + message_body (_type_): _description_ + recipients (_type_): _description_ + + Returns: + _type_: _description_ + """ publisher = preset.publisher node = publisher.node data_data = None data_mime = None if data is not None: - data_data = data['data'] - data_mime = data['mime_type'] + data_data = data["data"] + data_mime = data["mime_type"] + message_title = data["message_title"] + message_body = data["message_body"] - input_data = PublisherInput(publisher.type, preset.parameter_values, data_mime, data_data, message_title, - message_body, recipients) + input_data = PublisherInput(publisher.type, preset.parameter_values, data_mime, data_data, message_title, message_body, recipients) input_schema = PublisherInputSchema() return PublishersApi(node.api_url, node.api_key).publish(input_schema.dump(input_data)) diff --git a/src/publishers/crypto/README.md b/src/publishers/crypto/README.md new file mode 100644 index 000000000..41eebfb22 --- /dev/null +++ b/src/publishers/crypto/README.md @@ -0,0 +1,5 @@ +# A directory for certificates and private keys + +If you wish to sign or encrypt messages of Taranis NG, place the certificates here. Use either PEM file with both certificate and key for S/MIME or armored PGP for signing. +The signing file might be encrypted, you can specify the password in Taranis NG configuration. +For encryption include S/MIME certificate or armored PGP public key. diff --git a/src/publishers/managers/publishers_manager.py b/src/publishers/managers/publishers_manager.py index 3acfb9665..df6a1fd38 100644 --- a/src/publishers/managers/publishers_manager.py +++ b/src/publishers/managers/publishers_manager.py @@ -4,6 +4,7 @@ from publishers.wordpress_publisher import WORDPRESSPublisher from publishers.misp_publisher import MISPPublisher from shared.schema.publisher import PublisherInputSchema +from managers import log_manager publishers = {} @@ -24,6 +25,8 @@ def get_registered_publishers_info(): publishers_info = [] for key in publishers: publishers_info.append(publishers[key].get_info()) + log_manager.log_critical(publishers_info) + return publishers_info diff --git a/src/publishers/publishers/email_publisher.py b/src/publishers/publishers/email_publisher.py index bf8c70c0f..9b0e03f18 100644 --- a/src/publishers/publishers/email_publisher.py +++ b/src/publishers/publishers/email_publisher.py @@ -1,154 +1,120 @@ -import datetime -import smtplib -from email.message import Message -from email.mime.base import MIMEBase -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -import gnupg - +"""Publisher for publishing by email.""" +from datetime import datetime +from base64 import b64decode +import os +from managers import log_manager from .base_publisher import BasePublisher from shared.schema.parameter import Parameter, ParameterType +from envelope import Envelope +import mimetypes class EMAILPublisher(BasePublisher): + """_summary_. + + Arguments: + BasePublisher -- _description_ + + Returns: + _description_ + """ + type = "EMAIL_PUBLISHER" name = "EMAIL Publisher" description = "Publisher for publishing by email" parameters = [ Parameter(0, "SMTP_SERVER", "SMTP server", "SMTP server for sending emails", ParameterType.STRING), - Parameter(0, "SMTP_SERVER_PORT", "SMTP server port", "SMTP server port for sending emails", - ParameterType.STRING), + Parameter(0, "SMTP_SERVER_PORT", "SMTP server port", "SMTP server port for sending emails", ParameterType.STRING), Parameter(0, "EMAIL_USERNAME", "Email username", "Username for email account", ParameterType.STRING), Parameter(0, "EMAIL_PASSWORD", "Email password", "Password for email account", ParameterType.STRING), - Parameter(0, "EMAIL_RECIPIENT", "Email recipient", "Email address of recipient", ParameterType.STRING), + Parameter(0, "EMAIL_SENDER", "Email sender", "Email address of the sender", ParameterType.STRING), + Parameter(0, "EMAIL_RECIPIENT", "Email recipient", "Email address of the recipient", ParameterType.STRING), Parameter(0, "EMAIL_SUBJECT", "Email subject", "Text of email subject", ParameterType.STRING), Parameter(0, "EMAIL_MESSAGE", "Email message", "Text of email message", ParameterType.STRING), - Parameter(0, "EMAIL_ENCRYPTION", "Do you want use email encrypt (yes/no)", "Turn ON/OFF email encryption", - ParameterType.STRING) + Parameter(0, "EMAIL_SIGN", "Email signature", "File used for signing or auto", ParameterType.STRING), + Parameter(0, "EMAIL_SIGN_PASSWORD", "Email signature password", "Password for signing file", ParameterType.STRING), + Parameter(0, "EMAIL_ENCRYPT", "Email encryption", "File used for encryption or auto", ParameterType.STRING), ] parameters.extend(BasePublisher.parameters) def publish(self, publisher_input): - - smtp_server = publisher_input.parameter_values_map['SMTP_SERVER'] - smtp_server_port = publisher_input.parameter_values_map['SMTP_SERVER_PORT'] - email_user = publisher_input.parameter_values_map['EMAIL_USERNAME'] - email_password = publisher_input.parameter_values_map['EMAIL_PASSWORD'] - email_recipients = publisher_input.parameter_values_map['EMAIL_RECIPIENT'] - email_subject = publisher_input.parameter_values_map['EMAIL_SUBJECT'] - email_message = publisher_input.parameter_values_map['EMAIL_MESSAGE'] - email_encryption = publisher_input.parameter_values_map['EMAIL_ENCRYPTION'] - - file = 'file_' + datetime.datetime.now().strftime("%d-%m-%Y_%H:%M") + '.pdf' - - if publisher_input.data is not None: - data = publisher_input.data[:] + """_summary_. + + Arguments: + publisher_input -- _description_ + + Returns: + _description_ + """ + smtp_server = publisher_input.parameter_values_map["SMTP_SERVER"] + smtp_server_port = publisher_input.parameter_values_map["SMTP_SERVER_PORT"] + user = publisher_input.parameter_values_map["EMAIL_USERNAME"] + password = publisher_input.parameter_values_map["EMAIL_PASSWORD"] + sender = publisher_input.parameter_values_map["EMAIL_SENDER"] + recipients = publisher_input.parameter_values_map["EMAIL_RECIPIENT"] + subject = publisher_input.parameter_values_map["EMAIL_SUBJECT"] + message = publisher_input.parameter_values_map["EMAIL_MESSAGE"] + sign = publisher_input.parameter_values_map["EMAIL_SIGN"] + sign_password = publisher_input.parameter_values_map["EMAIL_SIGN_PASSWORD"] + encrypt = publisher_input.parameter_values_map["EMAIL_ENCRYPT"] + + now = datetime.now().strftime("%Y%m%d%H%M%S") + + smtp = {"host": smtp_server, "port": smtp_server_port, "user": user, "password": password} + + envelope = Envelope() + + # if attachment data available from presenter + if publisher_input.mime_type and publisher_input.data: + attachment_mimetype = publisher_input.mime_type + attachment_extension = mimetypes.guess_extension(attachment_mimetype) + attachment_data = publisher_input.data[:] + attachment_list = [ + ( + b64decode(attachment_data), + attachment_mimetype, + f"file_{now}{attachment_extension}", + False, + ) + ] + # it is possible to attach multiple files + envelope.attach(attachment_list) + + # when title available from presenter + if publisher_input.message_title: + subject = b64decode(publisher_input.message_title).decode("UTF-8") + # when body available from presenter + if publisher_input.message_body: + message = b64decode(publisher_input.message_body).decode("UTF-8") + + if not message: + envelope.message(" ") else: - data = None - - def get_attachment(file_name): - msg_attachment = Message() - msg_attachment.add_header(_name="Content-Type", _value='application/pdf', name=file_name) - msg_attachment.add_header(_name="Content-Transfer-Encoding", _value="base64") - msg_attachment.add_header(_name="Content-Disposition", _value="attachment", filename=file_name) - msg_attachment.set_payload(data) - return msg_attachment - - def get_body(message): - msg_body = Message() - msg_body.add_header(_name="Content-Type", _value="text/plain", charset="utf-8") - msg_body.add_header(_name="Content-Transfer-Encoding", _value="quoted-printable") - msg_body.set_payload(message + 2 * "\n") - return msg_body - - def get_encrypted_email_string(email_address_recipient, file_name, message): - def get_gpg_cipher_text(string, recipient_email_address): - gpg = gnupg.GPG() - encrypted_str = str(gpg.encrypt(string, recipient_email_address)) - return encrypted_str - - msg = Message() - msg.add_header(_name="Content-Type", _value="multipart/mixed") - msg["From"] = email_user - msg["To"] = email_address_recipient - msg['Subject'] = email_subject - - msg_text = Message() - msg_text.add_header(_name="Content-Type", _value="multipart/mixed") - msg_text.add_header(_name="Content-Language", _value="en-US") - - msg_body = get_body(message) - msg_attachment = get_attachment(file_name) - - msg_text.attach(msg_body) - msg_text.attach(msg_attachment) - msg.attach(msg_text) - - pgp_msg = MIMEBase(_maintype="multipart", _subtype="encrypted", protocol="application/pgp-encrypted") - pgp_msg["From"] = email_user - pgp_msg["To"] = email_address_recipient - pgp_msg['Subject'] = email_subject - - pgp_msg_part1 = Message() - pgp_msg_part1.add_header(_name="Content-Type", _value="application/pgp-encrypted") - pgp_msg_part1.add_header(_name="Content-Description", _value="PGP/MIME version identification") - pgp_msg_part1.set_payload("Version: 2" + "\n") - - pgp_msg_part2 = Message() - pgp_msg_part2.add_header(_name="Content-Type", _value="application/octet-stream", name="encrypted.asc") - pgp_msg_part2.add_header(_name="Content-Description", _value="OpenPGP encrypted message") - pgp_msg_part2.add_header(_name="Content-Disposition", _value="inline", filename="encrypted.asc") - pgp_msg_part2.set_payload(get_gpg_cipher_text(msg.as_string(), email_address_recipient)) - - pgp_msg.attach(pgp_msg_part1) - pgp_msg.attach(pgp_msg_part2) - - return pgp_msg.as_string() + envelope.message(message) + if not subject: + envelope.subject(" ") + else: + envelope.subject(subject) + envelope.from_(sender) + envelope.to(recipients) + envelope.smtp(smtp) + + if sign == "auto": + envelope.signature(key=sign) + elif os.path.isfile(sign): + log_manager.log_info(f"Signing email with file {sign}") + envelope.signature(key=open(sign), passphrase=sign_password) + + if encrypt == "auto": + envelope.encryption(key=encrypt) + elif os.path.isfile(encrypt): + log_manager.log_info(f"Encrypting email with file {encrypt}") + envelope.encryption(key=open(encrypt)) try: - - server = smtplib.SMTP(smtp_server, smtp_server_port) - server.starttls() - server.login(email_user, email_password) - - if publisher_input.recipients is not None: - recipients = publisher_input.recipients - else: - recipients = email_recipients.split(',') - - if email_encryption.lower() == 'yes': - for recipient in recipients: - email_msg = email_message - email_msg = get_encrypted_email_string(recipient, file, email_msg) - server.sendmail(email_user, recipient, email_msg) - else: - email_msg = MIMEMultipart() - email_msg['From'] = email_user - email_msg['To'] = email_recipients - - if publisher_input.message_title is not None: - email_msg['Subject'] = publisher_input.message_title - else: - email_msg['Subject'] = email_subject - - if publisher_input.message_body is not None: - body = publisher_input.message_body - else: - body = email_message - - email_msg.attach(MIMEText(body + 2 * "\n", 'plain')) - - if data is not None: - attachment = get_attachment(file) - email_msg.attach(attachment) - - text = email_msg.as_string() - - server.sendmail(email_user, recipients, text) - - server.quit() + envelope.send() except Exception as error: BasePublisher.print_exception(self, error) diff --git a/src/publishers/requirements.txt b/src/publishers/requirements.txt index 9b81bb629..74fbb9ec9 100644 --- a/src/publishers/requirements.txt +++ b/src/publishers/requirements.txt @@ -1,21 +1,22 @@ -certifi==2019.11.28 +certifi==2023.5.7 +envelope==2.0.2 Flask==1.1.4 Flask-Cors==3.0.10 Flask-RESTful==0.3.7 gevent==21.8.0 greenlet==1.1.1 gunicorn==20.0.4 -httplib2==0.18.1 +httplib2==0.22.0 idna==2.9 marshmallow==3.18.0 marshmallow-enum==1.5.1 oauth2client==4.1.3 Jinja2==2.11.3 +M2Crypto==0.38.0 MarkupSafe==1.1.0 -paramiko==2.10.1 +paramiko==3.2.0 python-dateutil==2.8.1 python-dotenv==0.10.3 -python-gnupg==0.4.6 pytz==2019.3 requests==2.26.0 schedule==0.6.0 @@ -25,4 +26,4 @@ urllib3==1.26.7 Werkzeug==0.16.0 zope.event==4.4 zope.interface==5.1.0 -pymisp==2.4.128 +pymisp==2.4.128 \ No newline at end of file