#!/usr/libexec/platform-python """ Checking accounts that were left in the system after incorrect removing from cPanel Version: 2.0 (25 Feb 2025) Author: Bogdan Kukharskiy, bogdan.kukharskiy@namecheap.com Based on script v.1.0-3 2017/12/05 by Vladimir Burov, vladimir.burov@namecheap.com Package: nc-nrpe-check-extra-accts """ import configparser import argparse import re import os import pwd import sys import subprocess VERSION = '2.0 2025/02/25' HOSTNAME = os.uname()[1] CONFIG_FILES = [ '/usr/share/nc_nagios/check_extra_accts/check_extra_accts_ignore.conf', '/usr/share/nc_nagios/check_extra_accts/check_extra_accts_regex_ignore.conf' ] def load_exclusions(config_files: list, hostname: str): """Load exclusions and regex exclusions from configuration files.""" config = configparser.ConfigParser(allow_no_value=True) existing_files = [f for f in config_files if os.path.exists(f)] config.read(existing_files) # Load the static exclusions from the 'default' section exclude = [] if config.has_section('default'): exclude = [key for key, _ in config.items('default')] # Load regex exclusions if defined. re_exclude = [] if config.has_section('regex'): re_exclude = [key for key, _ in config.items('regex')] # For any additional section (named with a regex pattern), # if the section name matches the hostname, add its keys to exclusions. for section in config.sections(): if section not in ['default', 'regex']: if re.match(section, hostname): exclude += [key for key, _ in config.items(section)] return exclude, re_exclude def get_system_accounts() -> list: """Return a sorted list of system account names using the pwd module.""" return sorted({p.pw_name for p in pwd.getpwall()}) def get_cpanel_accounts() -> list: """Return a list of cPanel accounts. The output is processed to remove leading/trailing spaces and any surrounding single quotes. """ try: # Run the whmapi1 command without using shell and process the output in Python. result = subprocess.run( ['/usr/sbin/whmapi1', 'listaccts', 'want=user'], check=True, encoding='utf8', stdout=subprocess.PIPE, stderr=subprocess.PIPE ) output = result.stdout.strip() except subprocess.CalledProcessError: output = "" # Filter lines that contain "user:" and extract the account name. raw_lines = output.splitlines() accounts = [] for line in raw_lines: if "user:" in line: parts = line.split(":", 1) if len(parts) > 1: accounts.append(parts[1].strip().strip("'")) return [acct for acct in accounts if acct] def check_home_directories(cpanel_accts: list) -> (list, list): """Return a tuple of (homedirs owned by root, homedirs with no valid owner).""" cpanel_homedirs = [os.path.expanduser(f'~{acct}') for acct in cpanel_accts] homes_owned_by_root = [] homes_with_no_user = [] for homedir in cpanel_homedirs: try: stat_info = os.stat(homedir) owner = pwd.getpwuid(stat_info.st_uid).pw_name if owner == 'root': homes_owned_by_root.append(homedir) except (FileNotFoundError, KeyError): homes_with_no_user.append(homedir) return homes_owned_by_root, homes_with_no_user def print_list(header: str, items: list): """Helper function to print a header and its sorted list items, one per line.""" print(f"{header}:") for item in sorted(items): print(item) print() # Blank line for separation def main(): description = ( f"Checking accounts that left in the system after incorrect removing from cPanel\n" f"Config files: {CONFIG_FILES}\nVersion {VERSION} bogdan.kukharskiy@namecheap.com" ) parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=description ) parser.add_argument("-v", "--verbose", action="store_true", help="more verbose output") args = parser.parse_args() # Load exclusions from config files. exclude, re_exclude = load_exclusions(CONFIG_FILES, HOSTNAME) system_accounts = get_system_accounts() cpanel_accounts = get_cpanel_accounts() # Identify extra accounts: those in the system that are not present in cPanel and are not excluded. excess = [acct for acct in system_accounts if acct not in cpanel_accounts and acct not in exclude] if re_exclude: regex_pattern = re.compile('|'.join(re_exclude)) excess = [acct for acct in excess if not regex_pattern.match(acct)] # Check /var/cpanel/users directory. try: var_cpanel_users = os.listdir('/var/cpanel/users') except OSError: var_cpanel_users = [] excess_var_cpanel_users = [ acct for acct in var_cpanel_users if acct not in cpanel_accounts and acct not in exclude and acct != 'system' ] homes_owned_by_root, homes_with_no_user = check_home_directories(cpanel_accounts) if args.verbose: print_list("Exclude list", exclude) print_list("RE Exclude list", re_exclude) print_list("System accounts", system_accounts) print_list("cPanel accounts", cpanel_accounts) print_list("/var/cpanel/users", var_cpanel_users) warnings = [] if excess: warnings.append(f"Extra accounts: {' '.join(excess)};") if args.verbose: print(f"Extra accounts: {' '.join(excess)}") if excess_var_cpanel_users: warnings.append(f"Extra /var/cpanel/users/: {' '.join(excess_var_cpanel_users)};") if args.verbose: print(f"cPanel account files exist without corresponding accounts: {' '.join(excess_var_cpanel_users)}") if homes_owned_by_root: warnings.append(f"Account home directory owned by root: {' '.join(homes_owned_by_root)};") if args.verbose: print(f"cPanel account home directories owned by root: {' '.join(homes_owned_by_root)}") if homes_with_no_user: warnings.append(f"Account home directory with no valid owner or not exist: {' '.join(homes_with_no_user)};") if args.verbose: print(f"cPanel account home directories with no valid owner or not exits: {' '.join(homes_with_no_user)}") if warnings: print("CRITICAL - " + ' '.join(warnings)) sys.exit(2) else: print("OK - There's no extra accounts") sys.exit(0) if __name__ == '__main__': main()