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 48  
49 49 # --- CHANGELOG --------------------------------------------------------------
50 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 57 # --- IMPORTS ----------------------------------------------------------------
... ... @@ -59,6 +63,8 @@ import email
59 63 import sys
60 64 import os
61 65 import logging
  66 +import logging.handlers
  67 +import datetime
62 68  
63 69 from socket import AF_INET6
64 70  
... ... @@ -76,6 +82,25 @@ TIMEOUT = 30 # Milter timeout in seconds
76 82 # CFG_DIR = "/etc/macromilter/"
77 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 105 # === CLASSES ================================================================
81 106  
... ... @@ -120,7 +145,7 @@ class MacroRaptorMilter(Milter.Base):
120 145 self.scope = None
121 146 self.IPname = IPname # Name from a reverse IP lookup
122 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 149 return Milter.CONTINUE
125 150  
126 151 @Milter.noreply
... ... @@ -137,8 +162,8 @@ class MacroRaptorMilter(Milter.Base):
137 162 # on the MTA.
138 163 self.canon_from = '@'.join(parse_addr(mailfrom))
139 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 167 return Milter.CONTINUE
143 168  
144 169 @Milter.noreply
... ... @@ -149,7 +174,7 @@ class MacroRaptorMilter(Milter.Base):
149 174 :param str:
150 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 178 return Milter.CONTINUE
154 179  
155 180 @Milter.noreply
... ... @@ -192,6 +217,19 @@ class MacroRaptorMilter(Milter.Base):
192 217 '''
193 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 233 def eom(self):
196 234 '''
197 235 This method is called when the end of the email message has been reached.
... ... @@ -201,6 +239,7 @@ class MacroRaptorMilter(Milter.Base):
201 239 try:
202 240 # set data pointer back to 0
203 241 self.message.seek(0)
  242 + self.archive_message()
204 243 result = self.check_mraptor()
205 244 if result is not None:
206 245 return result
... ... @@ -210,7 +249,7 @@ class MacroRaptorMilter(Milter.Base):
210 249 except Exception:
211 250 exc_type, exc_obj, exc_tb = sys.exc_info()
212 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 253 % (self.id, exc_type, fname, exc_tb.tb_lineno))
215 254 return Milter.ACCEPT
216 255  
... ... @@ -225,13 +264,13 @@ class MacroRaptorMilter(Milter.Base):
225 264 try:
226 265 for part in msg.walk():
227 266 # for name, value in part.items():
228   - # logging.debug(' - %s: %r' % (name, value))
  267 + # log.debug(' - %s: %r' % (name, value))
229 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 270 # TODO: handle any content-type, but check the file magic?
232 271 if not content_type.startswith('multipart'):
233 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 274 attachment = part.get_payload(decode=True)
236 275 attachment_lowercase = attachment.lower()
237 276 # check if this is a supported file type (if not, just skip it)
... ... @@ -249,38 +288,48 @@ class MacroRaptorMilter(Milter.Base):
249 288 m = mraptor.MacroRaptor(vba_code_all_modules)
250 289 m.scan()
251 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 292 % (self.id, filename))
254 293 part.set_payload('This attachment has been removed because it contains a suspicious macro.')
255 294 part.set_type('text/plain')
256 295 # TODO: handle case when CTE is absent
257 296 part.replace_header('Content-Transfer-Encoding', '7bit')
258 297 # for name, value in part.items():
259   - # logging.debug(' - %s: %r' % (name, value))
  298 + # log.debug(' - %s: %r' % (name, value))
260 299 # TODO: archive filtered e-mail to a file
261 300 else:
262   - logging.debug('The attachment %r is clean.'
  301 + log.debug('The attachment %r is clean.'
263 302 % filename)
264 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 305 # TODO: depending on error, decide to forward the e-mail as-is or not
267 306 result = Milter.DISCARD
268 307 # TODO: only do this if the body has actually changed
269 308 body = str(msg)
270 309 self.message = io.BytesIO(body)
271 310 self.replacebody(body)
272   - logging.info('[%d] Message relayed' % self.id)
  311 + log.info('[%d] Message relayed' % self.id)
273 312 return result
274 313  
275 314  
276 315 # === MAIN ===================================================================
277 316  
278 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 334 # Register to have the Milter factory create instances of the class:
286 335 Milter.factory = MacroRaptorMilter
... ... @@ -291,8 +340,8 @@ def main():
291 340 # set the "last" fall back to ACCEPT if exception occur
292 341 Milter.set_exception_policy(Milter.ACCEPT)
293 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 346 if __name__ == "__main__":
298 347 main()
... ...