Commit 34d3f3fd18ffe5740626ae9499aeb883b501ab62

Authored by decalage2
1 parent 51567c78

mraptor_milter: added logging to file with time rotation, archive each e-mail to…

… a file before filtering
Showing 1 changed file with 69 additions and 20 deletions
oletools/mraptor_milter.py 100644 → 100755
@@ -48,6 +48,10 @@ http://www.decalage.info/python/oletools @@ -48,6 +48,10 @@ http://www.decalage.info/python/oletools
48 48
49 # --- CHANGELOG -------------------------------------------------------------- 49 # --- CHANGELOG --------------------------------------------------------------
50 # 2016-08-08 v0.01 PL: - first version 50 # 2016-08-08 v0.01 PL: - first version
  51 +# 2016-08-12 v0.02 PL: - added logging to file with time rotation
  52 +# - archive each e-mail to a file before filtering
  53 +
  54 +__version__ = '0.02'
51 55
52 56
53 # --- IMPORTS ---------------------------------------------------------------- 57 # --- IMPORTS ----------------------------------------------------------------
@@ -59,6 +63,8 @@ import email @@ -59,6 +63,8 @@ import email
59 import sys 63 import sys
60 import os 64 import os
61 import logging 65 import logging
  66 +import logging.handlers
  67 +import datetime
62 68
63 from socket import AF_INET6 69 from socket import AF_INET6
64 70
@@ -76,6 +82,25 @@ TIMEOUT = 30 # Milter timeout in seconds @@ -76,6 +82,25 @@ TIMEOUT = 30 # Milter timeout in seconds
76 # CFG_DIR = "/etc/macromilter/" 82 # CFG_DIR = "/etc/macromilter/"
77 # LOG_DIR = "/var/log/macromilter/" 83 # LOG_DIR = "/var/log/macromilter/"
78 84
  85 +# TODO: different path on Windows:
  86 +LOGFILE_DIR = '/var/log/mraptor_milter'
  87 +# LOGFILE_DIR = '.'
  88 +LOGFILE_NAME = 'mraptor_milter.log'
  89 +LOGFILE_PATH = os.path.join(LOGFILE_DIR, LOGFILE_NAME)
  90 +
  91 +# Directory where to save a copy of each received e-mail:
  92 +ARCHIVE_DIR = '/var/log/mraptor_milter'
  93 +# ARCHIVE_DIR = '.'
  94 +
  95 +# === LOGGING ================================================================
  96 +
  97 +# Set up a specific logger with our desired output level
  98 +log = logging.getLogger('MRMilter')
  99 +
  100 +# disable logging by default - enable it in main app:
  101 +log.setLevel(logging.CRITICAL+1)
  102 +
  103 +# NOTE: all logging config is done in the main app, not here.
79 104
80 # === CLASSES ================================================================ 105 # === CLASSES ================================================================
81 106
@@ -120,7 +145,7 @@ class MacroRaptorMilter(Milter.Base): @@ -120,7 +145,7 @@ class MacroRaptorMilter(Milter.Base):
120 self.scope = None 145 self.scope = None
121 self.IPname = IPname # Name from a reverse IP lookup 146 self.IPname = IPname # Name from a reverse IP lookup
122 self.message = None # content 147 self.message = None # content
123 - logging.info("[%d] connect from host %s at %s" % (self.id, IPname, hostaddr)) 148 + log.info("[%d] connect from host %s at %s" % (self.id, IPname, hostaddr))
124 return Milter.CONTINUE 149 return Milter.CONTINUE
125 150
126 @Milter.noreply 151 @Milter.noreply
@@ -137,8 +162,8 @@ class MacroRaptorMilter(Milter.Base): @@ -137,8 +162,8 @@ class MacroRaptorMilter(Milter.Base):
137 # on the MTA. 162 # on the MTA.
138 self.canon_from = '@'.join(parse_addr(mailfrom)) 163 self.canon_from = '@'.join(parse_addr(mailfrom))
139 self.message.write('From %s %s\n' % (self.canon_from, time.ctime())) 164 self.message.write('From %s %s\n' % (self.canon_from, time.ctime()))
140 - logging.debug('[%d] Mail From %s %s\n' % (self.id, self.canon_from, time.ctime()))  
141 - logging.debug('[%d] mailfrom=%r, rest=%r' % (self.id, mailfrom, rest)) 165 + log.debug('[%d] Mail From %s %s\n' % (self.id, self.canon_from, time.ctime()))
  166 + log.debug('[%d] mailfrom=%r, rest=%r' % (self.id, mailfrom, rest))
142 return Milter.CONTINUE 167 return Milter.CONTINUE
143 168
144 @Milter.noreply 169 @Milter.noreply
@@ -149,7 +174,7 @@ class MacroRaptorMilter(Milter.Base): @@ -149,7 +174,7 @@ class MacroRaptorMilter(Milter.Base):
149 :param str: 174 :param str:
150 :return: Milter.CONTINUE 175 :return: Milter.CONTINUE
151 ''' 176 '''
152 - logging.debug('[%d] RCPT TO %r, rest=%r\n' % (self.id, to, rest)) 177 + log.debug('[%d] RCPT TO %r, rest=%r\n' % (self.id, to, rest))
153 return Milter.CONTINUE 178 return Milter.CONTINUE
154 179
155 @Milter.noreply 180 @Milter.noreply
@@ -192,6 +217,19 @@ class MacroRaptorMilter(Milter.Base): @@ -192,6 +217,19 @@ class MacroRaptorMilter(Milter.Base):
192 ''' 217 '''
193 return Milter.CONTINUE 218 return Milter.CONTINUE
194 219
  220 + def archive_message(self):
  221 + '''
  222 + Save a copy of the current message in its original form to a file
  223 + :return: nothing
  224 + '''
  225 + date_time = datetime.datetime.utcnow().isoformat('_')
  226 + # assumption: by combining datetime + milter id, the filename should be unique:
  227 + # (the only case for duplicates is when restarting the milter twice in less than a second)
  228 + fname = 'mail_%s_%d.eml' % (date_time, self.id)
  229 + fname = os.path.join(ARCHIVE_DIR, fname)
  230 + log.debug('Saving a copy of the original message to file %r' % fname)
  231 + open(fname, 'wb').write(self.message.getvalue())
  232 +
195 def eom(self): 233 def eom(self):
196 ''' 234 '''
197 This method is called when the end of the email message has been reached. 235 This method is called when the end of the email message has been reached.
@@ -201,6 +239,7 @@ class MacroRaptorMilter(Milter.Base): @@ -201,6 +239,7 @@ class MacroRaptorMilter(Milter.Base):
201 try: 239 try:
202 # set data pointer back to 0 240 # set data pointer back to 0
203 self.message.seek(0) 241 self.message.seek(0)
  242 + self.archive_message()
204 result = self.check_mraptor() 243 result = self.check_mraptor()
205 if result is not None: 244 if result is not None:
206 return result 245 return result
@@ -210,7 +249,7 @@ class MacroRaptorMilter(Milter.Base): @@ -210,7 +249,7 @@ class MacroRaptorMilter(Milter.Base):
210 except Exception: 249 except Exception:
211 exc_type, exc_obj, exc_tb = sys.exc_info() 250 exc_type, exc_obj, exc_tb = sys.exc_info()
212 fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 251 fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
213 - logging.exception("[%d] Unexpected error - fall back to ACCEPT: %s %s %s" 252 + log.exception("[%d] Unexpected error - fall back to ACCEPT: %s %s %s"
214 % (self.id, exc_type, fname, exc_tb.tb_lineno)) 253 % (self.id, exc_type, fname, exc_tb.tb_lineno))
215 return Milter.ACCEPT 254 return Milter.ACCEPT
216 255
@@ -225,13 +264,13 @@ class MacroRaptorMilter(Milter.Base): @@ -225,13 +264,13 @@ class MacroRaptorMilter(Milter.Base):
225 try: 264 try:
226 for part in msg.walk(): 265 for part in msg.walk():
227 # for name, value in part.items(): 266 # for name, value in part.items():
228 - # logging.debug(' - %s: %r' % (name, value)) 267 + # log.debug(' - %s: %r' % (name, value))
229 content_type = part.get_content_type() 268 content_type = part.get_content_type()
230 - logging.debug('[%d] Content-type: %r' % (self.id, content_type)) 269 + log.debug('[%d] Content-type: %r' % (self.id, content_type))
231 # TODO: handle any content-type, but check the file magic? 270 # TODO: handle any content-type, but check the file magic?
232 if not content_type.startswith('multipart'): 271 if not content_type.startswith('multipart'):
233 filename = part.get_filename(None) 272 filename = part.get_filename(None)
234 - logging.debug('[%d] Analyzing attachment %r' % (self.id, filename)) 273 + log.debug('[%d] Analyzing attachment %r' % (self.id, filename))
235 attachment = part.get_payload(decode=True) 274 attachment = part.get_payload(decode=True)
236 attachment_lowercase = attachment.lower() 275 attachment_lowercase = attachment.lower()
237 # check if this is a supported file type (if not, just skip it) 276 # check if this is a supported file type (if not, just skip it)
@@ -249,38 +288,48 @@ class MacroRaptorMilter(Milter.Base): @@ -249,38 +288,48 @@ class MacroRaptorMilter(Milter.Base):
249 m = mraptor.MacroRaptor(vba_code_all_modules) 288 m = mraptor.MacroRaptor(vba_code_all_modules)
250 m.scan() 289 m.scan()
251 if m.suspicious: 290 if m.suspicious:
252 - logging.warning('[%d] The attachment %r contains a suspicious macro: replace it with a text file' 291 + log.warning('[%d] The attachment %r contains a suspicious macro: replace it with a text file'
253 % (self.id, filename)) 292 % (self.id, filename))
254 part.set_payload('This attachment has been removed because it contains a suspicious macro.') 293 part.set_payload('This attachment has been removed because it contains a suspicious macro.')
255 part.set_type('text/plain') 294 part.set_type('text/plain')
256 # TODO: handle case when CTE is absent 295 # TODO: handle case when CTE is absent
257 part.replace_header('Content-Transfer-Encoding', '7bit') 296 part.replace_header('Content-Transfer-Encoding', '7bit')
258 # for name, value in part.items(): 297 # for name, value in part.items():
259 - # logging.debug(' - %s: %r' % (name, value)) 298 + # log.debug(' - %s: %r' % (name, value))
260 # TODO: archive filtered e-mail to a file 299 # TODO: archive filtered e-mail to a file
261 else: 300 else:
262 - logging.debug('The attachment %r is clean.' 301 + log.debug('The attachment %r is clean.'
263 % filename) 302 % filename)
264 except Exception: 303 except Exception:
265 - logging.exception('[%d] Error while processing the message' % self.id) 304 + log.exception('[%d] Error while processing the message' % self.id)
266 # TODO: depending on error, decide to forward the e-mail as-is or not 305 # TODO: depending on error, decide to forward the e-mail as-is or not
267 result = Milter.DISCARD 306 result = Milter.DISCARD
268 # TODO: only do this if the body has actually changed 307 # TODO: only do this if the body has actually changed
269 body = str(msg) 308 body = str(msg)
270 self.message = io.BytesIO(body) 309 self.message = io.BytesIO(body)
271 self.replacebody(body) 310 self.replacebody(body)
272 - logging.info('[%d] Message relayed' % self.id) 311 + log.info('[%d] Message relayed' % self.id)
273 return result 312 return result
274 313
275 314
276 # === MAIN =================================================================== 315 # === MAIN ===================================================================
277 316
278 def main(): 317 def main():
279 - # TODO: log to file with rotation  
280 - logging.basicConfig(format='%(levelname)8s: %(message)s', level=logging.DEBUG)  
281 -  
282 - logging.info('Starting MacroRaptorMilter - listening on %s' % SOCKET)  
283 - logging.info('Press Ctrl+C to stop.') 318 + # banner
  319 + print('mraptor_milter v%s - http://decalage.info/python/oletools' % __version__)
  320 + print('logging to file %s' % LOGFILE_PATH)
  321 + print('Press Ctrl+C to stop.')
  322 + # Add the log message handler to the logger
  323 + # log to files rotating once a day:
  324 + handler = logging.handlers.TimedRotatingFileHandler(LOGFILE_PATH, when='D', encoding='utf8')
  325 + # create formatter and add it to the handlers
  326 + formatter = logging.Formatter('%(asctime)s - %(levelname)8s: %(message)s')
  327 + handler.setFormatter(formatter)
  328 + log.addHandler(handler)
  329 + # enable logging:
  330 + log.setLevel(logging.DEBUG)
  331 +
  332 + log.info('Starting mraptor_milter v%s - listening on %s' % (__version__, SOCKET))
284 333
285 # Register to have the Milter factory create instances of the class: 334 # Register to have the Milter factory create instances of the class:
286 Milter.factory = MacroRaptorMilter 335 Milter.factory = MacroRaptorMilter
@@ -291,8 +340,8 @@ def main(): @@ -291,8 +340,8 @@ def main():
291 # set the "last" fall back to ACCEPT if exception occur 340 # set the "last" fall back to ACCEPT if exception occur
292 Milter.set_exception_policy(Milter.ACCEPT) 341 Milter.set_exception_policy(Milter.ACCEPT)
293 # start the milter 342 # start the milter
294 - Milter.runmilter("MacroRaptorMilter", SOCKET, TIMEOUT)  
295 - logging.info('Stopping MacroRaptorMilter.') 343 + Milter.runmilter("mraptor_milter", SOCKET, TIMEOUT)
  344 + log.info('Stopping mraptor_milter.')
296 345
297 if __name__ == "__main__": 346 if __name__ == "__main__":
298 main() 347 main()