#!/bin/bash # # Common Variables # PATH=/usr/lib64/ccache:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/root/bin:/root/.local/bin # Grab the server IP serverip=$(ip route get 1 | grep -oP 'src \K[0-9.]+') # Set default SSH options SSH_OPTIONS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o BatchMode=yes -q) # # Common Functions # # Function name: wpcli # Purpose: This function wraps the 'wp' command, which is used for managing WordPress installations from the command line. # The function adds a series of flags to the 'wp' command to modify its behavior in specific ways. function wpcli { # WPCLIFLAGS is a variable that contains several flags that will be used with every 'wp' command: # --allow-root: This allows 'wp' commands to be run as the root user. # --skip-plugins: This prevents plugins from being loaded when executing 'wp' commands. # --skip-themes: This prevents themes from being loaded when executing 'wp' commands. # --require=/bigscoots/includes/err_report.php: This causes 'wp' to load the err_report.php file before running any 'wp' commands. # The err_report.php file can contain any PHP code, which is typically used to set up environment variables or define helper functions. WPCLIFLAGS=(--allow-root --skip-plugins --skip-themes --require=/bigscoots/includes/err_report.php) # This line runs the 'wp' command with the flags defined in WPCLIFLAGS, followed by any arguments passed to the wpcli function. # For example, if you run 'wpcli plugin activate my-plugin', this line will run 'wp --allow-root --skip-plugins --skip-themes --require=/bigscoots/includes/err_report.php plugin activate my-plugin'. wp "${WPCLIFLAGS[@]}" "$@" } n_wpcli() { [ -f /bin/wp ] && chmod 755 /bin/wp [ -f /usr/bin/wp ] && chmod 755 /usr/bin/wp su -s /bin/bash -l nginx -c "source /bigscoots/includes/common.sh && wpcli $*" } n_wp() { [ -f /bin/wp ] && chmod 755 /bin/wp [ -f /usr/bin/wp ] && chmod 755 /usr/bin/wp su -s /bin/bash -l nginx -c "source /bigscoots/includes/common.sh && wp $*" } # Function name: validate_domain # Purpose: This function is used to validate the format of a domain name. validate_domain() { # We store the first argument passed to the function in a local variable 'domain'. local domain="$1" local script="$2" # Here, we check whether the 'domain' variable is empty or does not match the regular expression. # The regular expression checks if the domain name contains only alphanumeric characters, hyphens, periods, and at least one dot (.) character. # The domain name should also end with a sequence of alphabetic characters (a-z or A-Z), signifying a valid top-level domain (e.g., .com, .net, .org). if [[ -z "$domain" || ! "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]+$ ]] then # If the domain name is either empty or doesn't match the regular expression, we echo a JSON response with a "status" of "fail" and an appropriate error message. echo "{\"status\":\"fail\",\"msg\":\"Invalid domain name format. A valid domain name should contain a dot (.) and consist of alphanumeric characters, hyphens, and periods.\"}" # After displaying the error message, we exit the function with a non-zero status code (1), indicating that an error occurred. exit 1 fi # Check if the domain starts with "www." if [[ "$domain" == www.* ]] then send_slack_alert "#wpo-fail" ":x:" "Domain Validation" "$domain" "Domain starts with www. which means this should not exist in /home/nginx/domains/ \n *Script:* $script" exit 1 fi } validate_domain_in_path() { local domain="$1" local script="$2" local error_msg="" # 1. Check if the domain is empty or does not match regex if [[ -z "$domain" || ! "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]+$ ]]; then error_msg="Domain is empty or invalid format" # 2. Check if the domain starts with "www." elif [[ "$domain" == www.* ]]; then error_msg="Domain starts with www (should be root domain)" # 3. Check if the directory exists elif [ ! -d "/home/nginx/domains/$domain/public" ]; then error_msg="Public directory not found on server" fi # --- FAILURE HANDLER --- if [[ -n "$error_msg" ]]; then # 1. Send Slack Alert (Preserved your existing logic) send_slack_alert "#wpo-fail" ":x:" "Domain Validation" "$domain" "${error_msg}. \n *Script:* $script" # 2. Output JSON Failure # We escape quotes in $domain and $error_msg to ensure valid JSON printf '{"status": "failure", "domain": "%s", "error": "%s"}\n' "$domain" "$error_msg" return 1 fi # Optional: If you need a success JSON, uncomment the line below # printf '{"status": "success", "domain": "%s", "error": null}\n' "$domain" return 0 } # Function name: send_slack_alert # Purpose: This function is used to send a Slack alert with customizable parameters send_slack_alert() { # Declare local variables for the arguments # channel: The Slack channel to send the alert to local channel="$1" # emoji: The emoji to include in the alert local emoji="$2" # tag: A tag to include in the alert, typically to signify the source or type of alert local tag="$3" # Domain: Domain related to the error, may not always be included. local domain="$4" # message: The main content of the alert local message=$(echo "$5" | sed ':a;N;$!ba;s/\n/\\n/g') # Call the slack.sh script with the constructed message. # This script is assumed to handle the sending of the message to Slack. # It is passed the channel and a constructed message that includes # the emoji, tag, hostname of the local machine, server IP, and the main message content. bash /bigscoots/general/slack.sh "${channel}" "${emoji} *${tag}*\n*Hostname:* $(hostname)\n *Server IP:* ${serverip}\n *Domain:* ${domain}\n *Message:* ${message}" # Example usage: # send_slack_alert "#team-chat" ":smile:" "Error(Appears next to Emoji)" "bigscoots.com" "server on fire" } # Function name: correct_permissions_ownership # Purpose: This function is used to correct the ownership and permissions of files and directories correct_permissions_ownership() { ( set +m # Disable job control messages # Correct ownership nohup setsid bash -c 'find /home/nginx/domains/*/public \! -user nginx -exec chown nginx: {} \;' > /dev/null 2>&1 & # Correct file permissions nohup setsid bash -c 'find /home/nginx/domains/*/public -type f \! -perm 644 -exec chmod 644 {} \;' > /dev/null 2>&1 & # Correct directory permissions nohup setsid bash -c 'find /home/nginx/domains/*/public -type d \! -perm 755 -exec chmod 755 {} \;' > /dev/null 2>&1 & ) } skip_all_plugins_except() { local noskip_plugin="$1" local skipped_plugins="" local formatted_skipped_plugins="" # Build base command local cmd=(wpcli plugin list --field=name) # Only add --path if $domain is set if [ -n "$domain" ]; then cmd+=(--path="/home/nginx/domains/${domain}/public") fi # Run the command, filter out the plugin to keep, strip "/plugin-file.php" if present skipped_plugins=$("${cmd[@]}" 2>/dev/null | grep -v "${noskip_plugin}" | sed 's:/.*::') # Format as comma-separated formatted_skipped_plugins=$(echo "$skipped_plugins" | paste -sd,) echo "${formatted_skipped_plugins}" } # Function to start the timer start_timer() { START_TIME=$(date +%s) START_TIME_READABLE=$(date) } # Function to end the timer and display the elapsed time end_timer() { END_TIME=$(date +%s) END_TIME_READABLE=$(date) echo "Task started at: $START_TIME_READABLE" echo "Task ended at: $END_TIME_READABLE" ELAPSED_TIME=$((END_TIME - START_TIME)) ELAPSED_TIME_READABLE=$(date -u -d @"$ELAPSED_TIME" +'%T') echo "Total elapsed time: $ELAPSED_TIME seconds ($ELAPSED_TIME_READABLE)" } ngxreload_t() { local message="" noexit=false # parse args while [[ $# -gt 0 ]]; do case "$1" in --noexit) noexit=true ;; *) message="$1" ;; esac shift done local lineno="${BASH_LINENO[0]}" local caller_file="${BASH_SOURCE[1]}" if nginx -t > /dev/null 2>&1; then ngxreload > /dev/null 2>&1 else send_slack_alert "#team-chat" ":warning:" "nginx config fail" "N/A" \ "$message\n *Script throwing error:* \`${caller_file}\` *Line:* \`${lineno}\`" if $noexit; then return 1 # fail, but don’t exit the whole script else exit 1 fi fi } centmin_upgrade_124() { if ! grep -q ^132. /etc/centminmod-release > /dev/null 2>&1 then ( cmupdate > /dev/null 2>&1 && cmupdate update-stable > /dev/null 2>&1 && pushd /usr/local/src/centminmod > /dev/null 2>&1 && expect /bigscoots/wpo/manage/expect/centmin > /dev/null 2>&1 ) || send_slack_alert "#wpo-fail" ":x:" "Centmin Upgrade" "NA" "Failed at step $?" fi } fix_mariadb103_repo() { # Handle MariaDB 10.1 – alert and exit early if grep -q "10.1." /etc/yum.repos.d/mariadb.repo; then yum-config-manager --save --setopt=mariadb.skip_if_unavailable=true > /dev/null 2>&1 send_slack_alert "#wpo-fail" ":warning:" "mariadb-version" "NA" "I am running MariaDB10.1 please upgrade." return fi # Convert CentminMod-style MariaDB 10.3 CentOS 7 repo to archive if grep -q "http://yum.mariadb.org/10.3/centos7-amd64" /etc/yum.repos.d/mariadb.repo; then sed -i 's|http://yum.mariadb.org/10.3/centos7-amd64|https://archive.mariadb.org/mariadb-10.3/yum/centos7-amd64|' /etc/yum.repos.d/mariadb.repo sed -i 's|https://yum.mariadb.org/RPM-GPG-KEY-MariaDB|https://archive.mariadb.org/PublicKey|' /etc/yum.repos.d/mariadb.repo yum -q clean all fi # Convert CentminMod-style MariaDB 10.3 CentOS 8 repo to archive if grep -q "http://yum.mariadb.org/10.3/centos8-amd64" /etc/yum.repos.d/mariadb.repo; then sed -i 's|http://yum.mariadb.org/10.3/centos8-amd64|https://archive.mariadb.org/mariadb-10.3/yum/centos8-amd64|' /etc/yum.repos.d/mariadb.repo sed -i 's|https://yum.mariadb.org/RPM-GPG-KEY-MariaDB|https://archive.mariadb.org/PublicKey|' /etc/yum.repos.d/mariadb.repo yum -q clean all fi # Fix legacy Rackspace mirror usage if grep -q "mirror.rackspace.com/mariadb/yum/10.3" /etc/yum.repos.d/mariadb.repo; then cat > /etc/yum.repos.d/mariadb.repo < /etc/yum.repos.d/mariadb.repo </dev/null | awk '{print $2}') mariadb_version=$(mysql -V | grep -oP 'Distrib\s+\K10\.3') # --- Part 1: Handle Main WP-CLI Binary (Update/Downgrade logic) --- if [ "$mariadb_version" == "10.3" ]; then if [ "$current_wpcli_version" != "2.11.0" ]; then rm -f /usr/local/bin/wp /bin/wp curl -sSL -o /usr/bin/wp https://github.com/wp-cli/wp-cli/releases/download/v2.11.0/wp-cli-2.11.0.phar chmod +x /usr/bin/wp ln -fs /usr/bin/wp /usr/local/bin/wp wpcli cli cache clear --quiet fi else if ! err_output=$(wp cli update --yes --quiet 2>&1); then [ -d /root/.wp-cli ] && rm -rf /root/.wp-cli /usr/bin/wp /usr/local/bin/wp if bash /usr/local/src/centminmod/addons/wpcli.sh install > /dev/null 2>&1; then ln -fs /bin/wp /usr/local/bin/wp chmod 775 /bin/wp /usr/local/bin/wp else send_slack_alert "#wpo-fail" ":warning:" "wp cli update failed" "NA" "Attempt via \`wp cli update\` \`\`\` $err_output \`\`\` \n attempted \`bash /usr/local/src/centminmod/addons/wpcli.sh update\` as well." fi else if [ ! -L /usr/local/bin/wp ]; then rm -f /usr/local/bin/wp /bin/wp bash /usr/local/src/centminmod/addons/wpcli.sh install > /dev/null 2>&1 ln -fs /bin/wp /usr/local/bin/wp chmod 775 /bin/wp /usr/local/bin/wp fi fi fi # --- Part 2: Generate PHP Version Wrappers (wp74, wp81, etc.) --- # This loops through installed Remi PHP versions and creates shortcuts for php_bin in /opt/remi/php*/root/usr/bin/php; do # Ensure binary exists [ -e "$php_bin" ] || continue # Extract version number (e.g., "74" from path) if [[ $php_bin =~ /opt/remi/php([0-9]+)/root/usr/bin/php ]]; then version="${BASH_REMATCH[1]}" wrapper="/usr/local/bin/wp${version}" # Create the wrapper script cat < "$wrapper" #!/bin/bash # Auto-generated wrapper: Run WP-CLI using PHP ${version} exec "$php_bin" /usr/local/bin/wp --allow-root "\$@" EOF chmod +x "$wrapper" fi done } get_wpo_api_hash() { local wpo_api_hash if ! wpo_api_hash=$(curl -s https://www.bigscoots.com/downloads/uniquevisithash 2>/dev/null) then send_slack_alert "#wpo-errors" ":warning:" "wpo api hash fail" "Failed to pull the hash using CURL." exit 1 fi echo "$wpo_api_hash" } yum_check() { local lock_file="/var/lock/yum_check.lock" local expiration_time=86400 # 24 hours in seconds # Check if lock file exists and exit if it does if [[ -f "$lock_file" ]]; then local current_time=$(date +%s) local file_modification_time=$(stat -c %Y "$lock_file") # Check if lock file is older than 24 hours if (( current_time - file_modification_time < expiration_time )); then return 0 fi fi # Clean yum cache yum clean all > /dev/null 2>&1 # Check for available updates local output=$(yum check-update 2>&1) # Check the exit code local exit_code=$? if [[ $exit_code -ne 0 && $exit_code -ne 100 ]]; then local logfile="/root/.bigscoots/logs/yum_checkupdate-fail-$(date +%s).log" echo "$output" > "$logfile" send_slack_alert "#wpo-fail" ":x:" "YUM" "N/A" "A yum check failed, please check: $logfile" exit 1 fi # Create lock file touch "$lock_file" } check_process_runtime() { # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --kill) KILL_PROCESS=true shift ;; *) PROCESS_NAMES+=("$1") shift ;; esac done # Loop through each process name provided as arguments for process_name in "${PROCESS_NAMES[@]}" do # Get the PID and CPU time of the specified process, excluding awk itself ps_output=$(ps -eo pid,etime,args | awk -v process_name="$process_name" '$0 ~ process_name && !/awk/ {print}') # Loop through each line of the output while read -r line do # Split the line into PID, elapsed time, and command pid=$(echo "$line" | awk '{print $1}') etime=$(echo "$line" | awk '{print $2}') command=$(echo "$line" | awk '{$1=""; $2=""; print substr($0,3)}') # Remove leading zeros from the elapsed time etime=$(echo "$etime" | sed 's/^0*//') # Extract days, hours, minutes, and seconds from elapsed time IFS='-:' read -r days hours minutes seconds <<< "$etime" # Calculate total minutes total_minutes=$(( (10#$days * 24 * 60) + (10#$hours * 60) + (10#$minutes) )) # Check if the process has been running for more than 30 minutes if [ "$total_minutes" -gt 30 ] then send_slack_alert "#wpo-alerts" ":hourglass_flowing_sand:" "Long Running Process" "NA" "Process with PID $pid ($process_name) has been running for more than 30 minutes." # Kill the process if the --kill option is provided if [ "$KILL_PROCESS" = true ]; then kill -9 "$pid" send_slack_alert "#wpo-alerts" ":skull_and_crossbones:" "Process Killed" "NA" "Process with PID $pid ($process_name) has been terminated." fi fi done <<< "$ps_output" done } find_wp_installs() { local root_dir="/home/nginx/domains" local wp_includes="wp-includes/version.php" # Function to check if a directory contains a WordPress installation check_wp_install() { local dir=$1 if [[ -f "$dir/$wp_includes" ]]; then echo "$dir" fi } # Use find to locate wp-includes directories, excluding wp-content and wp-admin find "$root_dir"/*/public -mindepth 1 -maxdepth 2 \( -path "*/wp-content" -o -path "*/wp-admin" \) -prune -o -type d -name "wp-includes" -print | while read -r wp_includes_dir; do check_wp_install "$(dirname "$wp_includes_dir")" done } install_package() { if ! rpm -q $1 >/dev/null 2>&1 then yum install -y -q $1 fi } generate_json_response() { local success="$1" local message="$2" local result="$3" local ip="${4:-""}" local domain="${5:-""}" jq -n \ --argjson success "$success" \ --arg message "$message" \ --argjson result "$result" \ --arg ip "$ip" \ --arg domain "$domain" \ '{ "success": $success, "messages": [$message], "ip": $ip, "domain": $domain, "result": $result }' } send_webhook() { local webhook_url="$1" local auth_header="$2" local json_payload="$3" #echo "curl -s -w \"\\n%{http_code}\" -X POST -H \"Authorization: ${auth_header}\" -H \"Content-Type: application/json\" -d '${json_payload}' \"${webhook_url}\"" # Send the webhook and capture the response response=$(curl -s -w "\n%{http_code}" -X POST -H "Authorization: ${auth_header}" -H "Content-Type: application/json" -d "${json_payload}" "${webhook_url}") http_status=$(echo "$response" | tail -n1) body=$(echo "$response" | head -n1) # Determine success based on HTTP status code if [ "$http_status" -eq 200 ]; then generate_json_response true "Webhook has been sent successfully" "$body" else generate_json_response false "Webhook failed with status code $http_status" "$body" fi } # Initialize a default JSON response function init_json_response() { json='{"errors": [], "messages": [], "success": false, "result": {}}' } # Function to add an error to the JSON function add_json_error() { local error_message="$1" json=$(jq --arg error "$error_message" '.errors += [$error]' <<< "$json") } # Function to add a message to the JSON function add_json_message() { local message="$1" json=$(jq --arg message "$message" '.messages += [$message]' <<< "$json") } # Function to set success flag to true/false function set_json_success() { local value="${1:-true}" if [ "$value" = "false" ]; then json=$(jq '.success = false' <<< "$json") else json=$(jq '.success = true' <<< "$json") fi } # Function to add a raw JSON object as a message function add_actual_json_message() { local raw_json="$1" # Use --argjson so that the passed parameter is treated as actual JSON json=$(jq --argjson message "$raw_json" '.messages += [$message]' <<< "$json") } # Function to add a raw JSON object as a error function add_actual_json_error() { local raw_json="$1" json=$(jq --argjson error "$raw_json" '.errors += [$error]' <<< "$json") } # Function to set result data using a temporary file to avoid argument list too long function set_json_result() { local result_data="$1" local tmp_json tmp_json=$(mktemp) local tmp_result tmp_result=$(mktemp) # Write current json and result_data to temporary files echo "$json" > "$tmp_json" echo "$result_data" > "$tmp_result" # Use --argfile to pass the large JSON data from file json=$(jq --argfile result "$tmp_result" '.result = $result' "$tmp_json") rm -f "$tmp_json" "$tmp_result" } # Function to print the JSON response function print_json_response() { echo "$json" | jq . } # Function to compare nginx version compare_nginx_version() { # Split the version into major, minor, and patch local current_version="$1" local minimum_version="1.25.1" if [[ "$(echo -e "$current_version\n$minimum_version" | sort -V | head -n1)" == "$minimum_version" ]] then return 0 else return 1 fi } # Function to check the Nginx version and process config files check_nginx_and_update_http2() { # Get Nginx version local nginx_version=$(nginx -v 2>&1 | grep -oP "(?<=nginx/)[0-9.]+") if [ -z "$nginx_version" ]; then return 1 fi # Compare versions if compare_nginx_version "$nginx_version"; then # Initialize variable to track if changes were made local nginxt=false # Files to check local files=( "/usr/local/nginx/conf/conf.d/*.conf" "/usr/local/nginx/conf/conf.d/phpmyadmin_ssl.conf" ) for file in ${files[@]}; do if [ -f "$file" ]; then # Step 2: Check for 'listen' directive with http2 and modify if grep -q 'listen .*http2' "$file"; then # Remove all instances of 'http2 on;' first sed -i '/http2[[:space:]]\+on;/d' "$file" # Remove 'http2' from 'listen' directive and add 'http2 on;' after sed -i '/listen .*http2/s/http2//g' "$file" sed -i '/listen .*;/s/ ;/;/g' "$file" sed -i '/listen .*;/a \ http2 on;' "$file" # Set flag to true indicating a change was made nginxt=true fi fi done # After the loop, check if any changes were made and reload Nginx if necessary if [ "$nginxt" == true ]; then ngxreload_t "function: \`check_nginx_and_update_http2\`" --noexit fi fi } convert_wpsecure() { find /usr/local/nginx/conf/wpincludes/ -type f -name "wpsecure_*.conf" ! -name "*_blacklist.conf" ! -name "*_whitelist.conf" | while read -r file; do # Extract the domain name domain=$(basename "$file" | sed 's/wpsecure_//; s/\.conf//') # Define paths for blacklist and whitelist files blacklist_file="/usr/local/nginx/conf/wpincludes/${domain}/wpsecure_${domain}_blacklist.conf" whitelist_file="/usr/local/nginx/conf/wpincludes/${domain}/wpsecure_${domain}_whitelist.conf" # Find the SSL conf file ssl_conf_file="/usr/local/nginx/conf/conf.d/${domain}.ssl.conf" # Check if SSL conf file exists if [[ -f "$ssl_conf_file" ]]; then # Extract PHP includes, ignoring commented lines and capturing only the path after "include" php_includes=$(grep -h "^\s*include /usr/local/nginx/conf/php" "$ssl_conf_file" | awk '{print $2}' | sed 's/;$//' | sort | uniq -c | sort -rn | head -n 1 | awk '{print $2}') # If we didn't find any PHP includes, skip this domain if [[ -z "$php_includes" ]]; then echo "No PHP include found for $domain, skipping..." continue fi else echo "SSL conf file for $domain not found, skipping..." continue fi # Replace ${vhostname} and ${phpconf} in the template and write to the original file sed -e "s/\${vhostname}/$domain/g" -e "s|\${phpconf}|$php_includes|g" /bigscoots/wpo/nginx/includes/bs_wp_whitelist_v2.conf > "$file" # Create empty blacklist and whitelist files with the replaced domain name touch "$blacklist_file" touch "$whitelist_file" done } gimme_the_goods_les () { HISTCONTROL=ignorespace; # Load check: exit early if load > 2x CPU cores CPU_COUNT=$(nproc) LOAD_AVG=$(awk '{print $1}' /proc/loadavg) LOAD_LIMIT=$(echo "$CPU_COUNT * 2" | bc) COMPARE=$(echo "$LOAD_AVG > $LOAD_LIMIT" | bc) if [[ "$COMPARE" -gt 1 ]]; then echo -e "\033[1;31mSystem load ($LOAD_AVG) is too high to proceed (limit: $LOAD_LIMIT).\033[0m" return 1 fi sleep 1; history -d $((HISTCMD-1)); # Display Recent Commands echo -e "\n\033[1;36m=== Recent Commands ===\033[0m"; history | tail -n 10 | awk '{$1=""; print " "$0}'; # Display Current Users echo -e "\n\033[1;36m=== Current Users ===\033[0m"; if grep --color=auto -q "AlmaLinux release 9" /etc/redhat-release 2> /dev/null; then W_CMD="w -i"; else W_CMD="w"; fi; UNKNOWN_USER_DETECTED=0 $W_CMD | awk 'BEGIN { split("38.58.227.48:Zubair 67.202.70.147:Nexus 208.117.38.38:Justin 38.65.255.4:Justin 50.31.114.76:Jay 208.117.38.157:Shibin 208.117.38.23:Dean 198.175.25.80:Zach 208.117.38.24:Prasul 50.31.30.56:Zack 50.31.119.9:Julian 208.100.53.125:Sadiq 208.117.4.65:Michael 208.100.53.146:Khan 208.100.53.138:Gibu 74.121.204.16:JK 50.31.116.25:Ahmad 50.31.99.57:Les 38.65.224.44:Lijith 38.65.227.5:Chris 38.65.224.198:Abdal 38.65.227.37:Asher 38.65.227.35:David 38.65.227.38:Muhammad 38.58.224.253:Pell 38.58.225.37:Aleks 38.58.225.38:Roscoe 38.58.226.2:Pavle", pairs, " ") for (i in pairs) { split(pairs[i], p, ":") ips[p[1]] = p[2] } } $1 == "USER" { printf "\033[1m%-10s %-10s %-16s %-20s %s\033[0m\n", "USER", "TTY", "FROM", "NAME", "LOGIN@ IDLE JCPU PCPU WHAT" next } NF > 3 { if ($0 ~ /^[[:alnum:]]/) { cmd = substr($0, index($0, $4)) # Extract a valid IPv4 address only (four octets, 0-255) match($3, /([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/, ip_arr) ip = (ip_arr[1] != "") ? ip_arr[1] : "Unknown" name = (ip in ips) ? ips[ip] : "Unknown" # Check if an unknown user is detected if (name == "Unknown") { system("echo -e \"\033[1;31m\n🚨 ALERT: Unauthorized Access Detected! 🚨\n\nAn unknown user has connected from " $3 ".\nIf this is unexpected, please investigate immediately.\nBigScoots staff has been notified.\nEnsure you are connecting via a proper tunnel IP.\n\033[0m\""); system("UNKNOWN_USER_DETECTED=1") } printf "%-10s %-10s %-16s %-20s %s\n", $1, $2, $3, name, cmd } else { print } }' 2> /dev/null # Display Actively Running Tasks echo -e "\n\033[1;36m=== Actively Running Tasks ===\033[0m"; RUNNING_TASKS=$(ps -eo pid,cmd --sort=start_time | grep '[b]ash\|[s]h' | grep '/bigscoots/') if [[ -z "$RUNNING_TASKS" ]]; then echo "None" else echo -e "PID CMD\n$RUNNING_TASKS" fi # Display final alert if an unknown user was detected if [[ "$UNKNOWN_USER_DETECTED" -eq 1 ]]; then echo -e "\033[1;31m\n🚨 ALERT: Unauthorized Access Detected! 🚨\nPlease check the logs and take action immediately.\033[0m" fi # Check if remote database info file exists if [[ -f /root/.bigscoots/db/info ]]; then echo -e "\n\033[1;36m=== Remote Database Detected ===\033[0m" # Extract database information DBHOST=$(grep '^dbhost:' /root/.bigscoots/db/info | awk '{print $2}') DBHOSTPUBLIC=$(grep '^dbhostpublic:' /root/.bigscoots/db/info | awk '{print $2}') echo -e "Private Network IP: \033[1;32m$DBHOST\033[0m" echo -e "Public Network IP: \033[1;32m$DBHOSTPUBLIC\033[0m" fi } # nsgrep: A custom grep function that excludes common static file types (images, fonts, videos, audio, compressed files, and documents). # Usage: nsgrep # This function helps filter out static assets like .jpg, .png, .css, .js, fonts, videos, etc., from your search results. nsgrep() { local search="$1" shift grep -vE "/[^ ]+\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tiff|tif|css|scss|js|json|woff|woff2|ttf|eot|otf|svgz|webm|mp4|mp3|wav|ogg|flac|aac|zip|tar|gz|rar|7z|pdf|txt|md|xml)([? ].*)?$" "$@" | grep "$search" } clean_nginx_config() { local config_file="$1" if [[ ! -f "$config_file" ]]; then echo "Error: Config file $config_file not found!" return 1 fi # Create a timestamped backup epoch=$(date +%s) config_file_bk="${config_file}.bk.${epoch}" cp -rf "$config_file" "$config_file_bk" # Remove unwanted cache includes (wpcacheenabler, wpsupercache, rediscache) sed -i '/^\s*#\?include \/usr\/local\/nginx\/conf\/wpincludes\/.*\/\(wpcacheenabler\|wpsupercache\|rediscache\)_.*\.conf;/d' "$config_file" # Remove ngx_pagespeed includes sed -i '/^\s*#\?include \/usr\/local\/nginx\/conf\/pagespeed\(handler\|statslog\)\?\.conf;/d ; /ngx_pagespeed/d' "$config_file" # Remove all try_files and comments inside location / block awk ' BEGIN { inside_location = 0 } /^ location \/ \{/ { inside_location = 1 } inside_location && /^\s*(#|try_files|\s*$)/ { next } inside_location && /^\s*}/ { inside_location = 0 } { print } ' "$config_file" > "${config_file}.tmp" mv -f "${config_file}.tmp" "$config_file" # Ensure the correct try_files line is added sed -i '/^ location \/ {$/a\ \ \ \ try_files $uri $uri/ /index.php?$args;' "$config_file" # Remove consecutive blank lines **only inside the location block** awk ' BEGIN { inside_location = 0 } /^ location \/ \{/ { inside_location = 1 } inside_location && /^\s*$/ { blank++ } inside_location && /^\s*}/ { inside_location = 0; blank=0 } blank > 1 { next } { print } ' "$config_file" > "${config_file}.tmp" mv -f "${config_file}.tmp" "$config_file" # Reload Nginx with validation ngxreload_t "\`clean_nginx_config\` nginx conf failed. Backup available: $config_file_bk" --noexit } # Function: Count unique visitors per user agent ua_unique_visitors() { local log_files=("$@") [[ ${#log_files[@]} -eq 0 ]] && log_files=("access.log") { for f in "${log_files[@]}"; do [[ "$f" == *.gz ]] && zcat --force "$f" || cat "$f" done } 2>/dev/null | awk ' { ip = $1 n = split($0, q, /\"/) ua = q[n - 1] if (ua != "") { key = ua "|" ip if (!seen[key]++) { count[ua]++ } } } END { for (ua in count) { printf "%7d %s\n", count[ua], ua } }' | sort -rn } # Function: Fetch verified bot names from Cloudflare Radar API fetch_verified_bots() { local BOT_LIST_URL="https://www.bigscoots.com/verified-bots.txt" local BOT_LIST_FILE="/tmp/verified.bots.raw" local BOT_REGEX_LIST_FILE="/tmp/verified.bots" curl -s "$BOT_LIST_URL" -o "$BOT_LIST_FILE" sed 's/.*/".*&.*"/' "$BOT_LIST_FILE" | tr '[:upper:]' '[:lower:]' > "$BOT_REGEX_LIST_FILE" echo "$BOT_REGEX_LIST_FILE" } match_verified_bots_to_logs() { local token="$1" shift local log_files=("$@") [[ ${#log_files[@]} -eq 0 ]] && log_files=("access.log") # Fetch and load bot patterns bot_file=$(fetch_verified_bots "$token") mapfile -t bots < "$bot_file" tmp_ua_visitors="/tmp/ua_visitors.tmp" { for f in "${log_files[@]}"; do [[ "$f" == *.gz ]] && zcat --force "$f" || cat "$f" done } 2>/dev/null | awk ' { ip = $1 n = split($0, q, "\"") ua = tolower(q[n - 1]) if (ua != "") { key = ua "|" ip if (!seen[key]++) { count[ua]++ } total[ua]++ } } END { for (ua in count) { gsub(/"/, "", ua) print count[ua] "|" total[ua] "|" ua } }' > "$tmp_ua_visitors" # Output headers printf "%-10s %-15s %s\n" "UNIQUE" "TOTAL_REQS" "BOT_NAME" printf "%-10s %-15s %s\n" "------" "-----------" "---------" total_unique=0 total_requests=0 output_lines=() for bot in "${bots[@]}"; do matches=$(grep -i "|.*${bot//\"/}" "$tmp_ua_visitors") unique=0 total=0 while IFS="|" read -r u t ua; do unique=$((unique + u)) total=$((total + t)) done <<< "$matches" if [ "$total" -gt 0 ]; then output_lines+=("$(printf "%-10d %-15d %s" "$unique" "$total" "$bot")") total_unique=$((total_unique + unique)) total_requests=$((total_requests + total)) fi done # Print sorted output printf "%s\n" "${output_lines[@]}" | sort -k1 -rn # Print grand totals echo echo "========== GRAND TOTAL ==========" printf "%-10s %-15s\n" "UNIQUE" "TOTAL_REQS" printf "%-10s %-15s\n" "------" "-----------" printf "%-10d %-15d\n" "$total_unique" "$total_requests" } # Function: Detect IPs with a high number of unique UAs (likely spoofers) detect_ip_useragent_spammers() { local log_files=("$@") [[ ${#log_files[@]} -eq 0 ]] && log_files=("access.log") zcat -f "${log_files[@]}" 2>/dev/null | awk -F\" ' { split($1, parts, " ") ip = parts[1] ua = $6 ip_ua[ip][ua] = 1 } END { total_spoofed_ua_count = 0 total_bot_ips = 0 for (ip in ip_ua) { n = asorti(ip_ua[ip], tmp) if (n > 10) { print "BOT_IP", ip, "had", n, "unique user agents – treat as 1 visit or exclude" total_spoofed_ua_count += n total_bot_ips++ } } print "\nTotal spoofed UA count: ", total_spoofed_ua_count print "Total BOT_IPs detected: ", total_bot_ips }' } function deploy_opcache_gui { # Generate a UUID for filename UUID=$(uuidgen) FILE_NAME="${UUID}.php" # Download the file wget -q -O "${FILE_NAME}" https://raw.githubusercontent.com/amnuts/opcache-gui/refs/heads/master/index.php if [[ ! -f "${FILE_NAME}" ]]; then echo "Failed to download opcache-gui." return 1 fi # Get site URL SITE_URL=$(wp option get siteurl 2>/dev/null) if [[ -z "${SITE_URL}" ]]; then echo "Could not determine site URL via wp-cli." return 1 fi # Print access URL echo "Opcache GUI available at: ${SITE_URL}/${FILE_NAME}" # Background removal task using screen screen -dmS "remove_opcache_${UUID}" bash -c "sleep 86400 && rm -f \"$(pwd)/${FILE_NAME}\"" } function reinstall_all_plugins { if [[ "$1" == "--all-domains" ]]; then echo "Reinstalling all plugins across all domains (10 at a time)..." find_wp_installs | xargs -P10 -I{} bash -c ' WP="$0" source /bigscoots/includes/common.sh echo "wpcli core is-installed --path=$WP" if wpcli core is-installed --path="$WP" >/dev/null 2>&1; then echo "[INFO] Processing $WP" wpcli plugin list --field=name --path="$WP" | while read -r P; do echo "[INFO] Reinstalling $P in $WP" wpcli plugin install "$P" --force --path="$WP" done else echo "[WARN] Skipping $WP (not a valid WP install)" fi ' {} else source /bigscoots/includes/common.sh if ! wpcli core is-installed >/dev/null 2>&1; then echo "[ERROR] This is not a valid WordPress install (wp core is-installed failed)." return 1 fi echo "Reinstalling plugins in current WordPress install..." wpcli plugin list --field=name | while read -r P; do echo "[INFO] Reinstalling $P" wpcli plugin install "$P" --force done fi } check_domains_lxd() { local DOMAINS_ROOT="/home/nginx/domains" local OUTPUT_JSON=0 if [[ "${1:-}" == "--json" ]]; then OUTPUT_JSON=1 shift fi export OUTPUT_JSON # Get current server's last octet once local CURRENT CURRENT=$(curl -s --connect-timeout 5 --max-time 10 ipinfo.io/ip 2>/dev/null | awk -F'.' '{print $4}') [[ -z "$CURRENT" ]] && CURRENT="unknown" export CURRENT _check_one_domain() { local d="$1" local DOMAIN PAGE EXPECTED FINAL_URL REDIRECT_DOMAIN local REDIRECT_CHECK ROOT_STATUS ROOT_CHECK WPADMIN_STATUS WPADMIN_CHECK local CACHE_BUSTER local -a CURL_OPTS=(-sL --connect-timeout 5 --max-time 10) local -a CURL_OPTS_HEAD=(-sLI --connect-timeout 5 --max-time 10) DOMAIN=$(basename "$d") # Generate random string locally CACHE_BUSTER=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c4) # Use it in the URL PAGE=$(curl "${CURL_OPTS[@]}" "https://$DOMAIN/bigscoots.php?nocache=$CACHE_BUSTER" 2>/dev/null) EXPECTED=$(echo "$PAGE" | grep -oP '(?<=hostname-footer">)[0-9]+-\K[0-9]+(?=)' 2>/dev/null) FINAL_URL=$(curl "${CURL_OPTS_HEAD[@]}" -o /dev/null -w "%{url_effective}" "https://$DOMAIN" 2>/dev/null) REDIRECT_DOMAIN=$(echo "$FINAL_URL" | awk -F/ '{print $3}') local is_redirect=0 if [[ "$REDIRECT_DOMAIN" != "$DOMAIN" && "$REDIRECT_DOMAIN" != "www.$DOMAIN" && "$REDIRECT_DOMAIN" != "" ]]; then is_redirect=1 REDIRECT_CHECK="Redirect: $REDIRECT_DOMAIN" else ROOT_STATUS=$(curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" "https://$DOMAIN" 2>/dev/null) [[ "$ROOT_STATUS" =~ ^(200|301)$ ]] && ROOT_CHECK="βœ“" || ROOT_CHECK="βœ—($ROOT_STATUS)" WPADMIN_STATUS=$(curl "${CURL_OPTS[@]}" -o /dev/null -w "%{http_code}" "https://$DOMAIN/wp-admin" 2>/dev/null) [[ "$WPADMIN_STATUS" =~ ^(200|301|302)$ ]] && WPADMIN_CHECK="βœ“" || WPADMIN_CHECK="βœ—($WPADMIN_STATUS)" REDIRECT_CHECK="Frontend: $ROOT_CHECK WP-Admin: $WPADMIN_CHECK" fi if [[ "$OUTPUT_JSON" -eq 1 ]]; then # Booleans / status local status matched frontend_ok wpadmin_ok matched=false frontend_ok=false wpadmin_ok=false if [[ -z "$EXPECTED" ]]; then status="no_bigscoots" elif [[ "$CURRENT" == "unknown" ]]; then status="current_unknown" elif [[ "$EXPECTED" == "$CURRENT" ]]; then status="ok" matched=true else status="mismatch" fi if [[ "$is_redirect" -eq 0 ]]; then [[ "${ROOT_STATUS:-}" =~ ^(200|301)$ ]] && frontend_ok=true [[ "${WPADMIN_STATUS:-}" =~ ^(200|301|302)$ ]] && wpadmin_ok=true fi # JSON-safe values (domains are hostnames, so safe to embed) local expected_json current_json redirect_json root_json wpadmin_json [[ -n "$EXPECTED" ]] && expected_json="\"$EXPECTED\"" || expected_json="null" [[ "$CURRENT" != "unknown" ]] && current_json="\"$CURRENT\"" || current_json="null" [[ "$is_redirect" -eq 1 ]] && redirect_json="\"$REDIRECT_DOMAIN\"" || redirect_json="null" [[ -n "${ROOT_STATUS:-}" ]] && root_json="\"$ROOT_STATUS\"" || root_json="null" [[ -n "${WPADMIN_STATUS:-}" ]] && wpadmin_json="\"$WPADMIN_STATUS\"" || wpadmin_json="null" printf '{"domain":"%s","status":"%s","current_octet":%s,"expected_octet":%s,"redirect_domain":%s,"root_status":%s,"wpadmin_status":%s,"matched":%s,"frontend_ok":%s,"wpadmin_ok":%s}\n' \ "$DOMAIN" "$status" \ "$current_json" "$expected_json" "$redirect_json" \ "$root_json" "$wpadmin_json" \ "$matched" "$frontend_ok" "$wpadmin_ok" return 0 fi # Human output (original style) if [[ -n "$EXPECTED" && "$CURRENT" != "unknown" ]]; then if [[ "$EXPECTED" = "$CURRENT" ]]; then echo "βœ“ $DOMAIN (.$CURRENT) | $REDIRECT_CHECK" else echo "βœ— $DOMAIN (on .$EXPECTED, this is .$CURRENT) | $REDIRECT_CHECK" fi elif [[ -n "$EXPECTED" && "$CURRENT" == "unknown" ]]; then echo "? $DOMAIN (expected .$EXPECTED, current IP unknown) | $REDIRECT_CHECK" else echo "? $DOMAIN (no bigscoots.php) | $REDIRECT_CHECK" fi } export -f _check_one_domain if [[ "$OUTPUT_JSON" -eq 1 ]]; then # Collect one-JSON-object-per-line from workers, then wrap into an array. local objs objs=$( printf '%s\n' "$DOMAINS_ROOT"/*/ 2>/dev/null \ | xargs -P3 -n1 bash -c '_check_one_domain "$1"' _ ) # Join lines with commas into a valid JSON array echo "$objs" | awk ' BEGIN { print "[" } NF { if (n++ > 0) printf(",") printf("%s", $0) } END { print "]" } ' else printf '%s\n' "$DOMAINS_ROOT"/*/ 2>/dev/null \ | xargs -P3 -n1 bash -c '_check_one_domain "$1"' _ fi } update_nginx_drop_conf_robotstxt() { local target="/usr/local/nginx/conf/drop.conf" # Search for the simple one-line version if grep -q "location = /robots.txt.*access_log off" "$target"; then # Replace matches with the multi-line block # We use single quotes around the sed command to prevent $uri and $args from breaking sed -i 's|.*location = /robots.txt.*{.*access_log off.*}|location = /robots.txt {\n allow all;\n try_files $uri /index.php?$args;\n log_not_found off;\n access_log off;\n}|' "$target" echo "Success: Replaced robots.txt rule." else echo "Skipping: Target robots.txt line not found (or already updated)." fi }