#!/usr/bin/env bash

#########################################################################################
#### server-attack-report.sh — Analyze server logs for attack patterns               ####
#### Scans nginx access logs and SSH auth logs, produces a formatted report.         ####
#### Requires: bash, awk, grep, sort, uniq                                           ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./server-attack-report.sh                                                     ####
####   ./server-attack-report.sh --access /var/log/nginx/access.log                  ####
####   ./server-attack-report.sh --auth /var/log/auth.log                            ####
####   ./server-attack-report.sh --top 30 --geo                                      ####
####   ./server-attack-report.sh --markdown --output report.md                       ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Colors ────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[0;33m'
    CYAN='\033[0;36m'
    BOLD='\033[1m'
    DIM='\033[2m'
    RESET='\033[0m'
else
    RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
fi

# ── Defaults ──────────────────────────────────────────────────────────
ACCESS_LOG=""
AUTH_LOG=""
TOP_N=20
GEO_LOOKUP=false
MARKDOWN=false
OUTPUT_FILE=""
THRESHOLD=50

# ── Auto-detect log paths ────────────────────────────────────────────
detect_access_log() {
    local candidates=(
        /var/log/nginx/access.log
        /var/log/apache2/access.log
        /var/log/httpd/access_log
    )
    for f in "${candidates[@]}"; do
        if [[ -r "$f" && -s "$f" ]]; then
            echo "$f"
            return
        fi
    done
    echo ""
}

detect_auth_log() {
    local candidates=(
        /var/log/auth.log
        /var/log/secure
    )
    for f in "${candidates[@]}"; do
        if [[ -r "$f" && -s "$f" ]]; then
            echo "$f"
            return
        fi
    done
    echo ""
}

# ── Usage ─────────────────────────────────────────────────────────────
usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS]

Analyze server logs for attack patterns and produce a security report.

Options:
  --access PATH    Path to nginx/Apache access log
                   (auto-detects if not specified)
  --auth PATH      Path to SSH auth log
                   (auto-detects if not specified)
  --top N          Number of results per section (default: 20)
  --threshold N    Flag IPs with more than N requests (default: 50)
  --geo            Enable geographic IP lookups (requires curl + internet)
  --markdown       Output in markdown format
  --output FILE    Write report to file instead of stdout
  --no-color       Disable colored output
  -h, --help       Show this help

Examples:
  $(basename "$0")
  $(basename "$0") --access /var/log/nginx/access.log --top 30
  $(basename "$0") --geo --markdown --output report.md
  $(basename "$0") --auth /var/log/secure --threshold 100

EOF
    exit 0
}

# ── Parse arguments ───────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
    case "$1" in
        --access)    ACCESS_LOG="${2:?--access requires a path}"; shift 2 ;;
        --auth)      AUTH_LOG="${2:?--auth requires a path}"; shift 2 ;;
        --top)       TOP_N="${2:?--top requires a number}"; shift 2 ;;
        --threshold) THRESHOLD="${2:?--threshold requires a number}"; shift 2 ;;
        --geo)       GEO_LOOKUP=true; shift ;;
        --markdown)  MARKDOWN=true; shift ;;
        --output)    OUTPUT_FILE="${2:?--output requires a filename}"; shift 2 ;;
        --no-color)  RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""; shift ;;
        -h|--help)   usage ;;
        *)           echo "Unknown option: $1" >&2; usage ;;
    esac
done

# ── Output helpers ────────────────────────────────────────────────────
OUT_FD=1
if [[ -n "$OUTPUT_FILE" ]]; then
    exec 3>"$OUTPUT_FILE"
    OUT_FD=3
    # disable colors for file output
    RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
fi

out() { echo -e "$*" >&$OUT_FD; }

heading() {
    if $MARKDOWN; then
        out ""
        out "## $1"
        out ""
    else
        out ""
        out "${BOLD}${CYAN}═══ $1 ═══${RESET}"
        out ""
    fi
}

subheading() {
    if $MARKDOWN; then
        out "### $1"
        out ""
    else
        out "${BOLD}$1${RESET}"
    fi
}

separator() {
    if $MARKDOWN; then
        out "---"
        out ""
    else
        out "${DIM}$(printf '─%.0s' {1..70})${RESET}"
    fi
}

# ── Auto-detect logs ─────────────────────────────────────────────────
if [[ -z "$ACCESS_LOG" ]]; then
    ACCESS_LOG=$(detect_access_log)
fi

if [[ -z "$AUTH_LOG" ]]; then
    AUTH_LOG=$(detect_auth_log)
fi

HAS_ACCESS=false
HAS_AUTH=false

if [[ -n "$ACCESS_LOG" && -r "$ACCESS_LOG" && -s "$ACCESS_LOG" ]]; then
    HAS_ACCESS=true
fi

if [[ -n "$AUTH_LOG" && -r "$AUTH_LOG" && -s "$AUTH_LOG" ]]; then
    HAS_AUTH=true
fi

if ! $HAS_ACCESS && ! $HAS_AUTH; then
    echo "ERROR: No readable log files found." >&2
    echo "Specify paths with --access and/or --auth" >&2
    exit 1
fi

# ── Report header ─────────────────────────────────────────────────────
REPORT_TIME=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
HOSTNAME=$(hostname -f 2>/dev/null || hostname)

if $MARKDOWN; then
    out "# Server Attack Report"
    out ""
    out "- **Host:** ${HOSTNAME}"
    out "- **Generated:** ${REPORT_TIME}"
    [[ $HAS_ACCESS == true ]] && out "- **Access log:** ${ACCESS_LOG}"
    [[ $HAS_AUTH == true ]]   && out "- **Auth log:** ${AUTH_LOG}"
    out ""
else
    out "${BOLD}Server Attack Report${RESET}"
    out "Host:       ${HOSTNAME}"
    out "Generated:  ${REPORT_TIME}"
    [[ $HAS_ACCESS == true ]] && out "Access log: ${ACCESS_LOG}"
    [[ $HAS_AUTH == true ]]   && out "Auth log:   ${AUTH_LOG}"
fi

# ── Helper: format count table ────────────────────────────────────────
# Reads lines of "count value" from stdin and formats them
format_table() {
    local label_col="${1:-Item}"
    local count_col="${2:-Count}"

    if $MARKDOWN; then
        out "| ${count_col} | ${label_col} |"
        out "|------|------|"
        while IFS= read -r line; do
            local count value
            count=$(echo "$line" | awk '{print $1}')
            value=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //')
            out "| ${count} | ${value} |"
        done
        out ""
    else
        printf "${BOLD}%8s  %-s${RESET}\n" "$count_col" "$label_col" >&$OUT_FD
        while IFS= read -r line; do
            local count value
            count=$(echo "$line" | awk '{print $1}')
            value=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //')
            printf "%8s  %s\n" "$count" "$value" >&$OUT_FD
        done
    fi
}

# ── Helper: format multi-column table ─────────────────────────────────
format_table_multi() {
    local header="$1"

    if $MARKDOWN; then
        out "$header"
        # auto-generate separator from header pipe count
        local cols
        cols=$(echo "$header" | awk -F'|' '{print NF-2}')
        local sep="|"
        for ((i=0; i<cols; i++)); do sep+="------|"; done
        out "$sep"
        while IFS= read -r line; do
            out "$line"
        done
        out ""
    else
        out "$header"
        while IFS= read -r line; do
            out "$line"
        done
    fi
}

# ══════════════════════════════════════════════════════════════════════
#  ACCESS LOG ANALYSIS
# ══════════════════════════════════════════════════════════════════════

if $HAS_ACCESS; then

    TOTAL_REQUESTS=$(wc -l < "$ACCESS_LOG" | tr -dc '0-9')
    TOTAL_IPS=$(awk '{print $1}' "$ACCESS_LOG" | sort -u | wc -l | tr -dc '0-9')
    TOTAL_404=$(awk '$9 == 404' "$ACCESS_LOG" | wc -l | tr -dc '0-9')
    TOTAL_POST=$({ grep -c '"POST ' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    : "${TOTAL_REQUESTS:=0}" "${TOTAL_IPS:=0}" "${TOTAL_404:=0}" "${TOTAL_POST:=0}"

    heading "Summary"

    if $MARKDOWN; then
        out "| Metric | Value |"
        out "|--------|-------|"
        out "| Total requests | ${TOTAL_REQUESTS} |"
        out "| Unique IPs | ${TOTAL_IPS} |"
        out "| 404 responses | ${TOTAL_404} |"
        out "| POST requests | ${TOTAL_POST} |"
        out ""
    else
        out "Total requests:  ${BOLD}${TOTAL_REQUESTS}${RESET}"
        out "Unique IPs:      ${BOLD}${TOTAL_IPS}${RESET}"
        out "404 responses:   ${BOLD}${TOTAL_404}${RESET}"
        out "POST requests:   ${BOLD}${TOTAL_POST}${RESET}"
    fi

    # ── Top IPs ───────────────────────────────────────────────────────
    heading "Top IPs by Request Count"

    awk '{print $1}' "$ACCESS_LOG" \
        | sort | uniq -c | sort -rn | head -"$TOP_N" \
        | format_table "IP Address" "Requests"

    # ── High-volume IPs ───────────────────────────────────────────────
    HIGH_VOLUME=$(awk '{print $1}' "$ACCESS_LOG" \
        | sort | uniq -c | awk -v t="$THRESHOLD" '$1 > t' | sort -rn | wc -l)

    if [[ "$HIGH_VOLUME" -gt 0 ]]; then
        heading "High-Volume IPs (>${THRESHOLD} requests)"

        if $GEO_LOOKUP; then
            subheading "With geographic lookup"
            awk '{print $1}' "$ACCESS_LOG" \
                | sort | uniq -c | awk -v t="$THRESHOLD" '$1 > t' | sort -rn \
                | head -"$TOP_N" | while read -r count ip; do
                    country=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/country" 2>/dev/null || echo "??")
                    org=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/org" 2>/dev/null || echo "unknown")
                    if $MARKDOWN; then
                        out "| ${count} | ${ip} | ${country} | ${org} |"
                    else
                        printf "%8s  %-16s  %-4s  %s\n" "$count" "$ip" "$country" "$org" >&$OUT_FD
                    fi
                done
            out ""
        else
            awk '{print $1}' "$ACCESS_LOG" \
                | sort | uniq -c | awk -v t="$THRESHOLD" '$1 > t' | sort -rn \
                | head -"$TOP_N" \
                | format_table "IP Address" "Requests"
        fi
    fi

    # ── POST Requests ─────────────────────────────────────────────────
    if [[ "$TOTAL_POST" -gt 0 ]]; then
        heading "POST Requests"
        subheading "POST requests by IP and path"

        grep '"POST ' "$ACCESS_LOG" \
            | awk '{print $1, $7, $9}' | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | format_table "IP / Path / Status" "Count"
    fi

    # ── 404 Paths ─────────────────────────────────────────────────────
    if [[ "$TOTAL_404" -gt 0 ]]; then
        heading "Top 404 Paths (Scanner Targets)"

        awk '$9 == 404 {print $7}' "$ACCESS_LOG" \
            | sort | uniq -c | sort -rn | head -"$TOP_N" \
            | format_table "Path" "Hits"
    fi

    # ── Suspicious Paths ──────────────────────────────────────────────
    SUSPICIOUS=$({ grep -ciE '\.(env|git|php|asp|jsp|bak|sql|tar\.gz|zip)|wp-login|wp-admin|xmlrpc|phpmyadmin|pma|cgi-bin' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    : "${SUSPICIOUS:=0}"

    if [[ "$SUSPICIOUS" -gt 0 ]]; then
        heading "Suspicious Path Probes"

        subheading "Secret/config file hunting"
        grep -iE '\.(env|git|aws|ssh)' "$ACCESS_LOG" 2>/dev/null \
            | awk '{print $1, $7}' | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | format_table "IP / Path" "Hits"

        separator

        subheading "WordPress / PHP scanning"
        grep -iE 'wp-login|wp-admin|xmlrpc|\.php|phpmyadmin|pma|cgi-bin' "$ACCESS_LOG" 2>/dev/null \
            | awk '{print $1, $7}' | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | format_table "IP / Path" "Hits"
    fi

    # ── API Enumeration ───────────────────────────────────────────────
    API_PROBES=$({ grep -ciE '/api/|/v[0-9]+/|/swagger|/graphql|/actuator|/openapi|/api-docs|/debug/' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    : "${API_PROBES:=0}"

    if [[ "$API_PROBES" -gt 0 ]]; then
        heading "API Enumeration Probes"

        grep -iE '/api/|/v[0-9]+/|/swagger|/graphql|/actuator|/openapi|/api-docs|/debug/' "$ACCESS_LOG" \
            | awk '{print $1, $7}' | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | format_table "IP / Path" "Hits"
    fi

    # ── User Agents ───────────────────────────────────────────────────
    heading "Top User Agents"

    awk -F'"' '{print $6}' "$ACCESS_LOG" \
        | sort | uniq -c | sort -rn | head -"$TOP_N" \
        | format_table "User Agent" "Requests"

    # ── Known Bot User Agents ─────────────────────────────────────────
    BOT_HITS=$({ grep -ciE 'GPTBot|ClaudeBot|CCBot|Bytespider|PerplexityBot|meta-external|meta-webindexer|SemrushBot|AhrefsBot|MJ12bot|DotBot|Scrapy|python-requests|Go-http-client|curl/' "$ACCESS_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    : "${BOT_HITS:=0}"

    if [[ "$BOT_HITS" -gt 0 ]]; then
        heading "Known Bot / Scraper Traffic"

        grep -iE 'GPTBot|ClaudeBot|CCBot|Bytespider|PerplexityBot|meta-external|meta-webindexer|SemrushBot|AhrefsBot|MJ12bot|DotBot|Scrapy|python-requests|Go-http-client|curl/' "$ACCESS_LOG" \
            | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | format_table "Bot User Agent" "Requests"
    fi

    # ── Response Code Distribution ────────────────────────────────────
    heading "Response Code Distribution"

    awk '$9 ~ /^[0-9]{3}$/ {print $9}' "$ACCESS_LOG" \
        | sort | uniq -c | sort -rn \
        | format_table "Status Code" "Count"

    # ── Requests Per Hour ─────────────────────────────────────────────
    heading "Requests Per Hour (Recent 24)"

    awk '{gsub(/\[/,"",$4); print $4}' "$ACCESS_LOG" \
        | cut -d: -f1-2 | sort | uniq -c \
        | tail -24 \
        | format_table "Date/Hour" "Requests"

    # ── Bandwidth by IP ───────────────────────────────────────────────
    heading "Top IPs by Bandwidth"

    awk '$10 ~ /^[0-9]+$/ {sum[$1] += $10} END {for (ip in sum) printf "%d %s\n", sum[ip], ip}' "$ACCESS_LOG" \
        | sort -rn | head -"$TOP_N" \
        | while read -r bytes ip; do
            bytes="${bytes//[^0-9]/}"
            [[ -z "$bytes" ]] && bytes=0
            if [[ "$bytes" -ge 1073741824 ]]; then
                human=$(awk "BEGIN {printf \"%.1f GB\", $bytes/1073741824}")
            elif [[ "$bytes" -ge 1048576 ]]; then
                human=$(awk "BEGIN {printf \"%.1f MB\", $bytes/1048576}")
            elif [[ "$bytes" -ge 1024 ]]; then
                human=$(awk "BEGIN {printf \"%.1f KB\", $bytes/1024}")
            else
                human="${bytes} B"
            fi
            echo "${human} ${ip}"
        done \
        | format_table "IP Address" "Bandwidth"

fi

# ══════════════════════════════════════════════════════════════════════
#  SSH AUTH LOG ANALYSIS
# ══════════════════════════════════════════════════════════════════════

if $HAS_AUTH; then

    TOTAL_FAILED=$({ grep -c "Failed password" "$AUTH_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    TOTAL_INVALID=$({ grep -c "Invalid user" "$AUTH_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    TOTAL_ACCEPTED=$({ grep -c "Accepted" "$AUTH_LOG" 2>/dev/null || echo 0; } | tail -1 | tr -dc '0-9')
    : "${TOTAL_FAILED:=0}" "${TOTAL_INVALID:=0}" "${TOTAL_ACCEPTED:=0}"

    heading "SSH Authentication Summary"

    if $MARKDOWN; then
        out "| Metric | Value |"
        out "|--------|-------|"
        out "| Failed password attempts | ${TOTAL_FAILED} |"
        out "| Invalid user attempts | ${TOTAL_INVALID} |"
        out "| Successful logins | ${TOTAL_ACCEPTED} |"
        out ""
    else
        out "Failed password attempts:  ${BOLD}${RED}${TOTAL_FAILED}${RESET}"
        out "Invalid user attempts:     ${BOLD}${RED}${TOTAL_INVALID}${RESET}"
        out "Successful logins:         ${BOLD}${GREEN}${TOTAL_ACCEPTED}${RESET}"
    fi

    # ── SSH Brute Force IPs ───────────────────────────────────────────
    if [[ "$TOTAL_FAILED" -gt 0 ]]; then
        heading "Top SSH Brute Force IPs"

        grep "Failed password" "$AUTH_LOG" \
            | sed -n 's/.*from \([0-9.]*\) port.*/\1/p' | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | if $GEO_LOOKUP; then
                while read -r count ip; do
                    country=$(curl -s --max-time 3 "https://ipinfo.io/${ip}/country" 2>/dev/null || echo "??")
                    if $MARKDOWN; then
                        echo "| ${count} | ${ip} | ${country} |"
                    else
                        printf "%8s  %-16s  %s\n" "$count" "$ip" "$country"
                    fi
                done
            else
                format_table "IP Address" "Attempts"
            fi
    fi

    # ── Targeted Usernames ────────────────────────────────────────────
    if [[ "$TOTAL_FAILED" -gt 0 ]]; then
        heading "Most Targeted SSH Usernames"

        grep "Failed password" "$AUTH_LOG" \
            | sed -n 's/.*Failed password for invalid user \([^ ]*\) from.*/\1/p; s/.*Failed password for \([^ ]*\) from.*/\1/p' \
            | sort | uniq -c | sort -rn | head -"$TOP_N" \
            | format_table "Username" "Attempts"
    fi

    # ── Invalid Users ─────────────────────────────────────────────────
    if [[ "$TOTAL_INVALID" -gt 0 ]]; then
        heading "Invalid Usernames Attempted"

        grep "Invalid user" "$AUTH_LOG" \
            | sed -n 's/.*Invalid user \([^ ]*\) from.*/\1/p' \
            | sort | uniq -c | sort -rn \
            | head -"$TOP_N" \
            | format_table "Username" "Attempts"
    fi

    # ── Successful Logins ─────────────────────────────────────────────
    if [[ "$TOTAL_ACCEPTED" -gt 0 ]]; then
        heading "Successful SSH Logins (Verify These)"

        if $MARKDOWN; then
            out "| Date | User | IP | Method |"
            out "|------|------|----|--------|"
        fi

        grep "Accepted" "$AUTH_LOG" | tail -"$TOP_N" \
            | while IFS= read -r line; do
                local_date=$(echo "$line" | awk '{print $1, $2, $3}')
                user=$(echo "$line" | awk '{print $9}')
                ip=$(echo "$line" | awk '{print $11}')
                method=$(echo "$line" | awk '{print $7}')
                if $MARKDOWN; then
                    out "| ${local_date} | ${user} | ${ip} | ${method} |"
                else
                    printf "  %-15s  %-12s  %-16s  %s\n" "$local_date" "$user" "$ip" "$method" >&$OUT_FD
                fi
            done
        out ""
    fi

    # ── SSH Failures Per Hour ─────────────────────────────────────────
    if [[ "$TOTAL_FAILED" -gt 0 ]]; then
        heading "SSH Failures Per Hour (Recent)"

        grep "Failed password" "$AUTH_LOG" \
            | awk '{
                if ($1 ~ /^[0-9]{4}-/) {
                    # ISO 8601: 2026-04-25T21:11:13.346843+02:00
                    split($1, a, "T")
                    split(a[2], b, ":")
                    print a[1] " " b[1]":00"
                } else {
                    # BSD syslog: Apr 24 14:30:01
                    print $1, $2, substr($3,1,2)":00"
                }
            }' | sort | uniq -c \
            | tail -24 \
            | format_table "Time" "Failures"
    fi

fi

# ══════════════════════════════════════════════════════════════════════
#  REPORT FOOTER
# ══════════════════════════════════════════════════════════════════════

separator

if $MARKDOWN; then
    out ""
    out "*Report generated by [server-attack-report.sh](https://mylinux.work/projects/server-attack-report/) on ${REPORT_TIME}*"
else
    out "${DIM}Report generated by server-attack-report.sh on ${REPORT_TIME}${RESET}"
fi

if [[ -n "$OUTPUT_FILE" ]]; then
    exec 3>&-
    echo "Report written to: ${OUTPUT_FILE}"
fi
