Mini Shell

Direktori : /opt/sharedrads/domainchecker/
Upload File :
Current File : //opt/sharedrads/domainchecker/domainchecker.py

#!/opt/imh-python/bin/python3
'''
Checks domains in /etc/trueuserdomains and looks for obvious
fraud or phishing domains on cPanel servers.

For more documentation, please visit:
http://wiki.inmotionhosting.com/index.php?title=Domain_Checker
'''


import smtplib
import sys
import os
import re
import logging
import argparse
from pathlib import Path
from errno import EACCES
from collections import OrderedDict
import rads

__version__ = 'v0.1 Fraud Domain Checker'
__author__ = 'RileyL, KolbyH, SeanC, AlexK'

DEFAULT = {
    'email_address': 'abuse@inmotionhosting.com',
    'log_file': '/var/log/domainchecker.log',
    'badwords_file': '/opt/sharedrads/domainchecker/badwords',
    'whitelist_file': '.imh/domainchecker.whitelist',
    'domains_file': '/etc/trueuserdomains',
}


class DomainChecker:
    '''
    Checks for suspicious domain names on cPanel servers
    and sends an email notification if any are found.
    Both the badwords and whitelist are stored in a plain text file.

    Example usage:
    >>> from domainchecker import DomainChecker
    >>> checker = DomainChecker(
            badwords_file='/opt/sharedrads/domainchecker/badwords',
            whitelist_file='/opt/sharedrads/domainchecker/whitelist',
            domains_file='/etc/trueuserdomains',
            email_address='abuse@inmotionhosting.com'
            logger=rads_logger)
    >>> checker.run()
    '''

    def __init__(
        self,
        badwords_file: str,
        whitelist_file: str,
        domains_file: str,
        email_address: str,
        logger: logging.Logger,
    ):
        '''
        Load definitions, whitelist, and domains
        Initialize list for matches

        :param badwords_file: - plain text file of possible phishing terms.
        :param whitelist_file: - plain text file of known good domains.
        :param domains_file: - plain text file of domains on the server.
        :param logger: - logging object to use with functions named:
                         critical() error() warning() info() and debug()
        '''

        # Get logger object
        self.logger = logger

        # Get address
        self.email_address = email_address

        # Load definitions
        self.definitions = []
        self.definitions_file = badwords_file
        self.load_definitions(badwords_file)

        # Load domains
        self.domains = []
        self.domains_file = domains_file
        self.load_domains(domains_file)

        # Load cPanel users in the format: ['userna5', 'example.com']
        self.users = []
        self.load_users(domains_file)

        # Load whitelist
        self.whitelist = []
        self.whitelist_file = whitelist_file
        self.load_whitelists(whitelist_file)

        # Remove whitelist domains from domains list
        for user, domain in self.users:
            if domain in self.whitelist:
                self.users.remove([user, domain])

        # Remove whitelist domains from users list
        for user, domain in self.users:
            if domain in self.whitelist:
                self.users.remove([user, domain])
                print(user, domain)

        # Remove internal usernames and any associated domains
        for user, domain in self.users:
            for internal_user in rads.SYS_USERS:
                if user == internal_user:
                    self.logger.debug(
                        'Removed internal account: ' + user + '/' + domain
                    )
                    self.users.remove([user, domain])
                    self.domains.remove(domain)

        # List of matches found
        self.matches = []
        self.match_text = ''

    def load_definitions(self, target_file):
        '''
        Returns definitions of possible phishing words from file.

        :param file: File to load definitions from.
        '''
        try:
            with open(target_file, encoding='utf-8') as definitions_file:
                for line in definitions_file:
                    self.definitions.append(line.strip())
        except OSError:
            self.logger.error('Could not open ' + target_file)

        # Remove any blank lines from definitions
        for line in self.definitions:
            if line == '':
                self.definitions.remove(line)

    def load_whitelists(self, target_file: str):
        '''
        Returns whitelist domains from files.

        :param file: File to load whitelists from.
        '''
        for user, _ in self.users:
            user_whitelist = Path('/home', user, target_file.lstrip('/'))
            try:
                basedir = user_whitelist.parent
                if not basedir.exists():
                    basedir.mkdir(mode=0o600)
                    os.chown(basedir, 0, 0)
                user_whitelist.touch(mode=0o600, exist_ok=True)
                with open(user_whitelist, encoding='utf-8') as whitelist_file:
                    for line in whitelist_file:
                        self.whitelist.append(line.strip())
            except OSError:
                self.logger.error('Could not open %s', user_whitelist)

    def load_domains(self, target_file):
        '''
        Returns domains on the server as a list.

        :param file: File to load domains on the server.
        '''
        domain_pattern = re.compile(r'^(.*):')
        try:
            with open(target_file, encoding='utf-8') as domains_file:
                for line in domains_file:
                    # Strip away domain owner and just get domain
                    domain = domain_pattern.search(line)
                    line = domain.group()[:-1]
                    self.domains.append(line.strip())
        except OSError:
            self.logger.error('Could not open %s', target_file)

    def load_users(self, target_file):
        '''
        Load cPanel users in the format: ['userna5', 'example.com']

        :param file: File to load domains on the server.
        '''
        # regex patterns to get users and domains from trueuserdomains file
        users_pattern = re.compile(r':(.*)$')
        domain_pattern = re.compile(r'^(.*):')
        try:
            with open(target_file, encoding='utf-8') as domains_file:
                for line in domains_file:
                    # Get user from line
                    user = users_pattern.search(line)
                    the_user = user.group()[2:]
                    # Get domain from line
                    domain = domain_pattern.search(line)
                    the_domain = domain.group()[:-1]
                    # check if user is cPanel user
                    if rads.is_cpuser(the_user):
                        # add information to self.users
                        owner_info = [the_user, the_domain]
                        self.users.append(owner_info)
        except OSError:
            self.logger.error('Could not open %s', target_file)

    def regex_search(self, bad_word, domain):
        '''
        Uses the '*' character as "any" (globbing)
        the following link contains more information on globbing:
             http://www.tldp.org/LDP/abs/html/globbingref.html

        Searches for string bad_word in string domain
        if match is found, it's added to self.matches
        and self.match_text is updated

        :param bad_word: str - bad word to check
        :param domain: str - domain to check
        '''
        # Ensure no whitelisted domains were matched
        for allowed_domain in self.whitelist:
            if allowed_domain in domain:
                return

        # create search string
        search_string = ''
        segments = [i for i, ltr in enumerate(bad_word) if ltr == '*']
        last = 0
        for next_segment in segments:
            search_string += bad_word[last:next_segment] + r'(.*)'
            last = next_segment + 1
        search_string += bad_word[last : len(bad_word)]
        search_string = search_string.encode('string-escape')
        search_pattern = re.compile(search_string)

        # if bad_word found in search for string
        if search_pattern.search(domain):
            # if match found, add domain of match found to email
            self.matches.append(domain)
            self.match_text += domain + ': '
            # add cPanel account owner information to email
            for user, dom in self.users:
                # if there is a user, add it to the email
                if dom == domain and rads.is_cpuser(user):
                    self.match_text += user
            # if not on new line, add new line
            if not self.match_text.endswith('\n'):
                self.match_text += '\n'
        return

    def plaintext_search(self, bad_word, domain):
        '''
        searches for string bad_word in string domain
        if match is found, it's added to self.matches
        and self.match_text is updated

        :param bad_word: str - bad word to check
        :param domain: str - domain to check
        '''
        if bad_word in domain:
            # Ensure no whitelisted domains were matched
            for allowed_domain in self.whitelist:
                if allowed_domain in domain:
                    return
            # if match found, add domain of match found to email
            self.matches.append(domain)
            self.match_text += domain + ': '
            # add cPanel account owner information to email
            for user, dom in self.users:
                # if there is a user, add it to the email
                if dom == domain and rads.is_cpuser(user):
                    self.match_text += user
            if not self.match_text.endswith('\n'):
                self.match_text += '\n'
        return

    def run(self):
        '''
        Open /etc/trueuserdomains and checks every line from the badwords list.
        Matches found that are in the whitelist are ignored
        '''
        for domain in self.domains:
            for bad_word in self.definitions:
                if '*' in bad_word:  # If globbing, search with regex.
                    self.regex_search(bad_word, domain)
                else:  # If not globbing, search regularly.
                    self.plaintext_search(bad_word, domain)

        # Remove any duplicates
        self.matches = list(set(self.matches))

        # Reference: https://stackoverflow.com/questions/28518340
        self.match_text = '\n'.join(
            list(OrderedDict.fromkeys(self.match_text.split('\n')))
        )

        if self.matches:
            self.send_email()
        else:
            self.logger.info('No matches to possible phishing words found.')

    def add_to_whitelist(self, domains):
        '''
        Add domains to the whitelist file.

        :param domain: str - Domains to add to the whitelist.
        '''
        domain_list = domains.split(' ')
        for user, user_domain in self.users:
            if user_domain in domain_list:
                user_whitelist = '/home/' + user + '/' + self.whitelist_file
                try:
                    with open(user_whitelist, 'a+', encoding='utf-8') as file:
                        file.write(domains + '\n')
                except OSError as error:
                    self.logger.error(error)
                self.logger.info(domains + ' added to whitelist.')

    def send_email(self):
        '''
        Email report of possible fraudulent or phishing domains.
        '''
        to_addr = self.email_address
        subject = 'Possible fraudulent or phishing domains found!'
        body = (
            'The following domain(s) contain keywords that may be '
            + 'fraudulent activity:\n'
            + self.match_text[:-1]
        )
        self.logger.info('An email will now be sent for review by T2S staff')
        self.logger.debug(body)
        try:
            rads.send_email(to_addr, subject, body, errs=True)
        except (smtplib.SMTPException, OSError) as exc:
            self.logger.error(exc)
            self.logger.error(
                'Could not send email regarding '
                'possible phishing domain(s) failed.'
            )
        else:
            self.logger.info('Email sent successfully.')


def accessible(filename, mode):
    '''
    Verify access to file

    :param filename: str - name of file to check
    :param mode: str - mode to test
    '''
    try:
        with open(filename, mode) as _:  # pylint: disable=unspecified-encoding
            pass
        return True
    except OSError as error:
        print(error)
        return False


def parse_arguments():
    '''
    Parse the arguments using argparse
    '''
    parser = argparse.ArgumentParser(prog='domainchecker', description=__doc__)
    parser.add_argument(
        '-a',
        '--add-to-whitelist',
        dest='added_whitelist_domains',
        default='',
        help='domain to add to whitelist',
    )
    parser.add_argument(
        '-b',
        '--badwords',
        dest='badwords_file',
        default=DEFAULT['badwords_file'],
        help='file containing bad words definitions',
    )
    parser.add_argument(
        '-d',
        '--domains',
        dest='domains_file',
        default=DEFAULT['domains_file'],
        help='file containing list of domains to check',
    )
    parser.add_argument(
        '-e',
        '--email-address',
        dest='email_address',
        default=DEFAULT['email_address'],
        help='domain to add to whitelist',
    )
    parser.add_argument(
        '-l',
        '--loglevel',
        dest='loglevel',
        default='info',
        choices=['critical', 'error', 'warning', 'info', 'debug'],
        help='level of verbosity in logging',
    )
    parser.add_argument(
        '-o',
        '--output',
        dest='log_file',
        default=DEFAULT['log_file'],
        help='file to send logs to',
    )
    parser.add_argument(
        '-v',
        '--verbose',
        dest='verbose',
        default=False,
        action='store_true',
        help='output logs to command line',
    )
    parser.add_argument(
        '-w',
        '--whitelist',
        dest='whitelist_file',
        default=DEFAULT['whitelist_file'],
        help='file containing whitelisted domains',
    )

    args = parser.parse_args()  # Parse arguments

    if args.verbose:  # Set CLI output to std if verbose
        args.output = sys.stderr
    else:
        args.output = None

    # Set logging value from optional argment
    if args.loglevel == 'critical':
        args.loglevel = logging.CRITICAL
    elif args.loglevel == 'error':
        args.loglevel = logging.ERROR
    elif args.loglevel == 'warning':
        args.loglevel = logging.WARNING
    elif args.loglevel == 'info':
        args.loglevel = logging.INFO
    elif args.loglevel == 'debug':
        args.loglevel = logging.DEBUG
    else:  # Logging set to INFO by default.
        args.loglevel = logging.INFO

    # Verify access to files
    files = [
        (args.log_file, 'a+'),
        (args.badwords_file, 'r'),
        (args.domains_file, 'r'),
    ]

    for _file_, mode in files:
        if not accessible(_file_, mode):
            sys.exit(EACCES)

    return args


def main():
    '''
    usage: domainchecker [-h] [-a ADDED_WHITELIST_DOMAINS] [-b BADWORDS_FILE]
                         [-d DOMAINS_FILE] [-e EMAIL_ADDRESS]
                         [-l {critical,error,warning,info,debug}] [-o LOG_FILE]
                         [-v] [-w WHITELIST_FILE]

    Checks domains in /etc/trueuserdomains and looks for obvious
    fraud or phishing domains on cPanel servers.

    For more documentation, please visit:
    http://wiki.inmotionhosting.com/index.php?title=Domain_Checker

    optional arguments:
      -h, --help            show this help message and exit
      -a ADDED_WHITELIST_DOMAINS, --add-to-whitelist ADDED_WHITELIST_DOMAINS
                            domain to add to whitelist
      -b BADWORDS_FILE, --badwords BADWORDS_FILE
                            file containing bad words definitions
      -d DOMAINS_FILE, --domains DOMAINS_FILE
                            file containing list of domains to check
      -e EMAIL_ADDRESS, --email-address EMAIL_ADDRESS
                            domain to add to whitelist
      -l {critical,error,warning,info,debug}, --loglevel
                            level of verbosity in logging
      -o LOG_FILE, --output LOG_FILE
                            file to send logs to
      -v, --verbose         output logs to command line
      -w WHITELIST_FILE, --whitelist WHITELIST_FILE
                            file containing whitelisted domains
    '''
    # parse arguments
    args = parse_arguments()

    # set up logging
    rads_logger = rads.setup_logging(
        path=args.log_file,
        name='domainchecker',
        loglevel=args.loglevel,
        print_out=args.output,
    )

    # run domain checker
    checker = DomainChecker(
        badwords_file=args.badwords_file,
        whitelist_file=args.whitelist_file,
        domains_file=args.domains_file,
        email_address=args.email_address,
        logger=rads_logger,
    )

    # if we are just adding a domain to the whitelist, add the domain.
    if args.added_whitelist_domains:
        checker.add_to_whitelist(args.added_whitelist_domains)
    else:
        checker.run()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit()

Zerion Mini Shell 1.0