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 | 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() | ... | ... |