Files
ja4-platform/services/dashboard/ip-check.sh
toto d469e39da7 feat: ja4-platform monorepo — 5 services unified, tests & RPM builds standardized
Services:
- ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap)
- logcorrelator: JA4 log correlation engine (Go, ClickHouse)
- mod_reqin_log: Apache module (C, JSON request logging)
- bot_detector: ML bot detection pipeline (Python)
- dashboard: FastAPI/Streamlit analytics UI (Python)

Shared libraries:
- shared/go/ja4common: logger, config, shutdown, ipfilter (Go module)
- shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package)
- shared/clickhouse/: canonical SQL migrations (10 files)

Build & packaging:
- Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10)
- go.work workspace linking sentinel, correlator, ja4common
- Makefile with test-all, build-all, rpm-* targets

Fixes applied:
- go.work: 1.21 → 1.24.6 (required by sentinel)
- correlator Dockerfiles: golang:1.21 → golang:1.24
- replace directives in go.mod for ja4common local path
- pyproject.toml: setuptools.backends → setuptools.build_meta
- Removed static libpcap linking (unavailable on Rocky 9)
- Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32)
- Rewrote corrupted test files (logger_test.go × 2)

Test coverage:
- correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%)
- sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse)

Documentation:
- README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 16:42:59 +02:00

324 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# ip-check.sh — Vérifie le statut d'une ou plusieurs IPs via le Bot Detector Dashboard API
#
# Usage:
# ./ip-check.sh <ip>
# ./ip-check.sh <ip1> <ip2> ...
# ./ip-check.sh <ip1,ip2,...>
# ./ip-check.sh <cidr> (ex: 192.168.1.0/24, max 1024 hôtes)
# ./ip-check.sh -f <fichier> (une IP/CIDR par ligne, # ignorés)
# ./ip-check.sh --api <url> ...
# ./ip-check.sh --verbose JSON complet (défaut: résumé)
# ./ip-check.sh --pretty Formater le JSON (jq)
# ./ip-check.sh --parallel <n>
# ./ip-check.sh --endpoint investigation|reputation|reputation-summary
# ./ip-check.sh --help
set -euo pipefail
# ── Valeurs par défaut ────────────────────────────────────────────────────────
API_BASE="http://test-sdv-anubis.sdv.fr:8000"
PARALLEL=5
ENDPOINT="investigation"
PRETTY=false
VERBOSE=false
INPUT_FILE=""
IPS=()
# ── Couleurs ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
usage() {
echo -e "${BOLD}ip-check.sh${RESET} — Bot Detector IP Status Checker"
echo ""
echo -e "${BOLD}Usage:${RESET}"
echo " $0 [options] <ip|cidr> [<ip|cidr> ...]"
echo ""
echo -e "${BOLD}Options:${RESET}"
echo " --api <url> URL de base de l'API [défaut: http://localhost:8000]"
echo " --parallel <n> Requêtes simultanées [défaut: 5]"
echo " --endpoint <ep> investigation | reputation | reputation-summary [défaut: investigation]"
echo " --verbose, -v JSON complet (défaut: résumé condensé)"
echo " --pretty Formater le JSON avec jq"
echo " -f <fichier> Lire les IPs/CIDRs depuis un fichier"
echo " --help, -h Afficher cette aide"
echo ""
echo -e "${BOLD}Exemples:${RESET}"
echo " $0 1.2.3.4"
echo " $0 1.2.3.4 5.6.7.8 9.10.11.12"
echo " $0 192.168.0.0/24"
echo " $0 --verbose --pretty 1.2.3.4"
echo " $0 --endpoint reputation --parallel 10 10.0.0.0/28"
echo " $0 -f ips.txt --api http://monserveur:8000"
exit 0
}
err() { echo -e "${RED}[ERREUR]${RESET} $*" >&2; }
info() { echo -e "${CYAN}[INFO]${RESET} $*" >&2; }
# ── Dépendances ───────────────────────────────────────────────────────────────
check_deps() {
command -v curl &>/dev/null || { err "curl requis : apt install curl"; exit 1; }
command -v python3 &>/dev/null || { err "python3 requis pour les CIDRs."; exit 1; }
if $PRETTY && ! command -v jq &>/dev/null; then
err "--pretty nécessite jq : apt install jq"; exit 1
fi
}
# ── Expansion CIDR ────────────────────────────────────────────────────────────
expand_cidr() {
python3 - "$1" <<'PYEOF'
import sys, ipaddress
arg = sys.argv[1]
try:
net = ipaddress.ip_network(arg, strict=False)
if net.num_addresses > 1024:
print(f"__LARGE_CIDR__{arg}", flush=True)
elif net.num_addresses == 1:
print(str(net.network_address), flush=True)
else:
for ip in net.hosts():
print(str(ip), flush=True)
except ValueError:
print(arg, flush=True)
PYEOF
}
# ── Validation IPv4 ───────────────────────────────────────────────────────────
is_valid_ipv4() {
python3 -c "import ipaddress,sys; ipaddress.IPv4Address(sys.argv[1])" "$1" 2>/dev/null
}
# ── URL par endpoint ──────────────────────────────────────────────────────────
build_url() {
local ip="$1"
case "$ENDPOINT" in
investigation) echo "${API_BASE}/api/investigation/${ip}/summary" ;;
reputation) echo "${API_BASE}/api/reputation/ip/${ip}" ;;
reputation-summary) echo "${API_BASE}/api/reputation/ip/${ip}/summary" ;;
*) err "Endpoint inconnu: $ENDPOINT"; exit 1 ;;
esac
}
# ── Requête HTTP ──────────────────────────────────────────────────────────────
query_ip() {
local ip="$1"
local url
url=$(build_url "$ip")
local body http_code
body=$(curl -s -w "\n__HTTP_CODE__%{http_code}" --max-time 15 "$url" 2>/dev/null) || {
echo "{\"ip\":\"${ip}\",\"error\":\"timeout ou connexion refusée\",\"url\":\"${url}\"}"
return
}
http_code=$(echo "$body" | grep -o '__HTTP_CODE__[0-9]*' | grep -o '[0-9]*')
body=$(echo "$body" | sed 's/__HTTP_CODE__[0-9]*//')
if [[ "$http_code" == "200" ]]; then
echo "$body"
else
echo "{\"ip\":\"${ip}\",\"error\":\"HTTP ${http_code}\",\"url\":\"${url}\",\"detail\":${body}}"
fi
}
export -f query_ip build_url err
export API_BASE ENDPOINT
# ── Arguments ─────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h) usage ;;
--api) shift; API_BASE="${1%/}" ;;
--parallel) shift; PARALLEL="$1" ;;
--endpoint) shift; ENDPOINT="$1" ;;
--verbose|-v) VERBOSE=true ;;
--pretty) PRETTY=true ;;
-f) shift; INPUT_FILE="$1" ;;
*) IPS+=("$1") ;;
esac
shift
done
check_deps
# ── Collecter les IPs ──────────────────────────────────────────────────────────
ALL_IPS=()
LARGE_CIDRS=()
process_token() {
local token="$1"
IFS=',' read -ra parts <<< "$token"
for part in "${parts[@]}"; do
part=$(echo "$part" | tr -d '[:space:]')
[[ -z "$part" ]] && continue
if [[ "$part" == *"/"* ]]; then
while IFS= read -r expanded; do
if [[ "$expanded" == __LARGE_CIDR__* ]]; then
LARGE_CIDRS+=("${expanded#__LARGE_CIDR__}")
else
ALL_IPS+=("$expanded")
fi
done < <(expand_cidr "$part")
else
ALL_IPS+=("$part")
fi
done
}
for token in "${IPS[@]-}"; do process_token "$token"; done
if [[ -n "$INPUT_FILE" ]]; then
[[ ! -f "$INPUT_FILE" ]] && { err "Fichier introuvable: $INPUT_FILE"; exit 1; }
while IFS= read -r line || [[ -n "$line" ]]; do
line=$(echo "$line" | sed 's/#.*//' | tr -d '[:space:]')
[[ -z "$line" ]] && continue
process_token "$line"
done < "$INPUT_FILE"
fi
[[ ${#ALL_IPS[@]} -eq 0 && ${#LARGE_CIDRS[@]} -eq 0 ]] && {
err "Aucune IP fournie. Utilisez --help pour l'aide."
exit 1
}
for cidr in "${LARGE_CIDRS[@]+"${LARGE_CIDRS[@]}"}"; do
err "CIDR trop grand (>1024 hôtes), ignoré: $cidr — affinez le masque."
done
VALID_IPS=()
for ip in "${ALL_IPS[@]}"; do
if is_valid_ipv4 "$ip"; then
VALID_IPS+=("$ip")
else
err "IP invalide ignorée: $ip"
fi
done
[[ ${#VALID_IPS[@]} -eq 0 ]] && { err "Aucune IP valide à traiter."; exit 1; }
mapfile -t VALID_IPS < <(printf '%s\n' "${VALID_IPS[@]}" | sort -u)
info "Endpoint : ${ENDPOINT}${API_BASE}"
info "IPs : ${#VALID_IPS[@]} à vérifier (parallélisme: ${PARALLEL})"
$VERBOSE && info "Mode : verbose (JSON complet)" || info "Mode : résumé (--verbose pour le JSON complet)"
# ── Requêtes parallèles ───────────────────────────────────────────────────────
TMPDIR_WORK=$(mktemp -d)
trap 'rm -rf "$TMPDIR_WORK"' EXIT
run_batch() {
for ip in "$@"; do
query_ip "$ip" > "${TMPDIR_WORK}/${ip}.json" &
done
wait
}
total=${#VALID_IPS[@]}
i=0
while [[ $i -lt $total ]]; do
run_batch "${VALID_IPS[@]:$i:$PARALLEL}"
(( i += PARALLEL ))
done
# ── Assembler + condenser si non-verbose ─────────────────────────────────────
FINAL_JSON=$(python3 - "$TMPDIR_WORK" "$ENDPOINT" "$VERBOSE" <<'PYEOF'
import json, os, sys, glob
directory, endpoint, verbose_flag = sys.argv[1], sys.argv[2], sys.argv[3] == "true"
def summarize_investigation(d):
"""Résumé condensé du endpoint investigation."""
if "error" in d:
return d
ml = d.get("ml", {})
bf = d.get("bruteforce", {})
tcp = d.get("tcp_spoofing", {})
rot = d.get("ja4_rotation", {})
per = d.get("persistence", {})
flags = []
if bf.get("active"): flags.append("BRUTEFORCE")
if tcp.get("detected"):
flags.append("TCP_SPOOF" + ("/BOT_TOOL" if tcp.get("is_bot_tool") else ""))
if rot.get("rotating"): flags.append(f"JA4_ROTATION(x{rot.get('distinct_ja4_count',0)})")
if per.get("persistent"): flags.append(f"PERSISTENT(x{per.get('recurrence',0)})")
return {
"ip": d.get("ip"),
"risk_score": d.get("risk_score"),
"threat_level": ml.get("threat_level") or per.get("worst_threat_level") or "—",
"attack_type": ml.get("attack_type") or "—",
"total_detections": ml.get("total_detections", 0),
"distinct_hosts": ml.get("distinct_hosts", 0),
"distinct_ja4": ml.get("distinct_ja4", 0),
"ml_max_score": ml.get("max_score", 0),
"bruteforce": bf.get("active", False),
"bf_hits": bf.get("total_hits", 0) if bf.get("active") else None,
"tcp_spoof": tcp.get("detected", False),
"tcp_suspected_os": tcp.get("suspected_os") if tcp.get("detected") else None,
"tcp_declared_os": tcp.get("declared_os") if tcp.get("detected") else None,
"tcp_confidence": tcp.get("confidence") if tcp.get("detected") else None,
"ja4_rotating": rot.get("rotating", False),
"ja4_count": rot.get("distinct_ja4_count", 0) if rot.get("rotating") else None,
"persistent": per.get("persistent", False),
"recurrence": per.get("recurrence", 0) if per.get("persistent") else None,
"first_seen": per.get("first_seen") if per.get("persistent") else None,
"last_seen": per.get("last_seen") if per.get("persistent") else None,
"flags": flags,
}
def summarize_reputation(d):
"""Résumé condensé du endpoint reputation."""
if "error" in d:
return d
agg = d.get("aggregated", {})
flags = []
if agg.get("is_proxy"): flags.append("PROXY")
if agg.get("is_hosting"): flags.append("HOSTING")
if agg.get("is_vpn"): flags.append("VPN")
if agg.get("is_tor"): flags.append("TOR")
return {
"ip": d.get("ip"),
"threat_level": agg.get("threat_level", "—"),
"threat_score": agg.get("threat_score", 0),
"country": agg.get("country"),
"country_code": agg.get("country_code"),
"asn": agg.get("asn"),
"org": agg.get("org") or agg.get("asn_org"),
"is_proxy": agg.get("is_proxy", False),
"is_hosting": agg.get("is_hosting", False),
"is_vpn": agg.get("is_vpn", False),
"is_tor": agg.get("is_tor", False),
"flags": flags,
"warnings": agg.get("warnings", []),
}
results = []
for f in sorted(glob.glob(os.path.join(directory, "*.json"))):
with open(f) as fh:
try:
data = json.load(fh)
except json.JSONDecodeError:
results.append({"error": "invalid JSON", "raw": open(f).read()})
continue
if verbose_flag:
results.append(data)
elif endpoint == "investigation":
results.append(summarize_investigation(data))
elif endpoint == "reputation":
results.append(summarize_reputation(data))
else:
results.append(data) # reputation-summary est déjà compact
print(json.dumps({"count": len(results), "results": results}))
PYEOF
)
# ── Affichage ─────────────────────────────────────────────────────────────────
if $PRETTY; then
echo "$FINAL_JSON" | jq .
else
echo "$FINAL_JSON"
fi