#!/usr/bin/env bash
#
# import-client-keys.sh
#
# Import Wazuh 4.x agent registrations into a Wazuh 5.x manager, preserving each
# agent's ID, name, IP and key.
#
# This wraps the import loop documented at:
#   https://documentation.wazuh.com/5.0-beta/migration-to-5x/wazuh-agents.html
# and adds batching with token refresh, a dry-run mode and a per-entry result log.
# The API call itself (POST /agents/insert with id, name, ip, key) and the token
# request (POST /security/user/authenticate?raw=true) are exactly as documented.
#
# Companion to: https://maheshtata.com/wazuh/wazuh-4-to-5-migration/
# Tested against: Wazuh 5.0 beta5 documentation, September 2026. Re-verify at GA.
#
# Usage:
#   ./import-client-keys.sh --keys ./client.keys --manager 10.0.0.10 --user wazuh [--dry-run]
#
# The password is read from the WAZUH_API_PASSWORD environment variable, or
# prompted for. It is never passed on the command line.
#
# client.keys contains agent authentication keys. Keep it readable only by
# administrators and delete it from the 5.x manager once the import is verified.

set -euo pipefail

KEYS_FILE=""
MANAGER=""
API_PORT="55000"
API_USER="wazuh"
BATCH_SIZE="200"
DRY_RUN="no"
LOG_FILE="import-client-keys.log"

usage() {
  cat <<'USAGE'
Usage: import-client-keys.sh --keys <file> --manager <ip-or-host> [options]

Required:
  --keys <file>        Path to the client.keys file copied from the 4.x manager
  --manager <host>     IP address or hostname of the Wazuh 5.x manager

Options:
  --port <port>        Manager API port (default: 55000)
  --user <username>    Manager API user (default: wazuh)
  --batch <n>          Entries per API token (default: 200)
  --dry-run            Parse and report only; make no API calls
  --log <file>         Result log path (default: import-client-keys.log)
  -h, --help           Show this help

Environment:
  WAZUH_API_PASSWORD   Manager API password. Prompted for if unset.
USAGE
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --keys)    KEYS_FILE="$2"; shift 2 ;;
    --manager) MANAGER="$2"; shift 2 ;;
    --port)    API_PORT="$2"; shift 2 ;;
    --user)    API_USER="$2"; shift 2 ;;
    --batch)   BATCH_SIZE="$2"; shift 2 ;;
    --log)     LOG_FILE="$2"; shift 2 ;;
    --dry-run) DRY_RUN="yes"; shift ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown argument: $1" >&2; usage; exit 2 ;;
  esac
done

[[ -n "$KEYS_FILE" ]] || { echo "ERROR: --keys is required" >&2; exit 2; }
[[ -n "$MANAGER"   ]] || { echo "ERROR: --manager is required" >&2; exit 2; }
[[ -r "$KEYS_FILE" ]] || { echo "ERROR: cannot read $KEYS_FILE" >&2; exit 2; }
command -v curl >/dev/null || { echo "ERROR: curl not found" >&2; exit 2; }

API_BASE="https://${MANAGER}:${API_PORT}"

if [[ "$DRY_RUN" == "no" ]]; then
  if [[ -z "${WAZUH_API_PASSWORD:-}" ]]; then
    read -rsp "Manager API password for ${API_USER}: " WAZUH_API_PASSWORD
    echo
  fi
fi

# NOTE: -k skips certificate verification, which matches the documented examples.
# On a deployment with a private CA, replace -k with --cacert /path/to/root-ca.pem.
get_token() {
  curl -sk -u "${API_USER}:${WAZUH_API_PASSWORD}" \
    -X POST "${API_BASE}/security/user/authenticate?raw=true"
}

TOKEN=""
processed=0
imported=0
skipped=0
failed=0

: >"$LOG_FILE"

if [[ "$DRY_RUN" == "no" ]]; then
  TOKEN="$(get_token)"
  [[ -n "$TOKEN" ]] || { echo "ERROR: could not obtain an API token" >&2; exit 1; }
fi

# client.keys format: <id> <name> <ip> <key>
# Entries for removed agents are prefixed with '!' and are skipped, as in the
# documented import loop.
while read -r id name ip key || [[ -n "${id:-}" ]]; do
  [[ -n "${id:-}" ]] || continue
  if [[ "${id#!}" != "$id" ]]; then
    skipped=$((skipped + 1))
    echo "SKIP  removed-entry ${id}" >>"$LOG_FILE"
    continue
  fi
  [[ -n "${key:-}" ]] || { skipped=$((skipped + 1)); echo "SKIP  malformed ${id}" >>"$LOG_FILE"; continue; }

  if [[ "$DRY_RUN" == "yes" ]]; then
    processed=$((processed + 1))
    echo "DRY   ${id} ${name} ${ip}" >>"$LOG_FILE"
    continue
  fi

  # Refresh the token every BATCH_SIZE entries. The token has a limited lifetime
  # and a large fleet will outlive a single one.
  if (( processed > 0 && processed % BATCH_SIZE == 0 )); then
    TOKEN="$(get_token)"
    [[ -n "$TOKEN" ]] || { echo "ERROR: token refresh failed after ${processed} entries" >&2; exit 1; }
  fi

  response="$(curl -sk \
    -H "Authorization: Bearer ${TOKEN}" \
    -H "Content-Type: application/json" \
    -X POST "${API_BASE}/agents/insert" \
    -d "{\"id\":\"${id}\",\"name\":\"${name}\",\"ip\":\"${ip}\",\"key\":\"${key}\"}")"

  processed=$((processed + 1))

  # The documented success body carries "error":0. Anything else is recorded
  # verbatim rather than interpreted, so a changed API response is visible
  # rather than silently swallowed.
  if [[ "$response" == *'"error":0'* ]]; then
    imported=$((imported + 1))
    echo "OK    ${id} ${name}" >>"$LOG_FILE"
  else
    failed=$((failed + 1))
    echo "FAIL  ${id} ${name} :: ${response}" >>"$LOG_FILE"
  fi
done <"$KEYS_FILE"

echo
echo "Processed: ${processed}"
echo "Imported:  ${imported}"
echo "Skipped:   ${skipped}"
echo "Failed:    ${failed}"
echo "Log:       ${LOG_FILE}"
echo
if (( failed > 0 )); then
  echo "Review the FAIL lines in ${LOG_FILE}. An agent ID that already exists on the"
  echo "5.x manager must be deleted there before that entry can be reimported."
  exit 1
fi
