chore: checkpoint initial — Zektyc dataset, avant humanisation
- site vitrine + fingerprint test + zmap + z-panel - remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs) - suppression du token DuckDNS (plus utilisé)
This commit is contained in:
commit
8e7ec41f18
104 changed files with 19256 additions and 0 deletions
187
scripts/ipwatch.sh
Executable file
187
scripts/ipwatch.sh
Executable file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env bash
|
||||
# Zektyc — moniteur d'IP suspectes (staff only)
|
||||
#
|
||||
# Toutes les 30 s, analyse deux sources pour détecter des IP anormales qui
|
||||
# tapent sur la stack, et envoie un rapport (embed) sur le webhook Discord staff.
|
||||
#
|
||||
# Source 1 — pare-feu netfilter : log kernel "UFW BLOCK" (vraies IP externes,
|
||||
# probes, scanners, SYN floods résiduels au niveau réseau).
|
||||
#
|
||||
# Source 2 — reverse-proxy nginx : refus HTTP 429/503 émis par les limites
|
||||
# par IP réelle (limit_req / limit_conn). Comme la terminaison TLS a
|
||||
# été déplacée vers nginx (mode funnel tcp brut + PROXY protocol),
|
||||
# c'est ICI qu'apparaissent les attaquants d'applicatif (bursts,
|
||||
# h2load, etc.) que le pare-feu réseau ne voit plus. On relit le log
|
||||
# à partir d'un offset pour ne traiter que les lignes nouvelles.
|
||||
#
|
||||
# Anti-re-spam : une IP déjà signalée n'est renvoyée que si son compteur de
|
||||
# tentatives a augmenté depuis le dernier rapport.
|
||||
set -o pipefail
|
||||
|
||||
WEBHOOK="https://discord.com/api/webhooks/***REMOVED***"
|
||||
INTERVAL=30
|
||||
STATE="/home/riricdev/zektyc/scripts/.ipwatch-state" # IP:compteur déjà signalés
|
||||
NGINX_LOG="/var/log/nginx/access.log"
|
||||
# Taille (octets) du bloc de fin du log relue à chaque cycle puis filtrée au
|
||||
# timestamp : couvre INTERVAL s même au pic (h2load). Rotation-safe, pas d'offset.
|
||||
TL_BACK=16777216 # 16 Mo
|
||||
|
||||
# IP publique ? 0=oui(suspecte possible) 1=non. On exclut aussi le CGNAT
|
||||
# Tailscale (100.64.0.0/10) pour ne pas signaler le VPN légitime / les tests.
|
||||
is_public() {
|
||||
local ip="$1" a b c d
|
||||
IFS='.' read -r a b c d <<<"$ip"
|
||||
[[ "$a" == "127" ]] && return 1
|
||||
[[ "$a" == "10" ]] && return 1
|
||||
[[ "$a" == "192" && "$b" == "168" ]] && return 1
|
||||
[[ "$a" == "172" && "$b" -ge 16 && "$b" -le 31 ]] && return 1
|
||||
[[ "$a" == "100" && "$b" -ge 64 && "$b" -le 127 ]] && return 1
|
||||
[[ "$a" == "169" && "$b" == "254" ]] && return 1
|
||||
[[ "$a" -ge 224 && "$a" -le 239 ]] && return 1
|
||||
[[ "$a" == "0" || "$a" == "255" ]] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Envoie l'embed. Retour 0 si envoyé.
|
||||
send_report() {
|
||||
local report_txt="$1" now
|
||||
now=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
local json
|
||||
json=$(jq -n \
|
||||
--arg title "🚨 IP suspectes détectées" \
|
||||
--arg desc "$report_txt" \
|
||||
--arg time "$now" \
|
||||
--argjson color 15548997 \
|
||||
'{embeds:[{title:$title,description:$desc,color:$color,footer:{text:("Zektyc Sécu — "+$time)}}]}')
|
||||
curl -s -m 10 -H "Content-Type: application/json" -X POST "$WEBHOOK" -d "$json" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Charge l'état des IP déjà signalées (format "IP compteur" par ligne)
|
||||
load_state() {
|
||||
declare -gA last=()
|
||||
if [[ -f "$STATE" ]]; then
|
||||
while read -r k v; do last["$k"]="$v"; done <"$STATE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Sauvegarde l'état
|
||||
save_state() {
|
||||
: >"$STATE"
|
||||
for k in "${!last[@]}"; do printf '%s %s\n' "$k" "${last[$k]}" >>"$STATE"; done
|
||||
}
|
||||
|
||||
# --- Source 2 : relit la FIN du log nginx et garde les refus des 30 dernières s ---
|
||||
# Pas d'offset : on lit un bloc fixe de la fin du fichier (TL_BACK, dimensionné
|
||||
# pour couvrir INTERVAL s même au pic de trafic) puis on ne garde QUE les lignes
|
||||
# dont l'horodatage est dans la fenêtre [ début, date_actuelle ]. Robuste face
|
||||
# aux rotations/troncatures, aucun offset à resynchroniser manuellement.
|
||||
# Tout le filtrage se fait dans UN SEUL awk (pas de fork par ligne).
|
||||
read_nginx_refus() {
|
||||
local awkprog ip c ts uals from
|
||||
from="$(date -d "@$(( now_s - INTERVAL ))" +%d/%b/%Y:%H:%M:%S)"
|
||||
awkprog='
|
||||
function ispub(ip, a,b,c,d,p,n) {
|
||||
n=split(ip,p,"."); if (n!=4) return 0
|
||||
a=p[1]+0; b=p[2]+0; c=p[3]+0; d=p[4]+0
|
||||
if (a==127||a==10||(a==192&&b==168)||(a==172&&b>=16&&b<=31)\
|
||||
||(a==100&&b>=64&&b<=127)||(a==169&&b==254)) return 0
|
||||
if (a>=224&&a<=239||a==0||a==255) return 0
|
||||
return 1
|
||||
}
|
||||
{
|
||||
# Gardes de structure : le buffer tail -c commence en plein milieu d une
|
||||
# ligne -> son 1er enregistrement est un fragment casse. On n accepte que
|
||||
# les lignes completes : IP v4 en tete + timestamp nginx complet en $3.
|
||||
if ($1 !~ /^[0-9]{1,3}(\.[0-9]{1,3}){3}$/) next
|
||||
ts=$3; sub(/^\[/,"",ts)
|
||||
if (ts !~ /^[0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2}/) next
|
||||
# nginx logue en local time (+0200) : on n accepte que les lignes >= debut fenetre
|
||||
if (ts < from) next
|
||||
code=""
|
||||
for (i=1;i<=NF;i++)
|
||||
if ($i ~ /^[0-9]{3}$/ && (i+1)<=NF && $(i+1) ~ /^[0-9]+$/) code=$i
|
||||
if (code!="429" && code!="503") next
|
||||
ip=$1
|
||||
if (!ispub(ip)) next
|
||||
cnt[ip]++
|
||||
n=split($0,q,"\"")
|
||||
ua=(n>=2 ? substr(q[n-1],1,40) : "")
|
||||
if (ua!="" && !((ip SUBSEP ua) in seen)) { seen[ip SUBSEP ua]=1; ual[ip]=(ual[ip]==""?ua:ual[ip]","ua) }
|
||||
last[ip]=ts
|
||||
}
|
||||
END { for (ip in cnt) print ip "|" cnt[ip] "|" last[ip] "|" ual[ip] }
|
||||
'
|
||||
while IFS='|' read -r ip c ts uals; do
|
||||
cnt["$ip"]=$(( ${cnt["$ip"]:-0} + c ))
|
||||
[[ "${protos["$ip"]:-}" != *"http"* ]] && protos["$ip"]="${protos["$ip"]:-}http,"
|
||||
[[ "${dstports["$ip"]:-}" != *"443"* ]] && dstports["$ip"]="${dstports["$ip"]:-}443,"
|
||||
lastts["$ip"]="$ts"
|
||||
[[ -n "$uals" ]] && uas["$ip"]="${uals},"
|
||||
done < <(tail -c "$TL_BACK" "$NGINX_LOG" | awk -v "from=$from" "$awkprog")
|
||||
}
|
||||
|
||||
load_state
|
||||
echo "IP-watch démarré — fenêtre ${INTERVAL}s/signalement (ufw + nginx), webhook staff (actif)."
|
||||
|
||||
while true; do
|
||||
now_s=$(date +%s)
|
||||
since=$(( now_s - INTERVAL ))
|
||||
|
||||
declare -A cnt=()
|
||||
declare -A protos=()
|
||||
declare -A dstports=()
|
||||
declare -A lastts=()
|
||||
declare -A uas=()
|
||||
|
||||
# --- source 1 : blocs netfilter (log kernel UFW BLOCK) ---
|
||||
while IFS= read -r ln; do
|
||||
[[ "$ln" != *"UFW BLOCK"* ]] && continue
|
||||
if [[ "$ln" =~ \ SRC=([0-9.]+)\ ]]; then
|
||||
ip="${BASH_REMATCH[1]}"
|
||||
is_public "$ip" || continue
|
||||
cnt["$ip"]=$(( ${cnt["$ip"]:-0} + 1 ))
|
||||
if [[ "$ln" =~ \ PROTO=([A-Za-z0-9]+)\ ]]; then
|
||||
p="${BASH_REMATCH[1]}"
|
||||
[[ "${protos["$ip"]:-}" != *"$p"* ]] && protos["$ip"]="${protos["$ip"]:-}$p,"
|
||||
fi
|
||||
if [[ "$ln" =~ \ DPT=([0-9]+) ]]; then
|
||||
d="${BASH_REMATCH[1]}"
|
||||
[[ "${dstports["$ip"]:-}" != *"$d"* ]] && dstports["$ip"]="${dstports["$ip"]:-}$d,"
|
||||
fi
|
||||
if [[ "$ln" =~ ^[A-Z][a-z]{2}\ [0-9]{2}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2} ]]; then
|
||||
lastts["$ip"]="${BASH_REMATCH[0]}"
|
||||
fi
|
||||
fi
|
||||
done < <(sudo journalctl -k --no-pager --since "@${since}" 2>/dev/null | grep "UFW BLOCK")
|
||||
|
||||
# --- source 2 : refus HTTP nginx (429/503 par IP réelle) ---
|
||||
read_nginx_refus
|
||||
|
||||
# --- filtrer : ne signaler que les IP nouvelles ou dont le compteur a augmenté ---
|
||||
newfound=0
|
||||
report=""
|
||||
for ip in "${!cnt[@]}"; do
|
||||
c="${cnt[$ip]}"
|
||||
prev="${last["$ip"]:-0}"
|
||||
if (( c > prev )); then
|
||||
proto="${protos[$ip]%,}"
|
||||
dports="${dstports[$ip]%,}"
|
||||
ua="${uas[$ip]%,}"
|
||||
report+=$':shield: **`'"${ip}"$'`** — **'"${c}"$'** tentative(s) bloquée(s)\n'
|
||||
report+=$'└ Sources: `'"${proto:-?}"$'` | Ports cibles: `'"${dports:-?}"$'`\n'
|
||||
if [[ -n "$ua" ]]; then
|
||||
report+=$'└ User-Agent: `'"${ua}"$'`\n'
|
||||
fi
|
||||
report+=$'└ Dernière tentative: `'"${lastts[$ip]:-?}"$'`\n\n'
|
||||
last["$ip"]="$c"
|
||||
((newfound++))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( newfound > 0 )); then
|
||||
send_report "$report"
|
||||
fi
|
||||
|
||||
save_state
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
Loading…
Reference in a new issue