#!/usr/bin/env bash # # Wazuh API Backup # # Backs up Wazuh configuration objects via the REST API. # Exports agents, groups, rules, decoders, CDB lists, manager # configuration, API users, roles, policies, and cluster status # to individual JSON files in a dated directory. # # Usage: # WAZUH_USER="wazuh-wui" WAZUH_PASS="wazuh" ./wazuh-api-backup.sh # WAZUH_USER="wazuh-wui" WAZUH_PASS="wazuh" ./wazuh-api-backup.sh --dry-run # WAZUH_USER="wazuh-wui" WAZUH_PASS="wazuh" ./wazuh-api-backup.sh --install # # Parameters: # --dry-run Show what would be backed up without writing files # --install Create cron job for daily backup at 3am # --help Show usage # # Environment: # WAZUH_API Wazuh API base URL (default: https://localhost:55000) # WAZUH_USER API username (default: wazuh-wui) # WAZUH_PASS API password (default: wazuh) # BACKUP_DIR Base backup directory (default: /backup/wazuh-api) # RETENTION_DAYS Delete backups older than this many days (default: 30) # CURL_TIMEOUT API request timeout in seconds (default: 15) # # Author: Phil Connor # Contact: contact@mylinux.work # Website: https://mylinux.work # License: MIT # Version: 1.01 set -euo pipefail # --- Configuration --- readonly VERSION="1.0" readonly SCRIPT_NAME="$(basename "$0")" WAZUH_API="${WAZUH_API:-https://localhost:55000}" WAZUH_USER="${WAZUH_USER:-wazuh-wui}" WAZUH_PASS="${WAZUH_PASS:-wazuh}" BACKUP_DIR="${BACKUP_DIR:-/backup/wazuh-api}" RETENTION_DAYS="${RETENTION_DAYS:-30}" CURL_TIMEOUT="${CURL_TIMEOUT:-15}" DRY_RUN=false TOKEN="" # Backup endpoints: "api_path output_filename" readonly ENDPOINTS=( "agents?limit=500 agents.json" "groups groups.json" "rules?limit=500 rules.json" "decoders?limit=500 decoders.json" "lists cdb-lists.json" "manager/configuration manager-config.json" "manager/status manager-status.json" "security/users users.json" "security/roles roles.json" "security/policies policies.json" "security/rules security-rules.json" "cluster/status cluster-status.json" "syscheck syscheck-config.json" "active-response active-response.json" ) # Custom rule/decoder files to export (raw XML) readonly CUSTOM_FILES=( "rules/files/local_rules.xml local_rules.xml" "decoders/files/local_decoder.xml local_decoder.xml" ) # --- Functions --- usage() { cat </dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then echo "ERROR: Missing required commands: ${missing[*]}" >&2 echo "Install with: apt install ${missing[*]} OR dnf install ${missing[*]}" >&2 exit 1 fi } validate_config() { # Strip trailing slash WAZUH_API="${WAZUH_API%/}" } get_token() { local response response=$(curl -sf -k --max-time "$CURL_TIMEOUT" \ -u "${WAZUH_USER}:${WAZUH_PASS}" \ -X POST "${WAZUH_API}/security/user/authenticate" 2>/dev/null) || { echo "ERROR: Failed to authenticate to Wazuh API at ${WAZUH_API}" >&2 echo "Check WAZUH_USER, WAZUH_PASS, and API connectivity" >&2 exit 1 } TOKEN=$(echo "$response" | jq -r '.data.token // empty') if [[ -z "$TOKEN" ]]; then echo "ERROR: Authentication succeeded but no token returned" >&2 exit 1 fi } api_get() { local endpoint="$1" curl -sf -k --max-time "$CURL_TIMEOUT" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ "${WAZUH_API}/${endpoint}" 2>/dev/null || echo "" } backup_endpoint() { local endpoint="$1" local filename="$2" local output_dir="$3" local response if [[ "$DRY_RUN" == true ]]; then printf " %-40s → %s (dry-run)\n" "$endpoint" "$filename" return 0 fi response=$(api_get "$endpoint") if [[ -z "$response" ]]; then printf " %-40s → %-30s FAIL\n" "$endpoint" "$filename" return 1 fi echo "$response" | jq '.' > "${output_dir}/${filename}" 2>/dev/null || { # Not JSON (raw XML file) — write as-is echo "$response" > "${output_dir}/${filename}" } local size size=$(du -h "${output_dir}/${filename}" | cut -f1) printf " %-40s → %-30s OK %s\n" "$endpoint" "$filename" "$size" return 0 } backup_group_configs() { local output_dir="$1" local groups_response if [[ "$DRY_RUN" == true ]]; then printf " %-40s → %s (dry-run)\n" "groups/*/configuration" "group-*-config.json" return 0 fi groups_response=$(api_get "groups") if [[ -z "$groups_response" ]]; then printf " %-40s → %-30s FAIL\n" "groups/*/configuration" "group configs" return 1 fi local group_names group_names=$(echo "$groups_response" | jq -r '.data.affected_items[].name' 2>/dev/null) if [[ -z "$group_names" ]]; then printf " %-40s → %-30s SKIP (no groups)\n" "groups/*/configuration" "group configs" return 0 fi local count=0 while IFS= read -r group; do local config config=$(api_get "groups/${group}/configuration") if [[ -n "$config" ]]; then echo "$config" | jq '.' > "${output_dir}/group-${group}-config.json" 2>/dev/null ((count++)) || true fi done <<< "$group_names" printf " %-40s → %-30s OK %d groups\n" "groups/*/configuration" "group-*-config.json" "$count" return 0 } cleanup_old_backups() { if [[ ! -d "$BACKUP_DIR" ]]; then return fi local removed=0 while IFS= read -r dir; do rm -rf "$dir" ((removed++)) || true done < <(find "$BACKUP_DIR" -maxdepth 1 -mindepth 1 -type d -mtime +"$RETENTION_DAYS" 2>/dev/null) if [[ $removed -gt 0 ]]; then echo "Retention: removed $removed backup(s) older than ${RETENTION_DAYS} days" fi } install_cron() { if [[ $EUID -ne 0 ]]; then echo "ERROR: --install requires root" >&2 exit 1 fi local script_path script_path=$(readlink -f "$0") cat > /etc/cron.d/wazuh-api-backup </dev/null EOF chmod 644 /etc/cron.d/wazuh-api-backup echo "Installed cron job: /etc/cron.d/wazuh-api-backup" echo "Backups will be written to: ${BACKUP_DIR}/" } # --- Main --- main() { # Parse arguments for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; --install) check_dependencies validate_config install_cron exit 0 ;; --help|-h) usage ;; *) echo "Unknown option: $arg" >&2; usage ;; esac done check_dependencies validate_config local today today=$(date +%Y%m%d) local output_dir="${BACKUP_DIR}/${today}" if [[ "$DRY_RUN" == true ]]; then echo "Wazuh API Backup v${VERSION} (dry-run)" echo "Target: ${output_dir}" echo "" else mkdir -p "$output_dir" echo "Wazuh API Backup v${VERSION}" echo "Target: ${output_dir}" echo "" fi # Authenticate if [[ "$DRY_RUN" != true ]]; then get_token fi local ok=0 local fail=0 # Back up standard endpoints for entry in "${ENDPOINTS[@]}"; do local endpoint filename endpoint="${entry%% *}" filename="${entry##* }" if backup_endpoint "$endpoint" "$filename" "$output_dir"; then ((ok++)) || true else ((fail++)) || true fi done # Back up custom rule/decoder files (raw XML) for entry in "${CUSTOM_FILES[@]}"; do local endpoint filename endpoint="${entry%% *}" filename="${entry##* }" if backup_endpoint "$endpoint" "$filename" "$output_dir"; then ((ok++)) || true else ((fail++)) || true fi done # Back up per-group configurations if backup_group_configs "$output_dir"; then ((ok++)) || true else ((fail++)) || true fi echo "" if [[ "$DRY_RUN" == true ]]; then local total=$((${#ENDPOINTS[@]} + ${#CUSTOM_FILES[@]} + 1)) echo "Dry-run complete: ${total} backup targets" else local total_size total_size=$(du -sh "$output_dir" | cut -f1) echo "Complete: ${ok} OK, ${fail} failed, ${total_size} total" cleanup_old_backups fi } main "$@"