Files
cschantz 8f6cb6e91c Fix HIGH priority issues: library exit, unquoted paths, and globs
Fixed multiple HIGH severity issues found by QA scan:

1. Library exit usage (lib/http-attack-analyzer.sh):
   - Changed exit 1 to return 1
   - Libraries should return, not exit (would terminate caller)

2. Unquoted path expansions (9 fixes):
   - cleanup-toolkit-data.sh: Quoted $pattern in ls/rm commands
   - hardware-health-check.sh: Quoted /sys/block/$disk/queue paths
   - plesk-helpers.sh: Quoted /var/qmail/mailnames/$domain path
   - Prevents breakage with paths containing spaces

3. Unquoted globs in rm commands (3 fixes):
   - erase-toolkit-traces.sh: Quoted glob patterns
   - Prevents unintended file deletion from glob expansion

All changes improve robustness and prevent edge case failures.
2026-01-02 16:39:57 -05:00

303 lines
10 KiB
Bash

#!/bin/bash
#
# HTTP Attack Analyzer
# Analyzes Apache/Nginx log entries for attack patterns using signature database
#
# Requires: attack-signatures.sh
# Source attack signatures
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/attack-signatures.sh" 2>/dev/null || {
echo "ERROR: attack-signatures.sh not found" >&2
return 1
}
# Analyze a single HTTP request log line
# Input: Apache/Nginx combined log format
# Returns: threat_score||attack_types||matched_signatures||ip||uri
# Example: "85||SQLI,XSS||union_select,script_tag||192.168.1.100||/index.php?id=1"
analyze_http_log_line() {
local log_line="$1"
# Parse log line (Apache/Nginx combined format)
# 192.168.1.1 - - [12/Dec/2025:10:30:45 +0000] "GET /index.php?id=1 HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
# Extract components using regex
if [[ "$log_line" =~ ^([0-9.]+)[[:space:]].*\"([A-Z]+)[[:space:]]([^[:space:]]+)[[:space:]]HTTP/[0-9.]+\"[[:space:]]([0-9]+)[[:space:]]([0-9-]+)[[:space:]]\"([^\"]*)\"[[:space:]]\"([^\"]*)\" ]]; then
local ip="${BASH_REMATCH[1]}"
local method="${BASH_REMATCH[2]}"
local uri="${BASH_REMATCH[3]}"
local status="${BASH_REMATCH[4]}"
local size="${BASH_REMATCH[5]}"
local referer="${BASH_REMATCH[6]}"
local user_agent="${BASH_REMATCH[7]}"
else
# Failed to parse
echo "0||PARSE_ERROR||||||"
return 1
fi
# Build complete request string for analysis
local full_request="$method $uri HTTP/1.1
Referer: $referer
User-Agent: $user_agent"
# Detect attacks using signature database
local attack_result=$(detect_all_attack_signatures "$full_request" 2>/dev/null)
if [ -n "$attack_result" ]; then
# Parse result: max_severity||match_count||matches...
local max_severity="${attack_result%%||*}"
local temp="${attack_result#*||}"
local match_count="${temp%%||*}"
local matches="${temp#*||}"
# Extract attack types and signatures
local attack_types=()
local signatures=()
# Parse each match (format: severity||category||pattern||description)
IFS=' ' read -ra match_array <<< "$matches"
for match in "${match_array[@]}"; do
# Extract category and pattern name
local match_sev="${match%%||*}"
local match_temp="${match#*||}"
local category="${match_temp%%||*}"
match_temp="${match_temp#*||}"
local pattern="${match_temp%%||*}"
attack_types+=("$category")
signatures+=("$pattern")
done
# Remove duplicates
local unique_types=$(printf '%s\n' "${attack_types[@]}" | sort -u | tr '\n' ',' | sed 's/,$//')
local unique_sigs=$(printf '%s\n' "${signatures[@]}" | sort -u | tr '\n' ',' | sed 's/,$//')
# Calculate final threat score
local threat_score=$max_severity
# Boost score for multiple attack types
if [ "$match_count" -gt 1 ]; then
threat_score=$((threat_score + (match_count - 1) * 5))
fi
# Boost for suspicious status codes
case "$status" in
200) threat_score=$((threat_score + 10)) ;; # Success = higher threat
500|502|503) threat_score=$((threat_score + 5)) ;; # Error might indicate exploit attempt
esac
# Cap at 100
[ "$threat_score" -gt 100 ] && threat_score=100
# Return: threat_score||attack_types||signatures||ip||uri
echo "$threat_score||${unique_types}||${unique_sigs}||$ip||$uri"
return 0
else
# No pattern matches - check for suspicious indicators
local suspicious_score=0
local indicators=()
# Unusual HTTP methods
case "$method" in
PUT|DELETE|TRACE|CONNECT|OPTIONS)
suspicious_score=$((suspicious_score + 30))
indicators+=("unusual_method:$method")
;;
esac
# Very long URIs (>500 chars)
if [ "${#uri}" -gt 500 ]; then
suspicious_score=$((suspicious_score + 20))
indicators+=("long_uri:${#uri}")
fi
# Multiple encoding layers
if echo "$uri" | grep -q '%25'; then
suspicious_score=$((suspicious_score + 25))
indicators+=("double_encoding")
fi
# Suspicious user agents
if echo "$user_agent" | grep -iEq "(nikto|sqlmap|nmap|masscan|burp|metasploit|acunetix|nessus|w3af)"; then
suspicious_score=$((suspicious_score + 40))
indicators+=("scanner_ua")
fi
# Empty or suspicious referer
if [ "$referer" = "-" ] && [ "$method" = "POST" ]; then
suspicious_score=$((suspicious_score + 15))
indicators+=("no_referer_post")
fi
# Excessive parameters (possible fuzzing)
local param_count=$(echo "$uri" | grep -o '&' | wc -l)
if [ "$param_count" -gt 20 ]; then
suspicious_score=$((suspicious_score + 20))
indicators+=("excessive_params:$param_count")
fi
if [ "$suspicious_score" -gt 0 ]; then
local indicator_str=$(IFS=,; echo "${indicators[*]}")
echo "$suspicious_score||SUSPICIOUS||${indicator_str}||$ip||$uri"
return 0
fi
fi
# Clean request
echo "0||CLEAN||||$ip||$uri"
return 0
}
# Batch analyze multiple log lines
# Input: File path or stdin
# Output: Summary statistics + threat list
analyze_http_log_batch() {
local log_file="$1"
local time_window="${2:-300}" # Default 5 minutes (unused for now)
local total_requests=0
local clean_requests=0
local suspicious_requests=0
local attack_requests=0
local critical_attacks=0
declare -A ip_threats
declare -A attack_type_counts
# Process log lines
while IFS= read -r line; do
[ -z "$line" ] && continue
total_requests=$((total_requests + 1))
local result=$(analyze_http_log_line "$line")
local threat_score="${result%%||*}"
local temp="${result#*||}"
local attack_types="${temp%%||*}"
# Categorize
if [ "$threat_score" -eq 0 ]; then
clean_requests=$((clean_requests + 1))
elif [ "$threat_score" -lt 50 ]; then
suspicious_requests=$((suspicious_requests + 1))
else
attack_requests=$((attack_requests + 1))
# Count as critical if score >= 85
[ "$threat_score" -ge 85 ] && critical_attacks=$((critical_attacks + 1))
# Track by IP (extract IP from result)
local ip_temp="${result##*||}"
ip_temp="${ip_temp#*||}"
local ip="${ip_temp%%||*}"
ip_threats["$ip"]=$((${ip_threats[$ip]:-0} + threat_score))
# Track attack types
IFS=',' read -ra types <<< "$attack_types"
for type in "${types[@]}"; do
[ -n "$type" ] && attack_type_counts["$type"]=$((${attack_type_counts[$type]:-0} + 1))
done
fi
done < <(if [ -n "$log_file" ] && [ -f "$log_file" ]; then cat "$log_file"; else cat; fi)
# Generate summary
echo "SUMMARY||$total_requests||$clean_requests||$suspicious_requests||$attack_requests||$critical_attacks"
# Top threatening IPs
local top_ips=""
for ip in "${!ip_threats[@]}"; do
top_ips+="$ip:${ip_threats[$ip]} "
done
echo "TOP_IPS||$(echo "$top_ips" | tr ' ' '\n' | sort -t: -k2 -nr | head -10 | tr '\n' ' ' | sed 's/ $//')"
# Attack type distribution
local attack_dist=""
for type in "${!attack_type_counts[@]}"; do
attack_dist+="$type:${attack_type_counts[$type]} "
done
echo "ATTACK_TYPES||$(echo "$attack_dist" | tr ' ' '\n' | sort -t: -k2 -nr | tr '\n' ' ' | sed 's/ $//')"
}
# Real-time monitoring mode
# Watches log file and reports attacks as they happen
# Usage: monitor_http_log_realtime "/var/log/apache2/access_log" "callback_function_name"
monitor_http_log_realtime() {
local log_file="$1"
local callback_function="$2" # Function to call with results
if [ ! -f "$log_file" ]; then
echo "ERROR: Log file not found: $log_file" >&2
return 1
fi
tail -f "$log_file" 2>/dev/null | while IFS= read -r line; do
[ -z "$line" ] && continue
local result=$(analyze_http_log_line "$line")
local threat_score="${result%%||*}"
# Only report threats (score > 0)
if [ "$threat_score" -gt 0 ]; then
# Call callback function with result
if type "$callback_function" &>/dev/null; then
"$callback_function" "$result" "$line"
else
# Default: print to stdout
echo "[THREAT:$threat_score] $result"
fi
fi
done
}
# Parse analysis result into components
# Usage: parse_http_analysis_result "$result"
# Sets global variables: THREAT_SCORE, ATTACK_TYPES, SIGNATURES, IP_ADDR, URI
parse_http_analysis_result() {
local result="$1"
THREAT_SCORE="${result%%||*}"
local temp="${result#*||}"
ATTACK_TYPES="${temp%%||*}"
temp="${temp#*||}"
SIGNATURES="${temp%%||*}"
temp="${temp#*||}"
IP_ADDR="${temp%%||*}"
URI="${temp#*||}"
}
# Format threat for display
# Usage: format_threat_display "$result"
format_threat_display() {
local result="$1"
parse_http_analysis_result "$result"
local severity_label="LOW"
local color="\033[0;36m" # Cyan
if [ "$THREAT_SCORE" -ge 85 ]; then
severity_label="CRITICAL"
color="\033[0;31m" # Red
elif [ "$THREAT_SCORE" -ge 70 ]; then
severity_label="HIGH"
color="\033[1;31m" # Bright red
elif [ "$THREAT_SCORE" -ge 50 ]; then
severity_label="MEDIUM"
color="\033[1;33m" # Yellow
fi
echo -e "${color}[$severity_label:$THREAT_SCORE]${NC} $IP_ADDR$ATTACK_TYPES"
echo " URI: ${URI:0:100}"
[ -n "$SIGNATURES" ] && echo " Signatures: $SIGNATURES"
}
# Export functions for use in subshells
export -f analyze_http_log_line
export -f analyze_http_log_batch
export -f monitor_http_log_realtime
export -f parse_http_analysis_result
export -f format_threat_display