diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f838121 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Base on latest (edge) alpine image +FROM harbor.ervine.dev/library/x86_64/alpine:v3.12 +LABEL "Maintainer Jonathan Ervine " + +# Install updates +ENV LANG='en_US.UTF-8' \ + LANGUAGE='en_US.UTF-8' \ + +RUN apk update && \ + apk -U upgrade --ignore aline-baselayout && \ + apk -U add python3 gcc py3-pip python3-dev musl-dev libffi-dev git && \ + adduser -D python && \ + mkdir /data + +ADD requirements.txt /data/requirements.txt +ADD smtp2slack4qnap.py /data/smtp2slack4qnap.py + +RUN pip3 install -r /data/requirements.txt && \ + rm -rf /tmp/src && rm -rf /var/cache/apk/* + +#USER python + +CMD [ "/usr/bin/python3", "/data/smtp2slack4qnap.py" ] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..b11956f --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,37 @@ +podTemplate(yaml: """ +kind: Pod +spec: + containers: + - name: kaniko + image: gcr.io/kaniko-project/executor:debug-539ddefcae3fd6b411a95982a830d987f4214251 + imagePullPolicy: Always + command: + - /busybox/cat + tty: true + volumeMounts: + - name: jenkins-docker-cfg + mountPath: /kaniko/.docker + volumes: + - name: jenkins-docker-cfg + projected: + sources: + - secret: + name: regcred + items: + - key: .dockerconfigjson + path: config.json +""" + ) { + + node(POD_LABEL) { + stage('Build with Kaniko') { + git url: 'ssh://git@git.ervine.org/jonny/x86_64-alpine-qnap2slack.git', credentialsId: 'jenkins-to-git' + container('kaniko') { + sh '/kaniko/executor -f `pwd`/Dockerfile -c `pwd` --cache=true --destination=harbor.ervine.dev/public/x86_64/alpine/qnap2slack:v3.12.0 --destination=harbor.ervine.dev/public/x86_64/alpine/qnap2slack:v3.12' + } + } + stage('Notify gchat') { + hangoutsNotify message: "QNAP to Slack Notifier Application on Alpine Linux 3.12.0 has built",token: "A2ET831pVslqXTqAx6ycu573r",threadByJob: false + } + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..340582c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +asyncio +aiosmtpd +requests +html2text diff --git a/smtp2qnap4slack.py b/smtp2qnap4slack.py new file mode 100644 index 0000000..e2c85a9 --- /dev/null +++ b/smtp2qnap4slack.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# +# Compact SMTP to HTTP Gateway +# -> targeting Slack for QNAP-NAS notifications +# + +# generate self-signed cert (better than nothing): +# openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3650 -nodes -subj '/CN=localhost' + +import ssl +import asyncio +from aiosmtpd.controller import Controller +from aiosmtpd.smtp import SMTP as Server, syntax +from aiosmtpd.handlers import Debugging +from hashlib import sha256 +from base64 import b64encode, b64decode +import requests +import email +import json +import html2text +import re +import os + +### CONFIG DATA + +# for SMTP AUTH LOGIN (SECRET = sha256(password) avoiding storing plaintext) +USER = 'username' +SECRET = '1c18f3a76a7ad787ee1d5aea573bd51db1e559b85bbc4a3228076442e9a0bc90' + +# SMTP listener (set to localhost if running on QNAP device) +LHOST, LPORT = '192.168.0.50', 1025 + +# target slack authenticated webhook url (keep confidential!) +WEBHOOK_URL = 'https://hooks.slack.com/services/XXXXXXXXX/YYYYYYYYY/aaaaaaaaaaaaaaaaaaaaaaaa' + +### END OF CONFIG DATA + +# implemented LOGIN authentication (non-RFC compliant, works with QNAP-NAS) +# overkill for running locally, but mandatory for remote +class MyServer(Server): + authenticated = False + @syntax('AUTH LOGIN') + async def smtp_AUTH(self, arg): + if arg != 'LOGIN': + await self.push('501 Syntax: AUTH LOGIN') + return + await self.push('334 VXNlcm5hbWU=') # b64('Username') + username = await self._reader.readline() + username = b64decode(username.rstrip(b'\r\n')) + await self.push('334 UGFzc3dvcmQ=') # b64('Password') + password = await self._reader.readline() + password = b64decode(password.rstrip(b'\r\n')) + if username.decode() == USER and sha256(password).hexdigest() == SECRET: + self.authenticated = True + print("[+] Authenticated") + await self.push('235 2.7.0 Authentication successful') + else: + await self.push('535 Invalid credentials') + +# requires STARTTLS +# again, overkill for running locally, but mandatory for remote +class MyController(Controller): + def factory(self): + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain('cert.pem', 'key.pem') + return MyServer(self.handler, tls_context=context, require_starttls=True) + +def email2text(data): + body = email.message_from_bytes(data).get_payload() + h = html2text.HTML2Text() + h.ignore_tables = True + return re.sub(r'\n\s*\n', '\n\n', h.handle(body)) + +class CustomHandler: + async def handle_DATA(self, server, session, envelope): + if not server.authenticated: + return '500 Unauthenticated. Could not process your message' + mail_from = envelope.mail_from + data = envelope.content + text = email2text(data) + # tuned for slack, but can be anything else + requests.post(WEBHOOK_URL, data={'payload': json.dumps({'username': mail_from, 'text': text})}) + print("[+] Alert sent: {}".format(text.encode())) + return '250 OK' + +if __name__ == '__main__': + os.chdir(os.path.dirname(os.path.abspath(__file__))) + handler = CustomHandler() + controller = MyController(handler, hostname=LHOST, port=LPORT) + controller.start() + input('SMTP server is running. Press Return to stop server and exit.\n') + controller.stop()