Beispiel #1
0
class SPFPlugin(ScannerPlugin):
    """This plugin performs SPF validation using the pyspf module https://pypi.python.org/pypi/pyspf/
    by default, it just logs the result (test mode)

    to enable actual rejection of messages, add a config option on_<resulttype> with a valid postfix action. eg:

    on_fail = REJECT

    valid result types are: 'pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', and 'neutral'
    """
                                         
    def __init__(self,config,section=None):
        ScannerPlugin.__init__(self,config,section)
        self.logger=self._logger()
        
        self.requiredvars={
            'ip_whitelist_file':{
                'default':'',
                'description':'file containing a list of ip adresses to be exempted from SPF checks. Supports CIDR notation if the netaddr module is installed. 127.0.0.0/8 is always exempted',
            },
            'domain_selective_spf_file':{
                'default':'',
                'description':'if this is non-empty, only sender domains in this file will be checked for SPF',
            },
            'dbconnection':{
                'default':"mysql://root@localhost/spfcheck?charset=utf8",
                'description':'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query':{
                'default':"SELECT check_spf from domain where domain_name=:domain",
                'description':'get from sql database :domain will be replaced with the actual domain name. must return field check_spf',
            },
            'on_fail':{
                'default':'DUNNO',
                'description':'Action for SPF fail.',
            },
            'on_softfail':{
                'default':'DUNNO',
                'description':'Action for SPF softfail.',
            },
            'messagetemplate':{
                'default':'SPF ${result} for domain ${from_domain} from ${client_address} : ${explanation}'
            }
        }
        
        if HAVE_NETADDR:
            self.private_nets = [
                IPNetwork('10.0.0.0/8'), # private network
                IPNetwork('127.0.0.0/8'), # localhost
                IPNetwork('169.254.0.0/16'), # link local
                IPNetwork('172.16.0.0/12'), # private network
                IPNetwork('192.168.0.0/16'), # private network
                IPNetwork('fe80::/10'), # ipv6 link local
                IPNetwork('::1/128'), # localhost
            ]
        else:
            self.private_nets = None
        
        self.ip_whitelist_loader=None
        self.ip_whitelist=[] # either a list of plain ip adress strings or a list of IPNetwork if netaddr is available

        self.selective_domain_loader=None
    
    
    def check_this_domain(self, from_domain):
        do_check = False
        selective_sender_domain_file=self.config.get(self.section,'domain_selective_spf_file','').strip()
        if selective_sender_domain_file != '' and os.path.exists(selective_sender_domain_file):
            if self.selective_domain_loader is None:
                self.selective_domain_loader=FileList(selective_sender_domain_file,lowercase=True)
            if from_domain.lower() in self.selective_domain_loader.get_list():
                do_check = True
                
        if not do_check:
            dbconnection = self.config.get(self.section, 'dbconnection', '').strip()
            sqlquery = self.config.get(self.section, 'domain_sql_query')
            
            if dbconnection!='' and SQL_EXTENSION_ENABLED:
                cache = get_default_cache()
                do_check = get_domain_setting(from_domain, dbconnection, sqlquery, cache, self.section, False, self.logger)
                
            elif dbconnection!='' and not SQL_EXTENSION_ENABLED:
                self.logger.error('dbconnection specified but sqlalchemy not available - skipping db lookup')
                
        return do_check


    def is_private_address(self,addr):
        if HAVE_NETADDR:
            ipaddr = IPAddress(addr)
            private = False
            for net in self.private_nets:
                if ipaddr in net:
                    private = True
                    break
            return private
        else:
            if addr=='127.0.0.1' or addr=='::1' or addr.startswith('10.') or addr.startswith('192.168.') or addr.startswith('fe80:'):
                return True
            if not addr.startswith('172.'):
                return False
            for i in range(16,32):
                if addr.startswith('172.%s'%i):
                    return True
        return False


    def ip_whitelisted(self,addr):
        if self.is_private_address(addr):
            return True

        #check ip whitelist
        ip_whitelist_file=self.config.get(self.section,'ip_whitelist_file', '').strip()
        if ip_whitelist_file != '' and os.path.exists(ip_whitelist_file):
            plainlist = []
            if self.ip_whitelist_loader is None:
                self.ip_whitelist_loader=FileList(ip_whitelist_file,lowercase=True)

            if self.ip_whitelist_loader.file_changed():
                plainlist=self.ip_whitelist_loader.get_list()

                if HAVE_NETADDR:
                    self.ip_whitelist=[IPNetwork(x) for x in plainlist]
                else:
                    self.ip_whitelist=plainlist

            if HAVE_NETADDR:
                checkaddr=IPAddress(addr)
                for net in self.ip_whitelist:
                    if checkaddr in net:
                        return True
            else:
                if addr in plainlist:
                    return True
        return False


    def examine(self,suspect):
        if not HAVE_SPF:
            return DUNNO
        
        client_address=suspect.get_value('client_address')
        helo_name=suspect.get_value('helo_name')
        sender=suspect.get_value('sender')
        if client_address is None or helo_name is None or sender is None:
            self.logger.error('missing client_address or helo or sender')
            return DUNNO

        if self.ip_whitelisted(client_address):
            self.logger.info("Client %s is whitelisted - no SPF check"%client_address)
            return DUNNO

        sender_email = strip_address(sender)
        if sender_email=='' or sender_email is None:
            return DUNNO
        
        sender_domain = extract_domain(sender_email)
        if sender_domain is None:
            self.logger.error('no domain found in sender address %s' % sender_email)
            return DUNNO
        
        if not self.check_this_domain(sender_domain):
            self.logger.debug('skipping SPF check for %s' % sender_domain)
            return DUNNO

        result, explanation = spf.check2(client_address, sender_email, helo_name)
        suspect.tags['spf'] = result
        if result != 'none':
            self.logger.info('SPF client=%s, sender=%s, h=%s result=%s : %s' % (client_address, sender_email, helo_name, result,explanation))
        
        action = DUNNO
        message = apply_template(self.config.get(self.section, 'messagetemplate'), suspect, dict(result=result, explanation=explanation))

        configopt = 'on_%s' % result
        if self.config.has_option(self.section, configopt):
            action=string_to_actioncode(self.config.get(self.section, configopt))

        return action, message
         
        
    
    def lint(self):
        lint_ok = True
        
        if not HAVE_SPF:
            print('pyspf or pydns module not installed - this plugin will do nothing')
            lint_ok = False
            
        if not HAVE_NETADDR:
            print('WARNING: netaddr python module not installed - IP whitelist will not support CIDR notation')

        if not self.checkConfig():
            print('Error checking config')
            lint_ok = False
            
        selective_sender_domain_file=self.config.get(self.section,'domain_selective_spf_file','').strip()
        if selective_sender_domain_file != '' and not os.path.exists(selective_sender_domain_file):
            print("domain_selective_spf_file %s does not exist" % selective_sender_domain_file)
            lint_ok = False
            
        ip_whitelist_file=self.config.get(self.section,'ip_whitelist_file', '').strip()
        if ip_whitelist_file != '' and os.path.exists(ip_whitelist_file):
            print("ip_whitelist_file %s does not exist - IP whitelist is disabled" % ip_whitelist_file)
            lint_ok = False
        
        sqlquery = self.config.get(self.section, 'domain_sql_query')
        dbconnection = self.config.get(self.section, 'dbconnection', '').strip()
        if not SQL_EXTENSION_ENABLED and dbconnection != '':
            print('SQLAlchemy not available, cannot use SQL backend')
            lint_ok = False
        elif dbconnection == '':
            print('No DB connection defined. Disabling SQL backend')
        else:
            if not sqlquery.lower().startswith('select '):
                lint_ok = False
                print('SQL statement must be a SELECT query')
            if lint_ok:
                try:
                    conn=get_session(dbconnection)
                    conn.execute(sqlquery, {'domain':'example.com'})
                except Exception as e:
                    lint_ok = False
                    print(str(e))
        
        return lint_ok
    
    
    
    def __str__(self):
        return "SPF"
Beispiel #2
0
class SPFPlugin(ScannerPlugin):
    """This plugin performs SPF validation using the pyspf module https://pypi.python.org/pypi/pyspf/
    by default, it just logs the result (test mode)

    to enable actual rejection of messages, add a config option on_<resulttype> with a valid postfix action. eg:

    on_fail = REJECT

    valid result types are: 'pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', and 'neutral'
    """
    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.logger = self._logger()

        self.requiredvars = {
            'ip_whitelist_file': {
                'default':
                '',
                'description':
                'file containing a list of ip adresses to be exempted from SPF checks. Supports CIDR notation if the netaddr module is installed. 127.0.0.0/8 is always exempted',
            },
            'domain_selective_spf_file': {
                'default':
                '',
                'description':
                'if this is non-empty, only sender domains in this file will be checked for SPF',
            },
            'dbconnection': {
                'default':
                "mysql://root@localhost/spfcheck?charset=utf8",
                'description':
                'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query': {
                'default':
                "SELECT check_spf from domain where domain_name=:domain",
                'description':
                'get from sql database :domain will be replaced with the actual domain name. must return field check_spf',
            },
            'on_fail': {
                'default': 'DUNNO',
                'description': 'Action for SPF fail.',
            },
            'on_softfail': {
                'default': 'DUNNO',
                'description': 'Action for SPF softfail.',
            },
            'messagetemplate': {
                'default':
                'SPF ${result} for domain ${from_domain} from ${client_address} : ${explanation}'
            }
        }

        self.ip_whitelist_loader = None
        self.ip_whitelist = [
        ]  # either a list of plain ip adress strings or a list of IPNetwork if netaddr is available

        self.selective_domain_loader = None

    def check_this_domain(self, from_domain):
        global SETTINGSCACHE

        do_check = False
        selective_sender_domain_file = self.config.get(
            self.section, 'domain_selective_spf_file', '').strip()
        if selective_sender_domain_file != '' and os.path.exists(
                selective_sender_domain_file):
            if self.selective_domain_loader is None:
                self.selective_domain_loader = FileList(
                    selective_sender_domain_file, lowercase=True)
            if from_domain.lower() in self.selective_domain_loader.get_list():
                do_check = True

        if not do_check:
            dbconnection = self.config.get(self.section, 'dbconnection',
                                           '').strip()
            sqlquery = self.config.get(self.section, 'domain_sql_query')

            if dbconnection != '' and SQLALCHEMY_AVAILABLE:
                if SETTINGSCACHE is None:
                    SETTINGSCACHE = SettingsCache()
                do_check = get_domain_setting(from_domain, dbconnection,
                                              sqlquery, SETTINGSCACHE, False,
                                              self.logger)

            elif dbconnection != '' and not SQLALCHEMY_AVAILABLE:
                self.logger.error(
                    'dbconnection specified but sqlalchemy not available - skipping db lookup'
                )

        return do_check

    def is_private_address(self, addr):
        if addr == '127.0.0.1' or addr == '::1' or addr.startswith(
                '10.') or addr.startswith('192.168.') or addr.startswith(
                    'fe80:'):
            return True
        if not addr.startswith('172.'):
            return False
        for i in range(16, 32):
            if addr.startswith('172.%s' % i):
                return True
        return False

    def ip_whitelisted(self, addr):
        if self.is_private_address(addr):
            return True

        #check ip whitelist
        ip_whitelist_file = self.config.get(self.section, 'ip_whitelist_file',
                                            '').strip()
        if ip_whitelist_file != '' and os.path.exists(ip_whitelist_file):
            plainlist = []
            if self.ip_whitelist_loader is None:
                self.ip_whitelist_loader = FileList(ip_whitelist_file,
                                                    lowercase=True)

            if self.ip_whitelist_loader.file_changed():
                plainlist = self.ip_whitelist_loader.get_list()

                if HAVE_NETADDR:
                    self.ip_whitelist = [IPNetwork(x) for x in plainlist]
                else:
                    self.ip_whitelist = plainlist

            if HAVE_NETADDR:
                checkaddr = IPAddress(addr)
                for net in self.ip_whitelist:
                    if checkaddr in net:
                        return True
            else:
                if addr in plainlist:
                    return True
        return False

    def examine(self, suspect):
        if not HAVE_SPF:
            return DUNNO

        client_address = suspect.get_value('client_address')
        helo_name = suspect.get_value('helo_name')
        sender = suspect.get_value('sender')
        if client_address is None or helo_name is None or sender is None:
            self.logger.error('missing client_address or helo or sender')
            return DUNNO

        if self.ip_whitelisted(client_address):
            self.logger.info("Client %s is whitelisted - no SPF check" %
                             client_address)
            return DUNNO

        sender_email = strip_address(sender)
        if sender_email == '' or sender_email is None:
            return DUNNO

        sender_domain = extract_domain(sender_email)
        if sender_domain is None:
            self.logger.error('no domain found in sender address %s' %
                              sender_email)
            return DUNNO

        if not self.check_this_domain(sender_domain):
            self.logger.debug('skipping SPF check for %s' % sender_domain)
            return DUNNO

        result, explanation = spf.check2(client_address, sender_email,
                                         helo_name)
        suspect.tags['spf'] = result
        if result != 'none':
            self.logger.info(
                'SPF client=%s, sender=%s, h=%s result=%s : %s' %
                (client_address, sender_email, helo_name, result, explanation))

        action = DUNNO
        message = apply_template(
            self.config.get(self.section, 'messagetemplate'), suspect,
            dict(result=result, explanation=explanation))

        configopt = 'on_%s' % result
        if self.config.has_option(self.section, configopt):
            action = string_to_actioncode(
                self.config.get(self.section, configopt))

        return action, message

    def lint(self):
        lint_ok = True

        if not HAVE_SPF:
            print 'pyspf or pydns module not installed - this plugin will do nothing'
            lint_ok = False

        if not HAVE_NETADDR:
            print 'WARNING: netaddr python module not installed - IP whitelist will not support CIDR notation'

        if not self.checkConfig():
            print 'Error checking config'
            lint_ok = False

        selective_sender_domain_file = self.config.get(
            self.section, 'domain_selective_spf_file', '').strip()
        if selective_sender_domain_file != '' and not os.path.exists(
                selective_sender_domain_file):
            print "domain_selective_spf_file %s does not exist" % selective_sender_domain_file
            lint_ok = False

        ip_whitelist_file = self.config.get(self.section, 'ip_whitelist_file',
                                            '').strip()
        if ip_whitelist_file != '' and os.path.exists(ip_whitelist_file):
            print "ip_whitelist_file %s does not exist - IP whitelist is disabled" % ip_whitelist_file
            lint_ok = False

        sqlquery = self.config.get(self.section, 'domain_sql_query')
        dbconnection = self.config.get(self.section, 'dbconnection',
                                       '').strip()
        if not SQLALCHEMY_AVAILABLE and dbconnection != '':
            print 'SQLAlchemy not available, cannot use SQL backend'
            lint_ok = False
        elif dbconnection == '':
            print 'No DB connection defined. Disabling SQL backend'
        else:
            if not sqlquery.lower().startswith('select '):
                lint_ok = False
                print 'SQL statement must be a SELECT query'
            if lint_ok:
                try:
                    conn = get_session(dbconnection)
                    conn.execute(sqlquery, {'domain': 'example.com'})
                except Exception as e:
                    lint_ok = False
                    print str(e)

        return lint_ok

    def __str__(self):
        return "SPF"
Beispiel #3
0
class SPFPlugin(ScannerPlugin):
    """This plugin performs SPF validation using the pyspf module https://pypi.python.org/pypi/pyspf/
    by default, it just logs the result (test mode)

    to enable actual rejection of messages, add a config option on_<resulttype> with a valid postfix action. eg:

    on_fail = REJECT

    valid result types are: 'pass', 'permerror', 'fail', 'temperror', 'softfail', 'none', and 'neutral'
    """
                                         
    def __init__(self,config,section=None):
        ScannerPlugin.__init__(self,config,section)
        self.logger=self._logger()
        
        self.requiredvars={
            'ip_whitelist_file':{
                'default':'',
                'description':'file containing a list of ip adresses to be exempted from SPF checks. Supports CIDR notation if the netaddr module is installed. 127.0.0.0/8 is always exempted',
            },
            'domain_selective_spf_file':{
                'default':'',
                'description':'if this is non-empty, only sender domains in this file will be checked for SPF',
            },
            'on_fail':{
                'default':'DUNNO',
                'description':'Action for SPF fail.',
            },
            'on_softfail':{
                'default':'DUNNO',
                'description':'Action for SPF softfail.',
            },
            'messagetemplate':{
                'default':'SPF ${result} for domain ${from_domain} from ${client_address} : ${explanation}'
            }
        }
        
        self.ip_whitelist_loader=None
        self.ip_whitelist=[] # either a list of plain ip adress strings or a list of IPNetwork if netaddr is available

        self.selective_domain_loader=None


    def is_private_address(self,addr):
        if addr=='127.0.0.1' or addr=='::1' or addr.startswith('10.') or addr.startswith('192.168.') or addr.startswith('fe80:'):
            return True
        if not addr.startswith('172.'):
            return False
        for i in range(16,32):
            if addr.startswith('172.%s'%i):
                return True
        return False


    def ip_whitelisted(self,addr):
        if self.is_private_address(addr):
            return True

        #check ip whitelist
        ip_whitelist_file=self.config.get(self.section,'ip_whitelist_file')
        if ip_whitelist_file is not None:
            plainlist = []
            if self.ip_whitelist_loader is None:
                self.ip_whitelist_loader=FileList(ip_whitelist_file,lowercase=True)

            if self.ip_whitelist_loader.file_changed():
                plainlist=self.ip_whitelist_loader.get_list()

                if have_netaddr:
                    self.ip_whitelist=[IPNetwork(x) for x in plainlist]
                else:
                    self.ip_whitelist=plainlist

            if have_netaddr:
                checkaddr=IPAddress(addr)
                for net in self.ip_whitelist:
                    if checkaddr in net:
                        return True
            else:
                if addr in plainlist:
                    return True
        return False


    def examine(self,suspect):
        if not have_spf:
            return DUNNO
        
        client_address=suspect.get_value('client_address')
        helo_name=suspect.get_value('helo_name')
        sender=suspect.get_value('sender')
        if client_address is None or helo_name is None or sender is None:
            self.logger.error('missing client_address or helo or sender')
            return DUNNO

        if self.ip_whitelisted(client_address):
            self.logger.info("Client %s is whitelisted - no SPF check"%client_address)
            return DUNNO

        sender_email = strip_address(sender)
        if sender_email=='' or sender_email is None:
            return DUNNO

        selective_sender_domain_file=self.config.get(self.section,'domain_selective_spf_file')
        if selective_sender_domain_file!='':
            if self.selective_domain_loader is None:
                self.selective_domain_loader=FileList(selective_sender_domain_file,lowercase=True)
            try:
                sender_domain = extract_domain(sender_email)
                if sender_domain is None:
                    return DUNNO
            except ValueError as e:
                self.logger.warning(str(e))
                return DUNNO
            if not sender_domain.lower() in self.selective_domain_loader.get_list():
                return DUNNO

        result, explanation = spf.check2(client_address, sender_email, helo_name)
        suspect.tags['spf']=result
        if result!='none':
            self.logger.info('SPF client=%s, sender=%s, h=%s result=%s : %s' % (client_address, sender_email, helo_name, result,explanation))
        
        action = DUNNO
        message = apply_template(self.config.get(self.section,'messagetemplate'),suspect,dict(result=result,explanation=explanation))

        configopt='on_%s'%result
        if self.config.has_option(self.section,configopt):
            action=self.config.get(self.section,configopt)

        return action, message
         
        
    
    def lint(self):
        lint_ok = True
        
        if not have_spf:
            print 'pyspf or pydns module not installed - this plugin will do nothing'
            lint_ok = False
            
        if not have_netaddr:
            print 'WARNING: netaddr python module not installed - IP whitelist will not support CIDR notation'

        if not self.checkConfig():
            print 'Error checking config'
            lint_ok = False
        
        return lint_ok

    def __str__(self):
        return "SPF"