''' Advanced logger for sagator (c) 2003-2022 Jan ONDREJ (SAL) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. ''' from __future__ import absolute_import from avlib import * from .match import match_any from .report import report import db __all__ = ['log', 'log_syslog', 'log_sql', 'log_cleanup'] class log(report): ''' Advanced logger interscanner. This scanner can be used to log some special data. You can use these variables in format string: %(LEVEL)s - detected virus level %(VIRNAME)s - name of detected virus, empty if CLEAN %(STATUS)s - if mail is dropped, rejected, ... %(QNAME)s - quarantine file name %(SCANNER_NAME)s - scanner which reported this virus %(SENDER)s - scanned message sender %(RECIPIENTS)s - scanned message recipients %(RECIPIENT)s - scanned message recipients one by one (only for SQL) %(SUBJECT)s - message's header Subject %(SIZE)s - email size %(VERSION)s - sagator's version %(SENTBY_IP)s - sender's IP %(SENTBY_NAME)s - sender's hostname %(SENTBY_HELO)s - sender's HELO/EHLO string %(DATETIME)s - date and time in standard format ("%c") %(SYSLOG_DATE)s - date and time in syslog format ("%a %e %H:%M:%S") %(PID)s - current process ID You also can use log.FORMAT for default format or log.SUMMARY_REPORT for summary reporter script. Usage: log(logto,format,scanners...) Where: logto is a string, which defines filename to store these data If logto is an integer, it defines log level in standard log file. format is a string, which defines data format New format style is introduced in version 0.9.0, for older versions please use old format (for example "$STATUS" instead of "%(STATUS)s"). ''' name='log()' # default format FORMAT="level='%(LEVEL)s', virname='%(VIRNAME)s', status='%(STATUS)s', "\ "scanner='%(SCANNER_NAME)s', size='%(SIZE)s', sender='%(SENDER)s', "\ "recipients='%(RECIPIENTS)s', sentby_ip='%(SENTBY_IP)s', "\ "qname='%(QNAME)s'\n" SUMMARY_REPORT="REPORT: datetime='%(DATETIME)s', "+FORMAT.strip()+"\n" def __init__(self, logto, format, *scanners): self.logto = logto self.format = format match_any.__init__(self, scanners) def quote(self,s): return quote(s) def vars(self, level, detected, quote=str, **args): sender = mail.getsender() return { 'LEVEL': str(level), 'VIRNAME': quote(tostr(detected)), 'STATUS': globals.action(level), 'QNAME': quote(globals.QFNAME), 'SCANNER_NAME': globals.found_by.name, 'SENDER': quote(tostr(mail.sender.strip())), 'RECIPIENTS': quote(','.join(mail.recip)), 'RECIPIENT': quote(args.get('recipient') or ''), 'SUBJECT': str(mail.headers.get('Subject')), 'SIZE': str(len(mail.data)), 'VERSION': SG_VER_REL, 'SENTBY_IP': tostr(sender['ADDR']) or '127.0.0.1', 'SENTBY_NAME': tostr(sender['NAME']) or 'localhost', 'SENTBY_HELO': tostr(sender['HELO']), 'DATETIME': time.strftime("%c"), 'SYSLOG_DATE': time.strftime("%a %e %H:%M:%S"), 'PID': str(os.getpid()) } def repl_vars(self, level, detected): return replace_tmpl(self.format, self.vars(level, detected, self.quote)) def scanbuffer(self, buffer, args={}): level, detected, virlist = match_any.scanbuffer(self, buffer, args) if is_infected(level): if type(self.logto)==type(1): debug.echo(self.logto, self.repl_vars(level, detected).strip()) else: open(self.logto, 'a').write(self.repl_vars(level, detected)) return level, detected, virlist class log_syslog(log): ''' Syslog logger interscanner to log via syslog. For detailed description see log() scanner. Usage: log_syslog(format,scanners...) ''' name='log_syslog()' def __init__(self, format, *scanners): import syslog self.format = format self.SYSLOG = syslog self.SYSLOG.openlog('sagator', syslog.LOG_PID, syslog.LOG_MAIL) match_any.__init__(self, scanners) def scanbuffer(self, buffer, args={}): level, detected, virlist=match_any.scanbuffer(self, buffer, args) if is_infected(level): self.SYSLOG.syslog(self.repl_vars(level,detected).strip()) return level, detected, virlist class extendible_string(str): ''' Extendible string. Use .extend(column, data) to extend SQL command with specific column name and data value. ''' def extend(self, column, data): return extendible_string( self.replace( ')', ',%s)' % column, 1 ).replace( ');', ',%s);' % data, 1 ) ) class log_sql(log): ''' SQL interscanner for python DB-API 2.0 compatible DB modules. Usage: log_sql(db_connection,format,scanners...) log_sql(...).also_clean() Where: db_connection is a database connection. For these connections see doc/Databases.txt file. format is a string, which defines an SQL INSERT command with optional variables described in log() scanner. .also_clean() can be used to log also "CLEAN" emails Examples: log_sql(DB_ENGINE, log_sql.FORMAT) log_sql(DB_ENGINE, log_sql.FORMAT.extend('subject', '%(SUBJECT)s')) New in version 0.7.0. ''' FORMAT = extendible_string( "INSERT INTO log " \ +"(datetime,level,virname,status,qname,sender,recipient,size,ip)" \ +" VALUES " \ +"(CURRENT_TIMESTAMP,%(LEVEL)s,%(VIRNAME)s,%(STATUS)s,%(QNAME)s,%(SENDER)s,%(RECIPIENT)s,%(SIZE)s,%(SENTBY_IP)s);" ) def __init__(self, dbc, format, *scanners): self.dbc = dbc self.quote = self.dbc.quote self.format = format self.LOG_CLEAN = False match_any.__init__(self, scanners) def also_clean(self): self.LOG_CLEAN = True return self def scanbuffer(self, buffer, args={}): level, detected, virlist = match_any.scanbuffer(self, buffer, args) if self.LOG_CLEAN or is_infected(level): for TRY in range(3, 0, -1): try: if '%(' in self.format: for recipient in mail.recip: insert = self.vars(level, detected, recipient=recipient) debug.echo(5, "log_sql(): %s %s" % (self.format, insert)) self.dbc.execute(self.format, insert) else: for recipient in mail.recip: insert = self.repl_vars(level, detected) debug.echo(5, "log_sql(): ", str([insert])) self.dbc.execute(insert) self.dbc.commit() break except Exception as e: if TRY>1: debug.echo(3, "log_sql(): WARNING: Problem logging into database!") debug.traceback(5, "log_sql(): ") else: debug.echo(1, "log_sql(): ERROR: Error logging into database!") debug.echo(3, "log_sql(): ", e) debug.traceback(4, "log_sql(): ") # try to reconnect connection try: self.dbc.connect() except: debug.traceback(4, "log_sql('reconnect'): ") return level, detected, virlist def reinit(self): # create new connection for this process self.dbc.refresh() # call parent reinit() log.reinit(self) class log_cleanup(log): ''' Clean old records from SQL log database table. Usage: log_cleanup(older_than=768) Where: older_than is an integer, which defines number of hours for up-to-date records. All older records will be deleted. By default aprox. one month (768h) of records are kept. New in version 1.1.0. ''' name='log_cleanup()' def __init__(self, older_than=768): self.older_than = older_than def scanbuffer(self, buffer, args={}): t0 = time.time() args['dbc'].execute_cycle( "DELETE FROM log WHERE datetime<%s", [ time.strftime("%Y-%m-%d %H:%M:%S", time.localtime( time.time()-self.older_than*60*60)) ] ) if args['dbc'].rowcount: debug.echo(3, "%s: %d old records deleted in %5.3f s" % (self.name, args['dbc'].rowcount, time.time()-t0)) return 0.0, b'', []