#!/usr/bin/env bash
# pca-blocklist-pull.sh · Port Cyber Alliance · CC BY 4.0 · https://portcyberalliance.org/api-developers/ip-blocklist-cve-feed-howto/
# Pull the PCA IP blocklist, check every entry, apply it only if the whole answer is valid; otherwise keep the last good list.
#   pca-blocklist-pull.sh nginx   → writes the nginx snippet, runs nginx -t, reloads nginx
#   pca-blocklist-pull.sh ipset   → refills the ipsets pca-blocklist (IPv4) and pca-blocklist-v6 (IPv6)
# The key is read from a root-only file and sent with curl -H @file, so it never shows in the process list.
set -euo pipefail
MODE="${1:-nginx}"
URL="${PCA_URL:-https://portcyberalliance.org/wp-json/pca/v1/ip-blocklist}"
KEY_FILE="${PCA_API_KEY_FILE:-/etc/pca/api-key}"
STATE="${PCA_STATE_DIR:-/var/lib/pca-blocklist}"
SNIPPET="${PCA_NGINX_SNIPPET:-/etc/nginx/snippets/pca-blocklist.conf}"
SET="${PCA_IPSET:-pca-blocklist}"
TOKEN='^[0-9A-Fa-f:.]+(/[0-9]{1,3})?$'

umask 077; mkdir -p "$STATE"
body="$(mktemp "$STATE/body.XXXXXX")"; list="$(mktemp "$STATE/list.XXXXXX")"; hdr="$(mktemp "$STATE/hdr.XXXXXX")"
trap 'rm -f "$body" "$list" "$hdr"' EXIT
printf 'X-API-Key: %s\n' "$(tr -d '\r\n' < "$KEY_FILE")" > "$hdr"

case "$MODE" in nginx) fmt=nginx ;; ipset) fmt=csv ;; *) echo "usage: pca-blocklist-pull.sh nginx|ipset" >&2; exit 2 ;; esac
code="$(curl -sS --max-time 60 -H @"$hdr" -o "$body" -w '%{http_code}' "$URL?format=$fmt" || echo 000)"
[ "$code" = 200 ] || { echo "pca-blocklist: HTTP $code, nothing applied, last good list kept" >&2; exit 1; }

if [ "$MODE" = nginx ]; then
  sed -n 's/^deny \(.*\);$/\1/p' "$body" > "$list"
  if grep -Ev '^(deny [0-9A-Fa-f:.]+(/[0-9]{1,3})?;)?$' "$body" | grep -q .; then
    echo "pca-blocklist: unexpected line in the answer, nothing applied" >&2; exit 1
  fi
  mkdir -p "$(dirname "$SNIPPET")"
  [ -f "$SNIPPET" ] && cp -p "$SNIPPET" "$STATE/last-good.conf"
  { echo "# PCA IP blocklist · pulled $(date -u +%FT%TZ) · do not edit, rewritten by pca-blocklist-pull.sh"; cat "$body"; } > "$SNIPPET.new"
  chmod 644 "$SNIPPET.new"; mv "$SNIPPET.new" "$SNIPPET"
  if nginx -t -q; then
    systemctl reload nginx
  else
    if [ -f "$STATE/last-good.conf" ]; then cp -p "$STATE/last-good.conf" "$SNIPPET"; else : > "$SNIPPET"; fi
    echo "pca-blocklist: nginx -t failed, previous list restored" >&2; exit 1
  fi
else
  # CSV columns: ip,cidr,source,reason,last_seen · the entry is the range when there is one, else the address
  tail -n +2 "$body" | tr -d '"' | while IFS=, read -r ip cidr _; do
    if [ -n "$cidr" ]; then echo "$cidr"; elif [ -n "$ip" ]; then echo "$ip"; fi
  done > "$list"
  if grep -Ev "$TOKEN" "$list" | grep -q .; then
    echo "pca-blocklist: unexpected entry in the answer, nothing applied" >&2; exit 1
  fi
  ipset -exist create "$SET" hash:net family inet
  ipset -exist create "$SET-v6" hash:net family inet6
  ipset -exist create "$SET-new" hash:net family inet;     ipset flush "$SET-new"
  ipset -exist create "$SET-v6-new" hash:net family inet6; ipset flush "$SET-v6-new"
  while read -r t; do
    case "$t" in *:*) ipset -exist add "$SET-v6-new" "$t" ;; *) ipset -exist add "$SET-new" "$t" ;; esac
  done < "$list"
  ipset swap "$SET-new" "$SET";       ipset destroy "$SET-new"
  ipset swap "$SET-v6-new" "$SET-v6"; ipset destroy "$SET-v6-new"
fi
echo "pca-blocklist: $(grep -c . "$list" || true) entries applied ($MODE)"
