'''
Basic realscanners for sagator, version 0.6.1

(c) 2003-2004 Jan ONDREJ (SAL) <ondrejj(at)salstar.sk>

 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 avlib import *
import re,string,os,time,socket

__all__=['regexp_scan','string_scan','smtp_comm',
         'const','max_file_size','exec_any']

def iret(level,vname,ret):
    if vname:
      return level,vname,ret
    else:
      return 0.0,vname,ret

class regexp_scan(ascanner):
  '''
  Primitive regexp pattern scanner.
  
  Usage: regexp_scan({'VirName':['RegExp_Pattern...',...],...} [,size])

  Where: 'VirName' is a string, which identifies defined virus
         'RegExp_Pattern...' is a regexp pattern
         size is a number, which defines, how many bytes may be checked.
           If it is 0 or not defined, whole stream is scanned.
           If it is -1, email header is scanned.
    
  There can be more patterns for one virus. All patterns
  must match to assign an stream as virus.
  There can also be more virnames in one dictionary.
  '''
  name='RegExpScanner()'
  def __init__(self,regexps,size=0):
      self.regexps=regexps
      self.size=size
  def scanstream(self,stream):
      for vname,regs in self.regexps.items():
        for reg in regs:
          if self.size>0:
            reg1=re.search(reg,stream[:self.size])
          elif self.size<0:
            reg1=re.search(reg,stream[:len(mail.header)])
          else:
            reg1=re.search(reg,stream)
          if reg1:
            if reg==regs[-1]:
              mail.addheader("X-Sagator-RegExp",vname)
              return iret(1.0,vname,['regexp: '+vname+' FOUND, '+reg+'\n'])
            else:
              break
      return 0.0,'',[]

class string_scan(ascanner):
  '''
  Primitive string pattern scanner.
  
  Usage: string_scan({'VirName':['Pattern...',...],...} [,size])

  Where: 'VirName' is a string, which identifies defined virus
         'Pattern...' is a string pattern
         size is a number, which defines, how many bytes may be checked.
           If it is 0 or not defined, whole stream is scanned.
           If it is -1, email header is scanned.
    
  There can be more patterns for one virus. All patterns
  must match to assign an stream as virus.
  There can also be more virnames in one dictionary.
  '''
  name='StringScanner()'
  def __init__(self,strings,size=0):
      self.strings=strings
      self.size=size
  def scanstream(self,stream):
      for vname,regs in self.strings.items():
        for reg in regs:
          if self.size>0:
            str1=string.find(stream[:self.size],reg)
          elif self.size<0:
            str1=string.find(stream[:len(mail.header)],reg)
          else:
            str1=string.find(stream,reg)
          if str1>=0:
            if reg==regs[-1]:
              mail.addheader("X-Sagator-StringScan",vname)
              return iret(1.0,vname,['string: '+vname+' FOUND, '+reg+'\n'])
          else:
            break
      return 0.0,'',[]

class smtp_comm(ascanner):
  '''
  Primitive regexp pattern scanner for SMTP communication.
  
  Usage: smtp_comm({'VirName':['RegExp_Pattern...',...],...})

  Where: 'VirName' is a string, which identifies defined virus
         'RegExp_Pattern...' is a regexp pattern
    
  For more information about this scanner see documentation for
  regexp_scan realscanner.
  
  This scanner also can be used to apply some scnanner to defined
  email adresses. For example:

      match_all(
        smtp_comm({'anydomain:':['^RCPT TO:.*anydomain.dom']}),
        stream2mbox(libclam())
      )
  
  In this example, libclam() scanner is used only if mail is
  adressed to an user on anydomain.dom.
  '''
  name='smtp_comm()'
  def __init__(self,regexps):
      self.regexps=regexps
  def scanstream(self,stream):
      if globals.scan_only:
        return 0.0,'',[]
      for vname,regs in self.regexps.items():
        for reg in regs:
          if re.search(reg,mail.comm,re.IGNORECASE|re.MULTILINE):
            if reg==regs[-1]:
              mail.addheader("X-Sagator-SMTP-comm",vname)
              return iret(1.0,vname,['smtp_comm: '+vname+' FOUND, '+reg+'\n'])
          else:
            break
      return 0.0,'',[]
  def scanfile(self,files,dir=''):
      return self.scanstream('')

#####################################################################
### primitive class

class const(ascanner):
  '''
  const() realscanner
  
  Usage: const(level,'VirName',['virreturnstringline1',...])
      or const()
  
  Where: level is an float which defines returned infection level
           If level is not defined, after scanning an error is raised.
         'VirName',[...] is a returned virus.
  
  This scanner has no real functionality. It return always
  a defined virus, or clean (if virus is not defined).
  This scanner has no error codes.
  '''
  name='const()'
  ignore_name=True
  def __init__(self,level=None,vir='',ret=[]):
      self.level=level
      self.vir=vir
      self.ret=ret
      self.filename=''
  def scanstream(self,stream):
      if self.level==None:
        raise ScannerError,'Forced error by const() scanner'
      if self.filename:
        debug.echo(6,self.name,": Scanning file: ",self.filename)
      return self.level,self.vir,self.ret
  def scanfile(self,files,dirname=''):
      if self.level==None:
        raise ScannerError,'Forced error by const() scanner'
      for fn in files:
        debug.echo(6,self.name,": Scanning file: ",fn)
      return self.level,self.vir,self.ret

#####################################################################
### max_file_size class

class max_file_size(ascanner):
  '''
  max_file_size() realscanner
  
  Usage: max_file_size(size[,'name'])
  
  Where: size is a number of bytes, which if exceeded, virus is returned
         'name' is a string, which identifies the virus name.
            If it is not present, 'FileSizeOverrun' is returned.
  '''
  name='max_file_size()'
  def __init__(self,size,vname='FileSizeOverrun'):
      self.size=size
      self.vname=vname
  def scanstream(self,stream):
      if len(stream)<=self.size:
        return 0.0,'',[]
      else:
        return 1.0,self.vname,[self.filename+": "+self.vname]
  def scanfile(self,files,dir=''):
      for fn in files:
        if os.lstat(safe.fn(fn))[6]>self.size:
          return 1.0,self.vname,[fn+': '+self.vname]
      return 0.0,'',[]

#####################################################################
### exec_any class

class exec_any(ascanner):
  '''
  exec_any() realscanner
  
  Usage: exec_any(['command','arg1',...],[statusA1,...],[statusB1,...])
  
  Where: 'command' is a string, which command to run
         'argX' are command line arguments, as last argument the
           filename is added
         statusA1,... are integers, which defines "clean" exit codes
         statusB1,... are integers, which defines "virus found" exit codes
  
  If command returns exit status 0, no virus is returned.
  If exit code is any from statusX, virus is returned.
  Otherwise an error is raised.
  '''
  name='exec_any()'
  def __init__(self,arg,clean_status=[0],infected_status=[1]):
      self.arg=arg
      self.clean_status=clean_status
      self.infected_status=infected_status
  def scanfile(self,files,dir=''):
      for fn in files:
        pf=popen(self.arg+[fn])
        pf.wait()
        pf.close()
        if pf.exitstatus in self.clean_status:
          return 0.0,'',[]
        else:
          if pf.exitstatus in self.infected_status:
            return 1.0,'UnknownVirus',['ExitCode: '+str(pf.exitstatus)]
          else:
            raise ScannerError,'UnkonwnExitStatus: '+str(pf.exitstatus)
