blob: 776e786483ce4786671787594052bb99337d8f78 [file] [log] [blame]
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4# Copyright (c) 2020, Dennis Kalbhen <d.kalbhen@heinlein-support.de>
5# Copyright (c) 2020, Carsten Rosenberg <c.rosenberg@heinlein-support.de>
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
19###
20#
21# olefy is a little helper socket to use oletools with rspamd. (https://rspamd.com)
22# Please find updates and issues here: https://github.com/HeinleinSupport/olefy
23#
24###
25
26from subprocess import Popen, PIPE
27import sys
28import os
29import logging
30import asyncio
31import time
32import magic
33import re
34
35# merge variables from /etc/olefy.conf and the defaults
36olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1')
37olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050'))
38olefy_tmp_dir = os.getenv('OLEFY_TMPDIR', '/tmp')
39olefy_python_path = os.getenv('OLEFY_PYTHON_PATH', '/usr/bin/python3')
40olefy_olevba_path = os.getenv('OLEFY_OLEVBA_PATH', '/usr/local/bin/olevba3')
41# 10:DEBUG, 20:INFO, 30:WARNING, 40:ERROR, 50:CRITICAL
42olefy_loglvl = int(os.getenv('OLEFY_LOGLVL', 20))
43olefy_min_length = int(os.getenv('OLEFY_MINLENGTH', 500))
44olefy_del_tmp = int(os.getenv('OLEFY_DEL_TMP', 1))
45olefy_del_tmp_failed = int(os.getenv('OLEFY_DEL_TMP_FAILED', 1))
46
47# internal used variables
48request_time = '0000000000.000000'
49olefy_protocol = 'OLEFY'
50olefy_ping = 'PING'
51olefy_protocol_sep = '\n\n'
52olefy_headers = {}
53
54# init logging
55logger = logging.getLogger('olefy')
56logging.basicConfig(stream=sys.stdout, level=olefy_loglvl, format='olefy %(levelname)s %(funcName)s %(message)s')
57
58logger.debug('olefy listen address string: {} (type {})'.format(olefy_listen_addr_string, type(olefy_listen_addr_string)))
59
60if not olefy_listen_addr_string:
61 olefy_listen_addr = ""
62else:
63 addr_re = re.compile('[\[" \]]')
64 olefy_listen_addr = addr_re.sub('', olefy_listen_addr_string.replace("'", "")).split(',')
65
66# log runtime variables
67logger.info('olefy listen address: {} (type: {})'.format(olefy_listen_addr, type(olefy_listen_addr)))
68logger.info('olefy listen port: {}'.format(olefy_listen_port))
69logger.info('olefy tmp dir: {}'.format(olefy_tmp_dir))
70logger.info('olefy python path: {}'.format(olefy_python_path))
71logger.info('olefy olvba path: {}'.format(olefy_olevba_path))
72logger.info('olefy log level: {}'.format(olefy_loglvl))
73logger.info('olefy min file length: {}'.format(olefy_min_length))
74logger.info('olefy delete tmp file: {}'.format(olefy_del_tmp))
75logger.info('olefy delete tmp file when failed: {}'.format(olefy_del_tmp_failed))
76
77if not os.path.isfile(olefy_python_path):
78 logger.critical('python path not found: {}'.format(olefy_python_path))
79 exit(1)
80if not os.path.isfile(olefy_olevba_path):
81 logger.critical('olevba path not found: {}'.format(olefy_olevba_path))
82 exit(1)
83
84# olefy protocol function
85def protocol_split( olefy_line ):
86 header_lines = olefy_line.split('\n')
87 for line in header_lines:
88 if line == 'OLEFY/1.0':
89 olefy_headers['olefy'] = line
90 elif line != '':
91 kv = line.split(': ')
92 if kv[0] != '' and kv[1] != '':
93 olefy_headers[kv[0]] = kv[1]
94 logger.debug('olefy_headers: {}'.format(olefy_headers))
95
96# calling oletools
97def oletools( stream, tmp_file_name, lid ):
98 if olefy_min_length > stream.__len__():
99 logger.error('{} {} bytes (Not Scanning! File smaller than {!r})'.format(lid, stream.__len__(), olefy_min_length))
100 out = b'[ { "error": "File too small" } ]'
101 else:
102 tmp_file = open(tmp_file_name, 'wb')
103 tmp_file.write(stream)
104 tmp_file.close()
105
106 file_magic = magic.Magic(mime=True, uncompress=True)
107 file_mime = file_magic.from_file(tmp_file_name)
108 logger.info('{} {} (libmagic output)'.format(lid, file_mime))
109
110 # do the olefy
111 cmd_tmp = Popen([olefy_python_path, olefy_olevba_path, '-a', '-j' , '-l', 'error', tmp_file_name], stdout=PIPE, stderr=PIPE)
112 out, err = cmd_tmp.communicate()
113 out = bytes(out.decode('utf-8', 'ignore').replace(' ', ' ').replace('\t', '').replace('\n', '').replace('XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)', ''), encoding="utf-8")
114 failed = False
115 if out.__len__() < 30:
116 logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode,
117 out.decode('utf-8', 'ignore'), err.decode('utf-8', 'ignore')))
118 out = b'[ { "error": "Unhandled error - too short olevba response" } ]'
119 failed = True
120 elif err.__len__() > 10 and cmd_tmp.returncode == 9:
121 logger.error("{} olevba stderr >10 chars - rc: {!r}, response: {!r}".format(lid, cmd_tmp.returncode, err.decode("utf-8", "ignore")))
122 out = b'[ { "error": "Decrypt failed" } ]'
123 failed = True
124 elif err.__len__() > 10 and cmd_tmp.returncode > 9:
125 logger.error('{} olevba stderr >10 chars - rc: {!r}, response: {!r}'.format(lid, cmd_tmp.returncode, err.decode('utf-8', 'ignore')))
126 out = b'[ { "error": "Unhandled oletools error" } ]'
127 failed = True
128 elif cmd_tmp.returncode != 0:
129 logger.error('{} olevba exited with code {!r}; err: {!r}'.format(lid, cmd_tmp.returncode, err.decode('utf-8', 'ignore')))
130 failed = True
131
132 if failed and olefy_del_tmp_failed == 0:
133 logger.debug('{} {} FAILED: not deleting tmp file'.format(lid, tmp_file_name))
134 elif olefy_del_tmp == 1:
135 logger.debug('{} {} deleting tmp file'.format(lid, tmp_file_name))
136 os.remove(tmp_file_name)
137
138 logger.debug('{} response: {}'.format(lid, out.decode('utf-8', 'ignore')))
139 return out + b'\t\n\n\t'
140
141# Asyncio data handling, default AIO-Functions
142class AIO(asyncio.Protocol):
143 def __init__(self):
144 self.extra = bytearray()
145
146 def connection_made(self, transport):
147 global request_time
148 peer = transport.get_extra_info('peername')
149 logger.debug('{} new connection was made'.format(peer))
150 self.transport = transport
151 request_time = str(time.time())
152
153 def data_received(self, request, msgid=1):
154 peer = self.transport.get_extra_info('peername')
155 logger.debug('{} data received from new connection'.format(peer))
156 self.extra.extend(request)
157
158 def eof_received(self):
159 peer = self.transport.get_extra_info('peername')
160 olefy_protocol_err = False
161 proto_ck = self.extra[0:2000].decode('utf-8', 'ignore')
162
163 headers = proto_ck[0:proto_ck.find(olefy_protocol_sep)]
164
165 if olefy_protocol == headers[0:5]:
166 self.extra = bytearray(self.extra[len(headers)+2:len(self.extra)])
167 protocol_split(headers)
168 else:
169 olefy_protocol_err = True
170
171 if olefy_ping == headers[0:4]:
172 is_ping = True
173 else:
174 is_ping = False
175 rspamd_id = olefy_headers['Rspamd-ID'][:6] or ''
176 lid = 'Rspamd-ID' in olefy_headers and '<'+rspamd_id+'>'
177 tmp_file_name = olefy_tmp_dir+'/'+request_time+'.'+str(peer[1])+'.'+rspamd_id
178 logger.debug('{} {} choosen as tmp filename'.format(lid, tmp_file_name))
179
180 if not is_ping or olefy_loglvl == 10:
181 logger.info('{} {} bytes (stream size)'.format(lid, self.extra.__len__()))
182
183 if olefy_ping == headers[0:4]:
184 logger.debug('{} PING request'.format(peer))
185 out = b'PONG'
186 elif olefy_protocol_err == True or olefy_headers['olefy'] != 'OLEFY/1.0':
187 logger.error('{} Protocol ERROR: no OLEFY/1.0 found'.format(lid))
188 out = b'[ { "error": "Protocol error" } ]'
189 elif 'Method' in olefy_headers:
190 if olefy_headers['Method'] == 'oletools':
191 out = oletools(self.extra, tmp_file_name, lid)
192 else:
193 logger.error('Protocol ERROR: Method header not found')
194 out = b'[ { "error": "Protocol error: Method header not found" } ]'
195
196 self.transport.write(out)
197 if not is_ping or olefy_loglvl == 10:
198 logger.info('{} {} response send: {!r}'.format(lid, peer, out))
199 self.transport.close()
200
201
202# start the listeners
203loop = asyncio.get_event_loop()
204# each client connection will create a new protocol instance
205coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port)
206server = loop.run_until_complete(coro)
207for sockets in server.sockets:
208 logger.info('serving on {}'.format(sockets.getsockname()))
209
210# XXX serve requests until KeyboardInterrupt, not needed for production
211try:
212 loop.run_forever()
213except KeyboardInterrupt:
214 pass
215
216# graceful shutdown/reload
217server.close()
218loop.run_until_complete(server.wait_closed())
219loop.close()
220logger.info('stopped serving')