Exemple #1
0
 def __init__(self, config, section=None):
     ScannerPlugin.__init__(self, config, section)
     self.logger = self._logger()
     self.skiplist = FileList(filename=None,
                              strip=True,
                              skip_empty=True,
                              skip_comments=True,
                              lowercase=True)
     self.requiredvars = {
         'max_lookups': {
             'default': '10',
             'description':
             'maximum number of lookups (RFC defaults to 10)',
         },
         'skiplist': {
             'default':
             '',
             'description':
             'File containing a list of domains (one per line) which are not checked'
         },
         'temperror_retries': {
             'default': '3',
             'description': 'maximum number of retries on temp error',
         },
         'temperror_sleep': {
             'default': '3',
             'description':
             'waiting interval between retries on temp error',
         },
     }
Exemple #2
0
 def __init__(self, config, section=None):
     ScannerPlugin.__init__(self, config, section)
     self.requiredvars = {
         'domainsfile': {
             'description':
             "File containing a list of domains (one per line) which must be DKIM and/or SPF authenticated",
             'default': "/etc/fuglu/auth_required_domains.txt",
         },
         'failaction': {
             'default':
             'DUNNO',
             'description':
             "action if the message doesn't pass authentication (DUNNO, REJECT)",
         },
         'rejectmessage': {
             'default':
             'sender domain ${header_from_domain} must pass DKIM and/or SPF authentication',
             'description':
             "reject message template if running in pre-queue mode",
         },
     }
     self.logger = self._logger()
     self.filelist = FileList(filename=None,
                              strip=True,
                              skip_empty=True,
                              skip_comments=True,
                              lowercase=True)
Exemple #3
0
 def test_filelist(self):
     self.assertEqual(
         FileList(filename=self.filename,
                  strip=True,
                  skip_empty=True,
                  skip_comments=True,
                  lowercase=False,
                  additional_filters=None).get_list(),
         ['CASE?', 'stripped ?'])
     self.assertEqual(
         FileList(filename=self.filename,
                  strip=False,
                  skip_empty=True,
                  skip_comments=True,
                  lowercase=False,
                  additional_filters=None).get_list(),
         ['CASE?', '    stripped ?    ', '    '])
     self.assertEqual(
         FileList(filename=self.filename,
                  strip=True,
                  skip_empty=False,
                  skip_comments=False,
                  lowercase=False,
                  additional_filters=None).get_list(),
         ['CASE?', 'stripped ?', '', '', '# no comment!'])
     self.assertEqual(
         FileList(filename=self.filename,
                  strip=True,
                  skip_empty=True,
                  skip_comments=True,
                  lowercase=True,
                  additional_filters=None).get_list(),
         ['case?', 'stripped ?'])
Exemple #4
0
    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.logger = self._logger()
        self.filelist = FileList(strip=True,
                                 skip_empty=True,
                                 skip_comments=True,
                                 lowercase=True,
                                 additional_filters=None,
                                 minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default':
                '/etc/fuglu/spearphish-domains',
                'description':
                'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default':
                'DEFAULTVIRUSACTION',
                'description':
                "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default':
                'threat detected: ${virusname}',
                'description':
                "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
            'dbconnection': {
                'default':
                "mysql://root@localhost/spfcheck?charset=utf8",
                'description':
                'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query': {
                'default':
                "SELECT check_spearphish from domain where domain_name=:domain",
                'description':
                'get from sql database :domain will be replaced with the actual domain name. must return boolean field check_spearphish',
            },
            'check_display_part': {
                'default':
                'False',
                'description':
                "set to True to also check display part of From header (else email part only)",
            },
        }
Exemple #5
0
 def _init_nobounce(self):
     if self.nobounce is None:
         try:
             filepath = self.config.get('main', 'nobouncefile')
         except Exception:
             filepath = None
         if filepath and os.path.exists(filepath):
             self.nobounce = FileList(filepath)
         elif filepath:
             self.logger.warning('nobouncefile %s not found' % filepath)
    def _is_whitelisted(self, from_domain):
        whitelist_file = self.config.get(self.section, 'whitelist_file')
        if whitelist_file == '':
            return False

        if self.whitelist is None:
            self.whitelist = FileList(whitelist_file, lowercase=True)

        whitelisted = False
        if from_domain in self.whitelist.get_list():
            whitelisted = True

        return whitelisted
Exemple #7
0
    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.filelist = FileList(strip=True, skip_empty=True, skip_comments=True, lowercase=True,
                                 additional_filters=None, minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default': '/etc/fuglu/spearphish-domains',
                'description': 'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default': 'DEFAULTVIRUSACTION',
                'description': "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default': 'threat detected: ${virusname}',
                'description': "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
        }
Exemple #8
0
 def __init__(self, config, section=None):
     ScannerPlugin.__init__(self, config, section)
     self.logger = self._logger()
     self.skiplist = FileList(filename=None,
                              strip=True,
                              skip_empty=True,
                              skip_comments=True,
                              lowercase=True)
     self.requiredvars = {
         'skiplist': {
             'default':
             '',
             'description':
             'File containing a list of domains (one per line) which are not checked'
         },
     }
Exemple #9
0
    def _init_tldmagic(self):
        init_tldmagic = False
        extratlds = []

        if self.extratlds is None:
            extratldfile = self.config.get(self.section, 'extra_tld_file')
            if extratldfile and os.path.exists(extratldfile):
                self.extratlds = FileList(extratldfile, lowercase=True)
                init_tldmagic = True

        if self.extratlds is not None:
            extratlds = self.extratlds.get_list()
            if self.lasttlds != extratlds:  # extra tld file changed
                self.lasttlds = extratlds
                init_tldmagic = True

        if self.tldmagic is None or init_tldmagic:
            self.tldmagic = TLDMagic()
            for tld in extratlds:  # add extra tlds to tldmagic
                self.tldmagic.add_tld(tld)
Exemple #10
0
    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.filelist = FileList(strip=True,
                                 skip_empty=True,
                                 skip_comments=True,
                                 lowercase=True,
                                 additional_filters=None,
                                 minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default':
                '/etc/fuglu/spearphish-domains',
                'description':
                'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default':
                'DEFAULTVIRUSACTION',
                'description':
                "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default':
                'threat detected: ${virusname}',
                'description':
                "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
        }
    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.requiredvars = {
            'domainsfile': {
                'description': "File containing a list of domains (one per line) which must be DKIM and/or SPF authenticated",
                'default': "/etc/fuglu/auth_required_domains.txt",
            },
            'failaction': {
                'default': 'DUNNO',
                'description': "action if the message doesn't pass authentication (DUNNO, REJECT)",
            },

            'rejectmessage': {
                'default': 'sender domain ${header_from_domain} must pass DKIM and/or SPF authentication',
                'description': "reject message template if running in pre-queue mode",
            },
        }
        self.logger = self._logger()
        self.filelist=FileList(filename=None,strip=True, skip_empty=True, skip_comments=True,lowercase=True)
Exemple #12
0
    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.logger = self._logger()
        self.filelist = FileList(strip=True, skip_empty=True, skip_comments=True, lowercase=True,
                                 additional_filters=None, minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default': '/etc/fuglu/spearphish-domains',
                'description': 'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default': 'DEFAULTVIRUSACTION',
                'description': "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default': 'threat detected: ${virusname}',
                'description': "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
            'dbconnection':{
                'default':"mysql://root@localhost/spfcheck?charset=utf8",
                'description':'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query':{
                'default':"SELECT check_spearphish from domain where domain_name=:domain",
                'description':'get from sql database :domain will be replaced with the actual domain name. must return boolean field check_spearphish',
            },
            'check_display_part': {
                'default': 'False',
                'description': "set to True to also check display part of From header (else email part only)",
            },
        }
Exemple #13
0
class SpearPhishPlugin(ScannerPlugin):
    """Mark spear phishing mails as virus

    The spearphish plugin checks if the sender domain in the "From"-Header matches the envelope recipient Domain ("Mail
from my own domain") but the message usesa different envelope sender domain. This blocks many spearphish attempts.

    Note that this plugin can cause blocks of legitimate mail , for example if the recipient domain is using a third party service
    to send newsletters in their name. Such services often set the customers domain in the from headers but use their own domains in the envelope for
     bounce processing. Use the 'Plugin Skipper' or any other form of whitelisting in such cases.
    """
    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.filelist = FileList(strip=True,
                                 skip_empty=True,
                                 skip_comments=True,
                                 lowercase=True,
                                 additional_filters=None,
                                 minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default':
                '/etc/fuglu/spearphish-domains',
                'description':
                'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default':
                'DEFAULTVIRUSACTION',
                'description':
                "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default':
                'threat detected: ${virusname}',
                'description':
                "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
        }

    def should_we_check_this_domain(self, suspect):
        domainsfile = self.config.get(self.section, 'domainsfile')
        if domainsfile.strip() == '':  # empty config -> check all domains
            return True

        if not os.path.exists(domainsfile):
            return False

        self.filelist.filename = domainsfile
        envelope_recipient_domain = suspect.to_domain.lower()
        checkdomains = self.filelist.get_list()
        return envelope_recipient_domain in checkdomains

    def examine(self, suspect):
        if not self.should_we_check_this_domain(suspect):
            return DUNNO
        envelope_recipient_domain = suspect.to_domain.lower()
        envelope_sender_domain = suspect.from_domain.lower()
        if envelope_sender_domain == envelope_recipient_domain:
            return DUNNO  # we only check the message if the env_sender_domain differs. If it's the same it will be caught by other means (like SPF)

        header_from_domain = extract_from_domain(suspect)
        if header_from_domain is None:
            self._logger().warn(
                "%s: Could not extract header from domain for spearphish check"
                % suspect.id)
            return DUNNO

        if header_from_domain == envelope_recipient_domain:
            virusname = self.config.get(self.section, 'virusname')
            virusaction = self.config.get(self.section, 'virusaction')
            actioncode = string_to_actioncode(virusaction, self.config)

            logmsg = '%s: spear phish pattern detected, recipient=%s env_sender_domain=%s header_from_domain=%s' % (
                suspect.id, suspect.to_address, envelope_sender_domain,
                header_from_domain)
            self._logger().info(logmsg)
            self.flag_as_phish(suspect, virusname)

            message = apply_template(
                self.config.get(self.section, 'rejectmessage'), suspect,
                {'virusname': virusname})
            return actioncode, message

        return DUNNO

    def flag_as_phish(self, suspect, virusname):
        suspect.tags['%s.virus' %
                     self.config.get(self.section, 'virusenginename')] = {
                         'message content': virusname
                     }
        suspect.tags['virus'][self.config.get(self.section,
                                              'virusenginename')] = True

    def __str__(self):
        return "Spearphish Check"

    def lint(self):
        allok = self.checkConfig() and self.lint_file()
        return allok

    def lint_file(self):
        filename = self.config.get(self.section, 'domainsfile')
        if not os.path.exists(filename):
            print("Spearphish domains file %s not found" % (filename))
            return False
        return True
Exemple #14
0
class DKIMVerifyPlugin(ScannerPlugin):
    """**EXPERIMENTAL**
This plugin checks the DKIM signature of the message and sets tags...
DKIMVerify.sigvalid : True if there was a valid DKIM signature, False if there was an invalid DKIM signature
the tag is not set if there was no dkim header at all

DKIMVerify.skipreason: set if the verification has been skipped

The plugin does not take any action based on the DKIM test result since a failed DKIM validation by itself
should not cause a message to be treated any differently. Other plugins might use the DKIM result
in combination with other factors to take action (for example a "DMARC" plugin could use this information)

It is currently recommended to leave both header and body canonicalization as 'relaxed'. Using 'simple' can cause the signature to fail.
    """
    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.logger = self._logger()
        self.skiplist = FileList(filename=None,
                                 strip=True,
                                 skip_empty=True,
                                 skip_comments=True,
                                 lowercase=True)
        self.requiredvars = {
            'skiplist': {
                'default':
                '',
                'description':
                'File containing a list of domains (one per line) which are not checked'
            },
        }

    def __str__(self):
        return "DKIM Verify"

    def examine(self, suspect):
        if not DKIMPY_AVAILABLE:
            suspect.debug("dkimpy not available, can not check")
            suspect.set_tag('DKIMVerify.skipreason',
                            'dkimpy library not available')
            return DUNNO

        hdr_from_domain = extract_from_domain(suspect)
        if not hdr_from_domain:
            self.logger.debug(
                '%s DKIM Verification skipped, no header from address')
            suspect.set_tag("DKIMVerify.skipreason", 'no header from address')
            return DUNNO

        self.skiplist.filename = self.config.get(self.section, 'skiplist')
        skiplist = self.skiplist.get_list()
        if hdr_from_domain in skiplist:
            self.logger.debug(
                '%s DKIM Verification skipped, sender domain skiplisted')
            suspect.set_tag("DKIMVerify.skipreason",
                            'sender domain skiplisted')
            return DUNNO

        source = suspect.get_original_source()
        if "dkim-signature" not in suspect.get_message_rep():
            suspect.set_tag('DKIMVerify.skipreason', 'not dkim signed')
            suspect.write_sa_temp_header('X-DKIMVerify', 'unsigned')
            suspect.debug("No dkim signature header found")
            return DUNNO
        # use the local logger of the plugin but prepend the fuglu id
        d = DKIM(source,
                 logger=PrependLoggerMsg(self.logger,
                                         prepend=suspect.id,
                                         maxlevel=logging.INFO))

        try:
            try:
                valid = d.verify()
            except DKIMException as de:
                self.logger.warning("%s: DKIM validation failed: %s" %
                                    (suspect.id, str(de)))
                valid = False
            suspect.set_tag("DKIMVerify.sigvalid", valid)
            suspect.write_sa_temp_header('X-DKIMVerify',
                                         'valid' if valid else 'invalid')
        except NameError as ne:
            self.logger.warning(
                "%s: DKIM validation failed due to missing dependency: %s" %
                (suspect.id, str(ne)))
            suspect.set_tag('DKIMVerify.skipreason', 'plugin error')
        except Exception as e:
            self.logger.warning("%s: DKIM validation failed: %s" %
                                (suspect.id, str(e)))
            suspect.set_tag('DKIMVerify.skipreason', 'plugin error')

        return DUNNO

    def lint(self):
        if not DKIMPY_AVAILABLE:
            print("Missing dependency: dkimpy https://launchpad.net/dkimpy")
            print("(also requires either dnspython or pydns)")
            return False

        return self.check_config()
Exemple #15
0
class SPFPlugin(ScannerPlugin):
    """**EXPERIMENTAL**
This plugin checks the SPF status and sets tag 'SPF.status' to one of the official states 'pass', 'fail', 'neutral',
'softfail, 'permerror', 'temperror' or 'skipped' if the SPF check could not be peformed.
Tag 'SPF.explanation' contains a human readable explanation of the result.
Additionally information to be used by SA plugin is added

The plugin does not take any action based on the SPF test result since. Other plugins might use the SPF result
in combination with other factors to take action (for example a "DMARC" plugin could use this information)
    """
    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.logger = self._logger()
        self.skiplist = FileList(filename=None,
                                 strip=True,
                                 skip_empty=True,
                                 skip_comments=True,
                                 lowercase=True)
        self.requiredvars = {
            'max_lookups': {
                'default': '10',
                'description':
                'maximum number of lookups (RFC defaults to 10)',
            },
            'skiplist': {
                'default':
                '',
                'description':
                'File containing a list of domains (one per line) which are not checked'
            },
            'temperror_retries': {
                'default': '3',
                'description': 'maximum number of retries on temp error',
            },
            'temperror_sleep': {
                'default': '3',
                'description':
                'waiting interval between retries on temp error',
            },
        }

    def __str__(self):
        return "SPF Check"

    def _spf_lookup(self, ip, from_address, helo, retries=3):
        spf.MAX_LOOKUP = self.config.getint(self.section, 'max_lookups')
        result, explanation = spf.check2(ip, from_address, helo)
        if result == 'temperror' and retries > 0:
            time.sleep(self.config.getint(self.section, 'temperror_sleep'))
            retries -= 1
            result, explanation = self._spf_lookup(ip, from_address, helo,
                                                   retries)
        return result, explanation

    def examine(self, suspect):
        if not PYSPF_AVAILABLE:
            suspect.debug("pyspf not available, can not check")
            self.logger.warning("%s: SPF Check skipped, pyspf unavailable" %
                                suspect.id)
            suspect.set_tag('SPF.status', 'skipped')
            suspect.set_tag("SPF.explanation", 'missing dependency')
            return DUNNO

        self.skiplist.filename = self.config.get(self.section, 'skiplist')
        checkdomains = self.skiplist.get_list()
        if suspect.from_domain in checkdomains:
            self.logger.debug('%s SPF Check skipped, sender domain skiplisted')
            suspect.set_tag('SPF.status', 'skipped')
            suspect.set_tag("SPF.explanation", 'sender domain skiplisted')
            return DUNNO

        clientinfo = suspect.get_client_info(self.config)
        if clientinfo is None:
            suspect.debug("client info not available for SPF check")
            self.logger.warning(
                "%s: SPF Check skipped, could not get client info" %
                suspect.id)
            suspect.set_tag('SPF.status', 'skipped')
            suspect.set_tag("SPF.explanation",
                            'could not extract client information')
            return DUNNO

        helo, ip, revdns = clientinfo
        retries = self.config.getint(self.section, 'temperror_retries')
        try:
            result, explanation = self._spf_lookup(ip, suspect.from_address,
                                                   helo, retries)
            suspect.set_tag("SPF.status", result)
            suspect.set_tag("SPF.explanation", explanation)
            suspect.write_sa_temp_header('X-SPFCheck', result)
            suspect.debug("SPF status: %s (%s)" % (result, explanation))
        except Exception as e:
            suspect.set_tag('SPF.status', 'skipped')
            suspect.set_tag("SPF.explanation", str(e))
            self.logger.warning('%s SPF check failed for %s due to %s' %
                                (suspect.id, suspect.from_domain, str(e)))

        return DUNNO

    def lint(self):
        all_ok = self.check_config()

        if not PYSPF_AVAILABLE:
            print("Missing dependency: pyspf")
            all_ok = False

        if not DNSQUERY_EXTENSION_ENABLED:
            print(
                "Missing dependency: no supported DNS libary found: pydns or dnspython"
            )
            all_ok = False

        if not (IPADDR_AVAILABLE or IPADDRESS_AVAILABLE):
            print(
                "Missing dependency: no supported ip address libary found: ipaddr or ipaddress"
            )
            all_ok = False

        return all_ok
Exemple #16
0
class DomainAction(ScannerPlugin):
    """Perform Action based on Domains in message body"""
    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.logger = self._logger()

        self.requiredvars = {
            'blacklistconfig': {
                'default': '/etc/fuglu/rbl.conf',
                'description': 'RBL Lookup config file',
            },
            'checksubdomains': {
                'default':
                'yes',
                'description':
                'check subdomains as well (from top to bottom, eg. example.com, bla.example.com, blubb.bla.example.com',
            },
            'action': {
                'default': 'reject',
                'description': 'action on hit (reject, delete, etc)',
            },
            'message': {
                'default': '5.7.1 black listed URL ${domain} by ${blacklist}',
                'description': 'message template for rejects/ok messages',
            },
            'maxdomains': {
                'default': '10',
                'description':
                'maximum number of domains to check per message',
            },
            'extra_tld_file': {
                'default':
                '',
                'description':
                'directory containing files with extra TLDs (2TLD or inofficial TLDs)'
            },
        }

        self.rbllookup = None
        self.tldmagic = None
        self.extratlds = None
        self.lasttlds = None

    def _init_tldmagic(self):
        init_tldmagic = False
        extratlds = []

        if self.extratlds is None:
            extratldfile = self.config.get(self.section, 'extra_tld_file')
            if extratldfile and os.path.exists(extratldfile):
                self.extratlds = FileList(extratldfile, lowercase=True)
                init_tldmagic = True

        if self.extratlds is not None:
            extratlds = self.extratlds.get_list()
            if self.lasttlds != extratlds:  # extra tld file changed
                self.lasttlds = extratlds
                init_tldmagic = True

        if self.tldmagic is None or init_tldmagic:
            self.tldmagic = TLDMagic()
            for tld in extratlds:  # add extra tlds to tldmagic
                self.tldmagic.add_tld(tld)

    def examine(self, suspect):
        if not DOMAINMAGIC_AVAILABLE:
            self.logger.info('Not scanning - Domainmagic not available')
            return DUNNO

        if self.rbllookup is None:
            self.rbllookup = RBLLookup()
            self.rbllookup.from_config(
                self.config.get(self.section, 'blacklistconfig'))
        self._init_tldmagic()

        urls = suspect.get_tag('body.uris', defaultvalue=[])
        #self.logger.info("Body URIs to check: %s"%urls)
        domains = set(map(fqdn_from_uri, urls))

        counter = 0
        for domain in domains:
            counter += 1
            if counter > self.config.getint(self.section, 'maxdomains'):
                self.logger.info("maximum number of domains reached")
                break

            tldcount = self.tldmagic.get_tld_count(domain)
            parts = domain.split('.')

            if self.config.getboolean(self.section, 'checksubdomains'):
                subrange = range(tldcount + 1, len(parts) + 1)
            else:
                subrange = [tldcount + 1]

            for subindex in subrange:
                subdomain = '.'.join(parts[-subindex:])

                listings = self.rbllookup.listings(subdomain)
                for identifier, humanreadable in iter(listings.items()):
                    self.logger.info(
                        "%s : url host %s flagged as %s because %s" %
                        (suspect.id, domain, identifier, humanreadable))
                    return string_to_actioncode(
                        self.config.get(self.section, 'action'),
                        self.config), apply_template(
                            self.config.get(self.section, 'message'), suspect,
                            dict(domain=domain, blacklist=identifier))

        return DUNNO

    def lint(self):
        allok = True
        if not DOMAINMAGIC_AVAILABLE:
            print(
                "ERROR: domainmagic lib or one of its dependencies (dnspython/pygeoip) is not installed!"
            )
            allok = False

        if allok:
            allok = self.check_config()

        if allok:
            extratldfile = self.config.get(self.section, 'extra_tld_file')
            if extratldfile and not os.path.exists(extratldfile):
                allok = False
                print('WARNING: invalid extra_tld_file %s specified' %
                      extratldfile)

        return allok
Exemple #17
0
class DomainAuthPlugin(ScannerPlugin):

    """**EXPERIMENTAL**
This plugin checks the header from domain against a list of domains which must be authenticated by DKIM and/or SPF.
This is somewhat similar to DMARC but instead of asking the sender domain for a DMARC policy record this plugin allows you to force authentication on the recipient side.

This plugin depends on tags written by SPFPlugin and DKIMVerifyPlugin, so they must run beforehand.
    """

    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.requiredvars = {
            'domainsfile': {
                'description': "File containing a list of domains (one per line) which must be DKIM and/or SPF authenticated",
                'default': "/etc/fuglu/auth_required_domains.txt",
            },
            'failaction': {
                'default': 'DUNNO',
                'description': "action if the message doesn't pass authentication (DUNNO, REJECT)",
            },

            'rejectmessage': {
                'default': 'sender domain ${header_from_domain} must pass DKIM and/or SPF authentication',
                'description': "reject message template if running in pre-queue mode",
            },
        }
        self.logger = self._logger()
        self.filelist = FileList(
            filename=None, strip=True, skip_empty=True, skip_comments=True, lowercase=True)

    def examine(self, suspect):
        self.filelist.filename = self.config.get(self.section, 'domainsfile')
        checkdomains = self.filelist.get_list()

        envelope_sender_domain = suspect.from_domain.lower()
        header_from_domain = extract_from_domain(suspect)
        if header_from_domain is None:
            return

        if header_from_domain not in checkdomains:
            return

        # TODO: do we need a tag from dkim to check if the verified dkim domain
        # actually matches the header from domain?
        dkimresult = suspect.get_tag('DKIMVerify.sigvalid', False)
        if dkimresult == True:
            return DUNNO

        # DKIM failed, check SPF if envelope senderdomain belongs to header
        # from domain
        spfresult = suspect.get_tag('SPF.status', 'unknown')
        if (envelope_sender_domain == header_from_domain or envelope_sender_domain.endswith('.%s' % header_from_domain)) and spfresult == 'pass':
            return DUNNO

        failaction = self.config.get(self.section, 'failaction')
        actioncode = string_to_actioncode(failaction, self.config)

        values = dict(
            header_from_domain=header_from_domain)
        message = apply_template(
            self.config.get(self.section, 'rejectmessage'), suspect, values)
        return actioncode, message

    def flag_as_spam(self, suspect):
        suspect.tags['spam']['domainauth'] = True

    def __str__(self):
        return "DomainAuth"

    def lint(self):
        allok = self.checkConfig() and self.lint_file()
        return allok

    def lint_file(self):
        filename = self.config.get(self.section, 'domainsfile')
        if not os.path.exists(filename):
            print("domains file %s not found" % (filename))
            return False
        return True
class EBLLookup(ScannerPlugin):
    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.logger = self._logger()

        self.whitelist = None

        self.requiredvars = {
            'whitelist_file': {
                'default':
                '/etc/fuglu/conf.d/ebl-whitelist.txt',
                'description':
                'path to file containing whitelisted sender domains',
            },
            'dnszone': {
                'default': 'ebl.msbl.org',
                'description':
                'the DNS zone to query. defaults to ebl.msbl.org',
            },
            'hash': {
                'default':
                'sha1',
                'description':
                'hash function used by DNS zone. Use one of md5, sha1, sha224, sha256, sha384, sha512'
            },
            'response': {
                'default': '127.0.0.2',
                'description': 'expected response of zone query',
            },
            'action': {
                'default':
                'dunno',
                'description':
                'action on hit (dunno, reject, defer, delete). if set to dunno will tag as spam. do not use reject/defer in after queue mode',
            },
            'messagetemplate': {
                'default': '${sender} listed by ${dnszone} : ${message}',
                'description': 'reject message template',
            },
            'maxlookups': {
                'default':
                '10',
                'description':
                'maximum number of email addresses to check per message',
            },
            'check_always': {
                'default':
                'False',
                'description':
                'set to True to check every suspect. set to False to only check mail that has not yet been classified as spam or virus',
            },
            'normalisation': {
                'default':
                'ebl',
                'description':
                'type of normalisation to be applied to email addresses before hashing. choose one of ebl (full normalisation according to ebl.msbl.org standard), low (lowercase only)'
            }
        }

    def _is_whitelisted(self, from_domain):
        whitelist_file = self.config.get(self.section, 'whitelist_file')
        if whitelist_file == '':
            return False

        if self.whitelist is None:
            self.whitelist = FileList(whitelist_file, lowercase=True)

        whitelisted = False
        if from_domain in self.whitelist.get_list():
            whitelisted = True

        return whitelisted

    def _email_normalise_ebl(self, address):
        if not '@' in address:
            self.logger.error('Not an email address: %s' % address)
            return address

        address = address.lower()

        lhs, domain = address.rsplit('@', 1)
        domainparts = domain.split('.')

        if 'googlemail' in domainparts:  # replace googlemail with gmail
            tld = '.'.join(domainparts[1:])
            domain = 'gmail.%s' % tld
            domainparts = ['gmail', tld]

        if '+' in lhs:  # strip all '+' tags
            lhs = lhs.split('+')[0]

        if 'gmail' in domainparts:  # discard periods in gmail
            lhs = lhs.replace('.', '')

        if 'yahoo' in domainparts or 'ymail' in domainparts:  # strip - tags from yahoo
            lhs = lhs.split('-')[0]

        lhs = re.sub('^(envelope-from|id|r|receiver)=', '',
                     lhs)  # strip mail log prefixes

        return '%s@%s' % (lhs, domain)

    def _email_normalise_low(self, address):
        address = address.lower()
        return address

    def _email_normalise(self, address):
        n = self.config.get(self.section, 'normalisation')
        if n == 'ebl':
            address = self._email_normalise_ebl(address)
        elif n == 'low':
            address = self._email_normalise_low(address)
        return address

    def _create_hash(self, value):
        hashtype = self.config.get(self.section, 'hash').lower()

        if hasattr(hashlib, 'algorithms_guaranteed'):
            algorithms = hashlib.algorithms_guaranteed
        else:
            algorithms = ['md5', 'sha1']  #python 2.6

        if hashtype in algorithms:
            hasher = getattr(hashlib, hashtype)
            myhash = hasher(force_bString(value)).hexdigest()
        else:
            myhash = ''
        return myhash

    def _ebl_lookup(self, addr_hash):
        listed = False
        message = None

        dnszone = self.config.get(self.section, 'dnszone')
        response = self.config.get(self.section, 'response')
        query = '%s.%s' % (addr_hash, dnszone)
        result = lookup(query)
        if result is not None:
            for rec in result:
                if rec == response:
                    listed = True
                    result = lookup(query, qtype='TXT')
                    if result:
                        message = result[0]
                    break

        return listed, message

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

        if not self.config.getboolean(self.section, 'check_always'):
            # save the lookup if mail is already tagged as virus or spam
            if suspect.is_virus() or suspect.is_spam() or suspect.is_blocked():
                return DUNNO

        maxlookups = self.config.getint(self.section, 'maxlookups')
        emails = suspect.get_tag('emails', defaultvalue=[])[:maxlookups]
        emails = [self._email_normalise(email) for email in emails]
        emails = list(set(emails))

        #if emails:
        #    self.logger.debug('%s EBL checking addresses %s' % (suspect.id, ', '.join(emails)))

        listed = False
        action = DUNNO
        message = None
        email = None
        for email in emails:
            addr_hash = self._create_hash(email)
            listed, message = self._ebl_lookup(addr_hash)
            if listed:
                break

        suspect.tags['spam']['EBL'] = listed

        if listed:
            self.logger.debug('%s EBL hit for %s' % (suspect.id, email))
            action = string_to_actioncode(
                self.config.get(self.section, 'action'))
            suspect.tags['EBL.email'] = email
            suspect.tags['EBL.reason'] = message
            if action != DUNNO:
                values = {
                    'dnszone': self.config.get(self.section, 'dnszone'),
                    'message': message,
                }
                message = apply_template(
                    self.config.get(self.section, 'messagetemplate'), suspect,
                    values)

        return action, message

    def lint(self):
        dnszone = self.config.get(self.section, 'dnszone')
        print('querying zone %s' % dnszone)

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

        if not DNSQUERY_EXTENSION_ENABLED:
            print(
                "no DNS resolver library available - this plugin will do nothing"
            )
            lint_ok = False

        if hasattr(hashlib, 'algorithms_guaranteed'):
            algorithms = hashlib.algorithms_guaranteed
        else:
            algorithms = ['md5', 'sha1']  #python 2.6
            print('old version of hashlib, consider upgrade')

        hashtype = self.config.get(self.section, 'hash').lower()
        if hashtype not in algorithms:
            lint_ok = False
            print('unsupported hash type %s' % hashtype)

        normalisation = self.config.get(self.section, 'normalisation')
        if normalisation not in ['ebl', 'low']:
            lint_ok = False
            print('unsupported normalisation type %s' % normalisation)

        if lint_ok:
            addr_hash = self._create_hash('*****@*****.**')
            listed, message = self._ebl_lookup(addr_hash)
            if not listed:
                lint_ok = False
                print('test entry not found in dns zone')
            else:
                print('test entry found in dns zone: %s' % message)

        if lint_ok:
            whitelist_file = self.config.get(self.section, 'whitelist_file')
            if whitelist_file.strip() == '':
                print('No whitelist defined')

        return lint_ok
Exemple #19
0
class Bounce(object):
    """Send Mail (Bounces)"""
    def __init__(self, config):
        self.logger = logging.getLogger('fuglu.bouncer')
        self.config = config
        self.nobounce = None

    def _init_nobounce(self):
        if self.nobounce is None:
            try:
                filepath = self.config.get('main', 'nobouncefile')
            except Exception:
                filepath = None
            if filepath and os.path.exists(filepath):
                self.nobounce = FileList(filepath)
            elif filepath:
                self.logger.warning('nobouncefile %s not found' % filepath)

    def _add_required_headers(self, recipient, messagecontent):
        """add headers required for sending automated mail"""

        msgrep = email.message_from_bytes(force_bString(messagecontent))
        msgrep.set_charset(
            "utf-8")  # define unicode because the messagecontent is unicode

        if not 'to' in msgrep:
            msgrep['To'] = Header("<%s>" % recipient).encode()

        if not 'From' in msgrep:
            msgrep['from'] = Header("<MAILER-DAEMON@%s>" %
                                    socket.gethostname()).encode()

        if not 'auto-submitted' in msgrep:
            msgrep['auto-submitted'] = Header('auto-generated').encode()

        if not 'date' in msgrep:
            msgrep['Date'] = formatdate(localtime=True)

        if not 'Message-id' in msgrep:
            msgrep['Message-ID'] = make_msgid()

        return msgrep.as_string()

    def send_template_file(self, recipient, templatefile, suspect, values):
        """Send a E-Mail Bounce Message

        Args:
            recipient    (str):  Message recipient ([email protected])
            templatefile (str): Template to use
            suspect      (fuglu.shared.Suspect) suspect that caused the bounce
            values            :Values to apply to the template. ensure all values are of type <str>

        If the suspect has the 'nobounce' tag set, the message will not be sent. The same happens
        if the global configuration 'disablebounces' is set.
        """

        if not os.path.exists(templatefile):
            self.logger.error('Template file does not exist: %s' %
                              templatefile)
            return

        with open(templatefile) as fp:
            filecontent = fp.read()

        queueid = self.send_template_string(recipient, filecontent, suspect,
                                            values)
        return queueid

    def send_template_string(self, recipient, templatecontent, suspect,
                             values):
        """Send a E-Mail Bounce Message

        If the suspect has the 'nobounce' tag set, the message will not be sent. The same happens
        if the global configuration 'disablebounces' is set.

        Args:
            recipient       (unicode or str) : Message recipient ([email protected])
            templatecontent (unicode or str) : Template to use
            suspect         (fuglu.shared.Suspect) : suspect that caused the bounce
            values       : Values to apply to the template
        """
        if suspect.get_tag('nobounce'):
            self.logger.info(
                'Not sending bounce to %s - bounces disabled by plugin' %
                recipient)
            return

        message = apply_template(templatecontent, suspect, values)
        try:
            message = self._add_required_headers(recipient, message)
        except Exception as e:
            self.logger.warning(
                'Bounce message template could not be verified: %s' % str(e))

        self.logger.debug('Sending bounce message to %s' % recipient)
        fromaddress = "<>"
        queueid = self.send(fromaddress, recipient, message)
        return queueid

    def send(self, fromaddress, toaddress, message):
        """really send message"""
        if self.config.getboolean('main', 'disablebounces'):
            self.logger.info(
                'Bounces are disabled in config - not sending message to %s' %
                toaddress)
            return

        self._init_nobounce()
        if self.nobounce and extract_domain(
                toaddress) in self.nobounce.get_list():
            self.logger.info(
                'Bounces to this rcpt are disabled - not sending message to %s'
                % toaddress)
            return

        smtpServer = FugluSMTPClient(
            self.config.get('main', 'bindaddress'),
            self.config.getint('main', 'outgoingport'))
        helo = self.config.get('main', 'outgoinghelo')
        if helo.strip() == '':
            helo = socket.gethostname()
        smtpServer.helo(helo)
        smtpServer.sendmail(fromaddress, toaddress, message)
        smtpServer.quit()
        return smtpServer.queueid

    def _send(self, fromaddress, toaddress, message):
        """deprecated version of send()"""
        self.send(fromaddress, toaddress, message)
Exemple #20
0
class DomainAuthPlugin(ScannerPlugin):

    """**EXPERIMENTAL**
This plugin checks the header from domain against a list of domains which must be authenticated by DKIM and/or SPF.
This is somewhat similar to DMARC but instead of asking the sender domain for a DMARC policy record this plugin allows you to force authentication on the recipient side.

This plugin depends on tags written by SPFPlugin and DKIMVerifyPlugin, so they must run beforehand.
    """

    def __init__(self, config, section=None):
        ScannerPlugin.__init__(self, config, section)
        self.requiredvars = {
            'domainsfile': {
                'description': "File containing a list of domains (one per line) which must be DKIM and/or SPF authenticated",
                'default': "/etc/fuglu/auth_required_domains.txt",
            },
            'failaction': {
                'default': 'DUNNO',
                'description': "action if the message doesn't pass authentication (DUNNO, REJECT)",
            },

            'rejectmessage': {
                'default': 'sender domain ${header_from_domain} must pass DKIM and/or SPF authentication',
                'description': "reject message template if running in pre-queue mode",
            },
        }
        self.logger = self._logger()
        self.filelist = FileList(
            filename=None, strip=True, skip_empty=True, skip_comments=True, lowercase=True)

    def examine(self, suspect):
        self.filelist.filename = self.config.get(self.section, 'domainsfile')
        checkdomains = self.filelist.get_list()

        envelope_sender_domain = suspect.from_domain.lower()
        header_from_domain = self.extract_from_domain(suspect)
        if header_from_domain == None:
            return

        if header_from_domain not in checkdomains:
            return

        # TODO: do we need a tag from dkim to check if the verified dkim domain
        # actually matches the header from domain?
        dkimresult = suspect.get_tag('DKIMVerify.sigvalid', False)
        if dkimresult == True:
            return DUNNO

        # DKIM failed, check SPF if envelope senderdomain belongs to header
        # from domain
        spfresult = suspect.get_tag('SPF.status', 'unknown')
        if (envelope_sender_domain == header_from_domain or envelope_sender_domain.endswith('.%s' % header_from_domain)) and spfresult == 'pass':
            return DUNNO

        failaction = self.config.get(self.section, 'failaction')
        actioncode = string_to_actioncode(failaction, self.config)

        values = dict(
            header_from_domain=header_from_domain)
        message = apply_template(
            self.config.get(self.section, 'rejectmessage'), suspect, values)
        return actioncode, message

    def flag_as_spam(self, suspect):
        suspect.tags['spam']['domainauth'] = True

    def extract_from_domain(self, suspect):
        """
        Try to extract from header domain
        """
        try:
            msgrep = suspect.get_message_rep()
            address = msgrep.get('From')
            if address == None:
                return None

            start = address.find('<') + 1
            if start < 1:
                start = address.find(':') + 1

            if start >= 0:
                end = string.find(address, '>')
                if end < 0:
                    end = len(address)
            retaddr = address[start:end]
            retaddr = retaddr.strip()

            if '@' not in retaddr:
                return None

            domain = retaddr.split('@', 1)[1]

            return domain.lower()
        except:
            return None

    def __str__(self):
        return "DomainAuth"

    def lint(self):
        allok = (self.checkConfig() and self.lint_file())
        return allok

    def lint_file(self):
        filename = self.config.get(self.section, 'domainsfile')
        if not os.path.exists(filename):
            print("domains file %s not found" % (filename))
            return False
        return True
Exemple #21
0
class SpearPhishPlugin(ScannerPlugin):
    """Mark spear phishing mails as virus

    The spearphish plugin checks if the sender domain in the "From"-Header matches the envelope recipient Domain ("Mail
    from my own domain") but the message uses a different envelope sender domain. This blocks many spearphish attempts.

    Note that this plugin can cause blocks of legitimate mail , for example if the recipient domain is using a third party service
    to send newsletters in their name. Such services often set the customers domain in the from headers but use their own domains in the envelope for
    bounce processing. Use the 'Plugin Skipper' or any other form of whitelisting in such cases.
    """

    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.logger = self._logger()
        self.filelist = FileList(strip=True, skip_empty=True, skip_comments=True, lowercase=True,
                                 additional_filters=None, minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default': '/etc/fuglu/spearphish-domains',
                'description': 'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default': 'DEFAULTVIRUSACTION',
                'description': "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default': 'threat detected: ${virusname}',
                'description': "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
            'dbconnection':{
                'default':"mysql://root@localhost/spfcheck?charset=utf8",
                'description':'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query':{
                'default':"SELECT check_spearphish from domain where domain_name=:domain",
                'description':'get from sql database :domain will be replaced with the actual domain name. must return boolean field check_spearphish',
            },
            'check_display_part': {
                'default': 'False',
                'description': "set to True to also check display part of From header (else email part only)",
            },
        }


    def get_domain_setting(self, domain, dbconnection, sqlquery, cache, cachename, default_value=None, logger=None):
        if logger is None:
            logger = logging.getLogger()
        
        cachekey = '%s-%s' % (cachename, domain)
        cached = cache.get_cache(cachekey)
        if cached is not None:
            logger.debug("got cached setting for %s" % domain)
            return cached
    
        settings = default_value
    
        try:
            session = get_session(dbconnection)
    
            # get domain settings
            dom = session.execute(sqlquery, {'domain': domain}).fetchall()
    
            if not dom and not dom[0] and len(dom[0]) == 0:
                logger.warning(
                    "Can not load domain setting - domain %s not found. Using default settings." % domain)
            else:
                settings = dom[0][0]
    
            session.close()
    
        except Exception as e:
            logger.error("Exception while loading setting for %s : %s" % (domain, str(e)))
    
        cache.put_cache(cachekey, settings)
        logger.debug("refreshed setting for %s" % domain)
        return settings
    
    
    def should_we_check_this_domain(self,suspect):
        domainsfile = self.config.get(self.section, 'domainsfile')
        if domainsfile.strip()=='': # empty config -> check all domains
            return True

        if not os.path.exists(domainsfile):
            return False

        self.filelist.filename = domainsfile
        envelope_recipient_domain = suspect.to_domain.lower()
        checkdomains = self.filelist.get_list()
        if envelope_recipient_domain in checkdomains:
            return True
        
        dbconnection = self.config.get(self.section, 'dbconnection').strip()
        sqlquery = self.config.get(self.section,'domain_sql_query')
        do_check = False
        if dbconnection != '':
            cache = get_default_cache()
            cachename = self.section
            do_check = self.get_domain_setting(suspect.to_domain, dbconnection, sqlquery, cache, cachename, False, self.logger)
        return do_check
    
    
    def examine(self, suspect):
        if not self.should_we_check_this_domain(suspect):
            return DUNNO
        envelope_recipient_domain = suspect.to_domain.lower()
        envelope_sender_domain = suspect.from_domain.lower()
        if envelope_sender_domain == envelope_recipient_domain:
            return DUNNO  # we only check the message if the env_sender_domain differs. If it's the same it will be caught by other means (like SPF)
        
        header_from_domains = []
        header_from_domain = extract_from_domain(suspect)
        if header_from_domain is None:
            self.logger.warn("%s: Could not extract header from domain for spearphish check" % suspect.id)
            return DUNNO
        else:
            header_from_domains.append(header_from_domain)
            self.logger.debug('%s: checking domain %s (source: From header address part)' % (suspect.id, header_from_domain))
        
        if self.config.getboolean(self.section, 'check_display_part'):
            display_from_domain = extract_from_domain(suspect, False)
            if display_from_domain is not None and display_from_domain not in header_from_domains:
                header_from_domains.append(display_from_domain)
                self.logger.debug('%s: checking domain %s (source: From header display part)' % (suspect.id, display_from_domain))
        
        actioncode = DUNNO
        message = None
        
        for header_from_domain in header_from_domains:
            if header_from_domain == envelope_recipient_domain:
                virusname = self.config.get(self.section, 'virusname')
                virusaction = self.config.get(self.section, 'virusaction')
                actioncode = string_to_actioncode(virusaction, self.config)
                
                logmsg = '%s: spear phish pattern detected, env_rcpt_domain=%s env_sender_domain=%s header_from_domain=%s' % \
                         (suspect.id, envelope_recipient_domain, envelope_sender_domain, header_from_domain)
                self.logger.info(logmsg)
                self.flag_as_phish(suspect, virusname)
                
                message = apply_template(self.config.get(self.section, 'rejectmessage'), suspect, {'virusname': virusname})
                break
        
        return actioncode, message
    
    
    def flag_as_phish(self, suspect, virusname):
        suspect.tags['%s.virus' % self.config.get(self.section, 'virusenginename')] = {'message content': virusname}
        suspect.tags['virus'][self.config.get(self.section, 'virusenginename')] = True
    
    
    def __str__(self):
        return "Spearphish Check"
    
    
    def lint(self):
        allok = self.checkConfig() and self._lint_file() and self._lint_sql()
        return allok
    
    
    def _lint_file(self):
        filename = self.config.get(self.section, 'domainsfile')
        if not os.path.exists(filename):
            print("Spearphish domains file %s not found" % filename)
            return False
        return True
    
    
    def _lint_sql(self):
        lint_ok = True
        sqlquery = self.config.get(self.section, 'domain_sql_query')
        dbconnection = self.config.get(self.section, 'dbconnection').strip()
        if not 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
Exemple #22
0
class SpearPhishPlugin(ScannerPlugin):
    """Mark spear phishing mails as virus

    The spearphish plugin checks if the sender domain in the "From"-Header matches the envelope recipient Domain ("Mail
    from my own domain") but the message uses a different envelope sender domain. This blocks many spearphish attempts.

    Note that this plugin can cause blocks of legitimate mail , for example if the recipient domain is using a third party service
    to send newsletters in their name. Such services often set the customers domain in the from headers but use their own domains in the envelope for
    bounce processing. Use the 'Plugin Skipper' or any other form of whitelisting in such cases.
    """

    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.logger = self._logger()
        self.filelist = FileList(strip=True, skip_empty=True, skip_comments=True, lowercase=True,
                                 additional_filters=None, minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default': '/etc/fuglu/spearphish-domains',
                'description': 'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default': 'DEFAULTVIRUSACTION',
                'description': "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default': 'threat detected: ${virusname}',
                'description': "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
            'dbconnection':{
                'default':"mysql://root@localhost/spfcheck?charset=utf8",
                'description':'SQLAlchemy Connection string. Leave empty to disable SQL lookups',
            },
            'domain_sql_query':{
                'default':"SELECT check_spearphish from domain where domain_name=:domain",
                'description':'get from sql database :domain will be replaced with the actual domain name. must return boolean field check_spearphish',
            },
            'check_display_part': {
                'default': 'False',
                'description': "set to True to also check display part of From header (else email part only)",
            },
        }


    def get_domain_setting(self, domain, dbconnection, sqlquery, cache, cachename, default_value=None, logger=None):
        if logger is None:
            logger = logging.getLogger()
        
        cachekey = '%s-%s' % (cachename, domain)
        cached = cache.get_cache(cachekey)
        if cached is not None:
            logger.debug("got cached setting for %s" % domain)
            return cached
    
        settings = default_value
    
        try:
            session = get_session(dbconnection)
    
            # get domain settings
            dom = session.execute(sqlquery, {'domain': domain}).fetchall()
    
            if not dom and not dom[0] and len(dom[0]) == 0:
                logger.warning(
                    "Can not load domain setting - domain %s not found. Using default settings." % domain)
            else:
                settings = dom[0][0]
    
            session.close()
    
        except Exception as e:
            logger.error("Exception while loading setting for %s : %s" % (domain, str(e)))
    
        cache.put_cache(cachekey, settings)
        logger.debug("refreshed setting for %s" % domain)
        return settings
    
    
    def should_we_check_this_domain(self,suspect):
        domainsfile = self.config.get(self.section, 'domainsfile')
        if domainsfile.strip()=='': # empty config -> check all domains
            return True

        if not os.path.exists(domainsfile):
            return False

        self.filelist.filename = domainsfile
        envelope_recipient_domain = suspect.to_domain.lower()
        checkdomains = self.filelist.get_list()
        if envelope_recipient_domain in checkdomains:
            return True
        
        dbconnection = self.config.get(self.section, 'dbconnection').strip()
        sqlquery = self.config.get(self.section,'domain_sql_query')
        do_check = False
        if dbconnection != '':
            cache = get_default_cache()
            cachename = self.section
            do_check = self.get_domain_setting(suspect.to_domain, dbconnection, sqlquery, cache, cachename, False, self.logger)
        return do_check
    
    
    def examine(self, suspect):
        if not self.should_we_check_this_domain(suspect):
            return DUNNO
        envelope_recipient_domain = suspect.to_domain.lower()
        envelope_sender_domain = suspect.from_domain.lower()
        if envelope_sender_domain == envelope_recipient_domain:
            return DUNNO  # we only check the message if the env_sender_domain differs. If it's the same it will be caught by other means (like SPF)
        
        header_from_domains = []
        header_from_domain = extract_from_domain(suspect)
        if header_from_domain is None:
            self.logger.warn("%s: Could not extract header from domain for spearphish check" % suspect.id)
            return DUNNO
        else:
            header_from_domains.append(header_from_domain)
            self.logger.debug('%s: checking domain %s (source: From header address part)' % (suspect.id, header_from_domain))
        
        if self.config.getboolean(self.section, 'check_display_part'):
            display_from_domain = extract_from_domain(suspect, False)
            if display_from_domain is not None and display_from_domain not in header_from_domains:
                header_from_domains.append(display_from_domain)
                self.logger.debug('%s: checking domain %s (source: From header display part)' % (suspect.id, display_from_domain))
        
        actioncode = DUNNO
        message = None
        
        for header_from_domain in header_from_domains:
            if header_from_domain == envelope_recipient_domain:
                virusname = self.config.get(self.section, 'virusname')
                virusaction = self.config.get(self.section, 'virusaction')
                actioncode = string_to_actioncode(virusaction, self.config)
                
                logmsg = '%s: spear phish pattern detected, env_rcpt_domain=%s env_sender_domain=%s header_from_domain=%s' % \
                         (suspect.id, envelope_recipient_domain, envelope_sender_domain, header_from_domain)
                self.logger.info(logmsg)
                self.flag_as_phish(suspect, virusname)
                
                message = apply_template(self.config.get(self.section, 'rejectmessage'), suspect, {'virusname': virusname})
                break
        
        return actioncode, message
    
    
    def flag_as_phish(self, suspect, virusname):
        suspect.tags['%s.virus' % self.config.get(self.section, 'virusenginename')] = {'message content': virusname}
        suspect.tags['virus'][self.config.get(self.section, 'virusenginename')] = True
    
    
    def __str__(self):
        return "Spearphish Check"
    
    
    def lint(self):
        allok = self.checkConfig() and self._lint_file() and self._lint_sql()
        return allok
    
    
    def _lint_file(self):
        filename = self.config.get(self.section, 'domainsfile')
        if not os.path.exists(filename):
            print("Spearphish domains file %s not found" % filename)
            return False
        return True
    
    
    def _lint_sql(self):
        lint_ok = True
        sqlquery = self.config.get(self.section, 'domain_sql_query')
        dbconnection = self.config.get(self.section, 'dbconnection').strip()
        if not 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
Exemple #23
0
class SpearPhishPlugin(ScannerPlugin):
    """Mark spear phishing mails as virus

    The spearphish plugin checks if the sender domain in the "From"-Header matches the envelope recipient Domain ("Mail
from my own domain") but the message usesa different envelope sender domain. This blocks many spearphish attempts.

    Note that this plugin can cause blocks of legitimate mail , for example if the recipient domain is using a third party service
    to send newsletters in their name. Such services often set the customers domain in the from headers but use their own domains in the envelope for
     bounce processing. Use the 'Plugin Skipper' or any other form of whitelisting in such cases.
    """

    def __init__(self, section=None):
        ScannerPlugin.__init__(self, section)
        self.filelist = FileList(strip=True, skip_empty=True, skip_comments=True, lowercase=True,
                                 additional_filters=None, minimum_time_between_reloads=30)

        self.requiredvars = {
            'domainsfile': {
                'default': '/etc/fuglu/spearphish-domains',
                'description': 'Filename where we load spearphish domains from. One domain per line. If this setting is empty, the check will be applied to all domains.',
            },
            'virusenginename': {
                'default': 'Fuglu SpearPhishing Protection',
                'description': 'Name of this plugins av engine',
            },
            'virusname': {
                'default': 'TRAIT.SPEARPHISH',
                'description': 'Name to use as virus signature',
            },
            'virusaction': {
                'default': 'DEFAULTVIRUSACTION',
                'description': "action if spear phishing attempt is detected (DUNNO, REJECT, DELETE)",
            },
            'rejectmessage': {
                'default': 'threat detected: ${virusname}',
                'description': "reject message template if running in pre-queue mode and virusaction=REJECT",
            },
        }

    def should_we_check_this_domain(self,suspect):
        domainsfile = self.config.get(self.section, 'domainsfile')
        if domainsfile.strip()=='': # empty config -> check all domains
            return True

        if not os.path.exists(domainsfile):
            return False

        self.filelist.filename = domainsfile
        envelope_recipient_domain = suspect.to_domain.lower()
        checkdomains = self.filelist.get_list()
        return envelope_recipient_domain in checkdomains


    def examine(self, suspect):
        if not self.should_we_check_this_domain(suspect):
            return DUNNO
        envelope_recipient_domain = suspect.to_domain.lower()
        envelope_sender_domain = suspect.from_domain.lower()
        if envelope_sender_domain == envelope_recipient_domain:
            return DUNNO  # we only check the message if the env_sender_domain differs. If it's the same it will be caught by other means (like SPF)

        header_from_domain = extract_from_domain(suspect)
        if header_from_domain is None:
            self._logger().warn("%s: Could not extract header from domain for spearphish check" % suspect.id)
            return DUNNO

        if header_from_domain == envelope_recipient_domain:
            virusname = self.config.get(self.section, 'virusname')
            virusaction = self.config.get(self.section, 'virusaction')
            actioncode = string_to_actioncode(virusaction, self.config)

            logmsg = '%s: spear phish pattern detected, recipient=%s env_sender_domain=%s header_from_domain=%s' % (
            suspect.id, suspect.to_address, envelope_sender_domain, header_from_domain)
            self._logger().info(logmsg)
            self.flag_as_phish(suspect, virusname)

            message = apply_template(
                self.config.get(self.section, 'rejectmessage'), suspect, {'virusname': virusname})
            return actioncode, message

        return DUNNO

    def flag_as_phish(self, suspect, virusname):
        suspect.tags['%s.virus' % self.config.get(self.section, 'virusenginename')] = {'message content': virusname}
        suspect.tags['virus'][self.config.get(self.section, 'virusenginename')] = True

    def __str__(self):
        return "Spearphish Check"

    def lint(self):
        allok = (self.checkConfig() and self.lint_file())
        return allok

    def lint_file(self):
        filename = self.config.get(self.section, 'domainsfile')
        if not os.path.exists(filename):
            print("Spearphish domains file %s not found" % (filename))
            return False
        return True