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()