Files
Linux-Server-Management-Too…/lib/mysql-analyzer.sh
T
cschantz a51d968185 Initial commit: Server Management Toolkit v2.0
- Complete security menu restructure (3-mode: Analysis/Actions/Live)
- Intelligent cPHulk enablement with CSF whitelist import
- Live network security monitoring dashboard
- Multi-source threat detection and classification
- 50+ organized security tools across 4-level menu hierarchy
- System health diagnostics with cPanel/WHM integration
- Reference database for cross-module intelligence sharing
2025-11-03 18:21:40 -05:00

525 lines
16 KiB
Bash
Executable File

#!/bin/bash
#############################################################################
# MySQL/MariaDB Deep Analysis Library
# Forensic-level query analysis with WordPress plugin identification
#############################################################################
# Source dependencies
if [ -z "$TOOLKIT_BASE_DIR" ]; then
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common-functions.sh"
source "$SCRIPT_DIR/system-detect.sh"
source "$SCRIPT_DIR/user-manager.sh"
fi
#############################################################################
# WORDPRESS PLUGIN SIGNATURES
#############################################################################
# Map table name patterns to plugin names
declare -gA PLUGIN_SIGNATURES=(
# E-Commerce
["woocommerce"]="WooCommerce"
["wc_admin|wc_order|wc_product"]="WooCommerce"
["edd_"]="Easy Digital Downloads"
# SEO
["yoast"]="Yoast SEO"
["rank_math"]="Rank Math SEO"
["aioseo"]="All in One SEO"
# Security
["wfBlocks|wfConfig|wfCrawlers|wfHits|wfLocs|wfLogins"]="WordFence"
["itsec_"]="iThemes Security"
["defender_"]="Defender Security"
# Forms
["wpforms"]="WPForms"
["gf_|gravityforms"]="Gravity Forms"
["ninja_forms"]="Ninja Forms"
["frm_|formidable"]="Formidable Forms"
["cf7_|contact_form_7"]="Contact Form 7"
# Page Builders
["elementor"]="Elementor"
["siteorigin"]="SiteOrigin Page Builder"
["beaver_"]="Beaver Builder"
["fusion_"]="Avada Fusion Builder"
# Multilingual
["icl_|wpml"]="WPML"
["translations_"]="Polylang"
# Caching/Performance
["w3tc"]="W3 Total Cache"
["wp_rocket"]="WP Rocket"
["cache_"]="Various Cache Plugins"
# Email/Newsletter
["mailpoet"]="MailPoet"
["newsletter"]="Newsletter"
["wysija"]="MailPoet 2"
# Events/Booking
["em_|events_manager"]="Events Manager"
["booking"]="Booking Calendar"
["amelia"]="Amelia Booking"
# Backup
["duplicator"]="Duplicator"
["updraft"]="UpdraftPlus"
# Media/Gallery
["ngg_|nextgen"]="NextGEN Gallery"
["smush"]="Smush"
["ewww"]="EWWW Image Optimizer"
# Membership
["pmpro|members"]="Paid Memberships Pro"
["mepr_"]="MemberPress"
# Search
["searchwp"]="SearchWP"
["relevanssi"]="Relevanssi"
# Social
["social_warfare"]="Social Warfare"
["monarcht"]="Monarch Social Sharing"
# Redirects
["redirection"]="Redirection"
["simple_301"]="Simple 301 Redirects"
# WP Core/Action Scheduler
["actionscheduler"]="Action Scheduler (WooCommerce/Jetpack)"
["jetpack"]="Jetpack"
# LMS
["learndash"]="LearnDash"
["tutor"]="Tutor LMS"
# Other Popular
["acf_"]="Advanced Custom Fields"
["pods_"]="Pods Framework"
["tablepress"]="TablePress"
)
# Known problematic query patterns
declare -gA PROBLEM_PATTERNS=(
["SELECT.*wp_options.*autoload"]="Autoloaded options bloat"
["SELECT.*wp_postmeta.*meta_key"]="Postmeta table scan (missing index)"
["wp_woocommerce_sessions.*session_expiry"]="Expired WooCommerce sessions"
["actionscheduler.*scheduled_date.*pending"]="Action Scheduler backlog"
["wp_posts.*post_type.*LIKE"]="Inefficient post type query"
)
#############################################################################
# DATABASE MAPPING
#############################################################################
# Map database to user and domain
map_database_to_user_domain() {
local db_name="$1"
local map_file="${TEMP_SESSION_DIR}/db_user_domain_map.tmp"
# Return cached if exists
if [ -f "$map_file" ]; then
grep "^${db_name}|" "$map_file" 2>/dev/null
return
fi
# Build map for all databases
print_info "Building database to user/domain mapping..."
local all_dbs=$(mysql -Ns -e "SHOW DATABASES" 2>/dev/null | grep -v "^information_schema$\|^mysql$\|^performance_schema$\|^sys$")
for db in $all_dbs; do
# Extract potential username from database name
# Format: username_dbname
local potential_user=$(echo "$db" | cut -d_ -f1)
# Verify user exists
local users=($(list_all_users))
if [[ " ${users[@]} " =~ " ${potential_user} " ]]; then
local primary_domain=$(get_user_domains "$potential_user" | head -1)
echo "${db}|${potential_user}|${primary_domain}" >> "$map_file"
else
echo "${db}|unknown|unknown" >> "$map_file"
fi
done
grep "^${db_name}|" "$map_file" 2>/dev/null
}
# Get database owner
get_database_owner() {
local db_name="$1"
map_database_to_user_domain "$db_name" | cut -d'|' -f2
}
# Get database domain
get_database_domain() {
local db_name="$1"
map_database_to_user_domain "$db_name" | cut -d'|' -f3
}
#############################################################################
# QUERY CAPTURE
#############################################################################
# Capture live queries from processlist
capture_live_queries() {
local output_file="${TEMP_SESSION_DIR}/live_queries.tmp"
print_info "Capturing live queries..."
mysql -e "SHOW FULL PROCESSLIST" 2>/dev/null | grep -v "SHOW FULL PROCESSLIST" > "$output_file"
local query_count=$(wc -l < "$output_file")
print_success "Captured $query_count active queries"
echo "$output_file"
}
# Parse slow query log
parse_slow_query_log() {
local slow_log="${1:-/var/log/mysql/slow.log}"
local output_file="${TEMP_SESSION_DIR}/slow_queries.tmp"
if [ ! -f "$slow_log" ]; then
# Try alternative locations
slow_log=$(mysql -Ns -e "SHOW VARIABLES LIKE 'slow_query_log_file'" | awk '{print $2}')
fi
if [ ! -f "$slow_log" ]; then
print_warning "Slow query log not found"
touch "$output_file"
return 1
fi
print_info "Parsing slow query log: $slow_log"
# Extract queries that took > 1 second (adjustable)
grep -A 10 "Query_time:" "$slow_log" 2>/dev/null | tail -1000 > "$output_file"
local query_count=$(grep -c "Query_time:" "$output_file" 2>/dev/null || echo 0)
print_success "Found $query_count slow queries"
echo "$output_file"
}
#############################################################################
# TABLE ANALYSIS
#############################################################################
# Identify plugin from table name
identify_plugin_from_table() {
local table_name="$1"
# Remove prefix to get base table name
local base_table=$(echo "$table_name" | sed 's/^[a-z0-9]*_wp_//; s/^wp_//')
# Check against signatures
for pattern in "${!PLUGIN_SIGNATURES[@]}"; do
if echo "$base_table" | grep -qiE "$pattern"; then
echo "${PLUGIN_SIGNATURES[$pattern]}"
return 0
fi
done
# Check for WP core tables
if echo "$table_name" | grep -qE "wp_(posts|postmeta|users|usermeta|options|terms|term_relationships|term_taxonomy|comments|commentmeta|links)$"; then
echo "WordPress Core"
return 0
fi
echo "Unknown Plugin"
}
# Get table size
get_table_size() {
local db_name="$1"
local table_name="$2"
mysql -Ns -e "SELECT ROUND(((data_length + index_length) / 1024 / 1024), 2)
FROM information_schema.TABLES
WHERE table_schema='$db_name' AND table_name='$table_name'" 2>/dev/null
}
# Get all tables for database
get_database_tables() {
local db_name="$1"
mysql -Ns "$db_name" -e "SHOW TABLES" 2>/dev/null
}
# Analyze table for issues
analyze_table_structure() {
local db_name="$1"
local table_name="$2"
# Get table status
mysql -Ns -e "SHOW TABLE STATUS FROM \`$db_name\` LIKE '$table_name'" 2>/dev/null
}
#############################################################################
# QUERY ANALYSIS
#############################################################################
# Extract database from query
extract_database_from_query() {
local query="$1"
# Try to extract from USE statement
if echo "$query" | grep -qiE "^USE "; then
echo "$query" | grep -oiE "^USE \K[a-z0-9_]+" | head -1
return 0
fi
# Try to extract from db.table format
if echo "$query" | grep -qE "\`[a-z0-9_]+\`\."; then
echo "$query" | grep -oE "\`[a-z0-9_]+\`\." | head -1 | tr -d '`.'
return 0
fi
echo "unknown"
}
# Extract tables from query
extract_tables_from_query() {
local query="$1"
# Extract FROM and JOIN clauses
echo "$query" | grep -oiE "(FROM|JOIN)\s+\`?[a-z0-9_]+\`?" | awk '{print $2}' | tr -d '`' | sort -u
}
# Analyze query performance with EXPLAIN
explain_query() {
local db_name="$1"
local query="$2"
local explain_file="${TEMP_SESSION_DIR}/explain_${db_name}_$$.tmp"
# Clean query for EXPLAIN
local clean_query=$(echo "$query" | sed 's/^[^SELECT]*//')
mysql "$db_name" -e "EXPLAIN $clean_query" 2>/dev/null > "$explain_file"
# Check for problematic patterns
if grep -qiE "Using filesort|Using temporary" "$explain_file"; then
echo "WARNING: Inefficient query (filesort/temporary table)"
fi
if grep -qE "type.*ALL" "$explain_file"; then
echo "CRITICAL: Full table scan detected"
fi
cat "$explain_file"
}
#############################################################################
# PROBLEM IDENTIFICATION
#############################################################################
# Analyze queries and identify problems
analyze_queries_for_problems() {
local query_file="$1"
local problems_file="${TEMP_SESSION_DIR}/query_problems.tmp"
print_info "Analyzing queries for problems..."
> "$problems_file"
local line_num=0
while IFS= read -r line; do
((line_num++))
# Extract query time if from slow log
local query_time=""
if echo "$line" | grep -qE "Query_time:"; then
query_time=$(echo "$line" | grep -oE "Query_time: [0-9.]+" | awk '{print $2}')
fi
# Extract the actual query
local query=$(echo "$line" | grep -oiE "SELECT.*" | head -1)
[ -z "$query" ] && continue
# Extract database
local db_name=$(extract_database_from_query "$query")
# Extract tables
local tables=$(extract_tables_from_query "$query")
# Identify plugins
for table in $tables; do
local plugin=$(identify_plugin_from_table "$table")
local owner=$(get_database_owner "$db_name")
local domain=$(get_database_domain "$db_name")
# Check against problem patterns
for pattern in "${!PROBLEM_PATTERNS[@]}"; do
if echo "$query" | grep -qiE "$pattern"; then
local issue="${PROBLEM_PATTERNS[$pattern]}"
echo "PROBLEM|$domain|$owner|$db_name|$plugin|$table|$issue|$query_time|$query" >> "$problems_file"
fi
done
# Record all plugin queries for statistics
echo "QUERY|$domain|$owner|$db_name|$plugin|$table|$query_time" >> "$problems_file"
done
# Progress indicator
if [ $((line_num % 100)) -eq 0 ]; then
show_progress $line_num 1000 "Analyzing queries..."
fi
done < "$query_file"
finish_progress
echo "$problems_file"
}
#############################################################################
# STATISTICS & REPORTING
#############################################################################
# Generate plugin query statistics
generate_plugin_statistics() {
local problems_file="$1"
local stats_file="${TEMP_SESSION_DIR}/plugin_stats.tmp"
print_info "Generating plugin statistics..."
# Count queries per plugin per domain
awk -F'|' '$1=="QUERY" {print $2"|"$5}' "$problems_file" | sort | uniq -c | sort -rn > "$stats_file"
echo "$stats_file"
}
# Find largest tables
find_largest_tables() {
local limit="${1:-20}"
local output_file="${TEMP_SESSION_DIR}/largest_tables.tmp"
print_info "Finding largest tables..."
mysql -Ns -e "SELECT
table_schema,
table_name,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
FROM information_schema.TABLES
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
ORDER BY (data_length + index_length) DESC
LIMIT $limit" 2>/dev/null > "$output_file"
echo "$output_file"
}
# Check for bloated tables
check_table_bloat() {
local db_name="$1"
local table_name="$2"
# Check data_free (fragmentation)
local data_free=$(mysql -Ns -e "SELECT data_free FROM information_schema.TABLES
WHERE table_schema='$db_name' AND table_name='$table_name'" 2>/dev/null)
local data_length=$(mysql -Ns -e "SELECT data_length FROM information_schema.TABLES
WHERE table_schema='$db_name' AND table_name='$table_name'" 2>/dev/null)
if [ -n "$data_free" ] && [ -n "$data_length" ] && [ "$data_length" -gt 0 ]; then
local bloat_percent=$(awk "BEGIN {printf \"%.0f\", ($data_free/$data_length)*100}")
if [ "$bloat_percent" -gt 20 ]; then
echo "BLOATED: ${bloat_percent}% fragmentation"
return 0
fi
fi
echo "OK"
return 1
}
# Recommend fixes for common issues
recommend_fix() {
local issue="$1"
local db_name="$2"
local table_name="$3"
local plugin="$4"
case "$issue" in
*"Autoloaded options"*)
echo "Run: wp option list --autoload=yes --format=count (check if > 500)"
echo "Fix: Disable autoload for large options or remove unused plugins"
;;
*"Postmeta table scan"*)
echo "ALTER TABLE \`$table_name\` ADD INDEX idx_meta_key (meta_key(191));"
;;
*"Expired WooCommerce sessions"*)
echo "DELETE FROM \`$table_name\` WHERE session_expiry < UNIX_TIMESTAMP(NOW() - INTERVAL 7 DAY);"
;;
*"Action Scheduler"*)
echo "wp action-scheduler clean --batch-size=100 (if WP-CLI available)"
echo "Or: DELETE FROM \`$table_name\` WHERE status='complete' AND scheduled_date < NOW() - INTERVAL 30 DAY;"
;;
*"Full table scan"*)
echo "Run EXPLAIN on the query to identify missing indexes"
echo "Consider adding appropriate indexes based on WHERE/JOIN clauses"
;;
*)
if [ "$plugin" = "WordFence" ]; then
echo "Update WordFence to latest version"
echo "Consider adjusting scan frequency"
elif [ "$plugin" = "WooCommerce" ]; then
echo "Verify WooCommerce database tables are optimized"
echo "Enable WooCommerce session cleanup cron"
elif [ "$plugin" = "Yoast SEO" ]; then
echo "wp yoast index --reindex (rebuild indexables)"
fi
;;
esac
}
#############################################################################
# SUMMARY REPORT
#############################################################################
generate_summary_report() {
local problems_file="$1"
print_banner "MySQL Query Analysis Summary"
# Critical issues
local critical_count=$(grep -c "^PROBLEM" "$problems_file" 2>/dev/null || echo 0)
if [ "$critical_count" -gt 0 ]; then
echo -e "${RED}${BOLD} CRITICAL ISSUES FOUND: $critical_count${NC}"
echo ""
grep "^PROBLEM" "$problems_file" | head -10 | while IFS='|' read -r type domain owner db plugin table issue query_time query; do
echo -e "${RED}[!] $plugin - $domain${NC}"
echo " Database: $db"
echo " Table: $table"
echo " Issue: $issue"
[ -n "$query_time" ] && echo " Query Time: ${query_time}s"
echo " Fix: $(recommend_fix "$issue" "$db" "$table" "$plugin")"
echo ""
done
else
echo -e "${GREEN} No critical issues detected${NC}"
echo ""
fi
}
#############################################################################
# EXPORT FUNCTIONS
#############################################################################
# Make functions available to other scripts
export -f identify_plugin_from_table
export -f map_database_to_user_domain
export -f get_database_owner
export -f get_database_domain
export -f analyze_queries_for_problems
export -f generate_plugin_statistics
export -f recommend_fix