8f6cb6e91c
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.
303 lines
10 KiB
Bash
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
|