Commit 34d3f3fd18ffe5740626ae9499aeb883b501ab62
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() |