Your IP : 13.58.119.73
#!/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/<file>: {' '.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()