Mini Shell
Direktori : /opt/sharedrads/ |
|
Current File : //opt/sharedrads/cms_counter.py |
#!/opt/imh-python/bin/python3
"""
CMS Counter 2.1
06/03/2024
Corey S
-------
All Server Version
- Should be acceptable for any server environment
- Server Type determined by Fabric wrapper ckx.run_cms_counter() and passed to script via '-s'
- 3rd Party/Additional Panels added to determine_panel_type()
- Refactored NodeJS detection split into its own module
- '--new' switch added to only check newly provisioned accounts
- Overall code refactored to be more efficient
"""
import os
import subprocess
import glob
import platform
import sys
from pathlib import Path
import argparse
import json
import re
def run(command=None,check_flag=True,error_flag=True):
"""
check_flag ignores if the command exits w/ anything other than 0 if True
error_flag sends errors to /dev/null if set to True
"""
output = None
if command and str(command) and error_flag == True:
try:
output = subprocess.run(
command,
shell=True,
check = check_flag,
stdout=subprocess.PIPE
).stdout.decode("utf-8").strip()
except Exception as e:
print(e)
elif command and str(command) and error_flag == False:
try:
output = subprocess.run(
command,
shell=True,
check = check_flag,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL
).stdout.decode("utf-8").strip()
except:
return
return output
def determine_panel_type():
"""
Determines type of Panel in use on server, i.e. CWP, Platform i, Plesk, etc
Panels Currently Supported:
- cPanel
- CWP
- Platform i
- Webmin
- Plesk
- ISPConfig
- DirectAdmin
- CloudPanel
"""
if os.path.exists('/var/cpanel'): # cPanel
return 'cPanel'
elif os.path.exists('/usr/local/cwp'): # CWP
return 'CWP'
elif os.path.exists('/etc/profile.d/wp3.sh'): # Platform i
return 'Platform_i'
elif os.path.exists('/etc/webmin'): # Webmin
return 'Webmin'
elif os.path.exists('/etc/httpd/conf.d/zz010_psa_httpd.conf') or os.path.exists('/etc/apache2/conf.d/zz010_psa_httpd.conf'): # Plesk
return 'Plesk'
elif os.path.exists('/usr/local/directadmin'): # DirectAdmin
return 'DirectAdmin'
elif os.path.exists('/usr/local/bin/dploy'): # CloudPanel
return 'CloudPanel'
elif os.path.exists('/home/admispconfig/ispconfig/lib/config.inc.php'): # ISPConfig 2
return 'ISPConfig-2'
elif os.path.exists('/usr/local/ispconfig/interface/lib/config.inc.php'): # ISPConfig 3
return 'ISPConfig-3'
return 'cVPS'
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/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}/clientarea.php""": 'WHMCS',
f"""{docroot}/configuration.php""": 'Joomla',
f"""{docroot}/ut.php""": 'PHPList',
f"""{docroot}/passenger_wsgi.py""": 'Django',
f"""{docroot}/wp-content/plugins/boldgrid-inspirations""": 'BoldGrid',
f"""{docroot}/wp-config.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}/index.php"""):
try:
with open(f"""{docroot}/index.php""", encoding='utf-8') as index:
for line in index.readlines():
if re.search('CodeIgniter', line, re.IGNORECASE):
return 'CodeIgniter'
except Exception as e:
print(e)
if os.path.exists(f"""{docroot}/main.ts"""):
return 'Angular'
if os.path.exists(f"""{docroot}/main.js"""):
if os.path.exists(f"""{docroot}/index.php"""):
try:
with open(f"""{docroot}/index.php""", encoding='utf-8') as index:
for line in index.readlines():
if re.search('bootstrap', line, re.IGNORECASE):
return 'Bootstrap'
return 'Javascript'
except Exception as e:
print(e)
else:
return 'Javascript'
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:
for line in config.readlines():
if 'Moodle' in line:
return 'Moodle'
if os.path.exists(f"""{docroot}/cron.php"""):
with open(f"""{docroot}/cron.php""", encoding='utf-8') as cron:
for line in cron.readlines():
if 'phpBB' in line:
return 'phpBB'
return 'Undetermined'
else:
return 'Undetermined'
except Exception as e:
print(e)
if os.path.exists(f"""{docroot}/index.html"""):
return 'HTML'
return None
def double_check_wordpress(directory=None):
if directory and os.path.exists(directory):
if os.path.exists(f"""{directory}/wp-content/plugins/boldgrid-inspirations"""):
return 'BoldGrid'
return 'Wordpress'
def manual_find_docroot(domain=None, web_server_config=None, web_server=None):
if web_server == 'apache':
docroot_cmd = f"""grep 'ServerName {domain}' {web_server_config} -A3 | grep DocumentRoot | """ + r"""awk '{print $2}' | uniq"""
domain_name = domain
elif web_server == 'nginx':
domain_name = domain.removesuffix('.conf')
if r'*' in domain_name:
return # 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:
docroot = run(docroot_cmd)
else:
print(f"""Cannot determine docroot for: {domain_name}""")
return
return docroot
def cms_counter_no_cpanel(verbose=False, new=False, server=None, 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',
'wazuh',
'systemd-bus-proxy',
]
nginx_status = run("""systemctl status nginx &>/dev/null;echo $?""")
apache_status = run("""systemctl status httpd &>/dev/null;echo $?""")
# Determine Domain List
if str(apache_status) == '0': # Even if NGiNX server generally Apache is still in use, and is much less convaluted than the NGiNX config
apache = 1
web_server = 'apache'
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': # If Apache is not in use and NGiNX is, use NGiNX
nginx = 1
web_server = 'nginx'
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:
domains_list = run(domains_cmd) # Get list of domains
if domains_list:
for domain_name in domains_list.split():
docroot = manual_find_docroot(domain=domain_name,web_server_config=web_server_config,web_server=web_server)
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 and add them to list of user_installs
except:
pass
# Check sub-directories
bad_dirs = [
f"""{docroot}/wp-admin""",
f"""{docroot}/wp-includes""",
f"""{docroot}/wp-content""",
f"""{docroot}/wp-json""",
f"""{docroot}/admin""",
f"""{docroot}/cache""",
f"""{docroot}/temp""",
f"""{docroot}/tmp""",
f"""{docroot}/__wildcard__""",
f"""{docroot}/cgi-bin""",
f"""{docroot}/.well-known""",
f"""{docroot}/config""",
f"""{docroot}/language""",
f"""{docroot}/plugins""",
f"""{docroot}/media""",
f"""{docroot}/libraries""",
f"""{docroot}/images""",
f"""{docroot}/includes""",
f"""{docroot}/include""",
f"""{docroot}/modules""",
f"""{docroot}/components""",
f"""{docroot}/templates""",
f"""{docroot}/cli""",
f"""{docroot}/layouts""",
f"""{docroot}/storage""",
f"""{docroot}/logs""",
f"""{docroot}/bin""",
f"""{docroot}/uploads""",
f"""{docroot}/docs""",
f"""{docroot}/style""",
f"""{docroot}/files""",
f"""{docroot}/sounds""",
f"""{docroot}/cart""",
f"""{docroot}/shop""",
f"""{docroot}/assets""",
f"""{docroot}/system""",
f"""{docroot}/documentation""",
f"""{docroot}/application""",
f"""{docroot}/script""",
f"""{docroot}/scripts""",
f"""{docroot}/backups""",
f"""{docroot}/backup""",
]
docroot_dir = Path(docroot)
try:
for d in docroot_dir.iterdir():
if os.path.exists(d) and d.is_dir() and str(d) not in bad_dirs:
try:
node_installs += find_nodejs(docroot=str(d), domain=domain_name)
except:
continue
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)
except NotADirectoryError:
continue
except Exception as e:
print(e)
continue
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', encoding='utf-8') 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 verbose:
pt = determine_panel_type() # Determine Panel Type if needed
if get_cms and docroot_user and docroot_user not in sys_users:
domain = domains.get(f"""{docroot}""", None)
if verbose:
print(f"""{server} {pt} {docroot_user} {domain} {get_cms}""")
else:
print(f"""{docroot_user} {domain} {get_cms}""")
for install in node_installs:
if verbose:
print(f"""{server} {pt} {docroot_user} {install} NodeJS""")
else:
print(f"""{docroot_user} {install} NodeJS""")
return
def cms_counter_cpanel(verbose=False, new=False, server=None):
"""
Function to determine CMS counter on servers with cPanel
Attempting to use Softaculous first
Checking manually if that returns no results
Including users list as passed variable so we can run against specific users only as well, future-proofing/anticipating possible request
"""
# Get secur user for particular server
secur_nums = [n for n in platform.node().split('.')[0] if n.isdigit()]
secur_user = 'secur'
if len(secur_nums) > 0:
for n in secur_nums:
n.join(secur_user)
else:
secur_user = 'IGNOREME' # Arbitrary string so when it is inserted into the below list, bad_users, it's not None
# Establish Variables
softaculous = 0
# Softaculous omits the Name of some CMS in table view for some awesome reason; this is a fallback dictionary to use to manually retrieve Name if it isnt included
# Moodle and Magento only ones so far I've determined with this issue
softaculous_ids = {
'503' : 'Moodle 3.5',
'542' : 'Moodle',
'98' : 'Moodle 2.6',
'343' : 'Moodle 2.0',
'517' : 'Moodle 3.7',
'485' : 'Moodle 3.6',
'670' : 'Moodle 3.9',
'668' : 'Moodle 3.8',
'699' : 'Moodle 3.11',
'681' : 'Moodle 3.10',
'706' : 'Moodle 4.0',
'713' : 'Moodle 4.1',
'67' : 'Magento 1.9',
'545' : 'Magento 2.3',
'465' : 'Magento 1.7',
'486' : 'Magento 2.2',
'665' : 'Magento',
'688' : 'Magento 2.4.2',
'709' : 'Magnto 2.4.1',
}
cms_count = {}
cms_info = {}
site_owners = {}
user_list = []
no_install_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',
f"""{secur_user}""",
'inmotion',
'hubhost',
'tier2s',
'lldpd',
'patchman',
'moveuser',
'postgres',
'cpanelsolr',
'saslauth',
'nagios',
'wazuh',
'systemd-bus-proxy',
]
if os.path.exists('/var/cpanel/users'):
if new:
user_list_cmd = """tail -250 /etc/passwd | cut -d ':' -f 1"""
user_list = run(user_list_cmd).split('\n')
else:
users_dir = Path('/var/cpanel/users')
for u in users_dir.iterdir():
limit = len(u.parts) - 1
if not u.is_dir() and u.parts[limit] != 'system' and '.bak' not in u.parts[limit]:
user_list.append(u.parts[limit])
else:
sys.exit('Unable To Determine cPanel Users!\nQuitting...')
if len(user_list) == 0: # Confirm Users
sys.exit('No Users Detected. Quitting...')
if not os.path.exists('/var/cpanel'): # Confirm cPanel
sys.exit('cPanel not detected. Quitting...')
else:
if os.path.exists('/var/softaculous'): # Confirm Softaculous
softaculous = 1
if softaculous == 1:
for user in user_list:
if user in sys_users:
continue # Go to next user if system user detected
user_installs = None
user_installs_json = None
node_installs = []
cmd = f"""/usr/local/cpanel/3rdparty/bin/php /usr/local/cpanel/whostmgr/docroot/cgi/softaculous/cli.php --list_ins --user={user} --resp=json"""
user_installs = run(cmd, check_flag=False)
try:
user_installs_json = json.loads(user_installs)
except Exception as e:
user_installs_json = None
if 'error' in user_installs_json.keys():
no_install_users.append(user)
continue
elif user_installs_json:
install_list = [install for install in user_installs_json if install != 'error']
for install in install_list:
try:
user_cms = user_installs_json[install]['script_name']
url = user_installs_json[install]['softurl']
if not user_cms: # some scripts will give 'null' as the script_name
try:
sid = user_installs_json[install]['sid'] # Get script id
user_cms = softaculous_ids[sid] # determine CMS from script id
except:
print(f"""{user}: Error determining CMS""")
continue
if user_cms and url:
if user_cms == 'WordPress': # Softaculous only reports Wordpress, check if its really Boldgrid before recording it
docroot = user_installs_json[install]['softpath']
user_cms = double_check_wordpress(directory=docroot)
cms_info.update({url: user_cms})
site_owners.update({url: user})
except Exception as e:
print(e)
continue
else:
no_install_users.append(user)
continue
# Check for NodeJS installs
apache_status = run("""systemctl status httpd &>/dev/null;echo $?""", check_flag=False)
nginx_status = run("""systemctl status nginx &>/dev/null;echo $?""", check_flag=False)
get_domains_cmd = f"""grep -E '{user}$' /etc/userdomains"""
domain_list = run(get_domains_cmd, check_flag=False).split('\n') #.split(':')[0].strip()
for domain_string in domain_list: # This ensures the actual correct user is used and not one containing the same string
if len(domain_string.split(':')) > 1 and str(domain_string.split(':')[1]).strip() == str(user).strip():
domain=domain_string.split(':')[0]
if str(apache_status) == '0':
web_server_config = identify_apache_config()
web_server = 'apache'
elif str(nginx_status) == '0':
web_server_config = identify_nginx_config()
web_server = 'nginx'
else:
print('Unable to identify web server config')
return
docroot=manual_find_docroot(domain=domain,web_server_config=web_server_config,web_server=web_server)
if web_server_config and docroot:
try:
node_installs += find_nodejs(docroot=docroot, domain=domain_string.split(':')[0].strip())
except:
continue
if len(node_installs) > 0:
for install in node_installs:
# Add found NodeJS installs to dictionaries for each user, allowing reporting to print them at the end
cms_info.update({install.split(':')[1]: 'NodeJS'})
site_owners.update({install.split(':')[1]: user})
if verbose:
for url,cms in cms_info.items():
site_owner = site_owners.get(url, None)
print(f"""{server} cPanel {site_owner} {url} {cms}""")
else:
for url,cms in cms_info.items():
site_owner = site_owners.get(url, None)
print(f"""{site_owner} {url} {cms}""")
else: # cPanel present but no Softaculous
try:
cms_counter_no_cpanel(verbose=verbose, server=server, new=new)
except Exception as e:
print(e)
if len(no_install_users) >= 1: # Run no softaculous cms_counter for users w/o installs found
cms_counter_no_cpanel(user_list=no_install_users, verbose=verbose, server=server, new=new)
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.1')
parser.add_argument('-v', '--verbose', dest='verbose', help='Include Panel Type and Server Type in output', action='store_const', const=True, default=False)
parser.add_argument('-n', '--new', dest='new', help='Check the 250 most recently provisioned users only', action='store_const', const=True, default=False)
parser.add_argument('-s', '--server-type', dest='server', help='Server Type - passed by Fabric generally', action='store')
args = vars(parser.parse_args())
server_types = ['Shared', 'Reseller', 'Hub', 'VPS', 'Dedicated']
server_type = None
for st in server_types:
if args['server'] == st.lower() or args['server'] == st or args['server'] == st.upper():
server_type = st
break
if not server_type:
sys.exit('Server Type not given!\nQuitting...')
verb = args['verbose']
only_new = args['new']
if os.path.exists('/var/cpanel'):
cms_counter_cpanel(verbose=verb, new=only_new, server=server_type)
else:
cms_counter_no_cpanel(verbose=verb, new=only_new, server=server_type)
return
if __name__ == "__main__":
main()
Zerion Mini Shell 1.0