diff --git a/tools/toolkit-qa-check.sh b/tools/toolkit-qa-check.sh new file mode 100755 index 0000000..3b3b154 --- /dev/null +++ b/tools/toolkit-qa-check.sh @@ -0,0 +1,501 @@ +#!/bin/bash +# +# Server Toolkit QA Checker - Optimized for AI Assistant Development +# Designed to help Claude track bugs during development with minimal computation +# +# Features: +# - Structured output (easy to parse) +# - Fixed subshell counter bug +# - Groups by severity +# - Concise summaries +# - Quick file:line references +# + +TOOLKIT_PATH="${1:-/root/server-toolkit}" + +if [ ! -d "$TOOLKIT_PATH" ]; then + echo "ERROR: $TOOLKIT_PATH not found" + exit 1 +fi + +# Output file for easy review +REPORT="/tmp/qa-report.txt" +> "$REPORT" + +# Counters (will use temp file to avoid subshell issues) +TEMP_COUNTS="/tmp/qa-counts.$$" +echo "0 0 0 0" > "$TEMP_COUNTS" + +# Helper to increment counters (avoids subshell issue) +count_issue() { + local severity="$1" + read crit high med low < "$TEMP_COUNTS" + case "$severity" in + CRITICAL) ((crit++)) ;; + HIGH) ((high++)) ;; + MEDIUM) ((med++)) ;; + LOW) ((low++)) ;; + esac + echo "$crit $high $med $low" > "$TEMP_COUNTS" +} + +echo "═══════════════════════════════════════════════════════════════" +echo "SERVER TOOLKIT QA SCAN" +echo "Path: $TOOLKIT_PATH" +echo "Date: $(date '+%Y-%m-%d %H:%M:%S')" +echo "═══════════════════════════════════════════════════════════════" +echo "" + +# Create structured output file +{ +echo "QA SCAN REPORT" +echo "==============" +echo "Timestamp: $(date)" +echo "Path: $TOOLKIT_PATH" +echo "" +} > "$REPORT" + +#============================================================================== +# CHECK 1: grep -F with regex anchors (CRITICAL - causes wrong results) +#============================================================================== +echo "[1/8] Checking: grep -F with regex anchors..." +{ +echo "## CHECK 1: grep -F with regex anchors" +echo "Severity: CRITICAL" +echo "Issue: -F disables regex, \$ and ^ match literally" +echo "" + +# Use process substitution to avoid subshell +while IFS=: read -r file line_num line_content; do + if echo "$line_content" | grep -qE '"\$|".*\^|\\\$'; then + echo "CRITICAL|$file|$line_num|grep -F with regex anchor" + count_issue "CRITICAL" + fi +done < <(grep -rn 'grep -F' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 2: SCRIPT_DIR collisions (HIGH - causes path errors) +#============================================================================== +echo "[2/8] Checking: SCRIPT_DIR variable collisions..." +{ +echo "## CHECK 2: SCRIPT_DIR variable collisions" +echo "Severity: HIGH" +echo "Issue: Multiple files redefining same path variable" +echo "" + +script_dir_count=$(grep -l "^SCRIPT_DIR=" "$TOOLKIT_PATH"/**/*.sh 2>/dev/null | wc -l) +if [ "$script_dir_count" -gt 1 ]; then + files=$(grep -l "^SCRIPT_DIR=" "$TOOLKIT_PATH"/**/*.sh 2>/dev/null | tr '\n' ' ') + echo "HIGH|Multiple files|N/A|SCRIPT_DIR in $script_dir_count files: $files" + count_issue "HIGH" +fi + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 3: SYS_* variable resets (CRITICAL - breaks system detection) +#============================================================================== +echo "[3/8] Checking: SYS_* variable resets..." +{ +echo "## CHECK 3: SYS_* variable resets without protection" +echo "Severity: CRITICAL" +echo "Issue: Re-sourcing wipes all system detection variables" +echo "" + +while read -r file; do + if grep -q '^[[:space:]]*export SYS_.*=""' "$file" && ! grep -q "SYS_DETECTION_COMPLETE" "$file"; then + line_num=$(grep -n '^[[:space:]]*export SYS_.*=""' "$file" | head -1 | cut -d: -f1) + echo "CRITICAL|$file|$line_num|SYS reset without guard" + count_issue "CRITICAL" + fi +done < <(find "$TOOLKIT_PATH/lib" -name "*.sh" 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 4: Missing function exports (HIGH - functions unavailable) +#============================================================================== +echo "[4/8] Checking: Missing function exports..." +{ +echo "## CHECK 4: Missing function exports in libraries" +echo "Severity: HIGH" +echo "Issue: Functions not exported = unavailable in nested calls" +echo "" + +while read -r file; do + func_count=$(grep -cE "^[a-zA-Z_][a-zA-Z0-9_]*\(\)" "$file" 2>/dev/null || echo "0") + func_count=${func_count//[$'\n\r']/} # Remove newlines + export_count=$(grep -c "export -f" "$file" 2>/dev/null || echo "0") + export_count=${export_count//[$'\n\r']/} # Remove newlines + + if [ -n "$func_count" ] && [ -n "$export_count" ] && [ "$func_count" -gt 0 ] && [ "$export_count" -eq 0 ]; then + echo "HIGH|$file|N/A|$func_count functions, 0 exports" + count_issue "HIGH" + fi +done < <(find "$TOOLKIT_PATH/lib" -name "*.sh" 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 5: Integer comparisons without empty checks (HIGH - causes errors) +#============================================================================== +echo "[5/8] Checking: Unsafe integer comparisons (top 10)..." +{ +echo "## CHECK 5: Integer comparisons without empty checks" +echo "Severity: HIGH" +echo "Issue: Empty vars cause 'integer expression expected' errors" +echo "Note: Showing first 10 instances" +echo "" + +count=0 +while IFS=: read -r file line_num line_content; do + var=$(echo "$line_content" | grep -oE '\$[a-zA-Z_][a-zA-Z0-9_]*' | head -1) + echo "HIGH|$file|$line_num|Integer comparison: $var" + count_issue "HIGH" + ((count++)) + [ "$count" -ge 10 ] && break +done < <(grep -rn '\[ \$[a-zA-Z_][a-zA-Z0-9_]* -\(lt\|gt\|le\|ge\|eq\|ne\) [0-9]' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null) + +total=$(grep -r '\[ \$[a-zA-Z_][a-zA-Z0-9_]* -\(lt\|gt\|le\|ge\|eq\|ne\) [0-9]' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | wc -l) +echo "Total found: $total (showing first 10)" +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 6: Missing common-functions.sh (HIGH - command not found) +#============================================================================== +echo "[6/8] Checking: Missing common-functions.sh..." +{ +echo "## CHECK 6: Missing common-functions.sh sourcing" +echo "Severity: HIGH" +echo "Issue: Uses functions without sourcing = command not found" +echo "" + +while read -r file; do + if ! grep -q "source.*common-functions.sh" "$file"; then + echo "HIGH|$file|N/A|Uses common functions without sourcing" + count_issue "HIGH" + fi +done < <(grep -l 'cecho\|print_info\|print_warning\|print_error' "$TOOLKIT_PATH"/modules/**/*.sh 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 7: exit in libraries (HIGH - terminates parent script) +#============================================================================== +echo "[7/8] Checking: exit in library files..." +{ +echo "## CHECK 7: exit in sourced libraries" +echo "Severity: HIGH" +echo "Issue: Should use 'return' not 'exit'" +echo "" + +while read -r file; do + while IFS=: read -r line_num line_content; do + # Skip comments + if ! echo "$line_content" | grep -q "^[[:space:]]*#"; then + if echo "$line_content" | grep -qE '(^|[[:space:]])exit[[:space:]]'; then + echo "HIGH|$file|$line_num|Library uses exit" + count_issue "HIGH" + fi + fi + done < <(grep -n '\' "$file" 2>/dev/null | grep -v "^[[:space:]]*#" | head -5) +done < <(find "$TOOLKIT_PATH/lib" -name "*.sh" 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 8: bc command usage (LOW - external dependency) +#============================================================================== +echo "[8/11] Checking: bc command usage..." +{ +echo "## CHECK 8: bc command usage" +echo "Severity: LOW" +echo "Issue: Requires bc package - not installed on all systems" +echo "" + +while IFS=: read -r file line_num line_content; do + # Skip comments + if ! echo "$line_content" | grep -q "^[[:space:]]*#"; then + echo "LOW|$file|$line_num|Uses bc command" + count_issue "LOW" + fi +done < <(grep -rn ' bc\b' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | head -10) + +total_bc=$(grep -r ' bc\b' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | wc -l) +echo "Total found: $total_bc (showing first 10)" +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 9: Hardcoded /var/cpanel paths (MEDIUM - breaks multi-panel) +#============================================================================== +echo "[9/11] Checking: Hardcoded /var/cpanel paths..." +{ +echo "## CHECK 9: Hardcoded /var/cpanel paths" +echo "Severity: MEDIUM" +echo "Issue: Hardcoded paths break on non-cPanel systems" +echo "" + +while IFS=: read -r file line_num line_content; do + # Skip if using $SYS variables OR if it's a fallback value in ${VAR:-/var/cpanel} + if ! echo "$line_content" | grep -qE '(\$SYS|:-/var/cpanel)'; then + # Skip comments + if ! echo "$line_content" | grep -q "^[[:space:]]*#"; then + echo "MEDIUM|$file|$line_num|Hardcoded /var/cpanel path" + count_issue "MEDIUM" + fi + fi +done < <(grep -rn '/var/cpanel' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | head -10) + +total_cpanel=$(grep -r '/var/cpanel' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | grep -vE '(\$SYS|:-/var/cpanel)' | grep -v '^[[:space:]]*#' | wc -l) +echo "Total found: $total_cpanel (showing first 10)" +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 10: Undefined color variables (LOW - cosmetic issue) +#============================================================================== +echo "[10/11] Checking: Undefined color variables..." +{ +echo "## CHECK 10: Undefined color variables" +echo "Severity: LOW" +echo "Issue: Uses color variables without defining them" +echo "" + +while read -r file; do + # Skip if sources common-functions, defines colors, OR is common-functions.sh itself + if [[ "$file" != *"common-functions.sh" ]] && ! grep -q "source.*common-functions" "$file" && ! grep -q "^RED=" "$file"; then + # Check if uses color variables + if grep -q '\${RED}\|\${GREEN}\|\${BLUE}\|\${YELLOW}\|\${CYAN}\|\${MAGENTA}\|\${NC}' "$file"; then + line_num=$(grep -n '\${RED}\|\${GREEN}' "$file" | head -1 | cut -d: -f1) + echo "LOW|$file|$line_num|Uses color vars without definition" + count_issue "LOW" + fi + fi +done < <(find "$TOOLKIT_PATH" -name "*.sh" -type f 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# CHECK 11: Bash syntax validation (CRITICAL - prevents execution) +#============================================================================== +echo "[11/11] Checking: Bash syntax errors..." +{ +echo "## CHECK 11: Bash syntax validation" +echo "Severity: CRITICAL" +echo "Issue: Syntax errors prevent script from running" +echo "" + +while read -r file; do + if ! bash -n "$file" 2>/dev/null; then + errors=$(bash -n "$file" 2>&1 | head -1) + echo "CRITICAL|$file|N/A|Syntax: $errors" + count_issue "CRITICAL" + fi +done < <(find "$TOOLKIT_PATH" -name "*.sh" 2>/dev/null) + +echo "" +} >> "$REPORT" + +#============================================================================== +# PERFORMANCE CHECKS (INFO level - not counted as issues) +#============================================================================== +echo "" +echo "═══════════════════════════════════════════════════════════════" +echo "PERFORMANCE & OPTIMIZATION CHECKS (Informational)" +echo "═══════════════════════════════════════════════════════════════" +echo "" + +{ +echo "" +echo "═══════════════════════════════════════════════════════════════" +echo "PERFORMANCE ANALYSIS" +echo "═══════════════════════════════════════════════════════════════" +echo "" +} >> "$REPORT" + +# PERF 1: cat file | grep (inefficient) +echo "[PERF-1] Inefficient cat | grep patterns..." +{ +echo "## PERF-1: Inefficient pipe usage" +echo "Severity: INFO" +echo "Pattern: cat file | grep (should be: grep '' file)" +echo "" + +count=0 +while IFS=: read -r file line_num line_content; do + echo "INFO|$file|$line_num|cat | grep pattern" + ((count++)) + [ "$count" -ge 5 ] && break +done < <(grep -rn 'cat.*|.*grep' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null) + +total=$(grep -r 'cat.*|.*grep' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | wc -l) +echo "Found: $total instances (showing first 5)" +echo "Optimization: Replace 'cat file | grep pattern' with 'grep pattern file'" +echo "" +} >> "$REPORT" + +# PERF 2: Repeated file reads +echo "[PERF-2] Multiple zcat on same file..." +{ +echo "## PERF-2: Repeated decompression" +echo "Severity: INFO" +echo "Pattern: Multiple zcat/gunzip calls (expensive)" +echo "" + +zcat_files=$(grep -rh 'zcat [^|]*' "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | \ + grep -oE 'zcat [^ ]+' | sort | uniq -c | sort -rn | head -3) + +if [ -n "$zcat_files" ]; then + echo "$zcat_files" + echo "" + echo "Optimization: Decompress once, store in variable or temp file" +else + echo "None found" +fi +echo "" +} >> "$REPORT" + +# PERF 3: Subshells in loops (expensive) +echo "[PERF-3] Expensive patterns in loops..." +{ +echo "## PERF-3: Subshells in loops" +echo "Severity: INFO" +echo "Pattern: \$(...) or \`...\` inside while/for loops" +echo "" + +# Find loops with subshells (sample only) +count=0 +while read -r file; do + # Get line numbers of loops + grep -n 'while.*do\|for.*do' "$file" 2>/dev/null | while IFS=: read -r loop_line rest; do + # Check next 10 lines for subshells + start=$loop_line + end=$((loop_line + 10)) + if sed -n "${start},${end}p" "$file" 2>/dev/null | grep -q '\$(\|`'; then + echo "INFO|$file|$loop_line|Subshell in loop (potential slowdown)" + ((count++)) + [ "$count" -ge 5 ] && break 2 + fi + done +done < <(find "$TOOLKIT_PATH" -name "*.sh" -type f 2>/dev/null | head -20) + +echo "Note: Review loops for expensive nested operations" +echo "" +} >> "$REPORT" + +# PERF 4: Inefficient string operations +echo "[PERF-4] String operations that could use bash builtins..." +{ +echo "## PERF-4: Inefficient string operations" +echo "Severity: INFO" +echo "Pattern: sed/awk/cut when bash builtins work" +echo "" + +# Sample sed replacements +count=0 +while IFS=: read -r file line_num line_content; do + if echo "$line_content" | grep -q "sed 's/"; then + echo "INFO|$file|$line_num|sed replacement (consider \${var//pattern/replacement})" + ((count++)) + [ "$count" -ge 5 ] && break + fi +done < <(grep -rn "sed 's/" "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null) + +total_sed=$(grep -r "sed 's/" "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | wc -l) +echo "Found: $total_sed sed substitutions" +echo "Note: Many are justified, but consider bash builtins for simple cases" +echo "" +} >> "$REPORT" + +# PERF 5: Multiple grep on same large file +echo "[PERF-5] File read optimization opportunities..." +{ +echo "## PERF-5: Repeated file access" +echo "Severity: INFO" +echo "Pattern: Same file accessed multiple times" +echo "" + +# Check for common heavy files +echo "Common files that might benefit from caching:" +for common_file in "/etc/csf/csf.conf" "/var/log/messages" "/etc/passwd"; do + count=$(grep -r "$common_file" "$TOOLKIT_PATH" --include="*.sh" 2>/dev/null | wc -l) + if [ "$count" -gt 5 ]; then + echo " $common_file: $count references (consider caching)" + fi +done +echo "" +echo "Optimization: Cache frequently-read files in variables" +echo "" +} >> "$REPORT" + +#============================================================================== +# SUMMARY +#============================================================================== +read crit high med low < "$TEMP_COUNTS" +total=$((crit + high + med + low)) + +{ +echo "═══════════════════════════════════════════════════════════════" +echo "SUMMARY" +echo "═══════════════════════════════════════════════════════════════" +echo "Total Issues: $total" +echo " CRITICAL: $crit" +echo " HIGH: $high" +echo " MEDIUM: $med" +echo " LOW: $low" +echo "" +echo "Files Scanned: $(find "$TOOLKIT_PATH" -name "*.sh" 2>/dev/null | wc -l)" +echo "Report: $REPORT" +echo "═══════════════════════════════════════════════════════════════" +} | tee -a "$REPORT" + +echo "" +echo "DETAILED BREAKDOWN BY SEVERITY:" +echo "" + +# Group and display by severity +echo "CRITICAL ISSUES ($crit):" +grep "^CRITICAL|" "$REPORT" | while IFS='|' read -r sev file line issue; do + printf " %s:%s - %s\n" "$file" "$line" "$issue" +done +echo "" + +echo "HIGH ISSUES ($high):" +grep "^HIGH|" "$REPORT" | head -15 | while IFS='|' read -r sev file line issue; do + printf " %s:%s - %s\n" "$file" "$line" "$issue" +done +if [ "$high" -gt 15 ]; then + echo " ... and $((high - 15)) more (see $REPORT)" +fi +echo "" + +echo "MEDIUM ISSUES ($med):" +grep "^MEDIUM|" "$REPORT" | while IFS='|' read -r sev file line issue; do + printf " %s:%s - %s\n" "$file" "$line" "$issue" +done +echo "" + +echo "LOW ISSUES ($low):" +grep "^LOW|" "$REPORT" | while IFS='|' read -r sev file line issue; do + printf " %s:%s - %s\n" "$file" "$line" "$issue" +done + +# Cleanup +rm -f "$TEMP_COUNTS" + +echo "" +echo "Full report saved to: $REPORT" +echo "Exit code: $total" + +exit $total