#!/opt/imh-python/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 glob import platform 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 """ config_file = None if os.path.exists('/usr/local/apache/conf.d/vhosts'): return '/usr/local/apache/conf.d/vhosts/*.conf' elif os.path.exists('/usr/local/apache/conf/httpd.conf'): return '/usr/local/apache/conf/httpd.conf' elif os.path.exists('/etc/httpd/conf/httpd.conf'): return '/etc/httpd/conf/httpd.conf' elif os.path.exists('/etc/apache2/httpd.conf'): return '/etc/apache2/httpd.conf' elif os.path.exists('/etc/apache2/conf/httpd.conf'): return '/etc/apache2/conf/httpd.conf' else: 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' elif os.path.exists('/etc/nginx/conf.d/vhosts'): return '/etc/nginx/conf.d/vhosts' elif os.path.exists('/etc/nginx/nginx.conf'): return '/etc/nginx/nginx.conf' else: 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: install_list.append(f"""{domain}:{install_dir}""") # Dont want a dictionary as a single domain could have multiple subdir installs except: return if len(install_list) == 0: # If not found in htaccess, check via procs instead user_id = run(f"""id -u {user}""", check_flag=False, error_flag=False) if user_id and user_id.isdigit(): node_procs = run(f"""pgrep -U {user_id} node""", check_flag=False, error_flag=False).split('\n') #Only return procs whose true owner is the user ID of the currently checked user if len(node_procs) > 0: for pid in node_procs: try: cwd = run(f"""pwdx {pid} | cut -d ':' -f 2""", check_flag=False, error_flag=False) command = run(f"""ps --no-headers -o command {pid} | """ + """awk '{print $2}'""", check_flag=False, error_flag=False).split('.')[1] install_dir = cwd + command except: 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 """ cms = None 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' else: try: with open(f"""{docroot}/config.php""", encoding='utf-8') as config: if 'Moodle' in config.readline(): return 'Moodle' else: return 'phpBB' except Exception as e: print(e) return None def cms_counter_no_cpanel(verbose=False, user_list=[]): """ Function to get counts of CMS from all servers without cPanel """ # 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 | """ + r"""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 nginx_config = f"""{web_server_config}/{domain}""" # This is the file name, above we extracted the actual domain for use later 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: 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: 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: 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) cms_count = {} # Dictionary to hold CMS totals - not requested but will output for good measure anyways # Determine User List if len(user_list) >= 1: users = user_list if os.path.exists('/home') and len(user_list) == 0: # Get users if they weren't passed already - if cPanel was detected but not Softaculous for instance users_dir = Path('/home') try: with open('/etc/passwd') as passwd: passwd_file = passwd.readlines() except: 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()