Your IP : 3.133.143.118


Current Path : /lib64/nagios/plugins/nccustom/
Upload File :
Current File : //lib64/nagios/plugins/nccustom/check_unauthorized_user.sh

#!/bin/bash
#
# Shell script for tracking unauthorized system users and their logins.
# The script checks if unauthorized system users with enabled shell are presented on the system.
# By comparing with authorized users.
# The authorized users can have or don't have SSH access allowed.
# SSH access controls by flags: ssh_allowed and ssh_denied in authorized users list file.
# The script includes several main checks:
# 1) Check for any user other than root with UID=0.
# 2) Check for unauthorized system users with enabled shell.
# 3) Check for recent logins of unauthorized system users with enabled shell.
# In case something is found script creates lock file, logs a message and exits with code 2.
# The lock file needed for avoiding self-resolved cases.
# Presence of the lock file doesn't skip the main checks.  
# Bash strict mode.
set -uo pipefail
# Declare and assign global variables.
# Authorized users associative array.
declare -A AUTHORIZED_USERS
# Get the directory where the script is located.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
# Path to authorized users list file in the script's directory.
AUTHORIZED_USERS_FILE="${SCRIPT_DIR}/authorized_users.list"
# Auth log.
AUTH_LOG="/var/log/secure"
# cPanel users list.
CPANEL_USERS_LIST=""
# Shell patterns to exclude during the check.
EXCLUDED_SHELL_PATTERNS="(/[s]?bin/(false|nologin|halt|shutdown|sync)|/usr/local/cpanel/bin/noshell)"
# Determine a lock file.
LOCK_FILE="/var/lock/check_unauthorized_user.lock"
# Determine a log file.
LOG_FILE="/var/log/check_unauthorized_user.log"
# Set the PATH
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
# Systems users array.
SYSTEM_USERS=()
# System users with enabled shell array.
SYSTEM_USERS_WITH_ENABLED_SHELL=()

#######################################
# Logs a message with a timestamp.
# Globals:
#   LOG_FILE - The file where the logs will be appended.
# Arguments:
#   message - The message to be logged.
#   no_log - Optional, if provided, the message will be printed without timestamp and not logged to log file.
# Outputs:
#   Writes message to stdout or stdout and file.
#######################################
logger() {
  # Retrieve the message to be logged from the first argument.
  local message="${1}"
  # Retrieve the optional 'no_log' argument, if provided.
  local no_log="${2:-}"
  # Check if 'no_log' is set (not empty).
  if [[ -n "${no_log}" ]]; then
    # Print the message to stdout without a timestamp.
    echo "${message}"
  else
    # Check if the log file exists and is writable.
    if [[ ! -w "${LOG_FILE}" ]]; then
      echo "ERROR! Log file '${LOG_FILE}' does not exist or is not writable."
      echo "HINT: Log file '${LOG_FILE}' should have the following permissions: 600, owner nrpe:nrpe (CloudLinux/AlmaLinux)."
      exit 3
    fi
    # Generate a timestamp in the format: Month Day HH:MM:SS ±hhmm.
    local timestamp
    timestamp=$(date +"%b %d %H:%M:%S %z")
    # Append the message with the timestamp to the log file.
    echo "[${timestamp}] ${message}" >> "${LOG_FILE}"
  fi
}

#######################################
# Check for the existence of a lock file and create it if it does not exist.
# Globals:
#   LOCK_FILE
#   LOG_FILE
# Arguments:
#   None
# Returns:
# Exits with code 2 if the lock file exists.
#######################################
set_lock() {
  # Check if the lock file exists.
  if [[ -f "$LOCK_FILE" ]]; then
    logger "CRITICAL! ${LOCK_FILE} exists. Investigation needed." "no_log"
    logger "HINT: Check ${LOG_FILE} for details." "no_log"
    exit 2
  else
    # If the lock file does not exist, create it.
    touch "$LOCK_FILE"
    logger "Lock file was created: ${LOCK_FILE}"
  fi
}

#######################################
# Get the timestamp from a certain number of minutes ago
# Globals:
#   None
# Arguments:
#   Minutes ago (default: 30 minutes ago)
# Returns:
#   Timestamp string representing a specific time in the past.
#######################################
get_minutes_ago_timestamp() {
  # Retrieve the number of minutes ago from the first argument; default to 30 if not provided.
  local minutes_ago=${1:-30}
  # Use the `date` command to get the timestamp for the specified number of minutes ago.
  # Format the output as: Month Day HH:MM:SS.
  date --date="${minutes_ago} minutes ago" "+%b %e %H:%M:%S"
}

#######################################
# Get the user's shell based on the username.
# Globals:
#   None
# Arguments:
#   User to check.
# Returns:
#   User shell, 3 on error.
#######################################
get_user_shell() {
  local username="$1"
  # Check if the username is provided (not empty).
  if [[ -z "$username" ]]; then
    # Log an error message and exit with code 3 if username is not provided.
    logger "ERROR! Username is not provided" "no_log"
    exit 3
  fi
  # Get the login shell for the user by querying the passwd database.
  # `getent passwd "$username"` retrieves the user entry.
  # `cut -d: -f7` extracts the 7th field (the shell) from the entry.
  local user_shell
  user_shell=$(getent passwd "$username" | cut -d: -f7)
  echo "$user_shell"
}

#######################################
# Get the user's uid based on the username.
# Globals:
#   None
# Arguments:
#   User to check.
# Returns:
#   User uid, 3 on error.
#######################################
get_user_uid() {
  local username="$1"
  # Check if the username is provided (not empty).
  if [[ -z "$username" ]]; then
    # Log an error message and exit with code 3 if username is not provided.
    logger "ERROR! Username is not provided." "no_log"
    exit 3
  fi
  # Get the uid for the user by querying the passwd database.
  # `getent passwd "$username"` retrieves the user entry.
  # `cut -d: -f3` extracts the 7th field (the user uid) from the entry.
  local user_uid
  user_uid=$(getent passwd "${username}" | cut -d: -f3)
  echo "$user_uid"
}

#######################################
# Get any user other than root with UID=0.
# Globals:
#   None
# Arguments:
#   None
# Returns:
#   Total amount of non-root users with UID=0, exit with code 2.
#######################################
# New function to check
get_users_with_uid0() {
  local -i uid0_users_count=0
  local uid0_users_details=""
  # Loop through each line of the passwd file, splitting by colon.
  while IFS=: read -r username _ uid _; do
  # Check if the UID is 0 and the username is not root.
  if [[ "${uid}" -eq 0 && "${username}" != "root" ]]; then
    # Log a critical message if a non-root user with UID=0 is found.
    logger "CRITICAL! Non-root user ${username} has UID=0"
    # Increment the counter for users with UID=0.
    (( uid0_users_count += 1 ))
    # Append the user details.
    uid0_users_details+="${username}, "
  fi
  done < <(getent passwd) # Use getent to read the passwd database.
  # If any non-root users with UID=0 were found.
  if [[ "${uid0_users_count}" -gt 0 ]]; then
    # Invoke function to set lock file.
    set_lock
    # Remove trailing comma and space.
    uid0_users_details="${uid0_users_details%, }"
    # Log a critical message with the list of non-root users with UID=0.
    logger "CRITICAL! Non-root users with UID=0 found (${uid0_users_count}): ${uid0_users_details}" "no_log"
    # Exit with code 2 to indicate that any non-root users with UID=0 were found.
    exit 2
  fi
}

#######################################
# Get list of cPanel users from WHM API
# Globals:
#   CPANEL_USERS_LIST
# Arguments:
#   None
# Returns:
#   cPanel users list.
#######################################
get_cpanel_users() {
  # Get cPanel users using WHM API call and format the output.
  CPANEL_USERS_LIST=$(whmapi1 --output=jsonpretty listaccts | jq '.data.acct[].user' | tr -d "\"" | sort)
}

#######################################
# Get list of system users excluding cPanel users
# Globals:
#   CPANEL_USERS_LIST
#   SYSTEM_USERS
# Arguments:
#   None
# Returns:
#   System users list.
#######################################
get_system_users() {
  # Read each username from the list of system users.
  while read -r username; do
    # Check if the username is not present in the cPanel users list.
    # `grep -q "^${username}$"` searches for an exact match of the username.
    # `<<< "${CPANEL_USERS_LIST}"` provides the cPanel users list as input.
    if ! grep -q "^${username}$" <<< "${CPANEL_USERS_LIST}"; then
      # Add the username to the SYSTEM_USERS array if not in the cPanel users list.
      SYSTEM_USERS+=("${username}")
    fi
  done < <(getent passwd | cut -d: -f1 | sort) # Get the sorted list of system usernames.
}

#######################################
# Get list of system users with enabled shell.
# Globals:
#   EXCLUDED_SHELL_PATTERNS
#   SYSTEM_USERS_WITH_ENABLED_SHELL
#   SYSTEM_USERS
# Arguments:
#   None
# Returns:
#   System users with enabled shell list.
#######################################
get_system_users_with_enabled_shell() {
  # Iterate over each user in the SYSTEM_USERS array.
  for user in "${SYSTEM_USERS[@]}"; do
    # Get the login shell for the user by calling the get_user_shell function.
    local user_shell
    user_shell=$(get_user_shell "${user}")
    # Check if the user's shell does not match any of the excluded shell patterns.
    # `grep -q -E "${EXCLUDED_SHELL_PATTERNS}"` searches for any match of the excluded patterns.
    # `<<< "${user_shell}"` provides the user’s shell as input.
    if ! grep -q -E "${EXCLUDED_SHELL_PATTERNS}" <<< "${user_shell}"; then
      # Add the user to the SYSTEM_USERS_WITH_ENABLED_SHELL array if their shell is not excluded.
      SYSTEM_USERS_WITH_ENABLED_SHELL+=("${user}")
    fi
  done
}

#######################################
# Get unauthorized system users with enabled shell
# Globals:
#   AUTHORIZED_USERS
#   SYSTEM_USERS_WITH_ENABLED_SHELL
# Arguments:
#   None
# Returns:
#   Total amount of unauthorized system users with exit code 2
#######################################
get_unauthorized_system_users() {
  local -i unauthorized_system_users_count=0
  local unauthorized_system_users_details=""
  # Iterate over each user in the SYSTEM_USERS_WITH_ENABLED_SHELL array.
  for user in "${SYSTEM_USERS_WITH_ENABLED_SHELL[@]}"; do
    local authorized_user=false
    # Check if the user is in the authorized users list.
    if [[ -n "${AUTHORIZED_USERS[$user]+x}" ]]; then
      authorized_user=true
    fi
    # If the user is not in the authorized users list.
    if [[ "${authorized_user}" == false ]]; then
      # Get the user's UID by calling the get_user_uid function.
      local user_uid
      user_uid=$(get_user_uid "${user}")
      # Get the user's shell by calling the get_user_shell function.
      local user_shell
      user_shell=$(get_user_shell "${user}")
      # Log a message to log file if the user is not in the authorized users list.
      logger "CRITICAL! Found unauthorized system user ${user} with uid ${user_uid} and enabled ${user_shell} shell."
      # Increment the counter for unauthorized system users.
      (( unauthorized_system_users_count += 1 ))
      # Append the unauthorized user details to the string.
      unauthorized_system_users_details+="${user} (uid ${user_uid}), "
    fi
  done

  # If any unauthorized system users were found.
  if [[ "${unauthorized_system_users_count}" -gt 0 ]]; then
    # Invoke function to set lock file.
    set_lock
    # Remove trailing comma and space from details string.
    unauthorized_system_users_details="${unauthorized_system_users_details%, }"
    # Log a critical message with the count and details of unauthorized users.
    logger "CRITICAL! Unauthorized system users found (${unauthorized_system_users_count}): ${unauthorized_system_users_details}" "no_log"
    # Exit with code 2 to indicate that unauthorized users were found.
    exit 2
  fi
}

#######################################
# Get unauthorized system users recent logins using authentication logs
# Globals:
#   AUTHORIZED_USERS
#   AUTH_LOG
#   SYSTEM_USERS_WITH_ENABLED_SHELL
# Arguments:
#   None
# Returns:
#   Total amount of unauthorized system users to login exit code 2
#######################################
get_unauthorized_system_users_logins() {
  local threshold_timestamp
  # Get the timestamp from 20 minutes ago
  threshold_timestamp=$(get_minutes_ago_timestamp 20)
  local -i unauthorized_system_users_logins_count=0
  local unauthorized_system_users_logins_details=""
  # Iterate over each user in the SYSTEM_USERS_WITH_ENABLED_SHELL array.
  for user in "${SYSTEM_USERS_WITH_ENABLED_SHELL[@]}"; do
    local matches
    # Search for log entries related to "sshd" where a session was opened for the specified user.
    # Filter the results to include only those entries that are equal to or greater than the threshold timestamp.
    matches=$(grep -E "sshd.*: session opened for user ${user}" "${AUTH_LOG}" | \
             awk -v threshold="${threshold_timestamp}" '$0 >= threshold')
    # If there are any log entries matching the criteria.
    if [[ -n "${matches}" ]]; then
      local ssh_login_allowed=false
      # If the user is in the authorized users list and has ssh login allowed
      if [[ -n "${AUTHORIZED_USERS[$user]+x}" ]]; then
        if [[ "${AUTHORIZED_USERS[$user]}" == "ssh_allowed" ]]; then
          ssh_login_allowed=true
        fi
      fi
      # If the user is not in the authorized users list or has ssh_denied
      if [[ "${ssh_login_allowed}" == false ]]; then
        # Get the user's UID by calling the get_user_uid function.
        local user_uid
        user_uid=$(get_user_uid "${user}")
        # Get the user's shell by calling the get_user_shell function.
        local user_shell
        user_shell=$(get_user_shell "${user}")
        # Log messages to log file if unauthorized system user login('s) found.
        logger "CRITICAL! Recent login found for unauthorized system user ${user} with uid ${user_uid} and enabled ${user_shell} shell."
        logger "According to the following record('s) from ${AUTH_LOG} log: ${matches}"
        # Increment the counter for unauthorized system users logins.
        (( unauthorized_system_users_logins_count += 1 ))
        # Append the unauthorized user logins details to the string.
        unauthorized_system_users_logins_details+="${user} (uid ${user_uid}), "
      fi
    fi
  done
  # If any unauthorized system users logins were found.
  if [[ "${unauthorized_system_users_logins_count}" -gt 0 ]]; then
    # Invoke function to set lock file.
    set_lock
    # Remove trailing comma and space.
    unauthorized_system_users_logins_details="${unauthorized_system_users_logins_details%, }"
    logger "CRITICAL! Unauthorized system users SSH logins found (${unauthorized_system_users_logins_count}): ${unauthorized_system_users_logins_details}" "no_log"
    # Exit with code 2 to indicate that unauthorized users logins were found.
    exit 2
  fi
}

#######################################
# Get unauthorized system users and their logins.
# Globals:
#   AUTHORIZED_USERS_FILE
#   CPANEL_USERS_LIST
# Arguments:
#   None
#######################################
main() {
  # Check if the file exists and is not empty
  if [[ -f "${AUTHORIZED_USERS_FILE}" && -s "${AUTHORIZED_USERS_FILE}" ]]; then
    # Read the file line by line
    while IFS=, read -r user access; do
      # Skip lines that start with a hash (#) or if either user or access is empty.
      if [[ "$user" =~ ^# || -z "$user" || -z "$access" ]]; then
        continue
      fi
      # Trim any extra whitespace around user and access
      user=$(echo "$user" | xargs)
      access=$(echo "$access" | xargs)
      # Ensure both user and access are non-empty before updating the array
      if [[ -n "$user" && -n "$access" ]]; then
        # Update the array with valid user and access
        AUTHORIZED_USERS["$user"]="$access"
      else
        # Log an error message and exit with code 3 if user or access is invalid
        logger "ERROR! Invalid username or access in ${AUTHORIZED_USERS_FILE}. User: '${user}', Access: '${access}'." "no_log"
        exit 3
    fi
    done < "${AUTHORIZED_USERS_FILE}"
  else
    # Log an error message and exit with code 3 if the file is empty or not found
    logger "ERROR! File ${AUTHORIZED_USERS_FILE} is either empty or not found." "no_log"
    exit 3
  fi
  # Invoke function to get any user other than root with UID=0.
  get_users_with_uid0
  # Check if cPanel binary exists before calling the function.
  if [ ! -f /usr/local/cpanel/cpanel ]; then
    # Define cPanel users list as empty in case if there is no cPanel.
    CPANEL_USERS_LIST=""
  else
    # Invoke function to get cPanel users list if cPanel is installed.
    get_cpanel_users
  fi
  # Invoke function to get list of system users excluding cPanel users.
  get_system_users
  # Invoke function to get list of system users with enabled shell.
  get_system_users_with_enabled_shell
  # Invoke function to get recent logins of unauthorized system users.
  get_unauthorized_system_users_logins
  # Invoke function to get unauthorized system users with enabled shell.
  get_unauthorized_system_users
  # Handle the case when no unauthorized users were found but the lock file exists.
  if [[ -f "$LOCK_FILE" ]]; then
    # Invoke function to control lock file.
    set_lock
  else
    # Print a message if everything is ok and exit with code 0.
    logger "OK. Unauthorized system users and logins are NOT detected." "no_log"
    exit 0
  fi
}

# Run a default mode.
if [[ -z $* ]]; then
  # Invoke main function
  main
fi

?>