HEX
Server: Apache/2.4.65 (Unix) OpenSSL/1.1.1k
System: Linux vps109042.inmotionhosting.com 4.18.0 #1 SMP Mon Sep 30 15:36:27 MSK 2024 x86_64
User: cisa (1010)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: //opt/cwprads/cms_counter.py
#!/usr/lib/cwprads/venv/bin/python3
"""
CMS Counter 2.3
02/01/2024
Corey S
-------
CWP Specific Version
- Hardcoded Server Type as VPS and Panel Type as CWP
- Removed cms_counter_cpanel() function as cPanel will not exist
- Adjusted main() function to accommodate these differences
- Customized Apache config checks
"""
import os
import subprocess
import sys
from pathlib import Path
import argparse


def run(command=None, check_flag=True):
    output = None
    if command and str(command):
        try:
            output = (
                subprocess.run(
                    command,
                    shell=True,
                    check=check_flag,
                    stdout=subprocess.PIPE,
                )
                .stdout.decode("utf-8")
                .strip()
            )
        except Exception as e:
            print(e)
    return output


def identify_apache_config():
    """
    Function to determine Apache config file to use to check domains as
    different installs/versions can store the config in different files,
    starting w/ config for 2.2 and checking different 2.4 config locations
    to end
    """
    if os.path.exists('/usr/local/apache/conf.d/vhosts'):
        return '/usr/local/apache/conf.d/vhosts/*.conf'
    if os.path.exists('/usr/local/apache/conf/httpd.conf'):
        return '/usr/local/apache/conf/httpd.conf'
    if os.path.exists('/etc/httpd/conf/httpd.conf'):
        return '/etc/httpd/conf/httpd.conf'
    if os.path.exists('/etc/apache2/httpd.conf'):
        return '/etc/apache2/httpd.conf'
    if os.path.exists('/etc/apache2/conf/httpd.conf'):
        return '/etc/apache2/conf/httpd.conf'
    sys.exit('Unable to determine Apache config!\nQuitting...')


def identify_nginx_config():
    """
    Function to find NGINX config file
    """
    if os.path.exists('/etc/nginx/vhosts'):
        return '/etc/nginx/vhosts'
    if os.path.exists('/etc/nginx/conf.d/vhosts'):
        return '/etc/nginx/conf.d/vhosts'
    if os.path.exists('/etc/nginx/nginx.conf'):
        return '/etc/nginx/nginx.conf'
    sys.exit("Unable to locate NGINX config file! Quitting...")


def find_nodejs(docroot=None, domain=None):
    """
    Find possible Node.JS installs per doc root
    """
    install_list = []
    if docroot and '\n' not in str(docroot):
        user = docroot.split('/')[2]
        if os.path.exists(f"""{docroot}/.htaccess"""):
            try:
                with open(
                    f"""{docroot}/.htaccess""", encoding='utf-8'
                ) as htaccess:
                    for line in htaccess.readlines():
                        if 'PassengerAppRoot' in line:
                            install_dir = line.split()[1].strip().strip('"')
                            if (
                                f"""{domain}:{install_dir}"""
                                not in install_list
                            ):
                                # Dont want a dictionary as a single domain
                                # could have multiple subdir installs
                                install_list.append(
                                    f"""{domain}:{install_dir}"""
                                )
            except Exception:
                return
        # If not found in htaccess, check via procs instead
        if len(install_list) == 0:
            user_id = run(f"""id -u {user}""", check_flag=False)
            if user_id and user_id.isdigit():
                # Only return procs whose true owner is the user ID of the
                # currently checked user
                node_procs = run(
                    f"""pgrep -U {user_id} node""", check_flag=False
                ).split('\n')
                if len(node_procs) > 0:
                    for pid in node_procs:
                        try:
                            cwd = run(
                                f"""pwdx {pid} | cut -d ':' -f 2""",
                                check_flag=False,
                            )
                            command = run(
                                f"""ps --no-headers -o command {pid} | """
                                + """awk '{print $2}'""",
                                check_flag=False,
                            ).split('.')[1]
                            install_dir = cwd + command
                        except Exception:
                            return
                        if (
                            install_dir
                            and os.path.exists(install_dir)
                            and f"""{domain}:{install_dir}"""
                            not in install_list
                        ):
                            install_list.append(f"""{domain}:{install_dir}""")
    return install_list


def determine_cms(docroot=None):
    """
    Determine CMS manually with provided document root by matching expected
    config files for known CMS
    """
    docroot = str(docroot)
    cms_dictionary = {
        f"""{docroot}/concrete.php""": 'Concrete',
        f"""{docroot}/Mage.php""": 'Magento',
        f"""{docroot}/configuration.php""": 'Joomla',
        f"""{docroot}/ut.php""": 'PHPList',
        f"""{docroot}/passenger_wsgi.py""": 'Django',
        f"""{docroot}/wp-login.php""": 'Wordpress',
        f"""{docroot}/sites/default/settings.php""": 'Drupal',
        f"""{docroot}/includes/configure.php""": 'ZenCart',
        f"""{docroot}/config/config.inc.php""": 'Prestashop',
        f"""{docroot}/config/settings.inc.php""": 'Prestashop',
        f"""{docroot}/app/etc/env.php""": 'Magento',
        f"""{docroot}/app/etc/local.xml""": 'Magento',
        f"""{docroot}/vendor/laravel""": 'Laravel',
    }
    for config_file, content in cms_dictionary.items():
        if os.path.exists(config_file):
            return content
    if os.path.exists(f"""{docroot}/config.php"""):
        if os.path.exists(f"""{docroot}/admin/config.php"""):
            return 'OpenCart'
        try:
            with open(f"""{docroot}/config.php""", encoding='utf-8') as config:
                if 'Moodle' in config.readline():
                    return 'Moodle'
                return 'phpBB'
        except Exception as e:
            print(e)

    return None


def cms_counter_no_cpanel(verbose=False, user_list=None):
    """
    Function to get counts of CMS from all servers without cPanel
    """
    if not user_list:
        user_list = []
    # Set Variables
    nginx = 0
    apache = 0
    web_server_config = None
    domains_cmd = None
    domains_list = None
    domains = {}
    docroot_list = []
    users = []
    # List of system users not to run counter against - parsed from /etc/passwd
    sys_users = [
        'root',
        'bin',
        'daemon',
        'adm',
        'sync',
        'shutdown',
        'halt',
        'mail',
        'games',
        'ftp',
        'nobody',
        'systemd-network',
        'dbus',
        'polkitd',
        'rpc',
        'tss',
        'ntp',
        'sshd',
        'chrony',
        'nscd',
        'named',
        'mailman',
        'cpanel',
        'cpanelcabcache',
        'cpanellogin',
        'cpaneleximfilter',
        'cpaneleximscanner',
        'cpanelroundcube',
        'cpanelconnecttrack',
        'cpanelanalytics',
        'cpses',
        'mysql',
        'dovecot',
        'dovenull',
        'mailnull',
        'cpanelphppgadmin',
        'cpanelphpmyadmin',
        'rpcuser',
        'nfsnobody',
        '_imunify',
        'wp-toolkit',
        'redis',
        'nginx',
        'telegraf',
        'sssd',
        'scops',
        'clamav',
        'tier1adv',
        'inmotion',
        'hubhost',
        'tier2s',
        'lldpd',
        'patchman',
        'moveuser',
        'postgres',
        'cpanelsolr',
        'saslauth',
        'nagios',
    ]
    try:
        nginx_status = run(
            """systemctl status nginx 1>/dev/null 2>/dev/null;echo $?"""
        )
        apache_status = run(
            """systemctl status httpd 1>/dev/null 2>/dev/null;echo $?"""
        )
    except Exception as e:
        print(e)
    # Determine Domain List
    if str(apache_status) == '0':
        # If Apache detected we want that, it's easier, so elif here - only use
        # NGiNX if Apache is not detected
        apache = 1
        web_server_config = identify_apache_config()
        if web_server_config:
            domains_cmd = (
                f"""grep ServerName {web_server_config} | """
                + r"""awk '{print $2}' | sort -g | uniq"""
            )
    elif str(nginx_status) == '0':
        nginx = 1
        web_server_config = identify_nginx_config()
        if web_server_config:
            # THIS MAY NEED REFINED - is this compatible with all NGiNX configs
            # we're checking for? I think one doesn't end in .conf at least
            domains_cmd = (
                f"find {web_server_config} -type f -name '*.conf' -print | "
                "grep -Ev '\\.ssl\\.conf' | xargs -d '\n' -l basename"
            )

    if domains_cmd:
        try:
            domains_list = run(domains_cmd)  # Get list of domains
        except Exception as e:
            print(e)
    if domains_list:
        for domain in domains_list.split():
            if apache == 1:
                docroot_cmd = (
                    f"grep 'ServerName {domain}' {web_server_config} -A3 | "
                    "grep DocumentRoot | awk '{print $2}' | uniq"
                )
                domain_name = domain
            elif nginx == 1:
                domain_name = domain.removesuffix('.conf')
                if r'*' in domain_name:
                    continue  # Skip wildcard subdomain configs
                if domain_name.count('_') > 0:
                    new_domain = ''
                    if domain_name.split('_')[1] == '':  # user__domain_tld.conf
                        domain_name = domain_name.split('_')
                        limit = len(domain_name) - 1
                        start = 2
                        while start <= limit:
                            new_domain += domain_name[start]
                            if start != limit:
                                new_domain += '.'
                            start += 1
                        domain_name = new_domain
                    else:  # domain_tld.conf
                        limit = len(domain_name) - 1
                        start = 0
                        while start <= limit:
                            new_domain += domain_name[start]
                            if start != limit:
                                new_domain += '.'
                            start += 1
                        domain_name = new_domain
                # This is the file name, above we extracted the actual domain
                # for use later
                nginx_config = f"""{web_server_config}/{domain}"""
                if os.path.exists(nginx_config):
                    docroot_cmd = (
                        f"""grep root {nginx_config} | """
                        + r"""awk '{print $2}' | uniq | tr -d ';'"""
                    )
                else:
                    docroot_cmd = None
            if docroot_cmd:
                try:
                    docroot = run(docroot_cmd)
                except Exception:
                    print(f"""Cannot determine docroot for: {domain_name}""")
                    continue
            else:
                print(f"""Cannot determine docroot for: {domain_name}""")
                continue
            if docroot and os.path.exists(docroot):
                node_installs = []
                docroot_list.append(docroot)
                domains.update({f"""{docroot}""": f"""{domain_name}"""})
                try:
                    node_installs += find_nodejs(
                        docroot=docroot, domain=domain_name
                    )  # Try and find NodeJS installs
                except Exception:
                    pass
                # Check sub-directories
                bad_dirs = [
                    f"""{docroot}/wp-admin""",
                    f"""{docroot}/wp-includes""",
                    f"""{docroot}/wp-content""",
                    f"""{docroot}/admin""",
                    f"""{docroot}/cache""",
                    f"""{docroot}/temp""",
                    f"""{docroot}/tmp""",
                ]
                docroot_dir = Path(docroot)
                for d in docroot_dir.iterdir():
                    if d.is_dir() and d not in bad_dirs:
                        try:
                            node_installs += find_nodejs(
                                docroot=str(d), domain=domain_name
                            )
                        except Exception:
                            pass
                        dirname = str(os.path.basename(d))
                        d = str(d)  # Convert from Path object to String
                        docroot_list.append(d)
                        domains.update(
                            {f"""{d}""": f"""{domain_name}/{dirname}"""}
                        )
                        bad_dirs.append(d)

    # Determine User List
    if len(user_list) >= 1:
        users = user_list
    # Get users if they weren't passed already - if cPanel was detected but not
    # Softaculous for instance
    if os.path.exists('/home') and len(user_list) == 0:
        users_dir = Path('/home')
        try:
            with open('/etc/passwd', encoding="utf-8") as passwd:
                passwd_file = passwd.readlines()
        except Exception:
            sys.exit('Unable to read /etc/passwd!\nExiting...')
        for u in users_dir.iterdir():
            if u.is_dir():
                limit = len(u.parts) - 1
                for line in passwd_file:
                    if (
                        u.parts[limit] == line.split(':')[0]
                        and u.parts[limit] not in sys_users
                    ):
                        users.append(u.parts[limit])
    if len(users) >= 1 and len(docroot_list) >= 1:
        for docroot in docroot_list:
            get_cms = None
            docroot_user = None
            for user in users:
                if user in sys_users:
                    continue  # Go to next user if current user is System user
                if user in docroot.split('/'):
                    docroot_user = user
                    break
            get_cms = determine_cms(docroot=docroot)

            if get_cms and docroot_user and docroot_user not in sys_users:
                domain = domains.get(f"""{docroot}""", None)
                if verbose:
                    print(f"""VPS CWP {docroot_user} {domain} {get_cms}""")
                else:
                    print(f"""{docroot_user} {domain} {get_cms}""")
            for install in node_installs:
                if verbose:
                    print(f"""VPS CWP {docroot_user} {install} NodeJS""")
                else:
                    print(f"""{docroot_user} {install} NodeJS""")

    return


def main():
    """
    Main function, initializes global variables as globals, gets hostname and
    call relevant checks after determining enviroment
    """
    parser = argparse.ArgumentParser(
        description='CMS Counter 2.0 -- CWP Version'
    )
    parser.add_argument(
        '-v',
        '--verbose',
        dest='verbose',
        help='Include Panel Type and Server Type in output',
        action='store_const',
        const=True,
        default=False,
    )
    args = vars(parser.parse_args())

    verb = args['verbose']

    cms_counter_no_cpanel(verbose=verb)

    return


if __name__ == "__main__":
    main()