#!/bin/bash

# Wazuh installer
# Copyright (C) Wazuh Inc.
#
# This program is a free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License (version 2) as published by the FSF - Free Software
# Foundation.

indexer_config_path="/etc/wazuh-indexer"
manager_config_path="/etc/wazuh-manager"
dashboard_config_path="/etc/wazuh-dashboard"
adminpem="/etc/wazuh-indexer/certs/admin.pem"
adminkey="/etc/wazuh-indexer/certs/admin-key.pem"
manager_keystore="/var/wazuh-manager/bin/wazuh-manager-keystore"
manager_resolver="/var/wazuh-manager/bin/wazuh-manager-resolve-credentials"
readonly wazuh_major="5"
readonly wazuh_version="5.0.0"
readonly wazuh_install_vesion="0.1"
bucket="packages.wazuh.com"
staging_url_stage="rc2"
base_path="$(dirname "$(readlink -f "$0")")"
readonly base_path
config_file="${base_path}/config.yml"
readonly tar_file_name="wazuh-install-files.tar"
tar_file="${base_path}/${tar_file_name}"
artifact_urls_file_name="artifact_urls.yaml"
readonly artifact_urls_bucket_folder="artifact-urls"
readonly download_packages_directory="wazuh-install-packages"
readonly dashboard_cert_path="${dashboard_config_path}/certs"
readonly manager_cert_path="${manager_config_path}/certs"
readonly indexer_cert_path="${indexer_config_path}/certs"
readonly manager_conf_file="${manager_config_path}/wazuh-manager.conf"
readonly manager_api_conf_file="/var/wazuh-manager/api/configuration/api.yaml"
readonly credential_keys=( WAZUH_INDEXER_ADMIN_PASSWORD WAZUH_INDEXER_KIBANASERVER_PASSWORD WAZUH_INDEXER_MANAGER_PASSWORD WAZUH_MANAGER_API_PASSWORD WAZUH_MANAGER_WUI_PASSWORD )
readonly indexer_credential_keys=( WAZUH_INDEXER_ADMIN_PASSWORD WAZUH_INDEXER_KIBANASERVER_PASSWORD WAZUH_INDEXER_MANAGER_PASSWORD )
readonly dashboard_credential_keys=( WAZUH_INDEXER_KIBANASERVER_PASSWORD WAZUH_MANAGER_WUI_PASSWORD )
readonly manager_credential_keys=( WAZUH_INDEXER_MANAGER_PASSWORD WAZUH_MANAGER_API_PASSWORD WAZUH_MANAGER_WUI_PASSWORD )
readonly logfile="/var/log/wazuh-install.log"
debug=">> ${logfile} 2>&1"
readonly yum_lockfile="/var/run/yum.pid"
readonly apt_lockfile="/var/lib/dpkg/lock"
readonly base_dest_folder="wazuh-offline"
http_port=443
wazuh_aio_ports=( 9200 9300 1514 1515 1516 1517 55000 "${http_port}")
readonly wazuh_indexer_ports=( 9200 9300 )
readonly indexer_security_init="/usr/share/wazuh-indexer/bin/indexer-security-init.sh"
readonly indexer_cluster_timeout=300
readonly wazuh_manager_ports=( 1514 1515 1516 1517 55000 )
wazuh_dashboard_port="${http_port}"
wia_yum_dependencies=( systemd grep tar coreutils sed procps-ng gawk curl )
readonly wia_apt_dependencies=( systemd grep tar coreutils sed procps gawk curl )
readonly wazuh_yum_dependencies=( diffutils findutils gawk grep hostname iproute openssl sed util-linux )
readonly wazuh_apt_dependencies=( gnupg curl openssl iproute2 hostname util-linux diffutils )
readonly indexer_yum_dependencies=( coreutils diffutils hostname iproute openssl procps-ng util-linux )
readonly indexer_apt_dependencies=( debconf adduser procps openssl diffutils iproute2 )
readonly dashboard_yum_dependencies=( libcap diffutils openssl util-linux )
readonly dashboard_apt_dependencies=( tar curl libcap2-bin openssl diffutils )
wia_offline_dependencies=( curl tar gnupg openssl )
wia_dependencies_installed=()
readonly wazuh_gpg_key_fingerprints=( "0DCFCA5547B19D2A6099506096B3EE5F29111145" )
readonly wazuh_gpg_key_url="https://packages.wazuh.com/key/GPG-KEY-WAZUH"
assistant_yum_dependencies=( "${wia_yum_dependencies[@]}" )
assistant_apt_dependencies=( "${wia_apt_dependencies[@]}" )

trap installCommon_cleanExit SIGINT
export JAVA_HOME="/usr/share/wazuh-indexer/jdk/"
# ------------ checks.sh ------------ 
function checks_arch() {

    common_logger -d "Checking system architecture."
    architecture=$(uname -m)

    if [ "${architecture}" == "x86_64" ] || [ "${architecture}" == "aarch64" ]; then
        common_logger -d "System architecture: ${architecture}"
    else
        common_logger -e "Uncompatible system architecture: ${architecture}. Supported any 64-bit system"
        exit 1
    fi
}
function checks_arguments() {

    # -------------- Repository selection ---------------------

    if [ -n "${development}" ]; then
        if [ -z "${AIO}" ] && [ -z "${dashboard}" ] && [ -z "${indexer}" ] && [ -z "${wazuh}" ] && [ -z "${start_indexer_cluster}" ] && [ -z "${download}" ]; then
            common_logger -e "The -d|--development option must be used with -a, -wm, -s, -wi, -wd or -dw."
            exit 1
        fi
    fi

    if [ -n "${skip_signature_check}" ] && [ -z "${development}" ]; then
        common_logger -e "The --skip-signature-check option must be used with -d|--development."
        exit 1
    fi

    # -------------- Offline installation ---------------------

    if [ -n "${offline_install}" ]; then
        if [ -z "${AIO}" ] && [ -z "${dashboard}" ] && [ -z "${indexer}" ] && [ -z "${wazuh}" ] && [ -z "${start_indexer_cluster}" ]; then
            common_logger -e "The -of|--offline-installation option must be used with -a, -wm, -s, -wi, or -wd."
            exit 1
        fi
    fi

    if [ -n "${download}" ] && [ -z "${download_arch}" ]; then
        common_logger -e "To download the packages it is necessary to set the architecture in -da|--download-arch <amd64|x86_64|arm64|aarch64>"
        exit 1
    fi

    if [ -n "${download}" ] && [ -n "${download_arch}" ]; then
        if [ "${package_type}" = "deb" ]; then
            if [ "${arch}" != "amd64" ] && [ "${arch}" != "arm64" ]; then
                if [ "${arch}" = "x86_64" ]; then
                    common_logger -e "Architecture ${arch} not valid for package type ${package_type}. Use amd64 instead."
                elif [ "${arch}" = "aarch64" ]; then
                    common_logger -e "Architecture ${arch} not valid for package type ${package_type}. Use arm64 instead."
                else
                    common_logger -e "Architecture ${arch} not valid for package type ${package_type}"
                fi
                exit 1
            fi
        elif [ "${package_type}" = "rpm" ]; then
            if [ "${arch}" != "x86_64" ] && [ "${arch}" != "aarch64" ]; then
                if [ "${arch}" = "amd64" ]; then
                    common_logger -e "Architecture ${arch} not valid for package type ${package_type}. Use x86_64 instead."
                elif [ "${arch}" = "arm64" ]; then
                    common_logger -e "Architecture ${arch} not valid for package type ${package_type}. Use aarch64 instead."
                else
                    common_logger -e "Architecture ${arch} not valid for package type ${package_type}"
                fi
                exit 1
            fi
        fi
    fi

    # -------------- Configurations ---------------------------------

    if [ -f "${tar_file}" ]; then
        if [ -n "${AIO}" ] && [ -z "${offline_install}" ]; then
            rm -f "${tar_file}"
        fi
        if [ -n "${configurations}" ]; then
            common_logger -e "File ${tar_file} already exists. Please remove it if you want to use a new configuration."
            exit 1
        fi
    fi

    if [[ -n "${configurations}" && ( -n "${AIO}" || -n "${indexer}" || -n "${dashboard}" || -n "${wazuh}" || -n "${overwrite}" || -n "${start_indexer_cluster}" || -n "${uninstall}" ) ]]; then
        common_logger -e "The argument -g|--generate-config-files can't be used with -a|--all-in-one, -o|--overwrite, -s|--start-cluster, -u|--uninstall, -wd|--wazuh-dashboard, -wi|--wazuh-indexer, or -wm|--wazuh-manager."
        exit 1
    fi

    # -------------- Overwrite --------------------------------------

    if [ -n "${overwrite}" ] && [ -z "${AIO}" ] && [ -z "${indexer}" ] && [ -z "${dashboard}" ] && [ -z "${wazuh}" ]; then
        common_logger -e "The argument -o|--overwrite must be used in conjunction with -a|--all-in-one, -wd|--wazuh-dashboard, -wi|--wazuh-indexer, or -wm|--wazuh-manager."
        exit 1
    fi

    # -------------- Uninstall --------------------------------------

    if [ -n "${uninstall}" ]; then

        if [ -n "$AIO" ] || [ -n "$indexer" ] || [ -n "$dashboard" ] || [ -n "$wazuh" ]; then
            common_logger -e "It is not possible to uninstall and install in the same operation. If you want to overwrite the components use -o|--overwrite."
            exit 1
        fi

    fi

    # -------------- All-In-One -------------------------------------

    if [ -n "${AIO}" ]; then

        if [ -n "$indexer" ] || [ -n "$dashboard" ] || [ -n "$wazuh" ]; then
            common_logger -e "Argument -a|--all-in-one is not compatible with -wi|--wazuh-indexer, -wd|--wazuh-dashboard or -wm|--wazuh-manager."
            exit 1
        fi

        if [ -n "${overwrite}" ]; then
            installCommon_rollBack
        fi

        if  [ -z "${overwrite}" ] && { [ -n "${wazuh_installed}" ] || [ -n "${wazuh_remaining_files}" ]; }; then
            common_logger -e "Wazuh manager already installed."
            installedComponent=1
        fi
        if [ -z "${overwrite}" ] && { [ -n "${indexer_installed}" ] || [ -n "${indexer_remaining_files}" ]; };then
            common_logger -e "Wazuh indexer already installed."
            installedComponent=1
        fi
        if [ -z "${overwrite}" ] && { [ -n "${dashboard_installed}" ] || [ -n "${dashboard_remaining_files}" ]; }; then
            common_logger -e "Wazuh dashboard already installed."
            installedComponent=1
        fi
        if [ -n "${installedComponent}" ]; then
            common_logger "If you want to overwrite the current installation, run this script adding the option -o/--overwrite. This will erase all the existing configuration and data."
            exit 1
        fi

    fi

    # -------------- Indexer ----------------------------------

    if [ -n "${indexer}" ]; then

        if [ -n "${indexer_installed}" ] || [ -n "${indexer_remaining_files}" ]; then
            if [ -n "${overwrite}" ]; then
                installCommon_rollBack
            else
                common_logger -e "Wazuh indexer is already installed in this node or some of its files have not been removed. Use option -o|--overwrite to overwrite all components."
                exit 1
            fi
        fi
    fi

    # -------------- Wazuh dashboard --------------------------------

    if [ -n "${dashboard}" ]; then
        if [ -n "${dashboard_installed}" ] || [ -n "${dashboard_remaining_files}" ]; then
            if [ -n "${overwrite}" ]; then
                installCommon_rollBack
            else
                common_logger -e "Wazuh dashboard is already installed in this node or some of its files have not been removed. Use option -o|--overwrite to overwrite all components."
                exit 1
            fi
        fi
    fi

    # -------------- Wazuh ------------------------------------------

    if [ -n "${wazuh}" ]; then
        if [ -n "${wazuh_installed}" ] || [ -n "${wazuh_remaining_files}" ]; then
            if [ -n "${overwrite}" ]; then
                installCommon_rollBack
            else
                common_logger -e "Wazuh manager components are already installed in this node or some of their files have not been removed. Use option -o|--overwrite to overwrite all components."
                exit 1
            fi
        fi
    fi

    # -------------- Cluster start ----------------------------------

    if [[ -n "${start_indexer_cluster}" && ( -n "${AIO}" || -n "${indexer}" || -n "${dashboard}" || -n "${wazuh}" || -n "${overwrite}" || -n "${configurations}" || -n "${uninstall}") ]]; then
        common_logger -e "The argument -s|--start-cluster can't be used with -a|--all-in-one, -g|--generate-config-files,-o|--overwrite , -u|--uninstall, -wi|--wazuh-indexer, -wd|--wazuh-dashboard, -s|--start-cluster, -wm|--wazuh-manager."
        exit 1
    fi

    # -------------- Global -----------------------------------------

    if [ -z "${AIO}" ] && [ -z "${indexer}" ] && [ -z "${dashboard}" ] && [ -z "${wazuh}" ] && [ -z "${start_indexer_cluster}" ] && [ -z "${configurations}" ] && [ -z "${uninstall}" ] && [ -z "${download}" ]; then
        common_logger -e "At least one of these arguments is necessary -a|--all-in-one, -g|--generate-config-files, -wi|--wazuh-indexer, -wd|--wazuh-dashboard, -s|--start-cluster, -wm|--wazuh-manager, -u|--uninstall, -dw|--download-wazuh."
        exit 1
    fi

}
function check_curlVersion() {

    common_logger -d "Checking curl tool version."
    # --retry-connrefused was added in 7.52.0
    curl_version=$(curl -V | head -n 1 | awk '{ print $2 }')
    if [ $(check_versions ${curl_version} 7.52.0) == "0" ]; then
        curl_has_connrefused=0
    fi

}
function check_dist() {
    common_logger -d "Checking system distribution."
    dist_detect
    if  [ "${DIST_NAME}" != "rhel" ] &&
        [ "${DIST_NAME}" != "amzn" ] && [ "${DIST_NAME}" != "ubuntu" ]; then
        notsupported=1
    fi

    if [ "${DIST_NAME}" == "rhel" ] && { [ "${DIST_VER}" -ne "9" ] && [ "${DIST_VER}" -ne "10" ]; }; then
        notsupported=1
    fi

    if [ "${DIST_NAME}" == "amzn" ]; then
        if [ "${DIST_VER}" != "2023" ]; then
            notsupported=1
        else
            checks_specialDepsAL2023
        fi
    fi

    if [ "${DIST_NAME}" == "ubuntu" ]; then
        if [ "${DIST_VER}" == "24" ] || [ "${DIST_VER}" == "26" ]; then
            if [ "${DIST_SUBVER}" != "04" ]; then
                notsupported=1
            fi
        else
            notsupported=1
        fi
    fi

    if [ -n "${notsupported}" ]; then
        common_logger "The supported systems are: Red Hat Enterprise Linux 9, 10; Amazon Linux 2023; Ubuntu 24.04, 26.04."
        common_logger -w "The current system does not match with the list of supported systems. The $(installCommon_operationName) may not work properly."
    fi
    common_logger -d "Detected distribution name: ${DIST_NAME}"
    common_logger -d "Detected distribution version: ${DIST_VER}"

}
function checks_uninstallComponents() {

    local found=1

    if [ -z "${wazuh_installed}" ] && [ -z "${wazuh_remaining_files}" ]; then
        common_logger "Wazuh manager not found in the system so it was not uninstalled."
    else
        found=0
    fi

    if [ -z "${indexer_installed}" ] && [ -z "${indexer_remaining_files}" ]; then
        common_logger "Wazuh indexer not found in the system so it was not uninstalled."
    else
        found=0
    fi

    if [ -z "${dashboard_installed}" ] && [ -z "${dashboard_remaining_files}" ]; then
        common_logger "Wazuh dashboard not found in the system so it was not uninstalled."
    else
        found=0
    fi

    return ${found}

}
function checks_health() {

    checks_specifications

    common_logger -d "CPU cores detected: ${cores}"
    common_logger -d "Total RAM memory detected: ${ram_gb} MB"

    if [ -z "${AIO}" ] && [ -z "${indexer}" ] && [ -z "${wazuh}" ] && [ -z "${dashboard}" ]; then
        return 0
    fi

    # Components the host will hold: the ones being installed plus the ones
    # already installed (assisted per-component installs on the same host).
    local host_components=()
    if [ -n "${AIO}" ] || [ -n "${indexer}" ] || [ -n "${indexer_installed}" ]; then
        host_components+=("Wazuh indexer")
    fi
    if [ -n "${AIO}" ] || [ -n "${wazuh}" ] || [ -n "${wazuh_installed}" ]; then
        host_components+=("Wazuh manager")
    fi
    if [ -n "${AIO}" ] || [ -n "${dashboard}" ] || [ -n "${dashboard_installed}" ]; then
        host_components+=("Wazuh dashboard")
    fi

    # Documented minimums: Wazuh indexer and Wazuh manager 8 GB and 4 cores,
    # Wazuh dashboard 4 GB and 2 cores. Co-located components add up, capped
    # at the documented all-in-one minimum (8 GB and 4 cores).
    local required_ram=0 required_cores=0 component
    for component in "${host_components[@]}"; do
        case "${component}" in
            "Wazuh dashboard") required_ram=$((required_ram + 4)); required_cores=$((required_cores + 2)) ;;
            *) required_ram=$((required_ram + 8)); required_cores=$((required_cores + 4)) ;;
        esac
    done
    if [ "${required_ram}" -gt 8 ]; then required_ram=8; fi
    if [ "${required_cores}" -gt 4 ]; then required_cores=4; fi

    # free -m reports less than the nominal size (kernel and firmware reserve
    # memory), so allow 700 MB below each GB figure: 8 GB -> 7300 MB.
    local required_ram_mb=$((required_ram * 1000 - 700))

    common_logger -d "Components on this host: ${host_components[*]}. Required: ${required_ram} GB of RAM (${required_ram_mb} MB detected threshold) and ${required_cores} CPU cores."

    if [ "${cores}" -lt "${required_cores}" ] || [ "${ram_gb}" -lt "${required_ram_mb}" ]; then
        common_logger -w "Your system does not meet the recommended minimum hardware requirements of ${required_ram} GB of RAM and ${required_cores} CPU cores for the components on this host. The installation will continue, but it may not work properly."
    fi

}
function checks_names() {

    common_logger -d "Checking node names in the configuration file."
    if [ -n "${indxname}" ] && [ -n "${dashname}" ] && [ "${indxname}" == "${dashname}" ]; then
        common_logger -e "The node names for Wazuh indexer and Wazuh dashboard must be different."
        exit 1
    fi

    if [ -n "${indxname}" ] && [ -n "${winame}" ] && [ "${indxname}" == "${winame}" ]; then
        common_logger -e "The node names for Elastisearch and Wazuh must be different."
        exit 1
    fi

    if [ -n "${winame}" ] && [ -n "${dashname}" ] && [ "${winame}" == "${dashname}" ]; then
        common_logger -e "The node names for Wazuh manager and Wazuh indexer must be different."
        exit 1
    fi

    if [ -n "${winame}" ] && ! echo "${manager_node_names[@]}" | grep -w -q "${winame}"; then
        common_logger -e "The Wazuh manager node name ${winame} does not appear on the configuration file."
        exit 1
    fi

    if [ -n "${indxname}" ] && ! echo "${indexer_node_names[@]}" | grep -w -q "${indxname}"; then
        common_logger -e "The Wazuh indexer node name ${indxname} does not appear on the configuration file."
        exit 1
    fi

    if [ -n "${dashname}" ] && ! echo "${dashboard_node_names[@]}" | grep -w -q "${dashname}"; then
        common_logger -e "The Wazuh dashboard node name ${dashname} does not appear on the configuration file."
        exit 1
    fi

    if [[ "${dashname}" == -* ]] || [[ "${indxname}" == -* ]] || [[ "${winame}" == -* ]]; then
        common_logger -e "Node name cannot start with \"-\""
        exit 1
    fi

}
function checks_previousCertificate() {
    common_logger -d "Checking previous certificate existence."
    if [ ! -f "${tar_file}" ]; then
        common_logger -e "Cannot find ${tar_file}. Run the script with the option -g|--generate-config-files to create it or copy it from another node."
        exit 1
    fi

    if [ -z "${indxname}" ] && [ -z "${dashname}" ] && [ -z "${winame}" ]; then
        return 0
    fi

    checks_tarFiles "config.yml" "credentials.env" "root-ca.pem"

    if [ -n "${indxname}" ]; then
        checks_tarFiles "${indxname}.pem" "${indxname}-key.pem" "admin.pem" "admin-key.pem"
        checks_tarPasswords "${indexer_credential_keys[@]}"
    fi

    if [ -n "${dashname}" ]; then
        checks_tarFiles "${dashname}.pem" "${dashname}-key.pem"
        checks_tarPasswords "${dashboard_credential_keys[@]}"
    fi

    if [ -n "${winame}" ]; then
        checks_tarFiles "${winame}.pem" "${winame}-key.pem" "${winame}-remoted.pem" "${winame}-remoted-key.pem" \
            "${winame}-apid.pem" "${winame}-apid-key.pem"
        checks_tarPasswords "${manager_credential_keys[@]}"
    fi
}
function checks_tarFiles() {

    local file members

    members=$(tar -tf "${tar_file}" 2>/dev/null)
    for file in "$@"; do
        if ! grep -q -x -F "wazuh-install-files/${file}" <<< "${members}"; then
            common_logger -e "There is no ${file} in ${tar_file}. Create the file again with -g|--generate-config-files."
            exit 1
        fi
    done

}
function checks_tarPasswords() {

    local key value credentials

    credentials=$(tar -xOf "${tar_file}" wazuh-install-files/credentials.env 2>/dev/null)
    for key in "$@"; do
        value=$(sed -nE "s/^${key}=\"?([^\"]*)\"?$/\1/p" <<< "${credentials}" | tail -n 1)
        if [ -z "${value}" ]; then
            common_logger -e "There is no ${key} in the credentials.env file of ${tar_file}."
            exit 1
        fi
        if ! common_passwordValidate "${value}" 2>/dev/null; then
            common_logger -e "The ${key} in the credentials.env file of ${tar_file} does not follow the password policy."
            exit 1
        fi
    done

}
function checks_specialDepsAL2023() {

    # Change curl for curl-minimal
    wia_yum_dependencies=( "${wia_yum_dependencies[@]/curl/curl-minimal}" )
    assistant_yum_dependencies=( "${assistant_yum_dependencies[@]/curl/curl-minimal}" )
    wia_offline_dependencies=( "${wia_offline_dependencies[@]/curl/curl-minimal}" )

    # In containers, coreutils is replaced for coreutils-single
    if [ -f "/.dockerenv" ]; then
        wia_yum_dependencies=( "${wia_yum_dependencies[@]/coreutils/coreutils-single}" )
        assistant_yum_dependencies=( "${assistant_yum_dependencies[@]/coreutils/coreutils-single}" )
    fi
}
function checks_specifications() {

    cores=$(grep -c processor /proc/cpuinfo)
    ram_gb=$(free -m | awk 'FNR == 2 {print $2}')

}
function checks_portInUse() {
    if command -v lsof > /dev/null; then
        lsof -sTCP:LISTEN -i:"${1}" > /dev/null
    else
        # ss always exits 0, so look at its output (the first line is the header)
        [ -n "$(ss -ltn "sport = :${1}" 2>/dev/null | tail -n +2)" ]
    fi
}
function checks_ports() {

    common_logger -d "Checking ports availability."
    used_port=0
    ports=("$@")

    checks_firewall "${ports[@]}"

    if ! command -v lsof > /dev/null && ! command -v ss > /dev/null; then
        common_logger -w "Cannot find lsof or ss. Port checking will be skipped."
        return 1
    fi

    for i in "${!ports[@]}"; do
        if checks_portInUse "${ports[i]}"; then
            used_port=1
            common_logger -e "Port ${ports[i]} is being used by another process. Please, check it before installing Wazuh."
        fi
    done

    if [ "${used_port}" -eq 1 ]; then
        common_logger "The installation can not continue due to port usage by other processes."
        installCommon_rollBack
        exit 1
    fi

}
function check_versions() {

    if test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" == "$1"; then
        echo 0
    else
        echo 1
    fi
}
function checks_localArtifactURLs_exists() {
    common_logger -d "Checking if ${artifact_urls_file_name} exists locally."
    if [ ! -f "${base_path}/${artifact_urls_file_name}" ]; then
        common_logger -e "Cannot find ${artifact_urls_file_name} in ${base_path}."
        exit 1
    fi
}
function checks_ArtifactURLs_format() {
    common_logger -d "Checking ${artifact_urls_file_name} file format."
    artifact_file="${base_path}/${artifact_urls_file_name}"
    # Check that all values are valid URLs
    while IFS=': ' read -r key value; do
        # Skip empty lines and comments
        [[ -z "$key" || "$key" =~ ^#.*$ ]] && continue

        # Remove quotes and whitespace from value
        value=$(echo "$value" | tr -d '"' | xargs)

        # Validate URL format (must start with https://)
        if [[ ! "$value" =~ ^https:// ]]; then
            common_logger -e "Invalid URL format for key '${key}': ${value}"
            common_logger -e "All values in ${artifact_urls_file_name} must be valid URLs starting with https://"
            exit 1
        fi
    done < "$artifact_file"
}
function checks_ArtifactURLs_component_present() {
    common_logger -d "Checking required component is present in ${artifact_urls_file_name} file."
    artifact_file="${base_path}/${artifact_urls_file_name}"
    # Determine package type based on system
    if [ "${sys_type}" == "yum" ]; then
        pkg_type="rpm"
        # Determine architecture suffix for artifact keys
        if [ "${architecture}" == "x86_64" ]; then
            arch_suffix="x86_64"
        elif [ "${architecture}" == "aarch64" ]; then
            arch_suffix="aarch64"
        fi
    elif [ "${sys_type}" == "apt-get" ]; then
        pkg_type="deb"
        # Determine architecture suffix for artifact keys
        if [ "${architecture}" == "x86_64" ]; then
            arch_suffix="amd64"
        elif [ "${architecture}" == "aarch64" ]; then
            arch_suffix="arm64"
        fi
    fi

    indexer_key="wazuh_indexer_${arch_suffix}_${pkg_type}"
    dashboard_key="wazuh_dashboard_${arch_suffix}_${pkg_type}"
    manager_key="wazuh_manager_${arch_suffix}_${pkg_type}"

    # Check component-specific artifacts exist
    if [ -n "${AIO}" ] || [ -n "${indexer}" ]; then
        if ! grep -q "^${indexer_key}:" "$artifact_file"; then
            common_logger -e "Missing required artifact key: ${indexer_key}"
            exit 1
        fi
    fi
    if [ -n "${AIO}" ] || [ -n "${dashboard}" ]; then
        if ! grep -q "^${dashboard_key}:" "$artifact_file"; then
            common_logger -e "Missing required artifact key: ${dashboard_key}"
            exit 1
        fi
    fi
    if [ -n "${AIO}" ] || [ -n "${wazuh}" ]; then
        if ! grep -q "^${manager_key}:" "$artifact_file"; then
            common_logger -e "Missing required artifact key: ${manager_key}"
            exit 1
        fi
    fi
}
function checks_firewall(){
    ports_list=("$@")
    f_ports=""
    f_message="The system has firewall enabled. Please ensure that traffic is allowed on "
    firewalld_installed=0
    ufw_installed=0


    # Record of the ports that must be exposed according to the installation
    if [ -n "${AIO}" ]; then
        f_message+="these ports: 1515, 1517, 1514, ${http_port}"
    elif [ -n "${dashboard}" ]; then
        f_message+="this port: ${http_port}"
    else
        f_message+="these ports:"
        for port in "${ports_list[@]}"; do
            f_message+=" ${port},"
        done

        # Deletes last comma
        f_message="${f_message%,}"
    fi

    # Check if the firewall is installed
    if [ "${sys_type}" == "yum" ]; then
        rpm -q firewalld --quiet && firewalld_installed=1
        rpm -q ufw --quiet && ufw_installed=1
    elif [ "${sys_type}" == "apt-get" ]; then
        if apt list --installed 2>/dev/null | grep -q -E ^"firewalld"\/; then
            firewalld_installed=1
        fi
        if apt list --installed 2>/dev/null | grep -q -E ^"ufw"\/; then
            ufw_installed=1
        fi
    fi

    # Check if the firewall is running
    if [ "${firewalld_installed}" == "1" ]; then
        if firewall-cmd --state 2>/dev/null | grep -q -w "running"; then
            common_logger -w "${f_message/firewall/Firewalld}."
        fi
    fi
    if [ "${ufw_installed}" == "1" ]; then
        if ufw status 2>/dev/null | grep -q -w "active"; then
            common_logger -w "${f_message/firewall/UFW}."
        fi
    fi

}

# ------------ dashboard.sh ------------ 
function dashboard_configure() {

    common_logger -d "Configuring Wazuh dashboard."

    # dashboard configuration to connect to the indexer cluster
    if [ "${#indexer_node_names[@]}" -eq 1 ]; then
        eval "sed -i 's|opensearch.hosts:.*|opensearch.hosts: https://${indexer_node_ips[0]}:9200|' /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}"
    else
        ips_list="["
        for i in "${indexer_node_ips[@]}"; do
            ips_list+="\"https://${i}:9200\", "
        done
        ips_list=${ips_list%, }"]" # if there are more than one indexer, there will be a list of urls ["url1", "url2", ...]
        eval "sed -i 's|opensearch.hosts:.*|opensearch.hosts: ${ips_list}|' /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}"
    fi

    # dashboard configuration to connect to the wazuh api
    if [ -n "${AIO}" ]; then
        wazuh_api_address=${manager_node_ips[0]}
    else
        if [ -n "${manager_node_types[*]}" ]; then
            for i in "${!manager_node_types[@]}"; do
                if [[ "${manager_node_types[i]}" == "master" ]]; then
                    wazuh_api_address=${manager_node_ips[i]}
                fi
            done
        else
            wazuh_api_address=${manager_node_ips[0]}
        fi
    fi
    eval "sed -i 's|url:.*|url: https://${wazuh_api_address}|' /etc/wazuh-dashboard/opensearch_dashboards.yml ${debug}"

    common_logger "Wazuh dashboard post-install configuration finished."

}
function dashboard_copyCertificates() {

    common_logger -d "Placing the Wazuh dashboard certificates."
    eval "install -d -o root -g root -m 0750 ${dashboard_config_path} ${dashboard_cert_path} ${debug}"
    installCommon_placeFromTar "${dashname}.pem" "${dashboard_cert_path}/dashboard.pem" root root 0640
    installCommon_placeFromTar "${dashname}-key.pem" "${dashboard_cert_path}/dashboard-key.pem" root root 0640
    installCommon_placeFromTar "root-ca.pem" "${dashboard_cert_path}/indexer-ca.pem" root root 0640
    installCommon_placeFromTar "root-ca.pem" "${dashboard_cert_path}/manager-ca.pem" root root 0640

}
function dashboard_globalAddresses() {

    openssl x509 -in "${dashboard_cert_path}/dashboard.pem" -noout -ext subjectAltName 2>/dev/null \
        | grep -o 'IP Address:[^,[:space:]]*' | cut -d: -f2- \
        | grep -Ev '^(127\.|169\.254\.|0:0:0:0:0:0:0:1$|::1$|[Ff][Ee]80:)'

}
function dashboard_initialize() {

    common_logger "Initializing Wazuh dashboard web application."

    # The all-in-one config.yml is not read, and its node address would be 127.0.0.1 anyway.
    local dashboard_ip="127.0.0.1"
    local max_retries=20
    local delay=15
    if [ -z "${AIO}" ]; then
        max_retries=12
        delay=10
        dashboard_ip="${dashboard_node_ips[0]}"
        for i in "${!dashboard_node_names[@]}"; do
            if [ "${dashboard_node_names[i]}" == "${dashname}" ]; then
                dashboard_ip="${dashboard_node_ips[i]}"
            fi
        done
    fi

    # Without a global address in the certificate, the node address, unless it is loopback.
    local print_ips
    mapfile -t print_ips < <(dashboard_globalAddresses)
    if [ "${#print_ips[@]}" -eq 0 ]; then
        print_ips=("${dashboard_ip}")
        if [ "${dashboard_ip}" == "localhost" ] || [[ "${dashboard_ip}" == 127.* ]]; then
            print_ips=("<wazuh-dashboard-ip>")
        fi
    fi

    # The dashboard answers 503 until it is ready, and 000 is no connection.
    local code j=0
    code=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://${dashboard_ip}:${http_port}/status")
    until [ "${code}" != "000" ] && [ "${code}" != "503" ] || [ "${j}" -ge "${max_retries}" ]; do
        common_logger -d "Retrying Wazuh dashboard connection..."
        sleep "${delay}"
        j=$((j+1))
        code=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://${dashboard_ip}:${http_port}/status")
    done

    if [ "${code}" == "000" ] || [ "${code}" == "503" ]; then
        common_logger -e "Cannot connect to Wazuh dashboard."
        # Without credentials, an indexer answers 503 until its security is initialized.
        for i in "${indexer_node_ips[@]:-127.0.0.1}"; do
            code=$(curl -k -s -o /dev/null -w "%{http_code}" --max-time 10 "https://${i}:9200/")
            if [ "${code}" == "000" ]; then
                common_logger -e "Failed to connect with the Wazuh indexer at ${i}:9200."
            elif [ "${code}" == "503" ]; then
                common_logger -e "Wazuh indexer security settings not initialized in ${i}. Please run the installation assistant using -s|--start-cluster in one of the Wazuh indexer nodes."
            fi
        done
        installCommon_rollBack
        exit 1
    fi

    # A distributed dashboard node only receives its own passwords, not the admin one.
    local indexer_export="${indexer_config_path}/export/credentials.env"
    local password_source="${indexer_export}"
    local credentials_file="${indexer_export} and ${manager_config_path}/export/credentials.env"
    local password_command="sudo grep '^WAZUH_INDEXER_ADMIN_PASSWORD=' ${indexer_export} | cut -d= -f2-"
    if [ -z "${AIO}" ]; then
        password_source="${tar_file_name}"
        credentials_file="wazuh-install-files/credentials.env of ${tar_file_name}"
        password_command="sudo tar -xOf ${tar_file_name} wazuh-install-files/credentials.env | grep '^WAZUH_INDEXER_ADMIN_PASSWORD=' | cut -d= -f2-"
    fi

    # The password goes to an interactive terminal only, never to the log or redirected output.
    # The command that reads it is logged too.
    common_logger "Wazuh dashboard web application initialized."
    common_logger "--- Summary ---"
    local ip
    for ip in "${print_ips[@]}"; do
        [[ "${ip}" == *:* ]] && ip="[${ip}]"
        common_logger "You can access the web interface https://${ip}:${http_port}"
    done
    common_logger "    User: admin (Wazuh dashboard login and Wazuh indexer administrator)"
    local admin_password
    if [ -n "${AIO}" ] && dashboard_stdoutIsTerminal && admin_password=$(common_envGet "${indexer_export}" WAZUH_INDEXER_ADMIN_PASSWORD 2>/dev/null) && [ -n "${admin_password}" ]; then
        common_logger -nl "    Password: ${admin_password}"
        common_logger "    To read the password again, run:"
    else
        common_logger "    Password: to read it from ${password_source}, run:"
    fi
    common_logger "        ${password_command}"
    common_logger "The passwords of the other users are in ${credentials_file}."

    # Agents enroll only with a token, created on the manager master node.
    local token_node="this server"
    [ -z "${AIO}" ] && token_node="the Wazuh manager (the master node in a cluster)"
    common_logger "--- Next steps ---"
    common_logger "To enroll agents, use Deploy new agent in the Wazuh dashboard, or create an enrollment token on ${token_node}:"
    common_logger "    sudo /var/wazuh-manager/bin/wazuh-manager-authd --create-enrollment-token --address <WAZUH_MANAGER_ADDRESS>"
    common_logger "For more information, see https://documentation.wazuh.com/${wazuh_version%.*}/installation-guide/wazuh-agent/index.html"

}
function dashboard_stdoutIsTerminal() {
    [ -t 1 ]
}
function dashboard_install() {

    common_logger "Starting Wazuh dashboard installation."

    if [ -n "${offline_install}" ]; then
        download_dir="${offline_packages_path}"
    else
        download_dir="${base_path}/${download_packages_directory}"
    fi

    # Find the downloaded package file
    if [ "${sys_type}" == "yum" ]; then
        package_file=$(ls "${download_dir}"/wazuh-dashboard*.rpm 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "Wazuh dashboard package file not found in ${download_dir}."
            exit 1
        fi
        installCommon_yumInstall "${package_file}"
    elif [ "${sys_type}" == "apt-get" ]; then
        package_file=$(ls "${download_dir}"/wazuh-dashboard*.deb 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "Wazuh dashboard package file not found in ${download_dir}."
            exit 1
        fi
        installCommon_aptInstall "${package_file}"
    fi

    common_checkInstalled
    if [  "$install_result" != 0  ] || [ -z "${dashboard_installed}" ]; then
        common_logger -e "Wazuh dashboard installation failed."
        installCommon_rollBack
        exit 1
    else
        common_logger "Wazuh dashboard installation finished."
    fi

}

# ------------ indexer.sh ------------ 
function indexer_configure() {

    common_logger -d "Configuring Wazuh indexer."

    # Configure JVM options for Wazuh indexer
    ram_mb=$(free -m | awk 'FNR == 2 {print $2}')

    if [ "${AIO}" ]; then
        ram="$(( ram_mb / 4 ))"
    else
        ram="$(( ram_mb / 2 ))"
    fi

    if [ "${ram}" -eq "0" ]; then
        ram=1024;
    fi
    eval "sed -i "s/-Xms1g/-Xms${ram}m/" /etc/wazuh-indexer/jvm.options ${debug}"
    eval "sed -i "s/-Xmx1g/-Xmx${ram}m/" /etc/wazuh-indexer/jvm.options ${debug}"

    # On an all-in-one install the package already configured a single node with its own
    # certificates and nodes_dn. Only keep the indexer off the network, as before.
    if [ "${AIO}" ]; then
        eval "sed -i 's|network.host:.*|network.host: \"127.0.0.1\"|' /etc/wazuh-indexer/opensearch.yml ${debug}"
        common_logger "Wazuh indexer post-install configuration finished."
        return 0
    fi

    for i in "${!indexer_node_names[@]}"; do
        if [[ "${indexer_node_names[i]}" == "${indxname}" ]]; then
            indexer_ip=${indexer_node_ips[i]};
            break
        fi
    done
    indexer_configuration_ips=("${indexer_node_ips[@]}") # I'll take all the ips
    indexer_configuration_names=("${indexer_node_names[@]}") # I'll take all the names

    eval "sed -i 's|node.name:.*|node.name: ${indxname}|' /etc/wazuh-indexer/opensearch.yml ${debug}"
    eval "sed -i 's|network.host:.*|network.host: ${indexer_ip}|' /etc/wazuh-indexer/opensearch.yml ${debug}"
    eval "sed -i '/.*- \"node-.*/d' /etc/wazuh-indexer/opensearch.yml ${debug}"

    # cluster.initial_cluster_manager_nodes configuration
    indexer_master_nodes="cluster.initial_cluster_manager_nodes:\n"
    for node_name in "${indexer_configuration_names[@]}"; do
        indexer_master_nodes+="- \"${node_name}\"\n"
    done
    eval "sed -i 's|cluster.initial_cluster_manager_nodes:.*|${indexer_master_nodes}|' /etc/wazuh-indexer/opensearch.yml ${debug}"

    # seed_hosts configuration
    indexer_seed_hosts="discovery.seed_hosts:\n"
    for ip in "${indexer_configuration_ips[@]}"; do
        indexer_seed_hosts+="  - \"${ip}\"\n"
    done
    eval "sed -i 's|#discovery.seed_hosts:.*|${indexer_seed_hosts}|' /etc/wazuh-indexer/opensearch.yml ${debug}"

    # nodes_dn configuration. The package only lists the DN of this node; the cluster needs
    # every node, in the subject format of the certificates (RFC 2253, as the package writes it).
    eval "sed -i '/^plugins.security.nodes_dn:/,/^[^-]/{/^- /d;}' /etc/wazuh-indexer/opensearch.yml ${debug}"
    indexer_cn_nodes="plugins.security.nodes_dn:\n"
    for node_name in "${indexer_configuration_names[@]}"; do
        indexer_cn_nodes+="- \"C=US,L=California,O=Wazuh,OU=Wazuh,CN=${node_name}\"\n"
    done
    eval "sed -i 's|plugins.security.nodes_dn:.*|${indexer_cn_nodes}|' /etc/wazuh-indexer/opensearch.yml ${debug}"

    jv=$(java -version 2>&1 | grep -o -m1 '1.8.0' )
    if [ "$jv" == "1.8.0" ]; then
        {
        echo "wazuh-indexer hard nproc 4096"
        echo "wazuh-indexer soft nproc 4096"
        echo "wazuh-indexer hard nproc 4096"
        echo "wazuh-indexer soft nproc 4096"
        } >> /etc/security/limits.conf
        echo -ne "\nbootstrap.system_call_filter: false" >> /etc/wazuh-indexer/opensearch.yml
    fi

    common_logger "Wazuh indexer post-install configuration finished."
}
function indexer_copyCertificates() {

    common_logger -d "Placing the Wazuh indexer certificates."
    eval "install -d -o root -g root -m 0750 ${indexer_config_path} ${indexer_cert_path} ${indexer_config_path}/ca ${debug}"
    installCommon_placeFromTar "root-ca.pem" "${indexer_config_path}/ca/root-ca.pem" root root 0640
    installCommon_placeFromTar "${indxname}.pem" "${indexer_cert_path}/indexer.pem" root root 0640
    installCommon_placeFromTar "${indxname}-key.pem" "${indexer_cert_path}/indexer-key.pem" root root 0640
    installCommon_placeFromTar "admin.pem" "${indexer_cert_path}/admin.pem" root root 0600
    installCommon_placeFromTar "admin-key.pem" "${indexer_cert_path}/admin-key.pem" root root 0600

}
function indexer_install() {

    common_logger "Starting Wazuh indexer installation."

    if [ -n "${offline_install}" ]; then
        download_dir="${offline_packages_path}"
    else
        download_dir="${base_path}/${download_packages_directory}"
    fi

    # Find the downloaded package file
    if [ "${sys_type}" == "yum" ]; then
        package_file=$(ls "${download_dir}"/wazuh-indexer*.rpm 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "Wazuh indexer package file not found in ${download_dir}."
            exit 1
        fi
        installCommon_yumInstall "${package_file}"
    elif [ "${sys_type}" == "apt-get" ]; then
        package_file=$(ls "${download_dir}"/wazuh-indexer*.deb 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "Wazuh indexer package file not found in ${download_dir}."
            exit 1
        fi
        installCommon_aptInstall "${package_file}"
    fi

    common_checkInstalled
    if [  "$install_result" != 0  ] || [ -z "${indexer_installed}" ]; then
        common_logger -e "Wazuh indexer installation failed."
        installCommon_rollBack
        exit 1
    else
        common_logger "Wazuh indexer installation finished."
    fi

    eval "sysctl -q -w vm.max_map_count=262144 ${debug}"

}
function indexer_startCluster() {

    common_logger -d "Starting Wazuh indexer cluster."

    # The initializer waits with no output until the cluster elects a cluster manager node,
    # which needs the Wazuh indexer nodes of config.yml to be running.
    local nodes
    nodes=$(printf '%s, ' "${indexer_node_names[@]}")
    nodes="${nodes%, }"
    if [ -n "${start_indexer_cluster}" ]; then
        common_logger "Waiting up to ${indexer_cluster_timeout} seconds for the Wazuh indexer cluster to form. The Wazuh indexer service must be running on these nodes of config.yml: ${nodes}."
    fi

    # The package leaves loading the security configuration to the operator, once, from one
    # indexer node. Its wrapper finds the host, port and admin certificate by itself.
    # timeout runs it in its own process group, out of the reach of Ctrl+C, so it runs in the
    # background: wait returns on Ctrl+C and installCommon_cleanExit stops it.
    ( eval "timeout ${indexer_cluster_timeout} bash ${indexer_security_init} ${debug}; exit \${PIPESTATUS[0]}" ) &
    indexer_init_pid=$!
    wait "${indexer_init_pid}"
    e_code=$?
    indexer_init_pid=""

    if [ "${e_code}" == 124 ]; then
        common_logger -e "The Wazuh indexer cluster did not form in ${indexer_cluster_timeout} seconds."
        if [ -n "${start_indexer_cluster}" ]; then
            common_logger -e "Check that the Wazuh indexer service is running on these nodes of config.yml and that they can reach each other: ${nodes}. Then run the assistant with -s|--start-cluster again."
        fi
    fi
    if [  "${e_code}" != 0  ]; then
        common_logger -e "The Wazuh indexer cluster security configuration could not be initialized."
        installCommon_rollBack
        exit 1
    else
        common_logger "Wazuh indexer cluster security configuration initialized."
    fi

}

# ------------ installCommon.sh ------------ 
function installCommon_cleanExit() {

    rollback_conf=""

    if [ -n "$spin_pid" ]; then
        eval "kill -9 $spin_pid ${debug}"
    fi

    # The Wazuh indexer security initializer runs in its own process group, which Ctrl+C does not reach.
    # It must end before the rollback removes anything.
    if [ -n "${indexer_init_pid}" ]; then
        eval "pkill -INT -P ${indexer_init_pid} ${debug}"
        wait "${indexer_init_pid}"
    fi

    local question="Do you want to remove the ongoing installation?"
    if [ -n "${start_indexer_cluster}" ]; then
        question="Do you want to clean up the ongoing cluster initialization?"
    fi
    until [[ "${rollback_conf}" =~ ^[N|Y|n|y]$ ]]; do
        echo -ne "\n${question}[Y/N]"
        read -r rollback_conf
    done
    if [[ "${rollback_conf}" =~ [N|n] ]]; then
        exit 1
    else
        common_checkInstalled
        installCommon_rollBack
        exit 1
    fi

}
function installCommon_aptInstall() {

    package="${1}"
    version="${2}"
    attempt=0

    # Determine the installer package
    if [[ "${package}" == *.deb ]]; then
        installer="${package}"
    elif [ -n "${version}" ]; then
        installer="${package}${sep}${version}"
    else
        installer="${package}"
    fi

    # Override with offline package if needed
    if [ -n "${offline_install}" ] && [[ "${package}" != *.deb ]]; then
        package_name=$(ls ${offline_packages_path} | grep ${package})
        installer="${offline_packages_path}/${package_name}"
    fi

    # Build the installation command
    command="DEBIAN_FRONTEND=noninteractive apt-get install ${installer} -y -q"

    common_checkAptLock

    if [ "${attempt}" -ne "${max_attempts}" ]; then
        apt_output=$(eval "${command} 2>&1")
        install_result="${PIPESTATUS[0]}"
        eval "echo \"\${apt_output}\" ${debug}"
    fi

}
function installCommon_createCertificates() {

    common_logger -d "Creating Wazuh certificates."
    cert_readConfig

    cert_checkListenerReachability

    cert_tmp_path=""
    cert_createTmpDir

    cert_checkRootCA "create"
    cert_generateAdmincertificate
    cert_generateIndexercertificates
    cert_generateManagercertificates
    cert_generateDashboardcertificates
    cert_cleanFiles
    eval "chmod 400 ${cert_tmp_path}/* ${debug}"
    eval "mv ${cert_tmp_path}/* /tmp/wazuh-install-files ${debug}"
    eval "rm -rf ${cert_tmp_path} ${debug}"
    cert_verifyRemotedcertificates "/tmp/wazuh-install-files"

}
function installCommon_createPasswords() {

    local key value ca_dir

    common_logger -d "Generating the Wazuh passwords."
    if ! ca_dir=$(common_caGetDir 2>/dev/null); then
        common_logger -e "WAZUH_CA_DIR must be an absolute path."
        exit 1
    fi
    for key in "${credential_keys[@]}"; do
        if value=$(common_envGet "${ca_dir}/credentials.env" "${key}") && common_passwordValidate "${value}" 2>/dev/null; then
            continue
        fi
        if ! common_envSet "${ca_dir}/credentials.env" "${key}" "$(common_passwordGenerate)"; then
            common_logger -e "Could not write ${key} to ${ca_dir}/credentials.env."
            exit 1
        fi
    done

    if ! eval "cp '${ca_dir}/credentials.env' /tmp/wazuh-install-files/credentials.env ${debug}"; then
        common_logger -e "Could not copy the credentials file."
        exit 1
    fi
    eval "chmod 600 /tmp/wazuh-install-files/credentials.env ${debug}"

}
function installCommon_getTarCredential() {

    local key="${1}"

    tar -xOf "${tar_file}" wazuh-install-files/credentials.env 2>/dev/null \
        | sed -nE "s/^${key}=\"?([^\"]*)\"?$/\1/p" | tail -n 1

}
function installCommon_placeLocalEnv() {

    local config_path="${1}"
    local env_file="${config_path}/local.env"
    local key value current
    local -A values=()

    shift
    common_logger -d "Writing ${env_file}."
    for key in "$@"; do
        value=$(installCommon_getTarCredential "${key}")
        if [ -z "${value}" ]; then
            common_logger -e "There is no ${key} in the credentials.env file of ${tar_file}."
            exit 1
        fi
        if current=$(common_envGet "${env_file}" "${key}" 2>/dev/null) && [ "${current}" != "${value}" ]; then
            common_logger -e "${key} in ${env_file} is not the one of this deployment. Remove the file, or use the tar file of this deployment."
            exit 1
        fi
        values["${key}"]="${value}"
    done

    eval "install -d -o root -g root -m 0750 '${config_path}' ${debug}"
    for key in "$@"; do
        if ! common_envSet "${env_file}" "${key}" "${values[${key}]}"; then
            common_logger -e "Could not write ${key} to ${env_file}."
            exit 1
        fi
    done

}
function installCommon_placeFromTar() {

    local member="${1}" destination="${2}" owner="${3}" group="${4}" mode="${5}"
    local staged

    staged=$(mktemp)
    if ! tar -xOf "${tar_file}" "wazuh-install-files/${member}" > "${staged}" 2>/dev/null; then
        rm -f "${staged}"
        common_logger -e "Could not extract ${member} from ${tar_file}."
        exit 1
    fi

    if [ -e "${destination}" ]; then
        if cmp -s "${staged}" "${destination}"; then
            rm -f "${staged}"
            common_logger -d "${destination} is already in place."
            return 0
        fi
        rm -f "${staged}"
        common_logger -e "${destination} already exists and is not the one in ${tar_file}. Remove it, or use the tar file of this deployment."
        exit 1
    fi

    if ! install -o "${owner}" -g "${group}" -m "${mode}" "${staged}" "${destination}"; then
        rm -f "${staged}"
        common_logger -e "Could not place ${destination}."
        exit 1
    fi
    rm -f "${staged}"

}
function installCommon_createClusterKey() {

    openssl rand -hex 16 >> "/tmp/wazuh-install-files/clusterkey"

}
function installCommon_createInstallFiles() {

    if [ -d /tmp/wazuh-install-files ]; then
        eval "rm -rf /tmp/wazuh-install-files ${debug}"
    fi

    # mkdir fails if another user creates the directory first. The mode is the one the
    # directory has in the tar.
    eval "mkdir -m 755 /tmp/wazuh-install-files ${debug}; e_code=\${PIPESTATUS[0]}"
    if [ "${e_code}" == 0 ]; then
        common_logger "Generating configuration files."

        if [ -n "${configurations}" ]; then
            cert_checkOpenSSL
        fi
        installCommon_createCertificates
        if [ -n "${manager_node_types[*]}" ]; then
            installCommon_createClusterKey
        fi
        installCommon_createPasswords
        # root-ca.key stays on this host and is never added to the tar.
        eval "cp '${config_file}' '/tmp/wazuh-install-files/config.yml' ${debug}"
        eval "chown root:root /tmp/wazuh-install-files/* ${debug}"
        eval "tar -zcf '${tar_file}' -C '/tmp/' wazuh-install-files/ ${debug}"
        eval "rm -rf '/tmp/wazuh-install-files' ${debug}"
        common_logger "Created ${tar_file_name}. It contains the Wazuh cluster key, the passwords and the certificates necessary for installation. Remove it from every node once the installation finishes."
        common_logger "Keeping ${config_file}: the copy used for installation is the one inside ${tar_file_name}."
        common_logger "The root CA, its private key and the passwords are in $(common_caGetDir). Back them up: they are needed to add nodes or renew certificates."
    else
        common_logger -e "Unable to create /tmp/wazuh-install-files"
        exit 1
    fi
}
function installCommon_determinePorts {

    used_ports=()

    if [ -n "${AIO}" ]; then
        used_ports+=( "${wazuh_aio_ports[@]}" )
    elif [ -n "${wazuh}" ]; then
        used_ports+=( "${wazuh_manager_ports[@]}" )
    elif [ -n "${indexer}" ]; then
        used_ports+=( "${wazuh_indexer_ports[@]}" )
    elif [ -n "${dashboard}" ]; then
        used_ports+=( "${wazuh_dashboard_port[@]}" )
    fi
}
function installCommon_downloadArtifactURLs() {

    common_logger -d "Downloading artifact URLs file."
    if [ -n "${devrepo}" ] && [ "${devrepo}" == "pre-release" ]; then
        artifact_urls_file_name="artifact_urls_${wazuh_version}-${staging_url_stage}.yaml"
        artifact_url="https://${bucket}/pre-release/${wazuh_major}.x/${artifact_urls_bucket_folder}/${artifact_urls_file_name}"
    else
        artifact_urls_file_name="artifact_urls_${wazuh_version}.yaml"
        artifact_url="https://${bucket}/production/${wazuh_major}.x/${artifact_urls_bucket_folder}/${artifact_urls_file_name}"
    fi
    eval "common_curl -sSo ${base_path}/${artifact_urls_file_name} ${artifact_url} --max-time 300 --retry 5 --retry-delay 5 --fail ${debug}; curl_exit_code=\${PIPESTATUS[0]}"

    if [ "${curl_exit_code}" -ne 0 ]; then
        common_logger -e "Failed to download artifact URLs from ${artifact_url}. Exit code: ${curl_exit_code}"
        exit 1
    fi

    if [ ! -f "${artifact_urls_file_name}" ]; then
        common_logger -e "Failed to download artifact URLs from ${artifact_url}."
        exit 1
    fi

}
function installCommon_downloadComponent() {
    if [ -n "${offline_install}" ]; then
        common_logger -d "Skipping download in offline installation mode. Package already available."
        return 0
    fi

    if [ "$#" -ne 1 ]; then
        common_logger -e "installCommon_downloadComponent must be called with one argument (component name)."
        exit 1
    fi

    component="${1}"
    artifact_file="${base_path}/${artifact_urls_file_name}"
    download_dir="${base_path}/${download_packages_directory}"

    # Create download directory if it doesn't exist
    if [ ! -d "${download_dir}" ]; then
        eval "mkdir -m 700 -p ${download_dir} ${debug}"
        if [ ! -d "${download_dir}" ]; then
            common_logger -e "Failed to create download directory: ${download_dir}"
            exit 1
        fi
    fi

    # Packages are checked before installing them, so no other user may be able to replace them in between
    if [ -L "${download_dir}" ] || [ ! -O "${download_dir}" ] || [ -n "$(find "${download_dir}" -maxdepth 0 \( -perm -020 -o -perm -002 \))" ]; then
        common_logger -e "The download directory ${download_dir} must belong to the current user and not be writable by others."
        exit 1
    fi

    # Determine package type based on system
    if [ "${sys_type}" == "yum" ]; then
        pkg_type="rpm"
        # Determine architecture suffix for artifact keys
        if [ "${architecture}" == "x86_64" ]; then
            arch_suffix="x86_64"
        elif [ "${architecture}" == "aarch64" ]; then
            arch_suffix="aarch64"
        fi
    elif [ "${sys_type}" == "apt-get" ]; then
        pkg_type="deb"
        # Determine architecture suffix for artifact keys
        if [ "${architecture}" == "x86_64" ]; then
            arch_suffix="amd64"
        elif [ "${architecture}" == "aarch64" ]; then
            arch_suffix="arm64"
        fi
    fi

    # Build the artifact key
    artifact_key="${component}_${arch_suffix}_${pkg_type}"

    # Get the URL from the artifact file
    component_url=$(grep "^${artifact_key}:" "$artifact_file" | cut -d' ' -f2- | tr -d '"' | xargs)

    # Extract filename from URL (remove query parameters after ?)
    component_filename=$(basename "${component_url%%\?*}")
    component_filepath="${download_dir}/${component_filename}"

    common_logger "Downloading ${component} package: ${component_filename}"

    # Download the component to the download directory
    common_curl -sSLo '${component_filepath}' '${component_url}' --max-time 600 --retry 5 --retry-delay 5 --fail ${debug}
    curl_exit_code="${PIPESTATUS[0]}"

    # Check if download was successful
    if [ "${curl_exit_code}" -ne 0 ]; then
        common_logger -e "Failed to download ${component} from ${component_url}. Curl exit code: ${curl_exit_code}"
        # Remove incomplete file if it exists
        if [ -f "${component_filepath}" ]; then
            common_logger -d "Removing incomplete download: ${component_filepath}"
            eval "rm -f ${component_filepath} ${debug}"
        fi
        exit 1
    fi

    if [ ! -f "${component_filepath}" ]; then
        common_logger -e "Failed to download ${component} from ${component_url}."
        exit 1
    fi

    common_logger "${component} package downloaded successfully: ${component_filepath}"

}
function installCommon_extractConfig() {

    common_logger -d "Extracting Wazuh configuration."
    if ! tar -tf "${tar_file}" | grep -q wazuh-install-files/config.yml; then
        common_logger -e "There is no config.yml file in ${tar_file}."
        exit 1
    fi
    # A new directory with a random name, so no other user can create it or change
    # config.yml before it is read.
    if ! install_tmp_path="$(mktemp -d /tmp/wazuh-install-files.XXXXXXXXXX)"; then
        common_logger -e "Could not create a temporary directory to extract config.yml."
        exit 1
    fi
    eval "tar -xf ${tar_file} -C ${install_tmp_path} wazuh-install-files/config.yml ${debug}"
    config_file="${install_tmp_path}/wazuh-install-files/config.yml"

}
function installCommon_getConfig() {

    if [ "$#" -ne 2 ]; then
        common_logger -e "installCommon_getConfig should be called with two arguments"
        exit 1
    fi

    config_name="config_file_$(eval "echo ${1} | sed 's|/|_|g;s|.yml||'")"
    if [ -z "$(eval "echo \${${config_name}}")" ]; then
        common_logger -e "Unable to find configuration file ${1}. Exiting."
        installCommon_rollBack
        exit 1
    fi
    eval "echo \"\${${config_name}}\"" > "${2}"
}
function installCommon_installCheckDependencies() {

    if [ "${1}" == "assistant" ]; then
        installing_assistant_deps=1
        wia_dependencies_installed=()
        installCommon_installList "${assistant_deps_to_install[@]}"
    else
        installing_assistant_deps=0
        installCommon_installList "${wazuh_deps_to_install[@]}"
    fi
}
function installCommon_installList(){

    dependencies=("$@")
    if [ "${#dependencies[@]}" -gt 0 ]; then

        if [ "${sys_type}" == "apt-get" ]; then
            eval "apt-get update -q ${debug}"
        fi

        common_logger "--- Dependencies ----"
        for dep in "${dependencies[@]}"; do
            common_logger "Installing $dep."
            if [ "${sys_type}" = "apt-get" ]; then
                installCommon_aptInstall "${dep}"
            else
                installCommon_yumInstall "${dep}"
            fi
            if [ "${install_result}" != 0 ]; then
                common_logger -e "Cannot install dependency: ${dep}."
                installCommon_rollBack
                exit 1
            fi
            if [ "${installing_assistant_deps}" == 1 ]; then
                wia_dependencies_installed+=("${dep}")
            fi
        done
    fi

}
function installCommon_logRunHeader() {

    local header
    header="$(date +'%d/%m/%Y %H:%M:%S') INFO: --- Wazuh installation assistant ${wazuh_install_vesion} (Wazuh ${wazuh_version}). Options: ${*:-none} ---"
    if [ -s "${logfile}" ]; then
        header=$'\n'"${header}"
    fi
    { printf "%s\n" "${header}" >> "${logfile}"; } 2>/dev/null || true

}
function installCommon_operationName() {

    if [ -n "${start_indexer_cluster}" ]; then
        echo "cluster initialization"
    else
        echo "installation"
    fi

}
function installCommon_removeCentOSrepositories() {

    eval "rm -f ${centos_repo} ${debug}"
    eval "rm -f ${centos_key} ${debug}"
    eval "yum clean all ${debug}"
    centos_repos_configured=0
    common_logger -d "CentOS repositories and key deleted."

}
function installCommon_rollBack() {

    # The uninstall flow prints the header itself, before checking each component.
    # --start-cluster removes no component.
    if [ -z "${uninstall}" ] && [ -z "${start_indexer_cluster}" ]; then
        common_logger "--- Removing existing Wazuh installation ---"
    fi

    if [[ -n "${wazuh_installed}" && ( -n "${wazuh}" || -n "${AIO}" || -n "${uninstall}" ) ]];then
        common_logger "Removing Wazuh manager."
        if [ "${sys_type}" == "yum" ]; then
            common_checkYumLock
            if [ "${attempt}" -ne "${max_attempts}" ]; then
                eval "yum remove wazuh-manager -y ${debug}"
                rpm -q wazuh-manager --quiet && wazuh_failed_uninstall=1
            fi
        elif [ "${sys_type}" == "apt-get" ]; then
            common_checkAptLock
            eval "apt-get remove --purge wazuh-manager -y ${debug}"
            wazuh_failed_uninstall=$(apt list --installed 2>/dev/null | grep wazuh-manager)
        fi

        if [ -n "${wazuh_failed_uninstall}" ]; then
            common_logger -w "The Wazuh manager package could not be removed."
        else
            common_logger "Wazuh manager removed."
        fi

    fi

    if [[ ( -n "${wazuh_remaining_files}"  || -n "${wazuh_installed}" ) && ( -n "${wazuh}" || -n "${AIO}" || -n "${uninstall}" ) ]]; then
        eval "rm -rf /var/wazuh-manager/ ${debug}"
        eval "rm -rf ${manager_config_path}/ ${debug}"
    fi

    if [[ -n "${indexer_installed}" && ( -n "${indexer}" || -n "${AIO}" || -n "${uninstall}" ) ]]; then
        common_logger "Removing Wazuh indexer."
        if [ "${sys_type}" == "yum" ]; then
            common_checkYumLock
            if [ "${attempt}" -ne "${max_attempts}" ]; then
                eval "yum remove wazuh-indexer -y ${debug}"
                rpm -q wazuh-indexer --quiet && indexer_failed_uninstall=1
            fi
        elif [ "${sys_type}" == "apt-get" ]; then
            common_checkAptLock
            eval "apt-get remove --purge wazuh-indexer -y ${debug}"
            indexer_failed_uninstall=$(apt list --installed 2>/dev/null | grep wazuh-indexer)
        fi

        if [ -n "${indexer_failed_uninstall}" ]; then
            common_logger -w "The Wazuh indexer package could not be removed."
        else
            common_logger "Wazuh indexer removed."
        fi
    fi

    if [[ ( -n "${indexer_remaining_files}" || -n "${indexer_installed}" ) && ( -n "${indexer}" || -n "${AIO}" || -n "${uninstall}" ) ]]; then
        eval "rm -rf /var/lib/wazuh-indexer/ ${debug}"
        eval "rm -rf /usr/share/wazuh-indexer/ ${debug}"
        eval "rm -rf /etc/wazuh-indexer/ ${debug}"
    fi

    if [[ -n "${dashboard_installed}" && ( -n "${dashboard}" || -n "${AIO}" || -n "${uninstall}" ) ]]; then
        common_logger "Removing Wazuh dashboard."
        if [ "${sys_type}" == "yum" ]; then
            common_checkYumLock
            if [ "${attempt}" -ne "${max_attempts}" ]; then
                eval "yum remove wazuh-dashboard -y ${debug}"
                rpm -q wazuh-dashboard --quiet && dashboard_failed_uninstall=1
            fi
        elif [ "${sys_type}" == "apt-get" ]; then
            common_checkAptLock
            eval "apt-get remove --purge wazuh-dashboard -y ${debug}"
            dashboard_failed_uninstall=$(apt list --installed 2>/dev/null | grep wazuh-dashboard)
        fi

        if [ -n "${dashboard_failed_uninstall}" ]; then
            common_logger -w "The Wazuh dashboard package could not be removed."
        else
            common_logger "Wazuh dashboard removed."
        fi
    fi

    if [[ ( -n "${dashboard_remaining_files}" || -n "${dashboard_installed}" ) && ( -n "${dashboard}" || -n "${AIO}" || -n "${uninstall}" ) ]]; then
        eval "rm -rf /var/lib/wazuh-dashboard/ ${debug}"
        eval "rm -rf /usr/share/wazuh-dashboard/ ${debug}"
        eval "rm -rf /etc/wazuh-dashboard/ ${debug}"
        eval "rm -rf /run/wazuh-dashboard/ ${debug}"
    fi

    # Leftovers of the components this run removes. Other components on the host keep theirs.
    elements_to_remove=()
    if [[ -n "${wazuh}" || -n "${AIO}" || -n "${uninstall}" ]]; then
        elements_to_remove+=(   "/etc/systemd/system/multi-user.target.wants/wazuh-manager.service" )
    fi
    if [[ -n "${indexer}" || -n "${AIO}" || -n "${uninstall}" ]]; then
        elements_to_remove+=(   "/var/log/wazuh-indexer/"
                                "/etc/systemd/system/opensearch.service.wants/"
                                "/securityadmin_demo.sh"
                                "/etc/systemd/system/multi-user.target.wants/opensearch.service"
                                "/lib/firewalld/services/opensearch.xml" )
    fi
    if [[ -n "${dashboard}" || -n "${AIO}" || -n "${uninstall}" ]]; then
        elements_to_remove+=(   "/etc/systemd/system/multi-user.target.wants/wazuh-dashboard.service"
                                "/etc/systemd/system/wazuh-dashboard.service"
                                "/lib/firewalld/services/dashboard.xml" )
    fi

    if [ "${#elements_to_remove[@]}" -gt 0 ]; then
        eval "rm -rf ${elements_to_remove[*]} ${debug}"
    fi

    installCommon_removeWIADependencies

    eval "systemctl daemon-reload ${debug}"

    if [ -z "${uninstall}" ]; then
        local operation
        operation=$(installCommon_operationName)
        operation="${operation^}"
        if [ -n "${rollback_conf}" ] || [ -n "${overwrite}" ]; then
            common_logger "${operation} cleaned."
        else
            common_logger "${operation} cleaned. Check the ${logfile} file to learn more about the issue."
        fi
    fi

}
function installCommon_scanDependencies() {

    wazuh_deps=()
    if [ -n "${AIO}" ]; then
        if [ "${sys_type}" == "yum" ]; then
            wazuh_deps+=( "${indexer_yum_dependencies[@]}" "${wazuh_yum_dependencies[@]}" "${dashboard_yum_dependencies[@]}" )
        else
            wazuh_deps+=( "${indexer_apt_dependencies[@]}" "${wazuh_apt_dependencies[@]}" "${dashboard_apt_dependencies[@]}" )
        fi
    elif [ -n "${indexer}" ]; then
        if [ "${sys_type}" == "yum" ]; then
            wazuh_deps+=( "${indexer_yum_dependencies[@]}" )
        else
            wazuh_deps+=( "${indexer_apt_dependencies[@]}" )
        fi
    elif [ -n "${wazuh}" ]; then
        if [ "${sys_type}" == "yum" ]; then
            wazuh_deps+=( "${wazuh_yum_dependencies[@]}" )
        else
            wazuh_deps+=( "${wazuh_apt_dependencies[@]}" )
        fi
    elif [ -n "${dashboard}" ]; then
        if [ "${sys_type}" == "yum" ]; then
            wazuh_deps+=( "${dashboard_yum_dependencies[@]}" )
        else
            wazuh_deps+=( "${dashboard_apt_dependencies[@]}" )
        fi
    fi

    all_deps=( "${wazuh_deps[@]}" )
    if [ "${sys_type}" == "apt-get" ]; then
        assistant_deps+=( "${assistant_apt_dependencies[@]}" )
        command='! apt list --installed 2>/dev/null | grep -q -E ^"${dep}"\/'
    else
        assistant_deps+=( "${assistant_yum_dependencies[@]}" )
        command='! rpm -q ${dep} --quiet'
    fi

    # -g creates the CA with openssl.
    if [ -n "${configurations}" ]; then
        assistant_deps+=( openssl )
    fi

    # Remove openssl dependency if not necessary
    if [ -z "${configurations}" ] && [ -z "${AIO}" ]; then
        assistant_deps=( "${assistant_deps[@]/openssl}" )
    fi

    # Remove lsof dependency if not necessary
    if [ -z "${AIO}" ] && [ -z "${wazuh}" ] && [ -z "${indexer}" ] && [ -z "${dashboard}" ]; then
        assistant_deps=( "${assistant_deps[@]/lsof}" )
    fi

    # Delete duplicates and sort
    all_deps+=( "${assistant_deps[@]}" )
    all_deps=( $(echo "${all_deps[@]}" | tr ' ' '\n' | sort -u) )
    deps_to_install=()

    # Get not installed dependencies of Assistant and Wazuh
    for dep in "${all_deps[@]}"; do
        if eval "${command}"; then
            deps_to_install+=("${dep}")
            if [[ "${assistant_deps[*]}" =~ "${dep}" ]]; then
                assistant_deps_to_install+=("${dep}")
            else
                wazuh_deps_to_install+=("${dep}")
            fi
        fi
    done

    # Format and print the message if the option is not specified
    if [ -z "${install_dependencies}" ] && [ "${#deps_to_install[@]}" -gt 0 ]; then
        printf -v joined_deps_not_installed '%s, ' "${deps_to_install[@]}"
        printf -v joined_assistant_not_installed '%s, ' "${assistant_deps_to_install[@]}"
        joined_deps_not_installed="${joined_deps_not_installed%, }"
        joined_assistant_not_installed="${joined_assistant_not_installed%, }"

        local operation
        operation=$(installCommon_operationName)
        message="To perform the ${operation}, the following package/s must be installed: ${joined_deps_not_installed}."
        if [ "${#assistant_deps_to_install[@]}" -gt 0 ]; then
            message+=" The following package/s will be removed after the ${operation}: ${joined_assistant_not_installed}."
        fi
        message+=" Add the -id|--install-dependencies parameter to install them automatically or install them manually."
        common_logger -e "${message}"
        exit 1
    fi

}
function installCommon_startService() {

    local start_time e_code

    if [ "$#" -ne 1 ]; then
        common_logger -e "installCommon_startService must be called with 1 argument."
        exit 1
    fi

    common_logger "Starting service ${1}."
    start_time=$(date '+%Y-%m-%d %H:%M:%S')

    if [[ -d /run/systemd/system ]]; then
        eval "systemctl daemon-reload ${debug}"
        eval "systemctl enable ${1}.service ${debug}"
        eval "systemctl start ${1}.service ${debug}; e_code=\${PIPESTATUS[0]}"
        # A service can stop right after a successful start command.
        if [ "${e_code}" == 0 ] && ! systemctl is-active --quiet "${1}.service"; then
            e_code=1
        fi
    elif ps -p 1 -o comm= | grep "init"; then
        eval "chkconfig ${1} on ${debug}"
        eval "service ${1} start ${debug}"
        eval "/etc/init.d/${1} start ${debug}; e_code=\${PIPESTATUS[0]}"
    elif [ -x "/etc/rc.d/init.d/${1}" ] ; then
        eval "/etc/rc.d/init.d/${1} start ${debug}; e_code=\${PIPESTATUS[0]}"
    else
        common_logger -e "${1} could not start. No service manager found on the system."
        exit 1
    fi

    if [ "${e_code}" != 0 ]; then
        common_logger -e "${1} could not be started."
        installCommon_printStartProblems "${1}" "${start_time}"
        installCommon_rollBack
        exit 1
    fi
    common_logger "${1} service started."

}
function installCommon_printStartProblems() {

    local service="${1}" start_time="${2}" problems="" line
    local check_command=""

    case "${service}" in
        "wazuh-manager") check_command="${manager_resolver} --check" ;;
        "wazuh-indexer") check_command="/usr/share/wazuh-indexer/bin/resolve-credentials.sh --check" ;;
    esac

    if [ -n "$(command -v journalctl)" ]; then
        eval "journalctl -u ${service} --since '${start_time}' --no-pager >> ${logfile} 2>&1"
        problems=$(journalctl -u "${service}" --since "${start_time}" --no-pager -o cat 2>/dev/null)
    fi
    problems=$(sed -E 's/^resolve-credentials: //' <<< "${problems}" | grep -E '^(MISSING|INVALID|UNSAFE) ')
    if [ -z "${problems}" ] && [ -n "${check_command}" ] && [ -x "${check_command%% *}" ]; then
        problems=$(${check_command} 2>&1 | sed -E 's/^resolve-credentials: //' | grep -E '^(MISSING|INVALID|UNSAFE) ')
    fi
    problems=$(awk '!seen[$0]++' <<< "${problems}")
    if [ -z "${problems}" ]; then
        common_logger -e "Check the ${logfile} file to learn more about the issue."
        return 0
    fi
    common_logger -e "${service} found these problems with its credentials:"
    while IFS= read -r line; do
        common_logger -e "    ${line}"
    done <<< "${problems}"

}
function installCommon_restartService() {

    if [ "$#" -ne 1 ]; then
        common_logger -e "installCommon_restartService must be called with 1 argument."
        exit 1
    fi

    common_logger "Restarting service ${1}."

    if [[ -d /run/systemd/system ]]; then
        eval "systemctl restart ${1}.service ${debug}; e_code=\${PIPESTATUS[0]}"
        if [  "${e_code}" != 0  ]; then
            common_logger -e "${1} could not be restarted."
            if [ -n "$(command -v journalctl)" ]; then
                eval "journalctl -u ${1} >> ${logfile}"
            fi
            installCommon_rollBack
            exit 1
        else
            common_logger "${1} service restarted."
        fi
    elif ps -p 1 -o comm= | grep "init"; then
        eval "chkconfig ${1} on ${debug}"
        eval "service ${1} restart ${debug}"
        eval "/etc/init.d/${1} restart ${debug}; e_code=\${PIPESTATUS[0]}"
        if [  "${e_code}" != 0  ]; then
            common_logger -e "${1} could not be restarted."
            if [ -n "$(command -v journalctl)" ]; then
                eval "journalctl -u ${1} >> ${logfile}"
            fi
            installCommon_rollBack
            exit 1
        else
            common_logger "${1} service restarted."
        fi
    elif [ -x "/etc/rc.d/init.d/${1}" ] ; then
        eval "/etc/rc.d/init.d/${1} restart ${debug}; e_code=\${PIPESTATUS[0]}"
        if [  "${e_code}" != 0  ]; then
            common_logger -e "${1} could not be restarted."
            if [ -n "$(command -v journalctl)" ]; then
                eval "journalctl -u ${1} >> ${logfile}"
            fi
            installCommon_rollBack
            exit 1
        else
            common_logger "${1} service restarted."
        fi
    else
        common_logger -e "${1} could not restart. No service manager found on the system."
        exit 1
    fi

}
function installCommon_removeWIADependencies() {

    if [ "${sys_type}" == "yum" ]; then
        installCommon_yumRemoveWIADependencies
    elif [ "${sys_type}" == "apt-get" ]; then
        installCommon_aptRemoveWIADependencies
    fi

}
function installCommon_yumRemoveWIADependencies(){

    if [ "${#wia_dependencies_installed[@]}" -gt 0 ]; then
        common_logger "--- Dependencies ---"
        local wazuh_deps=()

        if [ -n "${AIO}" ]; then
            wazuh_deps=( $(echo "${wazuh_yum_dependencies[@]}" "${indexer_yum_dependencies[@]}" "${dashboard_yum_dependencies[@]}" | tr ' ' '\n' | sort -u) )
        else
            if [ -n "${wazuh}" ]; then
                wazuh_deps+=( "${wazuh_yum_dependencies[@]}" )
            fi
            if [ -n "${indexer}" ]; then
                wazuh_deps+=( "${indexer_yum_dependencies[@]}" )
            fi
            if [ -n "${dashboard}" ]; then
                wazuh_deps+=( "${dashboard_yum_dependencies[@]}" )
            fi

            if [ "${#wazuh_deps[@]}" -gt 0 ]; then
                mapfile -t wazuh_deps < <(printf '%s\n' "${wazuh_deps[@]}" | sort -u)
            fi
        fi

        for dep in "${wia_dependencies_installed[@]}"; do
            if [ "${dep}" != "systemd" ] && [ "${dep}" != "util-linux" ]; then
                if [[ " ${wazuh_deps[*]} " == *" ${dep} "* ]]; then
                    common_logger -d "Skipping removal of ${dep}: it is also a Wazuh component dependency."
                    continue
                fi
                common_logger "Removing $dep."
                yum_output=$(yum remove ${dep} -y 2>&1)
                yum_code="${PIPESTATUS[0]}"

                eval "echo \"\${yum_output}\" ${debug}"
                if [  "${yum_code}" != 0  ]; then
                    common_logger -e "Cannot remove dependency: ${dep}."
                    exit 1
                fi
            fi
        done
    fi

}
function installCommon_aptRemoveWIADependencies(){

    if [ "${#wia_dependencies_installed[@]}" -gt 0 ]; then
        common_logger "--- Dependencies ----"
        local wazuh_deps=()

        if [ -n "${AIO}" ]; then
            wazuh_deps=( $(echo "${wazuh_apt_dependencies[@]}" "${indexer_apt_dependencies[@]}" "${dashboard_apt_dependencies[@]}" | tr ' ' '\n' | sort -u) )
        else
            if [ -n "${wazuh}" ]; then
                wazuh_deps+=( "${wazuh_apt_dependencies[@]}" )
            fi
            if [ -n "${indexer}" ]; then
                wazuh_deps+=( "${indexer_apt_dependencies[@]}" )
            fi
            if [ -n "${dashboard}" ]; then
                wazuh_deps+=( "${dashboard_apt_dependencies[@]}" )
            fi

            if [ "${#wazuh_deps[@]}" -gt 0 ]; then
                mapfile -t wazuh_deps < <(printf '%s\n' "${wazuh_deps[@]}" | sort -u)
            fi
        fi

        for dep in "${wia_dependencies_installed[@]}"; do
            if [ "${dep}" != "systemd" ] && [ "${dep}" != "util-linux" ]; then
                if [[ " ${wazuh_deps[*]} " == *" ${dep} "* ]]; then
                    common_logger -d "Skipping removal of ${dep}: it is also a Wazuh component dependency."
                    continue
                fi
                common_logger "Removing $dep."
                apt_output=$(apt-get remove --purge ${dep} -y 2>&1)
                apt_code="${PIPESTATUS[0]}"

                eval "echo \"\${apt_output}\" ${debug}"
                if [  "${apt_code}" != 0  ]; then
                    common_logger -e "Cannot remove dependency: ${dep}."
                    exit 1
                fi
            fi
        done
    fi

}
function installCommon_removeDownloadPackagesDirectory() {

    if [ -n "${offline_install}" ]; then
        common_logger -d "Skipping removal of download packages directory in offline installation mode."
        return 0
    fi

    download_dir="${base_path}/${download_packages_directory}"
    if [ -d "${download_dir}" ]; then
        eval "rm -rf ${download_dir} ${debug}"
        common_logger -d "Removed download packages directory: ${download_dir}"
    else
        common_logger -w "Download packages directory does not exist: ${download_dir}"
    fi

}
function installCommon_getPackages() {

    components=()
    if [ -n "${AIO}" ] || [ -n "${indexer}" ]; then
        components+=("wazuh_indexer")
    fi
    if [ -n "${AIO}" ] || [ -n "${wazuh}" ]; then
        components+=("wazuh_manager")
    fi
    if [ -n "${AIO}" ] || [ -n "${dashboard}" ]; then
        components+=("wazuh_dashboard")
    fi

    if [ -n "${offline_install}" ]; then
        packages_dir="${offline_packages_path}"
    else
        packages_dir="${base_path}/${download_packages_directory}"
    fi
    if [ "${sys_type}" == "yum" ]; then
        package_extension="rpm"
    else
        package_extension="deb"
    fi

    for component in "${components[@]}"; do
        installCommon_downloadComponent "${component}"
    done
    for component in "${components[@]}"; do
        package_file=$(ls "${packages_dir}/${component//_/-}"*."${package_extension}" 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "The ${component//_/ } package was not found in ${packages_dir}."
            installCommon_rollBack
            exit 1
        fi
        installCommon_verifyPackageSignature "${package_file}"
    done

}
function installCommon_verifyPackageSignature() {

    package_file="${1}"
    if [ -n "${skip_signature_check}" ]; then
        common_logger -w "Skipping the signature check of ${package_file}. Use it only with development packages."
        return 0
    fi

    common_logger -d "Checking the signature of ${package_file}."
    verify_dir=$(mktemp -d)
    installCommon_getWazuhGPGKey "${verify_dir}"
    if [[ "${package_file}" == *.rpm ]]; then
        installCommon_verifyRpmSignature "${package_file}"
    elif [[ "${package_file}" == *.deb ]]; then
        installCommon_verifyDebSignature "${package_file}"
    fi
    rm -rf "${verify_dir}"
    common_logger "Signature of $(basename "${package_file}") verified with the Wazuh GPG key ${key_fingerprint}."

}
function installCommon_getWazuhGPGKey() {

    key_dir="${1}"
    if [ -n "${offline_install}" ]; then
        eval "cp ${base_path}/wazuh-offline/GPG-KEY-WAZUH ${key_dir}/wazuh.asc ${debug}"
    else
        eval "common_curl -sSo ${key_dir}/wazuh.asc ${wazuh_gpg_key_url} --max-time 300 --retry 5 --retry-delay 5 --fail ${debug}"
    fi
    if [ ! -s "${key_dir}/wazuh.asc" ]; then
        installCommon_signatureCheckFailed "Could not get the Wazuh GPG key."
    fi
    # rpm --import would also take a second key appended to the file.
    if [ "$(grep -c -- '-----BEGIN PGP PUBLIC KEY BLOCK-----' "${key_dir}/wazuh.asc")" -ne 1 ]; then
        installCommon_signatureCheckFailed "The Wazuh GPG key file must hold a single key."
    fi

    # The armored body is the base64 of the binary keyring that gpgv reads.
    sed '1,/^$/d; /^=/,$d; /^-----/d' "${key_dir}/wazuh.asc" | base64 -d > "${key_dir}/wazuh.gpg" 2>/dev/null
    key_fingerprint=$(installCommon_getGPGKeyFingerprint "${key_dir}/wazuh.gpg")
    if [ -z "${key_fingerprint}" ] || [[ " ${wazuh_gpg_key_fingerprints[*]} " != *" ${key_fingerprint} "* ]]; then
        installCommon_signatureCheckFailed "The Wazuh GPG key does not have the expected fingerprint."
    fi

}
function installCommon_getGPGKeyFingerprint() {

    key_bytes=( $(od -An -v -tu1 "${1}") )
    packet_start=0
    primary_keys=0
    primary_fingerprint=""
    while [ "${packet_start}" -lt "${#key_bytes[@]}" ]; do
        packet_tag_byte=${key_bytes[packet_start]}
        if (( (packet_tag_byte & 0x80) == 0 )); then
            return 0
        elif (( packet_tag_byte & 0x40 )); then
            packet_tag=$(( packet_tag_byte & 0x3f ))
            length_byte=${key_bytes[packet_start + 1]}
            if (( length_byte < 192 )); then
                packet_length=${length_byte}; header_length=2
            elif (( length_byte < 224 )); then
                packet_length=$(( ((length_byte - 192) << 8) + key_bytes[packet_start + 2] + 192 )); header_length=3
            elif (( length_byte == 255 )); then
                packet_length=$(( (key_bytes[packet_start + 2] << 24) + (key_bytes[packet_start + 3] << 16) + (key_bytes[packet_start + 4] << 8) + key_bytes[packet_start + 5] )); header_length=6
            else
                return 0
            fi
        else
            packet_tag=$(( (packet_tag_byte >> 2) & 0x0f ))
            case $(( packet_tag_byte & 3 )) in
                0) packet_length=${key_bytes[packet_start + 1]}; header_length=2 ;;
                1) packet_length=$(( (key_bytes[packet_start + 1] << 8) + key_bytes[packet_start + 2] )); header_length=3 ;;
                2) packet_length=$(( (key_bytes[packet_start + 1] << 24) + (key_bytes[packet_start + 2] << 16) + (key_bytes[packet_start + 3] << 8) + key_bytes[packet_start + 4] )); header_length=5 ;;
                *) return 0 ;;
            esac
        fi
        if [ "${packet_tag}" -eq 6 ]; then
            primary_keys=$(( primary_keys + 1 ))
            primary_fingerprint=$( { printf "\\x99\\x$(printf %02x $(( packet_length >> 8 )))\\x$(printf %02x $(( packet_length & 255 )))"; tail -c +$(( packet_start + header_length + 1 )) "${1}" | head -c "${packet_length}"; } | sha1sum | awk '{print toupper($1)}')
        fi
        packet_start=$(( packet_start + header_length + packet_length ))
    done
    if [ "${primary_keys}" -eq 1 ]; then
        echo "${primary_fingerprint}"
    fi

}
function installCommon_verifyRpmSignature() {

    package_file="${1}"
    key_fingerprint=$(installCommon_getGPGKeyFingerprint "${verify_dir}/wazuh.gpg")
    key_id="${key_fingerprint: -16}"
    if ! rpm -q "gpg-pubkey-${key_id: -8}" --quiet; then
        eval "rpm --import ${verify_dir}/wazuh.asc ${debug}"
    fi

    signature=$(rpm -qp --qf '%{RSAHEADER:pgpsig}' "${package_file}" 2>/dev/null)
    if [[ "${signature,,}" != *"key id ${key_id,,}"* ]]; then
        installCommon_signatureCheckFailed "${package_file} is not signed with the Wazuh key."
    fi
    if ! rpm -K "${package_file}" >/dev/null 2>&1; then
        installCommon_signatureCheckFailed "The signature of ${package_file} is not valid."
    fi

}
function installCommon_verifyDebSignature() {

    package_file="${1}"
    package_size=$(stat -c %s "${package_file}")
    offset=8
    touch "${verify_dir}/members"
    while [ "${offset}" -lt "${package_size}" ]; do
        member_header=$(dd if="${package_file}" bs=1 skip="${offset}" count=60 2>/dev/null)
        member_name=$(echo "${member_header:0:16}" | sed 's/[ /]*$//')
        member_size=$(echo "${member_header:48:10}" | tr -d ' ')
        member_start=$((offset + 61))
        if [ "${member_name}" == "_gpgbuilder" ]; then
            tail -c +"${member_start}" "${package_file}" | head -c "${member_size}" > "${verify_dir}/_gpgbuilder"
        elif [[ "${member_name}" != _gpg* ]]; then
            member_sha1=$(tail -c +"${member_start}" "${package_file}" | head -c "${member_size}" | sha1sum | awk '{print $1}')
            echo "${member_sha1} ${member_size} ${member_name}" >> "${verify_dir}/members"
        fi
        offset=$((offset + 60 + member_size + member_size % 2))
    done

    if [ ! -s "${verify_dir}/_gpgbuilder" ]; then
        installCommon_signatureCheckFailed "${package_file} is not signed."
    fi
    if ! gpgv --keyring "${verify_dir}/wazuh.gpg" --output "${verify_dir}/signed" "${verify_dir}/_gpgbuilder" >/dev/null 2>&1; then
        installCommon_signatureCheckFailed "${package_file} is not signed with the Wazuh key."
    fi
    # Signed lines: <md5> <sha1> <size> <member>
    signed_members=$(awk 'NF == 4 && length($1) == 32 && $1 ~ /^[0-9a-f]+$/ {print $2, $3, $4}' "${verify_dir}/signed" | sort)
    if [ -z "${signed_members}" ] || [ "${signed_members}" != "$(sort "${verify_dir}/members")" ]; then
        installCommon_signatureCheckFailed "The contents of ${package_file} do not match its signature."
    fi

}
function installCommon_signatureCheckFailed() {

    common_logger -e "${1}"
    common_logger -e "Wazuh packages are signed. Use --skip-signature-check along with -d only for unsigned development packages."
    rm -rf "${verify_dir}"
    installCommon_rollBack
    exit 1

}
function installCommon_yumInstall() {

    package="${1}"
    version="${2}"
    install_result=1

    # If package is a file path (contains .rpm), install directly
    if [[ "${package}" == *.rpm ]]; then
        installer="${package}"
        command="rpm -ivh ${installer}"
        common_logger -d "Installing local package: ${installer}"
    elif [ -n "${version}" ]; then
        installer="${package}-${version}"
        # Offline installation case: get package name and install it
        if [ -n "${offline_install}" ]; then
            package_name=$(ls ${offline_packages_path} | grep ${package})
            installer="${offline_packages_path}/${package_name}"
            command="rpm -ivh ${installer}"
            common_logger -d "Installing local package: ${installer}"
        else
            command="yum install ${installer} -y"
        fi
    else
        installer="${package}"
        # Offline installation case: get package name and install it
        if [ -n "${offline_install}" ]; then
            package_name=$(ls ${offline_packages_path} | grep ${package})
            installer="${offline_packages_path}/${package_name}"
            command="rpm -ivh ${installer}"
            common_logger -d "Installing local package: ${installer}"
        else
            command="yum install ${installer} -y"
        fi
    fi
    common_checkYumLock

    if [ "${attempt}" -ne "${max_attempts}" ]; then
        yum_output=$(eval "${command} 2>&1")
        install_result="${PIPESTATUS[0]}"
        eval "echo \"\${yum_output}\" ${debug}"
    fi

}
function installCommon_checkAptLock() {

    attempt=0
    seconds=30
    max_attempts=10

    while fuser "${apt_lockfile}" >/dev/null 2>&1 && [ "${attempt}" -lt "${max_attempts}" ]; do
        attempt=$((attempt+1))
        common_logger "Another process is using APT. Waiting for it to release the lock. Next retry in ${seconds} seconds (${attempt}/${max_attempts})"
        sleep "${seconds}"
    done

}

# ------------ installMain.sh ------------ 
function getHelp() {

    echo -e ""
    echo -e "NAME"
    echo -e "        $(basename "$0") - Install and configure Wazuh central components: Wazuh manager, Wazuh indexer, and Wazuh dashboard."
    echo -e ""
    echo -e "SYNOPSIS"
    echo -e "        $(basename "$0") [OPTIONS] -a | -s | -wi <indexer-node-name> | -wd <dashboard-node-name> | -wm <manager-node-name>"
    echo -e ""
    echo -e "DESCRIPTION"
    echo -e "        -a,  --all-in-one"
    echo -e "                Install and configure Wazuh manager, Wazuh indexer, Wazuh dashboard."
    echo -e ""
    echo -e "        -as, --agent-san <ip|dns>"
    echo -e "                Adds an extra address to the subject alternative name of the agent"
    echo -e "                listener certificate of every Wazuh manager node. Repeat it for more"
    echo -e "                than one. Use it for the address agents dial when the host cannot know"
    echo -e "                it: a load balancer shared by a cluster, a published name, a NAT"
    echo -e "                address. Must be used along with one of these options: -a, -g"
    echo -e ""
    echo -e "        -ap, --api-san <ip|dns>"
    echo -e "                Adds an extra address to the subject alternative name of the Server"
    echo -e "                API certificate of every Wazuh manager node. Repeat it for more than"
    echo -e "                one. Use it for the address API clients dial when the host cannot know"
    echo -e "                it: a load balancer, a published name, a NAT address. With -a, the"
    echo -e "                Server API also listens on all network interfaces instead of only"
    echo -e "                on the local host."
    echo -e "                Must be used along with one of these options: -a, -g"
    echo -e ""
    echo -e "        -d [pre-release|local],  --development"
    echo -e "                Use development repositories. By default it uses the pre-release package repository. If local is specified, it will use a local artifact_urls.yaml file located in the same directory as this script."
    echo -e ""
    echo -e "        --skip-signature-check"
    echo -e "                Install packages that are not signed with the Wazuh key. Only for development packages, must be used along with -d."
    echo -e ""
    echo -e "        -dw,  --download-wazuh <deb|rpm>"
    echo -e "                Download all the packages necessary for offline installation. Type of packages to download for offline installation (rpm, deb)"
    echo -e ""
    echo -e "        -da,  --download-arch <amd64|arm64|x86_64|aarch64>"
    echo -e "                Define the architecture of the packages to download for offline installation."
    echo -e ""
    echo -e "        -g,  --generate-config-files"
    echo -e "                Generate wazuh-install-files.tar file containing the files that will be needed for installation from config.yml. In distributed deployments you will need to copy this file to all hosts."
    echo -e ""
    echo -e "        -h,  --help"
    echo -e "                Display this help and exit."
    echo -e ""
    echo -e "        -id,  --install-dependencies"
    echo -e "                Installs automatically the necessary dependencies for the installation."
    echo -e ""
    echo -e "        -o,  --overwrite"
    echo -e "                Overwrites previously installed components. This will erase all the existing configuration and data."
    echo -e ""
    echo -e "        -of,  --offline-installation"
    echo -e "                Perform an offline installation. This option must be used with -a, -wm, -s, -wi, or -wd."
    echo -e ""
    echo -e "        -s,  --start-cluster"
    echo -e "                Initialize Wazuh indexer cluster security settings."
    echo -e ""
    echo -e "        -u,  --uninstall"
    echo -e "                Uninstalls all Wazuh components. This will erase all the existing configuration and data."
    echo -e ""
    echo -e "        -v,  --verbose"
    echo -e "                Shows the complete installation output."
    echo -e ""
    echo -e "        -V,  --version"
    echo -e "                Shows the version of the script and Wazuh packages."
    echo -e ""
    echo -e "        -wd,  --wazuh-dashboard <dashboard-node-name>"
    echo -e "                Install and configure Wazuh dashboard, used for distributed deployments."
    echo -e ""
    echo -e "        -wi,  --wazuh-indexer <indexer-node-name>"
    echo -e "                Install and configure Wazuh indexer, used for distributed deployments."
    echo -e ""
    echo -e "        -wm,  --wazuh-manager <manager-node-name>"
    echo -e "                Install and configure Wazuh manager, used for distributed deployments."
    exit "${1:-1}"

}
function main() {
    umask 177

    if [ -z "${1}" ]; then
        getHelp
    fi

    installCommon_logRunHeader "$@"

    declare -a agent_san=()
    declare -a api_san=()

    while [ -n "${1}" ]
    do
        case "${1}" in
            "-a"|"--all-in-one")
                AIO=1
                shift 1
                ;;
            "-as"|"--agent-san")
                if [[ -z "${2}" || "${2}" == -* ]]; then
                    common_logger -e "Error on arguments. Probably missing <ip|dns> after -as|--agent-san"
                    getHelp
                    exit 1
                else
                    agent_san+=("${2}")
                    shift 2
                fi
                ;;
            "-ap"|"--api-san")
                if [[ -z "${2}" || "${2}" == -* ]]; then
                    common_logger -e "Error on arguments. Probably missing <ip|dns> after -ap|--api-san"
                    getHelp
                    exit 1
                else
                    api_san+=("${2}")
                    shift 2
                fi
                ;;
            "-d"|"--development")
                development=1
                if [ -z "${2}" ] || [[ "${2}" == -* ]]; then
                    devrepo="pre-release"
                    bucket="packages-staging.xdrsiem.wazuh.info"
                    shift 1
                elif [ "${2}" = "pre-release" ] || [ "${2}" = "local" ]; then
                    devrepo="${2}"
                    bucket="packages-staging.xdrsiem.wazuh.info"
                    shift 2
                else
                    common_logger -e "Error: Invalid value '${2}' after -d|--development. Accepted values are 'pre-release' or 'local'. If no value is specified, 'pre-release' is used by default."
                    getHelp
                    exit 1
                fi
                ;;
            "-g"|"--generate-config-files")
                configurations=1
                shift 1
                ;;
            "-h"|"--help")
                getHelp 0
                ;;
            "-id"|"--install-dependencies")
                install_dependencies=1
                shift 1
                ;;
            "-o"|"--overwrite")
                overwrite=1
                shift 1
                ;;
            "--skip-signature-check")
                skip_signature_check=1
                shift 1
                ;;
            "-of"|"--offline-installation")
                offline_install=1
                shift 1
                ;;
            "-s"|"--start-cluster")
                start_indexer_cluster=1
                shift 1
                ;;
            "-u"|"--uninstall")
                uninstall=1
                shift 1
                ;;
            "-v"|"--verbose")
                debugEnabled=1
                debug="2>&1 | tee -a ${logfile}"
                shift 1
                ;;
            "-V"|"--version")
                showVersion=1
                shift 1
                ;;
            "-wd"|"--wazuh-dashboard")
                if [ -z "${2}" ]; then
                    common_logger -e "Error on arguments. Probably missing <node-name> after -wd|---wazuh-dashboard"
                    getHelp
                    exit 1
                fi
                dashboard=1
                dashname="${2}"
                shift 2
                ;;
            "-wi"|"--wazuh-indexer")
                if [ -z "${2}" ]; then
                    common_logger -e "Arguments contain errors. Probably missing <node-name> after -wi|--wazuh-indexer."
                    getHelp
                    exit 1
                fi
                indexer=1
                indxname="${2}"
                shift 2
                ;;
            "-wm"|"--wazuh-manager")
                if [ -z "${2}" ]; then
                    common_logger -e "Error on arguments. Probably missing <node-name> after -wm|--wazuh-manager"
                    getHelp
                    exit 1
                fi
                wazuh=1
                winame="${2}"
                shift 2
                ;;
            "-dw"|"--download-wazuh")
                if [ "${2}" != "deb" ] && [ "${2}" != "rpm" ]; then
                    common_logger -e "Error on arguments. Probably missing <deb|rpm> after -dw|--download-wazuh"
                    getHelp
                    exit 1
                fi
                download=1
                package_type="${2}"
                shift 2
                ;;
            "-da"|"--download-arch")
                if [ "${2}" != "amd64" ] && [ "${2}" != "x86_64" ] && [ "${2}" != "arm64" ] && [ "${2}" != "aarch64" ]; then
                    common_logger -e "Error on arguments. Probably missing <amd64|x86_64|arm64|aarch64> after -da|--download-arch"
                    getHelp
                    exit 1
                fi
                download_arch=1
                arch="${2}"
                shift 2
                ;;
            *)
                echo "Unknown option: ${1}"
                getHelp
        esac
    done

    if [ -z "${download}" ] && [ -z "${showVersion}" ]; then
        common_checkRoot
    fi

    if [ -n "${showVersion}" ]; then
        common_logger "Wazuh version: ${wazuh_version}"
        common_logger "Wazuh installation assistant version: ${wazuh_install_vesion}"
        exit 0
    fi

    common_logger "Starting Wazuh installation assistant. Wazuh version: ${wazuh_version}"
    common_logger "Verbose logging redirected to ${logfile}"

# -------------- Uninstall case  ------------------------------------

    common_checkSystem
    if [ -n "${AIO}" ] || [ -n "${indexer}" ] || [ -n "${dashboard}" ] || [ -n "${wazuh}" ]; then
        common_logger "Checking for an existing Wazuh installation."
    fi
    common_checkInstalled
    checks_arguments
    cert_validateAgentSan
    cert_validateApiSan
    check_dist

    if [ -z "${uninstall}" ] && [ -z "${offline_install}" ]; then
        installCommon_scanDependencies
        installCommon_installCheckDependencies "assistant"
        installCommon_installCheckDependencies "wazuh"
        installCommon_determinePorts
    elif [ -n "${offline_install}" ]; then
        offline_checkPrerequisites "wia_offline_dependencies" "${wia_offline_dependencies[@]}"
    fi

    if [ -n "${uninstall}" ]; then
        common_logger "--- Removing existing Wazuh installation ---"
        checks_uninstallComponents && uninstall_found=1
        installCommon_rollBack
        if [ -n "${uninstall_found}" ]; then
            common_logger "Uninstall finished."
        else
            common_logger "Nothing to uninstall."
        fi
        exit 0
    fi

    checks_arch

    common_logger "Verifying that your system meets the recommended minimum hardware requirements."
    checks_health


# -------------- Preliminary checks and Prerequisites --------------------------------

    if [ -z "${configurations}" ] && [ -z "${AIO}" ] && [ -z "${download}" ]; then
        checks_previousCertificate
    fi

    if [ -n "${AIO}" ]; then
        if [ -z "${offline_install}" ]; then
            rm -f "${tar_file}"
        fi
        checks_ports "${wazuh_aio_ports[@]}"
        if [ -n "${offline_install}" ]; then
            offline_checkPrerequisites "AIO"
        fi
    fi

    if [ -n "${indexer}" ]; then
        checks_ports "${wazuh_indexer_ports[@]}"
        if [ -n "${offline_install}" ]; then
            offline_checkPrerequisites "indexer"
        fi
    fi

    if [ -n "${wazuh}" ]; then
        checks_ports "${wazuh_manager_ports[@]}"
        if [ -n "${offline_install}" ]; then
            offline_checkPrerequisites "wazuh"
        fi
    fi

    if [ -n "${dashboard}" ]; then
        checks_ports "${wazuh_dashboard_port}"
        if [ -n "${offline_install}" ]; then
            offline_checkPrerequisites "dashboard"
        fi
    fi


# --------------  Wazuh repo  ----------------------

    # Offline installation case: extract the compressed files
    if [ -n "${offline_install}" ]; then
        offline_checkPreinstallation
        offline_extractFiles
    elif [ -n "${AIO}" ] || [ -n "${indexer}" ] || [ -n "${dashboard}" ] || [ -n "${wazuh}" ]; then
        check_curlVersion
        if [ -n "${development}" ] && [ "${devrepo}" = "local" ]; then
            checks_localArtifactURLs_exists
        else
            installCommon_downloadArtifactURLs
        fi
        checks_ArtifactURLs_format
        checks_ArtifactURLs_component_present
    fi

# -------------- Configuration creation case  -----------------------

    # Creation certificate case: only -g creates certificates. On an all-in-one install
    # the packages create the CA, their certificates and their passwords.
    if [ -z "${offline_install}" ] && [ -n "${configurations}" ]; then
        common_logger "--- Configuration files ---"
        installCommon_createInstallFiles
    fi

    if [ -z "${configurations}" ] && [ -z "${AIO}" ] && [ -z "${download}" ]; then
        installCommon_extractConfig
        cert_readConfig
    fi

    # Distributed architecture: node names must be different
    if [[ -z "${AIO}" && -z "${download}" && ( -n "${indexer}"  || -n "${dashboard}" || -n "${wazuh}" ) ]]; then
        checks_names
    fi

    if [ -n "${configurations}" ]; then
        installCommon_removeWIADependencies
    fi

# -------------- Packages ------------------------------------------

    # Every package is downloaded and its signature checked before any of them is installed.
    if [ -n "${AIO}" ] || [ -n "${indexer}" ] || [ -n "${dashboard}" ] || [ -n "${wazuh}" ]; then
        installCommon_getPackages
    fi

# -------------- Wazuh indexer case -------------------------------

    if [ -n "${indexer}" ]; then
        common_logger "--- Wazuh indexer ---"
        installCommon_placeLocalEnv "${indexer_config_path}" "${indexer_credential_keys[@]}"
        indexer_copyCertificates
        indexer_install
        indexer_configure
        installCommon_startService "wazuh-indexer"
        installCommon_removeWIADependencies
    fi

# -------------- Start Wazuh indexer cluster case  ------------------

    if [ -n "${start_indexer_cluster}" ]; then
        indexer_startCluster
        installCommon_removeWIADependencies
    fi

# -------------- Wazuh dashboard case  ------------------------------

    if [ -n "${dashboard}" ]; then
        common_logger "--- Wazuh dashboard ----"
        installCommon_placeLocalEnv "${dashboard_config_path}" "${dashboard_credential_keys[@]}"
        dashboard_copyCertificates
        dashboard_install
        dashboard_configure
        installCommon_startService "wazuh-dashboard"
        dashboard_initialize
        installCommon_removeWIADependencies

    fi

# -------------- Wazuh manager case  ---------------------------------------

    if [ -n "${wazuh}" ]; then
        common_logger "--- Wazuh manager ---"
        installCommon_placeLocalEnv "${manager_config_path}" "${manager_credential_keys[@]}"
        manager_copyCertificates
        manager_install
        manager_configure
        if [ -n "${manager_node_types[*]}" ]; then
            manager_startCluster
        fi
        installCommon_startService "wazuh-manager"
        installCommon_removeWIADependencies
    fi

# -------------- AIO case  ------------------------------------------

    if [ -n "${AIO}" ]; then

        common_logger "--- Wazuh indexer ---"
        indexer_install
        indexer_configure
        installCommon_startService "wazuh-indexer"
        indexer_startCluster
        common_logger "--- Wazuh manager ---"
        manager_setRemotedSans
        manager_setApidSans
        manager_install
        manager_setApiLoopback
        installCommon_startService "wazuh-manager"
        common_logger "--- Wazuh dashboard ---"
        dashboard_install
        installCommon_startService "wazuh-dashboard"
        dashboard_initialize
        installCommon_removeWIADependencies

    fi

# -------------- Offline case  ------------------------------------------

    if [ -n "${download}" ]; then
        common_logger "--- Download Packages ---"
        if [ -n "${development}" ] && [ "${devrepo}" = "local" ]; then
            checks_localArtifactURLs_exists
        else
            installCommon_downloadArtifactURLs
        fi
        checks_ArtifactURLs_format
        offline_checkArtifactURLs_component_present
        offline_download
    fi


# -------------------------------------------------------------------

    if [ -n "${install_tmp_path}" ]; then
        eval "rm -rf ${install_tmp_path} ${debug}"
    fi

    if [ -n "${AIO}" ] || [ -n "${indexer}" ] || [ -n "${dashboard}" ] || [ -n "${wazuh}" ]; then
        installCommon_removeDownloadPackagesDirectory
        common_logger "Installation finished."
    elif [ -n "${start_indexer_cluster}" ]; then
        common_logger "Wazuh indexer cluster started."
    fi

}

# ------------ manager.sh ------------ 
function manager_startCluster() {

    common_logger -d "Starting Wazuh manager cluster."
    for i in "${!manager_node_names[@]}"; do
        if [[ "${manager_node_names[i]}" == "${winame}" ]]; then
            pos="${i}";
        fi
    done

    for i in "${!manager_node_types[@]}"; do
        if [[ "${manager_node_types[i],,}" == "master" ]]; then
            master_address=${manager_node_ips[i]}
        fi
    done

    key=$(tar -axf "${tar_file}" wazuh-install-files/clusterkey -O)
    bind_address="0.0.0.0"
    port="1516"
    hidden="no"
    disabled="no"
    lstart=$(grep -n "<cluster>" "${manager_conf_file}" | cut -d : -f 1)
    lend=$(grep -n "</cluster>" "${manager_conf_file}" | cut -d : -f 1)

    sed -i -e "${lstart},${lend}s/<name>.*<\/name>/<name>wazuh_cluster<\/name>/" \
        -e "${lstart},${lend}s/<node_name>.*<\/node_name>/<node_name>${winame}<\/node_name>/" \
        -e "${lstart},${lend}s/<node_type>.*<\/node_type>/<node_type>${manager_node_types[pos],,}<\/node_type>/" \
        -e "${lstart},${lend}s/<key>.*<\/key>/<key>${key}<\/key>/" \
        -e "${lstart},${lend}s/<port>.*<\/port>/<port>${port}<\/port>/" \
        -e "${lstart},${lend}s/<bind_addr>.*<\/bind_addr>/<bind_addr>${bind_address}<\/bind_addr>/" \
        -e "${lstart},${lend}s/<node>.*<\/node>/<node>${master_address}<\/node>/" \
        -e "${lstart},${lend}s/<hidden>.*<\/hidden>/<hidden>${hidden}<\/hidden>/" \
        -e "${lstart},${lend}s/<disabled>.*<\/disabled>/<disabled>${disabled}<\/disabled>/" \
        "${manager_conf_file}"

}
function manager_configure(){

    common_logger -d "Configuring Wazuh manager."

    # The pairs were placed before the package created the wazuh-manager group. On RPM
    # the package resolver runs before the package fixes their owner, so it is run again
    # once the owner is right. --upgrade never creates a CA or issues a pair.
    eval "chown root:wazuh-manager ${manager_cert_path} ${manager_cert_path}/*.pem ${debug}"
    eval "chmod 750 ${manager_cert_path} ${debug}"
    eval "chmod 640 ${manager_cert_path}/*.pem ${debug}"
    common_logger -d "Running the Wazuh manager credentials resolver again now that ${manager_cert_path} belongs to wazuh-manager."
    eval "${manager_resolver} --upgrade -H /var/wazuh-manager ${debug}"

    for i in "${!indexer_node_ips[@]}"; do
        if [ $i -eq 0 ]; then
            eval "sed -i 's/<host>.*<\/host>/<host>https:\/\/${indexer_node_ips[0]}:9200<\/host>/g' ${manager_conf_file} ${debug}"
        else
            sed -i "/<hosts>/a\      <host>https://${indexer_node_ips[$i]}:9200</host>" "${manager_conf_file}"
        fi
    done

}
function manager_setRemotedSans() {

    local san value
    local -a sans=()

    if [ "${#agent_san[@]}" -eq 0 ]; then
        return 0
    fi

    while IFS= read -r san; do
        [ -n "${san}" ] || continue
        if cert_isIP "${san}"; then
            sans+=("IP:${san}")
        else
            sans+=("DNS:${san}")
        fi
    done < <(printf '%s\n' "${agent_san[@]}"; cert_hostAddresses)

    value=$(printf '%s\n' "${sans[@]}" | awk '!seen[$0]++' | paste -sd, -)
    manager_setLocalEnv WAZUH_MANAGER_REMOTED_CERT_SANS "${value}"
    common_logger -d "Agent listener addresses: ${value}"

}
function manager_setApidSans() {

    local san value
    local -a sans=()

    if [ "${#api_san[@]}" -eq 0 ]; then
        return 0
    fi

    while IFS= read -r san; do
        [ -n "${san}" ] || continue
        if cert_isIP "${san}"; then
            sans+=("IP:${san}")
        else
            sans+=("DNS:${san}")
        fi
    done < <(printf '%s\n' "${api_san[@]}"; cert_hostAddresses)

    value=$(printf '%s\n' "${sans[@]}" | awk '!seen[$0]++' | paste -sd, -)
    manager_setLocalEnv WAZUH_MANAGER_APID_CERT_SANS "${value}"
    common_logger -d "Server API addresses: ${value}"

}
function manager_setApiLoopback() {

    if [ "${#api_san[@]}" -ne 0 ]; then
        common_logger -d "Server API addresses given with -ap|--api-san: the Wazuh server API keeps listening on all network interfaces."
        return 0
    fi

    common_logger -d "Setting the Wazuh server API to listen on 127.0.0.1 and ::1 only."
    eval "sed -i -E \"s|^(# )?host:.*|host: ['127.0.0.1', '::1']|\" ${manager_api_conf_file} ${debug}"
    if ! grep -qx "host: \['127.0.0.1', '::1'\]" "${manager_api_conf_file}" 2>/dev/null; then
        common_logger -w "Could not set the host option in ${manager_api_conf_file}. The Wazuh server API listens on all network interfaces."
    fi

}
function manager_setLocalEnv() {

    eval "install -d -o root -g root -m 0750 ${manager_config_path} ${debug}"
    if ! common_envSet "${manager_config_path}/local.env" "${1}" "${2}"; then
        common_logger -e "Could not write ${1} to ${manager_config_path}/local.env."
        exit 1
    fi

}
function manager_install() {

    common_logger "Starting the Wazuh manager installation."

    if [ -n "${offline_install}" ]; then
        download_dir="${offline_packages_path}"
    else
        download_dir="${base_path}/${download_packages_directory}"
    fi

    # Find the downloaded package file
    if [ "${sys_type}" == "yum" ]; then
        package_file=$(ls "${download_dir}"/wazuh-manager*.rpm 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "Wazuh manager package file not found in ${download_dir}."
            exit 1
        fi
        installCommon_yumInstall "${package_file}"
    elif [ "${sys_type}" == "apt-get" ]; then
        package_file=$(ls "${download_dir}"/wazuh-manager*.deb 2>/dev/null | head -n 1)
        if [ -z "${package_file}" ]; then
            common_logger -e "Wazuh manager package file not found in ${download_dir}."
            exit 1
        fi
        installCommon_aptInstall "${package_file}"
    fi

    common_checkInstalled
    if [  "$install_result" != 0  ] || [ -z "${wazuh_installed}" ]; then
        common_logger -e "Wazuh installation failed."
        installCommon_rollBack
        exit 1
    else
        common_logger "Wazuh manager installation finished."
    fi
}
function manager_copyCertificates() {

    common_logger -d "Placing the Wazuh manager certificates."
    eval "install -d -o root -g root -m 0750 ${manager_config_path} ${manager_cert_path} ${debug}"
    eval "install -d -o root -g root -m 0700 ${manager_config_path}/ca ${debug}"
    installCommon_placeFromTar "root-ca.pem" "${manager_config_path}/ca/root-ca.pem" root root 0644
    installCommon_placeFromTar "root-ca.pem" "${manager_cert_path}/root-ca.pem" root root 0640
    installCommon_placeFromTar "root-ca.pem" "${manager_cert_path}/indexer-ca.pem" root root 0640
    installCommon_placeFromTar "${winame}.pem" "${manager_cert_path}/indexer-connector.pem" root root 0640
    installCommon_placeFromTar "${winame}-key.pem" "${manager_cert_path}/indexer-connector-key.pem" root root 0640
    installCommon_placeFromTar "${winame}-remoted.pem" "${manager_cert_path}/remoted.pem" root root 0640
    installCommon_placeFromTar "${winame}-remoted-key.pem" "${manager_cert_path}/remoted-key.pem" root root 0640
    installCommon_placeFromTar "${winame}-apid.pem" "${manager_cert_path}/apid.pem" root root 0640
    installCommon_placeFromTar "${winame}-apid-key.pem" "${manager_cert_path}/apid-key.pem" root root 0640

}

# ------------ wazuh-offline-download.sh ------------ 
function offline_checkArtifactURLs_component_present() {
    common_logger -d "Checking required components are present in ${artifact_urls_file_name} file for download."
    common_logger -d "Architecture: ${arch}"
    common_logger -d "Package type: ${package_type}"
    artifact_file="${base_path}/${artifact_urls_file_name}"

    # Validate supported architectures
    case "${arch}" in
      x86_64|amd64|aarch64|arm64)
        ;;
      *)
        common_logger -e "Unsupported architecture: ${arch}"
        exit 1
        ;;
    esac

    indexer_key="wazuh_indexer_${arch}_${package_type}"
    dashboard_key="wazuh_dashboard_${arch}_${package_type}"
    manager_key="wazuh_manager_${arch}_${package_type}"

    # Check that all required artifacts exist for offline download
    missing_keys=()

    if ! grep -q "^${indexer_key}:" "$artifact_file"; then
        missing_keys+=("${indexer_key}")
    fi

    if ! grep -q "^${dashboard_key}:" "$artifact_file"; then
        missing_keys+=("${dashboard_key}")
    fi

    if ! grep -q "^${manager_key}:" "$artifact_file"; then
        missing_keys+=("${manager_key}")
    fi

    if [ "${#missing_keys[@]}" -gt 0 ]; then
        common_logger -e "Missing required artifact keys in ${artifact_urls_file_name}:"
        for key in "${missing_keys[@]}"; do
            common_logger -e "  - ${key}"
        done
        exit 1
    fi

    common_logger -d "All required artifact keys found for offline download."
}
function offline_download() {

  common_logger "Starting Wazuh packages download."
  common_logger "Downloading Wazuh ${package_type} packages for ${arch}."
  dest_path="${base_dest_folder}/wazuh-packages"

  if [ -d "${dest_path}" ]; then
    eval "rm -f ${dest_path}/* ${debug}" # Clean folder before downloading specific versions
    eval "chmod 700 ${dest_path} ${debug}"
  else
    eval "mkdir -m700 -p ${dest_path} ${debug}" # Create folder if it does not exist
  fi

    # Validate supported architectures
    case "${arch}" in
      x86_64|amd64|aarch64|arm64)
        ;;
      *)
        common_logger -e "Unsupported architecture: ${arch}"
        exit 1
        ;;
    esac

  artifact_file="${base_path}/${artifact_urls_file_name}"

  # Define components to download
  components=("wazuh_indexer" "wazuh_dashboard" "wazuh_manager")

  for component in "${components[@]}"; do
    # Build the artifact key
    artifact_key="${component}_${arch}_${package_type}"

    # Get the URL from the artifact file
    component_url=$(grep "^${artifact_key}:" "$artifact_file" | cut -d' ' -f2- | tr -d '"' | xargs)

    if [ -z "${component_url}" ]; then
      common_logger -e "Could not find URL for ${artifact_key} in ${artifact_urls_file_name}"
      exit 1
    fi

    # Extract filename from URL (remove query parameters after ?)
    component_filename=$(basename "${component_url%%\?*}")
    component_filepath="${dest_path}/${component_filename}"

    common_logger "Downloading ${component} package: ${component_filename}"

    # Download the component to the destination directory
    common_curl -sSLo '${component_filepath}' '${component_url}' --max-time 300 --retry 5 --retry-delay 5 --fail ${debug}

    if [ ! -f "${component_filepath}" ]; then
      common_logger -e "Failed to download ${component} from ${component_url}."
      exit 1
    fi

    common_logger "The ${component} package was downloaded."
  done


  # The installation checks the packages against this key, so it is checked here too.
  key_dir=$(mktemp -d)
  installCommon_getWazuhGPGKey "${key_dir}"
  eval "cp ${key_dir}/wazuh.asc ${base_dest_folder}/GPG-KEY-WAZUH ${debug}"
  rm -rf "${key_dir}"

  eval "chmod 500 ${base_dest_folder} ${debug}"

  common_logger "Creating wazuh-offline.tar.gz with all packages."

  eval "tar -czf ${base_dest_folder}.tar.gz ${base_dest_folder} ${debug}"
  eval "chmod -R 700 ${base_dest_folder} && rm -rf ${base_dest_folder} ${debug}"

  common_logger "The packages are in ${base_dest_folder}.tar.gz"
  common_logger "You can follow the installation guide here https://documentation.wazuh.com/${wazuh_version%.*}/deployment-options/offline-installation/index.html"
}

# ------------ wazuh-offline-installation.sh ------------ 
function offline_checkPrerequisites(){

    if [ "$1" == "wia_offline_dependencies" ]; then
        dependencies=( "${@:2}" )
        common_logger "Checking dependencies for Wazuh installation assistant."
    else
        common_logger "Checking prerequisites for Offline installation."
        if [ "${sys_type}" == "yum" ]; then
            if [ "$1" == "AIO" ]; then
                dependencies=( $(echo "${wazuh_yum_dependencies[@]}" "${indexer_yum_dependencies[@]}" "${dashboard_yum_dependencies[@]}" | tr ' ' '\n' | sort -u) )
            elif [ "$1" == "indexer" ]; then
                dependencies=( "${indexer_yum_dependencies[@]}" )
            elif [ "$1" == "dashboard" ]; then
                dependencies=( "${dashboard_yum_dependencies[@]}" )
            elif [ "$1" == "wazuh" ]; then
                dependencies=( "${wazuh_yum_dependencies[@]}" )
            fi
        elif [ "${sys_type}" == "apt-get" ]; then
            if [ "$1" == "AIO" ]; then
                dependencies=( $(echo "${wazuh_apt_dependencies[@]}" "${indexer_apt_dependencies[@]}" "${dashboard_apt_dependencies[@]}" | tr ' ' '\n' | sort -u) )
            elif [ "$1" == "indexer" ]; then
                dependencies=( "${indexer_apt_dependencies[@]}" )
            elif [ "$1" == "dashboard" ]; then
                dependencies=( "${dashboard_apt_dependencies[@]}" )
            elif [ "$1" == "wazuh" ]; then
                dependencies=( "${wazuh_apt_dependencies[@]}" )
            fi
        fi
    fi

    for dep in "${dependencies[@]}"; do
        if [ "${sys_type}" == "yum" ]; then
            eval "yum list installed 2>/dev/null | grep -q -E ^"${dep}"\\."
        elif [ "${sys_type}" == "apt-get" ]; then
            eval "apt list --installed 2>/dev/null | grep -q -E ^"${dep}"\/"
        fi

        if [ "${PIPESTATUS[0]}" != 0 ]; then
            common_logger -e "${dep} is necessary for the offline installation."
            exit 1
        fi
    done
    if [ "$1" == "wia_offline_dependencies" ]; then
        common_logger -d "Dependencies for Wazuh installation assistant are installed."
    else
        common_logger -d "Prerequisites for Offline installation are installed."
    fi
}
function offline_checkPreinstallation() {

    offline_tarfile="${base_dest_folder}.tar.gz"
    common_logger "Checking ${offline_tarfile} file."
    if [ ! -f "${base_path}/${offline_tarfile}" ]; then
        common_logger -e "The ${offline_tarfile} file was not found in ${base_path}."
        exit 1
    fi
    common_logger -d "${offline_tarfile} was found correctly."

}
function offline_extractFiles() {

    common_logger -d "Extracting files from ${offline_tarfile}"
    if [ ! -d "${base_path}/wazuh-offline/" ]; then
        eval "tar -xzf ${offline_tarfile} ${debug}"

        if [ ! -d "${base_path}/wazuh-offline/" ]; then
            common_logger -e "The ${offline_tarfile} file could not be decompressed."
            exit 1
        fi
    fi

    offline_packages_path="${base_path}/wazuh-offline/wazuh-packages"
    required_files=("${base_path}/wazuh-offline/GPG-KEY-WAZUH")

    if [ "${sys_type}" == "apt-get" ]; then
        required_files+=("${offline_packages_path}/wazuh-dashboard*.deb" "${offline_packages_path}/wazuh-indexer*.deb" "${offline_packages_path}/wazuh-manager*.deb")
    elif [ "${sys_type}" == "yum" ]; then
        required_files+=("${offline_packages_path}/wazuh-dashboard*.rpm" "${offline_packages_path}/wazuh-indexer*.rpm" "${offline_packages_path}/wazuh-manager*.rpm")
    fi

    for file in "${required_files[@]}"; do
        if ! compgen -G "${file}" > /dev/null; then
            common_logger -e "Missing necessary offline file: ${file}"
            exit 1
        fi
    done

    common_logger -d "Offline files extracted successfully."
}

function dist_detect() {


DIST_NAME="Linux"
DIST_VER="0"
DIST_SUBVER="0"

if [ -r "/etc/os-release" ]; then
    . /etc/os-release
    DIST_NAME=$ID
    DIST_VER=$(echo $VERSION_ID | sed -rn 's/[^0-9]*([0-9]+).*/\1/p')
    if [ "X$DIST_VER" = "X" ]; then
        DIST_VER="0"
    fi
    if [ "$DIST_NAME" = "amzn" ] && [ "$DIST_VER" = "2018" ]; then
        DIST_VER="1"
    fi
    DIST_SUBVER=$(echo $VERSION_ID | sed -rn 's/[^0-9]*[0-9]+\.([0-9]+).*/\1/p')
    if [ "X$DIST_SUBVER" = "X" ]; then
        DIST_SUBVER="0"
    fi
fi

if [ ! -r "/etc/os-release" ] || [ "$DIST_NAME" = "centos" ]; then
    # CentOS
    if [ -r "/etc/centos-release" ]; then
        DIST_NAME="centos"
        DIST_VER=`sed -rn 's/.* ([0-9]{1,2})\.*[0-9]{0,2}.*/\1/p' /etc/centos-release`
        DIST_SUBVER=`sed -rn 's/.* [0-9]{1,2}\.*([0-9]{0,2}).*/\1/p' /etc/centos-release`

    # Fedora
    elif [ -r "/etc/fedora-release" ]; then
        DIST_NAME="fedora"
        DIST_VER=`sed -rn 's/.* ([0-9]{1,2}) .*/\1/p' /etc/fedora-release`

    # RedHat
    elif [ -r "/etc/redhat-release" ]; then
        if grep -q "CentOS" /etc/redhat-release; then
            DIST_NAME="centos"
        else
            DIST_NAME="rhel"
        fi
        DIST_VER=`sed -rn 's/.* ([0-9]{1,2})\.*[0-9]{0,2}.*/\1/p' /etc/redhat-release`
        DIST_SUBVER=`sed -rn 's/.* [0-9]{1,2}\.*([0-9]{0,2}).*/\1/p' /etc/redhat-release`

    # Ubuntu
    elif [ -r "/etc/lsb-release" ]; then
        . /etc/lsb-release
        DIST_NAME="ubuntu"
        DIST_VER=$(echo $DISTRIB_RELEASE | sed -rn 's/.*([0-9][0-9])\.[0-9][0-9].*/\1/p')
        DIST_SUBVER=$(echo $DISTRIB_RELEASE | sed -rn 's/.*[0-9][0-9]\.([0-9][0-9]).*/\1/p')

    # Gentoo
    elif [ -r "/etc/gentoo-release" ]; then
        DIST_NAME="gentoo"
        DIST_VER=`sed -rn 's/.* ([0-9]{1,2})\.[0-9]{1,2}.*/\1/p' /etc/gentoo-release`
        DIST_SUBVER=`sed -rn 's/.* [0-9]{1,2}\.([0-9]{1,2}).*/\1/p' /etc/gentoo-release`

    # SuSE
    elif [ -r "/etc/SuSE-release" ]; then
        DIST_NAME="suse"
        DIST_VER=`sed -rn 's/.*VERSION = ([0-9]{1,2}).*/\1/p' /etc/SuSE-release`
        DIST_SUBVER=`sed -rn 's/.*PATCHLEVEL = ([0-9]{1,2}).*/\1/p' /etc/SuSE-release`
        if [ "$DIST_SUBVER" = "" ]; then #openSuse
          DIST_SUBVER=`sed -rn 's/.*VERSION = ([0-9]{1,2})\.([0-9]{1,2}).*/\1/p' /etc/SuSE-release`
        fi

    # Arch
    elif [ -r "/etc/arch-release" ]; then
        DIST_NAME="arch"
        DIST_VER=$(uname -r | sed -rn 's/[^0-9]*([0-9]+).*/\1/p')
        DIST_SUBVER=$(uname -r | sed -rn 's/[^0-9]*[0-9]+\.([0-9]+).*/\1/p')

    # Debian
    elif [ -r "/etc/debian_version" ]; then
        DIST_NAME="debian"
        DIST_VER=`sed -rn 's/[^0-9]*([0-9]+).*/\1/p' /etc/debian_version`
        DIST_SUBVER=`sed -rn 's/[^0-9]*[0-9]+\.([0-9]+).*/\1/p' /etc/debian_version`

    # Slackware
    elif [ -r "/etc/slackware-version" ]; then
        DIST_NAME="slackware"
        DIST_VER=`sed -rn 's/.* ([0-9]{1,2})\.[0-9].*/\1/p' /etc/slackware-version`
        DIST_SUBVER=`sed -rn 's/.* [0-9]{1,2}\.([0-9]).*/\1/p' /etc/slackware-version`

    # Darwin
    elif [ "$(uname)" = "Darwin" ]; then
        DIST_NAME="darwin"
        DIST_VER=$(uname -r | sed -En 's/[^0-9]*([0-9]+).*/\1/p')
        DIST_SUBVER=$(uname -r | sed -En 's/[^0-9]*[0-9]+\.([0-9]+).*/\1/p')

    # Solaris / SunOS
    elif [ "$(uname)" = "SunOS" ]; then
        DIST_NAME="sunos"
        DIST_VER=$(uname -r | cut -d\. -f1)
        DIST_SUBVER=$(uname -r | cut -d\. -f2)

    # HP-UX
    elif [ "$(uname)" = "HP-UX" ]; then
        DIST_NAME="HP-UX"
        DIST_VER=$(uname -r | cut -d\. -f2)
        DIST_SUBVER=$(uname -r | cut -d\. -f3)

    # AIX
    elif [ "$(uname)" = "AIX" ]; then
        DIST_NAME="AIX"
        DIST_VER=$(oslevel | cut -d\. -f1)
        DIST_SUBVER=$(oslevel | cut -d\. -f2)

    # BSD
    elif [ "X$(uname)" = "XOpenBSD" -o "X$(uname)" = "XNetBSD" -o "X$(uname)" = "XFreeBSD" -o "X$(uname)" = "XDragonFly" ]; then
        DIST_NAME="bsd"
        DIST_VER=$(uname -r | sed -rn 's/[^0-9]*([0-9]+).*/\1/p')
        DIST_SUBVER=$(uname -r | sed -rn 's/[^0-9]*[0-9]+\.([0-9]+).*/\1/p')

    elif [ "X$(uname)" = "XLinux" ]; then
        DIST_NAME="Linux"

    fi
    if [ "X$DIST_SUBVER" = "X" ]; then
        DIST_SUBVER="0"
    fi
fi
}
function common_checkAptLock() {

    attempt=0
    seconds=30
    max_attempts=10

    while fuser "${apt_lockfile}" >/dev/null 2>&1 && [ "${attempt}" -lt "${max_attempts}" ]; do
        attempt=$((attempt+1))
        common_logger "Another process is using APT. Waiting for it to release the lock. Next retry in ${seconds} seconds (${attempt}/${max_attempts})"
        sleep "${seconds}"
    done

}
function common_logger() {

    now=$(date +'%d/%m/%Y %H:%M:%S')
    mtype="INFO:"
    debugLogger=
    nolog=
    if [ -n "${1}" ]; then
        while [ -n "${1}" ]; do
            case ${1} in
                "-e")
                    mtype="ERROR:"
                    shift 1
                    ;;
                "-w")
                    mtype="WARNING:"
                    shift 1
                    ;;
                "-d")
                    debugLogger=1
                    mtype="DEBUG:"
                    shift 1
                    ;;
                "-nl")
                    nolog=1
                    shift 1
                    ;;
                *)
                    message="${1}"
                    shift 1
                    ;;
            esac
        done
    fi

    if [ -z "${debugLogger}" ] || { [ -n "${debugLogger}" ] && [ -n "${debugEnabled}" ]; }; then
        if [ -z "${nolog}" ] && { [ "$EUID" -eq 0 ] || [[ -n "${cert_tool_script_name}" && "$(basename "$0")" =~ ${cert_tool_script_name} ]]; }; then
            printf "%s\n" "${now} ${mtype} ${message}" | tee -a ${logfile}
        else
            printf "%b\n" "${now} ${mtype} ${message}"
        fi
    fi

}
function common_checkRoot() {

    common_logger -d "Checking root permissions."
    if [ "$EUID" -ne 0 ]; then
        echo "This script must be run as root."
        exit 1;
    fi

}
function common_checkInstalled() {

    common_logger -d "Checking Wazuh installation."
    wazuh_installed=""
    indexer_installed=""
    dashboard_installed=""

    if [ "${sys_type}" == "yum" ]; then
        rpm -q wazuh-manager --quiet && wazuh_installed=1
    elif [ "${sys_type}" == "apt-get" ]; then
        common_isDebInstalled wazuh-manager && wazuh_installed=1
    fi

    if [ -d "/var/wazuh-manager" ]; then
        common_logger -d "There are Wazuh remaining files."
        wazuh_remaining_files=1
    fi

    if [ "${sys_type}" == "yum" ]; then
        rpm -q wazuh-indexer --quiet && indexer_installed=1

    elif [ "${sys_type}" == "apt-get" ]; then
        common_isDebInstalled wazuh-indexer && indexer_installed=1
    fi

    if [ -d "/var/lib/wazuh-indexer/" ] || [ -d "/usr/share/wazuh-indexer" ] || [ -d "/etc/wazuh-indexer" ] || [ -f "${base_path}/search-guard-tlstool*" ]; then
        common_logger -d "There are Wazuh indexer remaining files."
        indexer_remaining_files=1
    fi

    if [ "${sys_type}" == "yum" ]; then
        eval "rpm -q wazuh-dashboard --quiet && dashboard_installed=1"
    elif [ "${sys_type}" == "apt-get" ]; then
        common_isDebInstalled wazuh-dashboard && dashboard_installed=1
    fi

    if [ -d "/var/lib/wazuh-dashboard/" ] || [ -d "/usr/share/wazuh-dashboard" ] || [ -d "/etc/wazuh-dashboard" ] || [ -d "/run/wazuh-dashboard/" ]; then
        common_logger -d "There are Wazuh dashboard remaining files."
        dashboard_remaining_files=1
    fi

}
function common_isDebInstalled() {

    local status
    status=$(dpkg-query -W -f='${db:Status-Status}' "${1}" 2>/dev/null)
    [ -n "${status}" ] && [ "${status}" != "not-installed" ] && [ "${status}" != "config-files" ]

}
function common_checkSystem() {

    if [ -n "$(command -v yum)" ]; then
        sys_type="yum"
        sep="-"
        common_logger -d "YUM package manager will be used."
    elif [ -n "$(command -v apt-get)" ]; then
        sys_type="apt-get"
        sep="="
        common_logger -d "APT package manager will be used."
    else
        common_logger -e "Couldn't find YUM or APT package manager. Try installing the one corresponding to your operating system and then, launch the installation assistant again."
        exit 1
    fi

}
function common_checkWazuhConfigYaml() {

    common_logger -d "Checking Wazuh YAML configuration file."
    filecorrect=$(cert_parseYaml "${config_file}" | grep -Ev '^#|^\s*$' | grep -Pzc "\A(\s*(nodes_indexer__name|nodes_indexer__ip|nodes_manager__name|nodes_manager__ip|nodes_manager__node_type|nodes_dashboard__name|nodes_dashboard__ip)=.*?)+\Z")
    if [[ "${filecorrect}" -ne 1 ]]; then
        common_logger -e "The configuration file ${config_file} does not have a correct format."
        exit 1
    fi

}
function common_curl() {

    if [ -n "${curl_has_connrefused}" ]; then
        eval "curl --retry-connrefused $@; e_code=\${PIPESTATUS[0]}"
    else
        retries=0
        eval "curl $@; e_code=\${PIPESTATUS[0]}"
        while [ "${e_code}" -eq 7 ] && [ "${retries}" -ne 12 ]; do
            retries=$((retries+1))
            sleep 5
            eval "curl $@; e_code=\${PIPESTATUS[0]}"
        done
    fi
    return "${e_code}"

}
function common_checkYumLock() {

    attempt=0
    seconds=30
    max_attempts=10

    while [ -f "${yum_lockfile}" ] && [ "${attempt}" -lt "${max_attempts}" ]; do
        attempt=$((attempt+1))
        common_logger "Another process is using YUM. Waiting for it to release the lock. Next retry in ${seconds} seconds (${attempt}/${max_attempts})"
        sleep "${seconds}"
    done

}
function common_envGet() {

    local file="${1}" key="${2}" value

    if [ ! -e "${file}" ] && [ ! -L "${file}" ]; then
        return 1
    fi
    if [ -L "${file}" ] || [ ! -f "${file}" ] || [ "$(stat -c %u "${file}")" != "0" ]; then
        echo "${file} must be a regular file owned by root." >&2
        return 2
    fi

    value=$(sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*//p" "${file}" | tail -n 1)
    if ! grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${file}"; then
        return 1
    fi
    value="${value%"${value##*[![:space:]]}"}"
    if [[ "${value}" =~ ^\"(.*)\"$ ]] || [[ "${value}" =~ ^\'(.*)\'$ ]]; then
        value="${BASH_REMATCH[1]}"
    fi
    printf '%s\n' "${value}"

}
function common_envSet() {

    local file="${1}" key="${2}" value="${3}" tmp

    if [ -L "${file}" ] || { [ -e "${file}" ] && [ ! -f "${file}" ]; }; then
        echo "${file} must be a regular file." >&2
        return 1
    fi
    if ! tmp=$(mktemp "${file}.XXXXXX"); then
        return 1
    fi
    if [ -f "${file}" ] && grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${file}"; then
        KEY="${key}" VALUE="${value}" awk '
            $0 ~ "^[[:space:]]*" ENVIRON["KEY"] "[[:space:]]*=" { if (!done) print ENVIRON["KEY"] "=" ENVIRON["VALUE"]; done = 1; next }
            { print }
        ' "${file}" > "${tmp}"
    else
        [ -f "${file}" ] && cat "${file}" > "${tmp}"
        printf '%s=%s\n' "${key}" "${value}" >> "${tmp}"
    fi
    if ! chown root:root "${tmp}" || ! chmod 600 "${tmp}" || ! mv -f "${tmp}" "${file}"; then
        rm -f "${tmp}"
        return 1
    fi

}
function common_passwordGenerate() {

    local password=""

    until common_passwordValidate "${password}" 2>/dev/null; do
        password=$(LC_ALL=C tr -dc 'A-Za-z0-9.,_+:@%^=~-' < /dev/urandom 2>/dev/null | head -c 32)
    done
    printf '%s\n' "${password}"

}
function common_passwordValidate() {

    local password="${1}"

    if [[ "${password}" =~ [^ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,_+:@%^=~-] ]]; then
        echo "password must only contain characters from A-Z a-z 0-9 . , _ + : @ % ^ = ~ -" >&2
        return 1
    fi
    if [ "${#password}" -lt 12 ] || [ "${#password}" -gt 64 ]; then
        echo "password must contain between 12 and 64 characters" >&2
        return 1
    fi
    if [[ ! "${password}" =~ [abcdefghijklmnopqrstuvwxyz] ]]; then
        echo "password must contain at least one lowercase letter" >&2
        return 1
    fi
    if [[ ! "${password}" =~ [ABCDEFGHIJKLMNOPQRSTUVWXYZ] ]]; then
        echo "password must contain at least one uppercase letter" >&2
        return 1
    fi
    if [[ ! "${password}" =~ [0123456789] ]]; then
        echo "password must contain at least one digit" >&2
        return 1
    fi
    if [[ ! "${password}" =~ [.,_+:@%^=~-] ]]; then
        echo "password must contain at least one symbol from . , _ + : @ % ^ = ~ -" >&2
        return 1
    fi

}
function common_caGetDir() {

    local dir="${WAZUH_CA_DIR:-${base_path}/wazuh-ca}"

    if [[ "${dir}" != /* ]]; then
        echo "WAZUH_CA_DIR must be an absolute path." >&2
        return 1
    fi
    printf '%s\n' "${dir}"

}
function common_caValidate() {

    local dir="${1}"
    local cert="${dir}/root-ca.pem" key="${dir}/root-ca.key"

    if [ -L "${dir}" ] || [ ! -d "${dir}" ]; then
        echo "${dir} is not a directory." >&2
        return 1
    fi
    if [ ! -f "${cert}" ]; then
        echo "There is no root-ca.pem in ${dir}." >&2
        return 1
    fi
    if ! openssl x509 -in "${cert}" -noout -checkend 0 > /dev/null 2>&1; then
        echo "${cert} is not a valid or current certificate." >&2
        return 1
    fi
    if ! openssl x509 -in "${cert}" -noout -text 2>/dev/null | grep -q 'CA:TRUE'; then
        echo "${cert} is not a CA certificate." >&2
        return 1
    fi
    if ! openssl verify -CAfile "${cert}" "${cert}" > /dev/null 2>&1; then
        echo "${cert} is not a self-signed CA." >&2
        return 1
    fi
    if [ ! -e "${key}" ]; then
        return 0
    fi
    if [ "$(openssl x509 -in "${cert}" -noout -pubkey 2>/dev/null)" != "$(openssl pkey -in "${key}" -pubout 2>/dev/null)" ]; then
        echo "root-ca.key does not match root-ca.pem in ${dir}." >&2
        return 1
    fi

}
function common_caEnsure() {

    local dir="${1}" tmp

    if [ -e "${dir}/root-ca.pem" ] || [ -e "${dir}/root-ca.key" ]; then
        if [ ! -e "${dir}/root-ca.pem" ]; then
            echo "root-ca.key exists without root-ca.pem in ${dir}." >&2
            return 1
        fi
        common_caValidate "${dir}"
        return $?
    fi

    if ! (umask 077 && mkdir -p "${dir}") || [ -L "${dir}" ]; then
        echo "Could not create ${dir}." >&2
        return 1
    fi
    chown root:root "${dir}" && chmod 700 "${dir}" || return 1

    tmp=$(mktemp -d "${dir}/.root-ca.XXXXXX") || return 1
    printf '%s\n' '[req]' 'distinguished_name = dn' 'x509_extensions = v3_ca' 'prompt = no' \
        '[dn]' 'OU = Wazuh' 'O = Wazuh' 'L = California' \
        '[v3_ca]' 'basicConstraints = critical, CA:TRUE' 'keyUsage = critical, keyCertSign, cRLSign' \
        'subjectKeyIdentifier = hash' 'authorityKeyIdentifier = keyid:always' > "${tmp}/openssl.cnf"
    if ! (umask 077 && openssl req -x509 -new -nodes -newkey rsa:2048 -sha256 -days 3650 -batch \
        -config "${tmp}/openssl.cnf" -keyout "${tmp}/root-ca.key" -out "${tmp}/root-ca.pem" > /dev/null 2>&1); then
        rm -rf "${tmp}"
        echo "OpenSSL could not create the root CA." >&2
        return 1
    fi
    chmod 400 "${tmp}/root-ca.key" && chmod 644 "${tmp}/root-ca.pem" \
        && mv "${tmp}/root-ca.key" "${dir}/root-ca.key" && mv "${tmp}/root-ca.pem" "${dir}/root-ca.pem"
    local e_code=$?
    rm -rf "${tmp}"
    return "${e_code}"

}
function cert_validatePath() {
    local path="$1"
    local path_type="${2:-file}"

    # Check if path is empty
    if [[ -z "${path}" ]]; then
        common_logger -e "Path cannot be empty."
        return 1
    fi

    # Prevent path traversal attacks - reject paths with suspicious patterns
    if [[ "${path}" =~ \.\./|\.\.\\ ]]; then
        common_logger -e "Path traversal detected in: ${path}"
        return 1
    fi

    # Reject paths with newlines, carriage returns, or tabs (specific problematic characters)
    if [[ "${path}" =~ $'\n'|$'\r'|$'\t' ]]; then
        common_logger -e "Invalid characters detected in path: ${path}"
        return 1
    fi

    # For absolute paths validation
    if [[ "${path}" == /* ]]; then
        # Resolve to canonical path to prevent symlink attacks
        if command -v realpath >/dev/null 2>&1; then
            local canonical_path
            canonical_path=$(realpath -m "${path}" 2>/dev/null) || return 1

            # Ensure the canonical path doesn't escape expected boundaries
            if [[ ! "${canonical_path}" =~ ^/[a-zA-Z0-9/_.\-]+$ ]]; then
                common_logger -e "Invalid canonical path: ${canonical_path}"
                return 1
            fi
        fi
    fi

    return 0
}
function cert_sanitizeFilename() {
    local filename="$1"

    # Remove any path components
    filename="${filename##*/}"

    # Only allow alphanumeric, dash, underscore, and dot
    filename=$(echo "${filename}" | sed 's/[^a-zA-Z0-9._-]/_/g')

    # Prevent hidden files
    filename="${filename#.}"

    # Limit length to 255 characters
    if [[ ${#filename} -gt 255 ]]; then
        filename="${filename:0:255}"
    fi

    echo "${filename}"
}
function cert_sanitizeNodeName() {
    local component_name="$1"
    local node_names_var="$2"

    # Use nameref for safe dynamic array access
    declare -n component_node_names="${node_names_var}"

    for i in "${!component_node_names[@]}"; do

        # Only allow alphanumeric, dash, underscore, and dot (typical for hostnames)
        if [[ ! "${component_node_names[$i]}" =~ ^[a-zA-Z0-9._-]+$ ]]; then
            common_logger -e "Invalid ${component_name} node name: ${component_node_names[$i]}. Only alphanumeric characters, dots, dashes, and underscores are allowed."
            exit 1
        fi

        # Prevent names starting with dash or dot
        if [[ "${component_node_names[$i]}" =~ ^[-\.] ]]; then
            common_logger -e "${component_name} node name cannot start with dash or dot: ${component_node_names[$i]}"
            exit 1
        fi

        # Limit length
        if [[ ${#component_node_names[$i]} -gt 253 ]]; then
            common_logger -e "${component_name} node name too long: ${component_node_names[$i]}"
            exit 1
        fi
    done

    return 0
}
function cert_cleanFiles() {

    # Validate cert_tmp_path before use
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    # Remove files
    rm -f "${cert_tmp_path}"/*.csr
    rm -f "${cert_tmp_path}"/*.srl
    rm -f "${cert_tmp_path}"/*.conf
    rm -f "${cert_tmp_path}"/admin-key-temp.pem
    # The root CA key never leaves the CA directory.
    rm -f "${cert_tmp_path}"/root-ca.key
    # Single-use CA workspaces of the server leaves, in case one was left behind.
    for workspace in "${cert_tmp_path}"/*-ca; do
        if [ -d "${workspace}" ]; then
            rm -rf "${workspace}"
        fi
    done

}
function cert_createTmpDir() {

    if [ -z "${cert_tmp_path}" ]; then
        if ! cert_tmp_path="$(mktemp -d /tmp/wazuh-certificates.XXXXXXXXXX)"; then
            common_logger -e "Could not create a temporary directory for the certificates."
            exit 1
        fi
        return 0
    fi

    # Without the trailing slashes, -L checks the link itself and not its target.
    while [[ "${cert_tmp_path}" == */ && "${cert_tmp_path}" != "/" ]]; do
        cert_tmp_path="${cert_tmp_path%/}"
    done

    if [ -L "${cert_tmp_path}" ]; then
        common_logger -e "The temporary directory ${cert_tmp_path} is a symbolic link. Use another path."
        exit 1
    fi

    # The last mkdir has no -p, so it fails if another user creates the directory first.
    if [ -e "${cert_tmp_path}" ]; then
        if [ ! -d "${cert_tmp_path}" ] || [ "$(stat -c %u "${cert_tmp_path}")" != "$(id -u)" ] || [ -n "$(ls -A "${cert_tmp_path}")" ]; then
            common_logger -e "The temporary directory ${cert_tmp_path} must be an empty directory owned by root. Remove it or use another path."
            exit 1
        fi
    elif ! mkdir -p "$(dirname "${cert_tmp_path}")" || ! mkdir "${cert_tmp_path}"; then
        common_logger -e "Could not create the temporary directory ${cert_tmp_path}."
        exit 1
    fi

    chmod 700 "${cert_tmp_path}"

}
function cert_checkOpenSSL() {

    local missing=()

    common_logger -d "Checking if OpenSSL and the commands the root CA needs are installed."

    if [ -z "$(command -v openssl)" ]; then
        missing+=("openssl (package openssl)")
    fi
    if ! stat -c '%a' / > /dev/null 2>&1; then
        missing+=("GNU stat (package coreutils)")
    fi

    if [ "${#missing[@]}" -gt 0 ]; then
        common_logger -e "The following commands are required and could not be found: $(IFS=','; echo "${missing[*]}" | sed 's/,/, /g'). Install them and run the tool again."
        exit 1
    fi

}
function cert_checkRootCA() {

    local mode="${1:-validate}"
    local ca_error

    common_logger -d "Checking the root CA."

    if [ "${mode}" == "create" ]; then
        cert_generateRootCAcertificate
    else
        if ! cert_ca_dir=$(common_caGetDir 2>/dev/null); then
            common_logger -e "WAZUH_CA_DIR must be an absolute path."
            cert_cleanFiles
            exit 1
        fi
        if ! ca_error=$(common_caValidate "${cert_ca_dir}" 2>&1); then
            common_logger -e "There is no valid root CA in ${cert_ca_dir}: ${ca_error} Create it with -ca|--root-ca-certificates, or set WAZUH_CA_DIR to the directory of an existing one."
            cert_cleanFiles
            exit 1
        fi
    fi

    rootca="${cert_ca_dir}/root-ca.pem"
    rootcakey="${cert_ca_dir}/root-ca.key"

    if [ ! -f "${rootcakey}" ]; then
        common_logger -e "The root CA in ${cert_ca_dir} has no private key (root-ca.key), so it cannot sign certificates. Run the tool on the host that holds the key."
        cert_cleanFiles
        exit 1
    fi

    if ! cp "${rootca}" "${cert_tmp_path}/root-ca.pem"; then
        common_logger -e "Could not copy ${rootca} to ${cert_tmp_path}."
        cert_cleanFiles
        exit 1
    fi

}
function cert_executeAndValidate() {

    command_output=$("$@" 2>&1)
    e_code="${PIPESTATUS[0]}"

    if [ "${e_code}" -ne 0 ]; then
        common_logger -e "Error generating the certificates."
        common_logger -d "Error executing command: $@"
        common_logger -d "Error output: ${command_output}"
        cert_cleanFiles
        exit 1
    fi

}
function cert_generateAdmincertificate() {

    common_logger "Generating Admin certificates."

    # Validate cert_tmp_path
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    common_logger -d "Generating Admin private key."
    cert_executeAndValidate openssl genrsa -out "${cert_tmp_path}/admin-key-temp.pem" 2048
    common_logger -d "Converting Admin private key to PKCS8 format."
    cert_executeAndValidate openssl pkcs8 -inform PEM -outform PEM -in "${cert_tmp_path}/admin-key-temp.pem" -topk8 -nocrypt -v1 PBE-SHA1-3DES -out "${cert_tmp_path}/admin-key.pem"
    common_logger -d "Generating Admin CSR."
    # Same subject order as the Wazuh indexer package, which writes this DN in admin_dn.
    cert_executeAndValidate openssl req -new -key "${cert_tmp_path}/admin-key.pem" -out "${cert_tmp_path}/admin.csr" -batch -subj '/CN=admin/OU=Wazuh/O=Wazuh/L=California/C=US'

    # The admin certificate carries no SAN: it is never dialled, it authenticates
    # against the indexer security API. Hence an extension file of its own rather
    # than cert_generateCertificateconfiguration, which builds a SAN.
    cat > "${cert_tmp_path}/admin.conf" <<- EOF
        [ v3_admin ]
        authorityKeyIdentifier=keyid,issuer
        basicConstraints = critical, CA:FALSE
        keyUsage = critical, digitalSignature, keyEncipherment
        extendedKeyUsage = clientAuth
	EOF

    common_logger -d "Creating Admin certificate."
    cert_executeAndValidate openssl x509 -days 3650 -req -in "${cert_tmp_path}/admin.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -sha256 -out "${cert_tmp_path}/admin.pem" -extfile "${cert_tmp_path}/admin.conf" -extensions v3_admin

}
function cert_generateCertificateconfiguration() {

    common_logger -d "Generating certificate configuration."

    local node_name="$1"
    local extended_key_usage="$2"

    # Validate cert_tmp_path
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    if [ -z "${extended_key_usage}" ]; then
        common_logger -e "No extendedKeyUsage specified for ${node_name}."
        exit 1
    fi

    # Subject order of the Wazuh manager and dashboard packages. With cert_cn_first set, the
    # order of the Wazuh indexer package, which writes that DN in nodes_dn and admin_dn.
    local subject="C = US
        L = California
        O = Wazuh
        OU = Wazuh
        CN = cname"
    if [ -n "${cert_cn_first}" ]; then
        subject="CN = cname
        OU = Wazuh
        O = Wazuh
        L = California
        C = US"
    fi

    cat > "${cert_tmp_path}/${node_name}.conf" <<- EOF
        [ req ]
        prompt = no
        default_bits = 2048
        default_md = sha256
        distinguished_name = req_distinguished_name
        x509_extensions = v3_req

        [req_distinguished_name]
        ${subject}

        [ v3_req ]
        authorityKeyIdentifier=keyid,issuer
        basicConstraints = critical, CA:FALSE
        keyUsage = critical, digitalSignature, keyEncipherment
        extendedKeyUsage = ekusage
        subjectAltName = @alt_names

        [alt_names]
        IP.1 = cip
	EOF


    conf="$(awk '{sub("CN = cname", "CN = '"${node_name}"'"); sub("extendedKeyUsage = ekusage", "extendedKeyUsage = '"${extended_key_usage}"'")}1' "${cert_tmp_path}/${node_name}.conf")"
    echo "${conf}" > "${cert_tmp_path}/${node_name}.conf"

    if [ "${#@}" -gt 2 ]; then
        sed -i '/IP.1/d' "${cert_tmp_path}/${node_name}.conf"
        local ip_counter=0
        local dns_counter=0
        for (( i=3; i<=${#@}; i++ )); do
            if cert_isIP "${!i}"; then
                ip_counter=$((ip_counter+1))
                printf '%s\n' "        IP.${ip_counter} = ${!i}" >> "${cert_tmp_path}/${node_name}.conf"
            elif cert_isDNS "${!i}"; then
                dns_counter=$((dns_counter+1))
                printf '%s\n' "        DNS.${dns_counter} = ${!i}" >> "${cert_tmp_path}/${node_name}.conf"
            else
                common_logger -e "Invalid IP or DNS ${!i}"
                exit 1
            fi
        done
    else
        common_logger -e "No IP or DNS specified"
        exit 1
    fi

}
function cert_generateIndexercertificates() {

    if [ ${#indexer_node_names[@]} -gt 0 ]; then
        common_logger "Generating Wazuh indexer certificates."

        for i in "${!indexer_node_names[@]}"; do
            indexer_node_name=${indexer_node_names[$i]}

            common_logger -d "Creating the certificates for ${indexer_node_name} indexer node."
            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n idx_ip="indexer_node_ip_${j}"
            declare -n idx_dns="indexer_node_dns_${j}"
            declare -a idx_san=()
            if [ "${#idx_ip[@]}" -gt 0 ]; then
                idx_san+=("${idx_ip[@]}")
            fi
            if [ "${#idx_dns[@]}" -gt 0 ]; then
                idx_san+=("${idx_dns[@]}")
            fi
            cert_cn_first=1 cert_generateCertificateconfiguration "${indexer_node_name}" "serverAuth, clientAuth" "${idx_san[@]}"
            common_logger -d "Creating the Wazuh indexer tmp key pair."
            cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${indexer_node_name}-key.pem" -out "${cert_tmp_path}/${indexer_node_name}.csr" -config "${cert_tmp_path}/${indexer_node_name}.conf"
            common_logger -d "Creating the Wazuh indexer certificates."
            cert_executeAndValidate openssl x509 -req -in "${cert_tmp_path}/${indexer_node_name}.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -out "${cert_tmp_path}/${indexer_node_name}.pem" -extfile "${cert_tmp_path}/${indexer_node_name}.conf" -extensions v3_req -days 3650
        done
    else
        return 1
    fi

}
function cert_generateManagercertificates() {

    if [ ${#manager_node_names[@]} -gt 0 ]; then
        common_logger "Generating Wazuh manager certificates."

        for i in "${!manager_node_names[@]}"; do
            manager_name="${manager_node_names[i]}"

            common_logger -d "Generating the certificates for ${manager_name} manager node."
            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n manager_ip="manager_node_ip_${j}"
            declare -n mgr_dns="manager_node_dns_${j}"
            declare -a manager_san=()
            if [ "${#manager_ip[@]}" -gt 0 ]; then
                manager_san+=("${manager_ip[@]}")
            fi
            if [ "${#mgr_dns[@]}" -gt 0 ]; then
                manager_san+=("${mgr_dns[@]}")
            fi
            cert_generateCertificateconfiguration "${manager_name}" "clientAuth" "${manager_san[@]}"
            common_logger -d "Creating the Wazuh manager tmp key pair."
            cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${manager_name}-key.pem" -out "${cert_tmp_path}/${manager_name}.csr" -config "${cert_tmp_path}/${manager_name}.conf"
            common_logger -d "Creating the Wazuh manager certificates."
            cert_executeAndValidate openssl x509 -req -in "${cert_tmp_path}/${manager_name}.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -out "${cert_tmp_path}/${manager_name}.pem" -extfile "${cert_tmp_path}/${manager_name}.conf" -extensions v3_req -days 3650
            # Agent-facing listener leaf for this manager node: the node SAN plus the
            # addresses given with --agent-san, which every node shares. Only this leaf
            # gets them; the manager certificate above keeps the node SAN alone.
            cert_generateRemotedcertificate "${manager_name}" "${manager_san[@]}" "${agent_san[@]}"
            # Server API leaf: the node SAN, loopback and the addresses given with
            # --api-san. It never takes the --agent-san addresses.
            cert_generateApidcertificate "${manager_name}" "${manager_san[@]}" localhost 127.0.0.1 ::1 "${api_san[@]}"
        done
    else
        return 1
    fi

}
function cert_generateServerLeafconfiguration() {

    common_logger -d "Generating server leaf certificate configuration."

    local conf_file="$1"
    local node_name="$2"
    local ip_counter=0
    local dns_counter=0
    local san
    local -A listed=()

    # Validate cert_tmp_path
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        exit 1
    fi

    if [ "${#@}" -le 2 ]; then
        common_logger -e "No IP or DNS specified"
        exit 1
    fi

    {
        printf '%s\n' "[ req ]"
        printf '%s\n' "prompt = no"
        printf '%s\n' "default_bits = 2048"
        printf '%s\n' "default_md = sha256"
        printf '%s\n' "distinguished_name = req_distinguished_name"
        printf '%s\n' "x509_extensions = v3_remoted"
        printf '\n'
        printf '%s\n' "[req_distinguished_name]"
        printf '%s\n' "C = US"
        printf '%s\n' "L = California"
        printf '%s\n' "O = Wazuh"
        printf '%s\n' "OU = Wazuh"
        printf '%s\n' "CN = ${node_name}"
        printf '\n'
        printf '%s\n' "[ v3_remoted ]"
        printf '%s\n' "authorityKeyIdentifier = keyid,issuer"
        printf '%s\n' "subjectKeyIdentifier = hash"
        printf '%s\n' "basicConstraints = critical,CA:FALSE"
        printf '%s\n' "keyUsage = critical,digitalSignature,keyEncipherment"
        printf '%s\n' "extendedKeyUsage = serverAuth"
        printf '%s\n' "subjectAltName = @alt_names"
        printf '\n'
        printf '%s\n' "[alt_names]"
    } > "${conf_file}"

    for (( i=3; i<=${#@}; i++ )); do
        san="${!i}"
        if cert_isIP "${san}"; then
            if [ -n "${listed[${san}]+listed}" ]; then
                continue
            fi
            listed["${san}"]=1
            ip_counter=$((ip_counter+1))
            printf '%s\n' "IP.${ip_counter} = ${san}" >> "${conf_file}"
        elif cert_isDNS "${san}"; then
            if [ -n "${listed[${san,,}]+listed}" ]; then
                continue
            fi
            listed["${san,,}"]=1
            dns_counter=$((dns_counter+1))
            printf '%s\n' "DNS.${dns_counter} = ${san}" >> "${conf_file}"
        else
            common_logger -e "Invalid IP or DNS ${san}"
            exit 1
        fi
    done

    if cert_isDNS "${node_name}" && [ -z "${listed[${node_name,,}]+listed}" ]; then
        dns_counter=$((dns_counter+1))
        printf '%s\n' "DNS.${dns_counter} = ${node_name}" >> "${conf_file}"
    fi

}
function cert_generateRemotedcertificateconfiguration() {

    local node_name="$1"
    shift

    cert_generateServerLeafconfiguration "${cert_tmp_path}/${node_name}-remoted.conf" "${node_name}" "$@"

}
function cert_generateRemotedCAworkspace() {

    local ca_dir="$1"

    rm -rf "${ca_dir}"
    if ! mkdir -p "${ca_dir}/newcerts"; then
        common_logger -e "Could not create the temporary CA directory ${ca_dir}."
        cert_cleanFiles
        exit 1
    fi
    chmod 700 "${ca_dir}"
    : > "${ca_dir}/index.txt"
    printf '%s\n' "unique_subject = no" > "${ca_dir}/index.txt.attr"
    # A random 16-byte serial: each node gets its own workspace, so an incrementing
    # counter would hand every manager node the same serial from the same CA.
    openssl rand -hex 16 > "${ca_dir}/serial"

    {
        printf '%s\n' "[ ca ]"
        printf '%s\n' "default_ca = CA_remoted"
        printf '\n'
        printf '%s\n' "[ CA_remoted ]"
        printf '%s\n' "dir = ${ca_dir}"
        printf '%s\n' "database = ${ca_dir}/index.txt"
        printf '%s\n' "serial = ${ca_dir}/serial"
        printf '%s\n' "new_certs_dir = ${ca_dir}/newcerts"
        printf '%s\n' "certificate = ${cert_tmp_path}/root-ca.pem"
        printf '%s\n' "private_key = ${rootcakey}"
        printf '%s\n' "default_md = sha256"
        printf '%s\n' "preserve = yes"
        printf '%s\n' "email_in_dn = no"
        printf '%s\n' "policy = policy_remoted"
        printf '\n'
        printf '%s\n' "[ policy_remoted ]"
        printf '%s\n' "countryName = optional"
        printf '%s\n' "stateOrProvinceName = optional"
        printf '%s\n' "localityName = optional"
        printf '%s\n' "organizationName = optional"
        printf '%s\n' "organizationalUnitName = optional"
        printf '%s\n' "commonName = supplied"
    } > "${ca_dir}/ca.conf"

}
function cert_generateServerLeaf() {

    local prefix="$1"
    local node_name="$2"
    local ca_dir="${cert_tmp_path}/${prefix}-ca"
    local start_date
    local end_date

    shift 2

    common_logger -d "Creating the server leaf certificate ${prefix}."

    cert_generateServerLeafconfiguration "${cert_tmp_path}/${prefix}.conf" "${node_name}" "$@"
    common_logger -d "Creating the ${prefix} tmp key pair."
    cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${prefix}-key.pem" -out "${cert_tmp_path}/${prefix}.csr" -config "${cert_tmp_path}/${prefix}.conf"

    # Two-digit years: OpenSSL reads them as 20YY below 50, and the notAfter of a
    # 3650-day certificate is well before 2049.
    start_date="$(date -u -d '-1 day' '+%y%m%d%H%M%SZ' 2>/dev/null)"
    end_date="$(date -u -d '+3650 days' '+%y%m%d%H%M%SZ' 2>/dev/null)"
    if [[ -z "${start_date}" || -z "${end_date}" ]]; then
        common_logger -e "Could not compute the validity dates of the ${prefix} certificate."
        cert_cleanFiles
        exit 1
    fi

    common_logger -d "Creating the ${prefix} certificate, valid from ${start_date} to ${end_date}."
    cert_generateRemotedCAworkspace "${ca_dir}"
    cert_executeAndValidate openssl ca -batch -notext -md sha256 -config "${ca_dir}/ca.conf" -in "${cert_tmp_path}/${prefix}.csr" -out "${cert_tmp_path}/${prefix}.pem" -extfile "${cert_tmp_path}/${prefix}.conf" -extensions v3_remoted -startdate "${start_date}" -enddate "${end_date}"
    rm -rf "${ca_dir}"

    if ! cat "${cert_tmp_path}/root-ca.pem" >> "${cert_tmp_path}/${prefix}.pem"; then
        common_logger -e "Could not append root-ca.pem to ${prefix}.pem."
        cert_cleanFiles
        exit 1
    fi

}
function cert_generateRemotedcertificate() {

    local node_name="$1"
    shift

    common_logger -d "Creating the remoted (agent listener) certificate for ${node_name}."

    cert_generateServerLeaf "${node_name}-remoted" "${node_name}" "$@"

}
function cert_generateApidcertificate() {

    local node_name="$1"
    shift

    common_logger -d "Creating the Server API certificate for ${node_name}."

    cert_generateServerLeaf "${node_name}-apid" "${node_name}" "$@"

}
function cert_verifyRemotedcertificates() {

    local certs_dir="${1}"
    local manager_name
    local leaf

    if [ ${#manager_node_names[@]} -eq 0 ]; then
        return 0
    fi

    if ! cert_validatePath "${certs_dir}" "directory"; then
        common_logger -e "Invalid certificates directory."
        exit 1
    fi

    for manager_name in "${manager_node_names[@]}"; do
        for leaf in "${manager_name}-remoted" "${manager_name}-apid"; do
            if ! openssl verify -CAfile "${certs_dir}/root-ca.pem" "${certs_dir}/${leaf}.pem" > /dev/null 2>&1; then
                common_logger -e "The certificate ${certs_dir}/${leaf}.pem does not verify against ${certs_dir}/root-ca.pem."
                exit 1
            fi
            common_logger -d "Verified ${leaf}.pem against root-ca.pem."
        done
    done

}
function cert_generateDashboardcertificates() {
    if [ ${#dashboard_node_names[@]} -gt 0 ]; then
        common_logger "Generating Wazuh dashboard certificates."

        for i in "${!dashboard_node_names[@]}"; do
            dashboard_node_name="${dashboard_node_names[i]}"

            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n dash_ip="dashboard_node_ip_${j}"
            declare -n dash_dns="dashboard_node_dns_${j}"
            declare -a dash_san=()
            if [ "${#dash_ip[@]}" -gt 0 ]; then
                dash_san+=("${dash_ip[@]}")
            fi
            if [ "${#dash_dns[@]}" -gt 0 ]; then
                dash_san+=("${dash_dns[@]}")
            fi
            cert_generateCertificateconfiguration "${dashboard_node_name}" "serverAuth" "${dash_san[@]}"
            common_logger -d "Creating the Wazuh dashboard tmp key pair."
            cert_executeAndValidate openssl req -new -nodes -newkey rsa:2048 -keyout "${cert_tmp_path}/${dashboard_node_name}-key.pem" -out "${cert_tmp_path}/${dashboard_node_name}.csr" -config "${cert_tmp_path}/${dashboard_node_name}.conf"
            common_logger -d "Creating the Wazuh dashboard certificates."
            cert_executeAndValidate openssl x509 -req -in "${cert_tmp_path}/${dashboard_node_name}.csr" -CA "${cert_tmp_path}/root-ca.pem" -CAkey "${rootcakey}" -CAcreateserial -out "${cert_tmp_path}/${dashboard_node_name}.pem" -extfile "${cert_tmp_path}/${dashboard_node_name}.conf" -extensions v3_req -days 3650
        done
    else
        return 1
    fi

}
function cert_generateLoadbalancercertificates() {

    if [ ${#lb_node_names[@]} -gt 0 ]; then
        common_logger "Generating load balancer certificates."

        for i in "${!lb_node_names[@]}"; do
            lb_node_name="${lb_node_names[i]}"

            common_logger -d "Creating the certificate for ${lb_node_name} load balancer."
            j=$((i+1))
            # Use nameref for safe dynamic array access
            declare -n lb_ip="lb_node_ip_${j}"
            declare -n lb_dns="lb_node_dns_${j}"
            declare -a lb_san=()
            if [ "${#lb_ip[@]}" -gt 0 ]; then
                lb_san+=("${lb_ip[@]}")
            fi
            if [ "${#lb_dns[@]}" -gt 0 ]; then
                lb_san+=("${lb_dns[@]}")
            fi
            cert_generateServerLeaf "${lb_node_name}" "${lb_node_name}" "${lb_san[@]}"
        done
    else
        return 1
    fi

}
function cert_verifyLoadbalancercertificates() {

    local certs_dir="${1}"
    local lb_name

    if [ ${#lb_node_names[@]} -eq 0 ]; then
        return 0
    fi

    if ! cert_validatePath "${certs_dir}" "directory"; then
        common_logger -e "Invalid certificates directory."
        exit 1
    fi

    for lb_name in "${lb_node_names[@]}"; do
        if ! openssl verify -CAfile "${certs_dir}/root-ca.pem" "${certs_dir}/${lb_name}.pem" > /dev/null 2>&1; then
            common_logger -e "The certificate ${certs_dir}/${lb_name}.pem does not verify against ${certs_dir}/root-ca.pem."
            exit 1
        fi
        common_logger -d "Verified ${lb_name}.pem against root-ca.pem."
    done

}
function cert_generateRootCAcertificate() {

    local ca_error

    if ! cert_ca_dir=$(common_caGetDir 2>/dev/null); then
        common_logger -e "WAZUH_CA_DIR must be an absolute path."
        cert_cleanFiles
        exit 1
    fi

    if [ -e "${cert_ca_dir}/root-ca.pem" ] || [ -e "${cert_ca_dir}/root-ca.key" ]; then
        common_logger "Using the existing root CA in ${cert_ca_dir}."
    else
        common_logger "Generating the root certificate in ${cert_ca_dir}."
        # The nodes of an existing deployment only trust the CA they were issued from. See #1049.
        common_logger -w "There is no root CA in ${cert_ca_dir}, so a new one is created. The certificates issued from it do not chain to the root CA of any existing deployment. To add nodes to an existing deployment, stop here and run the tool where its root CA is, or set WAZUH_CA_DIR to a directory with a copy of that root CA and its key."
    fi

    if ! ca_error=$(common_caEnsure "${cert_ca_dir}" 2>&1); then
        common_logger -e "The root CA in ${cert_ca_dir} could not be created or is not valid: ${ca_error}"
        cert_cleanFiles
        exit 1
    fi

}
function cert_normalizeYamlFormat() {

    # Normalize the certs-tool YAML schema regardless of incoming indentation.
    # It supports optional node fields (ip, dns, node_type). Both ip and dns may be
    # given as a scalar value or as a list; node_type is always a scalar.
    #
    # list_key holds the field whose list is currently being read, so that the items
    # under it are folded into that field instead of being taken for new nodes.
    awk '
    function ltrim(str) {
        sub(/^[ \t]+/, "", str)
        return str
    }
    function rtrim(str) {
        sub(/[ \t]+$/, "", str)
        return str
    }
    function trim(str) {
        return rtrim(ltrim(str))
    }
    BEGIN {
        in_nodes = 0
        nodes_indent = 0
        current_section = ""
        in_node = 0
        list_key = ""
    }
    {
        line = $0
        match(line, /^[ \t]*/)
        indent = RLENGTH

        if (match(line, /^[ \t]*$/)) {
            print ""
            list_key = ""
            next
        }

        if (match(line, /^[ \t]*#/)) {
            print line
            next
        }

        stripped = trim(line)

        if (stripped == "nodes:") {
            print "nodes:"
            in_nodes = 1
            nodes_indent = indent
            current_section = ""
            in_node = 0
            list_key = ""
            next
        }

        if (in_nodes == 1 && current_section != "" && match(stripped, /^-[ \t]/)) {
            list_payload = trim(substr(stripped, 2))

            if (match(list_payload, /^name:[ \t]*/)) {
                name_value = trim(substr(list_payload, 6))
                print "    - name: " name_value
                in_node = 1
                list_key = ""
                next
            }

            if (in_node == 1 && list_key != "") {
                print "        - " list_payload
                next
            }

            print "    - " list_payload
            in_node = 1
            list_key = ""
            next
        }

        if (in_nodes == 1 && in_node == 1 && list_key != "" && match(stripped, /^-[ \t]/)) {
            item_value = trim(substr(stripped, 2))
            print "        - " item_value
            next
        }

        if (in_nodes == 1 && match(stripped, /^[a-zA-Z0-9_ ]+:[ \t]*/)) {
            key_name = trim(substr(stripped, 1, index(stripped, ":") - 1))
            key_value = trim(substr(stripped, index(stripped, ":") + 1))

            if (key_name == "node type") {
                key_name = "node_type"
            }

            if (in_node == 1 && key_name == "name") {
                print "    - name: " key_value
                list_key = ""
                next
            }

            if (in_node == 1 && key_name == "node_type") {
                print "      node_type: " key_value
                list_key = ""
                next
            }

            if (in_node == 1 && (key_name == "ip" || key_name == "dns")) {
                if (key_value == "") {
                    print "      " key_name ":"
                    list_key = key_name
                } else {
                    print "      " key_name ": " key_value
                    list_key = ""
                }
                next
            }

            if (key_value == "" && (in_node == 0 || indent <= nodes_indent + 2)) {
                print "  " key_name ":"
                current_section = key_name
                in_node = 0
                list_key = ""
                next
            }

            if (in_node == 1) {
                if (key_value == "") {
                    print "      " key_name ":"
                } else {
                    print "      " key_name ": " key_value
                }
                list_key = ""
                next
            }
        }

        if (!match(stripped, /^-[ \t]/)) {
            list_key = ""
        }

        print line
    }
    '
}
function cert_parseYaml() {

    local config_file_path="$1"
    local prefix="$2"
    local separator="${3:-_}"
    local indexfix

    # Detect awk flavor
    if awk --version 2>&1 | grep -q "GNU Awk" ; then
    # GNU Awk detected
    indexfix=-1
    elif awk -Wv 2>&1 | grep -q "mawk" ; then
    # mawk detected
    indexfix=0
    fi

    local s='[[:space:]]*' sm='[ \t]*' w='[a-zA-Z0-9_]*' fs=${fs:-$(echo @|tr @ '\034')} i=${i:-  }

    # Normalize YAML format first to handle both valid YAML indentation styles
    cat $config_file_path 2>/dev/null | cert_normalizeYamlFormat | \
    awk -F$fs "{multi=0;
        if(match(\$0,/$sm\|$sm$/)){multi=1; sub(/$sm\|$sm$/,\"\");}
        if(match(\$0,/$sm>$sm$/)){multi=2; sub(/$sm>$sm$/,\"\");}
        while(multi>0){
            str=\$0; gsub(/^$sm/,\"\", str);
            indent=index(\$0,str);
            indentstr=substr(\$0, 0, indent+$indexfix) \"$i\";
            obuf=\$0;
            getline;
            while(index(\$0,indentstr)){
                obuf=obuf substr(\$0, length(indentstr)+1);
                if (multi==1){obuf=obuf \"\\\\n\";}
                if (multi==2){
                    if(match(\$0,/^$sm$/))
                        obuf=obuf \"\\\\n\";
                        else obuf=obuf \" \";
                }
                getline;
            }
            sub(/$sm$/,\"\",obuf);
            print obuf;
            multi=0;
            if(match(\$0,/$sm\|$sm$/)){multi=1; sub(/$sm\|$sm$/,\"\");}
            if(match(\$0,/$sm>$sm$/)){multi=2; sub(/$sm>$sm$/,\"\");}
        }
    print}" | \
    sed  -e "s|^\($s\)?|\1-|" \
        -ne "s|^$s#.*||;s|$s#[^\"']*$||;s|^\([^\"'#]*\)#.*|\1|;t1;t;:1;s|^$s\$||;t2;p;:2;d" | \
    sed -ne "s|,$s\]$s\$|]|" \
        -e ":1;s|^\($s\)\($w\)$s:$s\(&$w\)\?$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: \3[\4]\n\1$i- \5|;t1" \
        -e "s|^\($s\)\($w\)$s:$s\(&$w\)\?$s\[$s\(.*\)$s\]|\1\2: \3\n\1$i- \4|;" \
        -e ":2;s|^\($s\)-$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1- [\2]\n\1$i- \3|;t2" \
        -e "s|^\($s\)-$s\[$s\(.*\)$s\]|\1-\n\1$i- \2|;p" | \
    sed -ne "s|,$s}$s\$|}|" \
        -e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1$i\3: \4|;t1" \
        -e "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1$i\2|;" \
        -e ":2;s|^\($s\)\($w\)$s:$s\(&$w\)\?$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1\2: \3 {\4}\n\1$i\5: \6|;t2" \
        -e "s|^\($s\)\($w\)$s:$s\(&$w\)\?$s{$s\(.*\)$s}|\1\2: \3\n\1$i\4|;p" | \
    sed  -e "s|^\($s\)\($w\)$s:$s\(&$w\)\(.*\)|\1\2:\4\n\3|" \
        -e "s|^\($s\)-$s\(&$w\)\(.*\)|\1- \3\n\2|" | \
    sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)\(---\)\($s\)||" \
        -e "s|^\($s\)\(\.\.\.\)\($s\)||" \
        -e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p;t" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p;t" \
        -e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\?\(.*\)$s\$|\1$fs\2$fs\3|" \
        -e "s|^\($s\)[\"']\?\([^&][^$fs]\+\)[\"']$s\$|\1$fs$fs$fs\2|" \
        -e "s|^\($s\)[\"']\?\([^&][^$fs]\+\)$s\$|\1$fs$fs$fs\2|" \
        -e "s|$s\$||p" | \
    awk -F$fs "{
        gsub(/\t/,\"        \",\$1);
        gsub(\"name: \", \"\");
        if(NF>3){if(value!=\"\"){value = value \" \";}value = value  \$4;}
        else {
        if(match(\$1,/^&/)){anchor[substr(\$1,2)]=full_vn;getline};
        indent = length(\$1)/length(\"$i\");
        vname[indent] = \$2;
        value= \$3;
        for (i in vname) {if (i > indent) {delete vname[i]; idx[i]=0}}
        if(length(\$2)== 0){  vname[indent]= ++idx[indent] };
        vn=\"\"; for (i=0; i<indent; i++) { vn=(vn)(vname[i])(\"$separator\")}
        vn=\"$prefix\" vn;
        full_vn=vn vname[indent];
        if(vn==\"$prefix\")vn=\"$prefix$separator\";
        if(vn==\"_\")vn=\"__\";
        }
        assignment[full_vn]=value;
        if(!match(assignment[vn], full_vn))assignment[vn]=assignment[vn] \" \" full_vn;
        if(match(value,/^\*/)){
            ref=anchor[substr(value,2)];
            if(length(ref)==0){
            printf(\"%s=\\\"%s\\\"\n\", full_vn, value);
            } else {
            for(val in assignment){
                if((length(ref)>0)&&index(val, ref)==1){
                    tmpval=assignment[val];
                    sub(ref,full_vn,val);
                if(match(val,\"$separator\$\")){
                    gsub(ref,full_vn,tmpval);
                } else if (length(tmpval) > 0) {
                    printf(\"%s=\\\"%s\\\"\n\", val, tmpval);
                }
                assignment[val]=tmpval;
                }
            }
        }
    } else if (length(value) > 0) {
        printf(\"%s=\\\"%s\\\"\n\", full_vn, value);
    }
    }END{
        for(val in assignment){
            if(match(val,\"$separator\$\"))
                printf(\"%s=\\\"%s\\\"\n\", val, assignment[val]);
        }
    }"

}
function cert_checkPrivateIp() {

    local ip="$1"
    common_logger -d "Checking if ${ip} is private."

    # Check private IPv4 ranges
    if [[ $ip =~ ^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^(127\.) ]]; then
        return 0
    fi

    # Check RFC 6598 shared address space (100.64.0.0/10)
    if [[ $ip =~ ^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\. ]]; then
        return 0
    fi

    # Check private IPv6 ranges (fc00::/7 prefix)
    if [[ $ip =~ ^fc ]]; then
        return 0
    fi

    return 1

}
function cert_isIPv4() {

    local ip="$1"
    local octet

    [[ ${ip} =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1

    for octet in ${ip//./ }; do
        # A leading zero is read as octal by some resolvers and as decimal by others,
        # so 010.0.0.1 is not one address but two. OpenSSL rejects both forms below.
        if [[ ${#octet} -gt 1 && ${octet:0:1} == "0" ]]; then
            return 1
        fi
        if [ "${octet}" -gt 255 ]; then
            return 1
        fi
    done

    return 0

}
function cert_isIPv6() {

    local ip="$1"
    [[ ${ip} =~ ^(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,7}:|:([0-9A-Fa-f]{1,4}:){1,7}|([0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:((:[0-9A-Fa-f]{1,4}){1,6})|:(:[0-9A-Fa-f]{1,4}){1,7}|::)$ ]]

}
function cert_isIP() {

    local ip="$1"
    cert_isIPv4 "${ip}" || cert_isIPv6 "${ip}"

}
function cert_isDNS() {

    local dns="$1"
    if ! cert_isIP "${dns}" && [[ ${dns} =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$ ]]; then
        return 0
    fi
    return 1

}
function cert_hostAddresses() {

    local address
    local name
    local -a addresses=()

    if command -v hostname > /dev/null 2>&1; then
        mapfile -t addresses < <(hostname -I 2>/dev/null | tr ' ' '\n')
    fi

    if [ "${#addresses[@]}" -eq 0 ] && command -v ip > /dev/null 2>&1; then
        mapfile -t addresses < <(ip -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d / -f 1)
    fi

    for address in "${addresses[@]}"; do
        if [ -z "${address}" ] || ! cert_isIP "${address}"; then
            continue
        fi
        # Link-local addresses are no more dialable from another host than loopback is.
        if [[ "${address}" =~ ^127\. ]] || [ "${address}" == "::1" ] || [[ "${address}" =~ ^169\.254\. ]] || [[ "${address}" =~ ^fe[89abAB] ]]; then
            continue
        fi
        printf '%s\n' "${address}"
    done

    for name in "$(hostname -f 2>/dev/null)" "$(hostname 2>/dev/null)"; do
        # localhost.localdomain is what a host with no name of its own reports, and it
        # names this host to every other one.
        if [ -n "${name}" ] && [[ ! "${name,,}" =~ ^localhost(\..*)?$ ]] && cert_isDNS "${name}"; then
            printf '%s\n' "${name}"
        fi
    done

}
function cert_listenerSanIsUnreachable() {

    local san

    for san in "$@"; do
        if cert_isIP "${san}"; then
            if [[ ! "${san}" =~ ^127\. ]] && [ "${san}" != "::1" ]; then
                return 1
            fi
        elif cert_isDNS "${san}"; then
            if [ "${san,,}" != "localhost" ]; then
                return 1
            fi
        fi
    done

    return 0

}
function cert_checkListenerReachability() {

    local severity="${1:-error}"
    local i
    local j
    local -a listener_san=()

    for i in "${!manager_node_names[@]}"; do
        j=$((i+1))
        declare -n manager_ip="manager_node_ip_${j}"
        declare -n mgr_dns="manager_node_dns_${j}"
        listener_san=()
        if [ "${#manager_ip[@]}" -gt 0 ]; then
            listener_san+=("${manager_ip[@]}")
        fi
        if [ "${#mgr_dns[@]}" -gt 0 ]; then
            listener_san+=("${mgr_dns[@]}")
        fi
        if [ "${#agent_san[@]}" -gt 0 ]; then
            listener_san+=("${agent_san[@]}")
        fi

        if cert_listenerSanIsUnreachable "${listener_san[@]}"; then
            if [ "${severity}" == "warning" ]; then
                common_logger -w "The agent listener certificate of the Wazuh manager node ${manager_node_names[$i]} only names loopback addresses, so no agent on another host can verify it. Set the address agents dial in the ip or dns field of the node in ${config_file}, or pass it with -as|--agent-san."
            else
                common_logger -e "The agent listener certificate of the Wazuh manager node ${manager_node_names[$i]} would only name loopback addresses, and no agent could verify it. Set the address agents dial in the ip or dns field of the node in ${config_file}, or pass it with -as|--agent-san."
                exit 1
            fi
        fi
    done

}
function cert_validateAgentSan() {

    local san

    if [ "${#agent_san[@]}" -eq 0 ]; then
        return 0
    fi

    if [[ -z "${all}" && -z "${cmanager}" && -z "${AIO}" && -z "${configurations}" ]]; then
        common_logger -e "The option -as|--agent-san must be used along with one of these options: -A, -wm in wazuh-certs-tool.sh, or -a, -g in wazuh-install.sh"
        exit 1
    fi

    for san in "${agent_san[@]}"; do
        if ! cert_isIP "${san}" && ! cert_isDNS "${san}"; then
            common_logger -e "Invalid IP or DNS in -as|--agent-san: ${san}."
            exit 1
        fi
    done

}
function cert_validateApiSan() {

    local san

    if [ "${#api_san[@]}" -eq 0 ]; then
        return 0
    fi

    if [[ -z "${all}" && -z "${cmanager}" && -z "${AIO}" && -z "${configurations}" ]]; then
        common_logger -e "The option -ap|--api-san must be used along with one of these options: -A, -wm in wazuh-certs-tool.sh, or -a, -g in wazuh-install.sh"
        exit 1
    fi

    for san in "${api_san[@]}"; do
        if ! cert_isIP "${san}" && ! cert_isDNS "${san}"; then
            common_logger -e "Invalid IP or DNS in -ap|--api-san: ${san}."
            exit 1
        fi
    done

}
function cert_validateComponentSanValues() {

    local component_name="$1"
    local node_names_var="$2"
    local node_ip_prefix="$3"
    local node_dns_prefix="$4"
    local i
    local j

    # Use nameref for safe dynamic array access
    declare -n component_node_names="${node_names_var}"

    for i in "${!component_node_names[@]}"; do
        j=$((i+1))
        # Use namerefs for dynamic array names
        declare -n component_ip="${node_ip_prefix}_${j}"
        declare -n component_dns="${node_dns_prefix}_${j}"

        if [ "${#component_ip[@]}" -eq 0 ] && [ "${#component_dns[@]}" -eq 0 ]; then
            common_logger -e "${component_name} node ${component_node_names[$i]} requires at least one field: ip or dns."
            exit 1
        fi

        for ip in "${component_ip[@]}"; do
            if ! cert_isIP "${ip}"; then
                common_logger -e "Invalid IP in field ip for ${component_name,,} node ${component_node_names[$i]}: ${ip}."
                exit 1
            fi
            # A public address is legitimate: a manager agents reach over the internet,
            # a node behind a cloud load balancer. It is worth naming, not refusing.
            if ! cert_checkPrivateIp "$ip"; then
                common_logger -w "The IP ${ip} of ${component_name,,} node ${component_node_names[$i]} is public. Make sure it is meant to be reachable from outside your network."
            fi
        done

        for dns in "${component_dns[@]}"; do
            if ! cert_isDNS "${dns}"; then
                common_logger -e "Invalid DNS in field dns for ${component_name,,} node ${component_node_names[$i]}: ${dns}."
                exit 1
            fi
        done
    done

}
function cert_validateComponentDuplicatedValues() {

    local component_name="$1"
    local node_names_var="$2"
    local node_ips_var="$3"
    local node_dns_prefix="$4"
    local i
    local j

    # Use namerefs for safe dynamic array access
    declare -n component_node_names="${node_names_var}"
    declare -n component_node_ips="${node_ips_var}"
    declare -a component_node_dns=()

    for i in "${!component_node_names[@]}"; do
        j=$((i+1))
        # Use nameref for dynamic DNS array
        declare -n node_dns="${node_dns_prefix}_${j}"
        if [ "${#node_dns[@]}" -gt 0 ]; then
            component_node_dns+=("${node_dns[@]}")
        fi
    done

    unique_names=($(echo "${component_node_names[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
    if [ "${#unique_names[@]}" -ne "${#component_node_names[@]}" ]; then
        common_logger -e "Duplicated ${component_name,,} node names."
        exit 1
    fi

    unique_ips=($(echo "${component_node_ips[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
    if [ "${#unique_ips[@]}" -ne "${#component_node_ips[@]}" ]; then
        common_logger -e "Duplicated ${component_name,,} node ips."
        exit 1
    fi

    unique_dns=($(echo "${component_node_dns[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
    if [ "${#unique_dns[@]}" -ne "${#component_node_dns[@]}" ]; then
        common_logger -e "Duplicated ${component_name,,} node dns."
        exit 1
    fi

}
function cert_validateManagerNodeTypes() {

    for i in "${manager_node_types[@]}"; do
        if ! echo "$i" | grep -ioq master && ! echo "$i" | grep -ioq worker; then
            common_logger -e "Incorrect node_type $i must be master or worker"
            exit 1
        fi
    done

    if [ "${#manager_node_names[@]}" -le 1 ]; then
        if [ "${#manager_node_types[@]}" -ne 0 ]; then
            common_logger -e "The tag node_type can only be used with more than one Wazuh manager."
            exit 1
        fi
    elif [ "${#manager_node_names[@]}" -gt "${#manager_node_types[@]}" ]; then
        common_logger -e "The tag node_type needs to be specified for all Wazuh manager nodes."
        exit 1
    elif [ "${#manager_node_names[@]}" -lt "${#manager_node_types[@]}" ]; then
        common_logger -e "Found extra node_type tags."
        exit 1
    elif [ "$(grep -io master <<< "${manager_node_types[*]}" | wc -l)" -ne 1 ]; then
        common_logger -e "Wazuh cluster needs a single master node."
        exit 1
    elif [ "$(grep -io worker <<< "${manager_node_types[*]}" | wc -l)" -ne $(( ${#manager_node_types[@]} - 1 )) ]; then
        common_logger -e "Incorrect number of workers."
        exit 1
    fi

}
function cert_firstAddressPerNode() {

    local target_var="$1"
    local node_names_var="$2"
    local node_ip_prefix="$3"
    local node_dns_prefix="$4"
    local i
    local j

    declare -n target_array="${target_var}"
    declare -n component_node_names="${node_names_var}"

    target_array=()

    for i in "${!component_node_names[@]}"; do
        j=$((i+1))
        declare -n component_ip="${node_ip_prefix}_${j}"
        declare -n component_dns="${node_dns_prefix}_${j}"
        if [ "${#component_ip[@]}" -gt 0 ]; then
            target_array+=("${component_ip[0]}")
        else
            target_array+=("${component_dns[0]}")
        fi
    done

}
function cert_readConfig() {

    common_logger -d "Reading configuration file."

    if [ -f "${config_file}" ]; then
        if [ ! -s "${config_file}" ]; then
            common_logger -e "File ${config_file} is empty"
            exit 1
        fi
        # Convert CRLF to LF without eval
        cert_convertCRLFtoLF "${config_file}"

        # Use mapfile for safe array assignment (prevents command injection)
        mapfile -t indexer_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t manager_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t dashboard_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t lb_node_names < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+load_balancer[_]+[0-9]+=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t indexer_node_all_ips < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+[0-9]+[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t manager_node_all_ips < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+[0-9]+[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t dashboard_node_all_ips < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+[0-9]+[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        mapfile -t manager_node_types < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+[0-9]+[_]+node_type=" | cut -d = -f 2 | sed 's/^"//;s/"$//')

        # Parse DNS entries for each indexer node
        for i in "${!indexer_node_names[@]}"; do
            j=$((i+1))
            # Create dynamic arrays using declare and mapfile
            mapfile -t "indexer_node_ip_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+${j}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
            mapfile -t "indexer_node_dns_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+indexer[_]+${j}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        # Parse DNS entries for each dashboard node
        for i in "${!dashboard_node_names[@]}"; do
            j=$((i+1))
            mapfile -t "dashboard_node_ip_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+${j}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
            mapfile -t "dashboard_node_dns_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+dashboard[_]+${j}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        for i in $(seq 1 "${#manager_node_names[@]}"); do
            mapfile -t "manager_node_ip_$i" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+${i}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//' | sed -r 's/\s+//g')
            mapfile -t "manager_node_dns_$i" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+manager[_]+${i}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        # Parse the addresses of each load balancer entry
        for i in "${!lb_node_names[@]}"; do
            j=$((i+1))
            mapfile -t "lb_node_ip_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+load_balancer[_]+${j}[_]+ip([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
            mapfile -t "lb_node_dns_${j}" < <(cert_parseYaml "${config_file}" | grep -E "nodes[_]+load_balancer[_]+${j}[_]+dns([_]+[0-9]+)?=" | cut -d = -f 2 | sed 's/^"//;s/"$//')
        done

        # The installer addresses a node by its position in these arrays, so they hold
        # one entry per node. A node reachable at several addresses puts them all in
        # its certificate through the per-node arrays above; the first one is the
        # address the components are configured with.
        cert_firstAddressPerNode "indexer_node_ips" "indexer_node_names" "indexer_node_ip" "indexer_node_dns"
        cert_firstAddressPerNode "manager_node_ips" "manager_node_names" "manager_node_ip" "manager_node_dns"
        cert_firstAddressPerNode "dashboard_node_ips" "dashboard_node_names" "dashboard_node_ip" "dashboard_node_dns"

        cert_validateComponentSanValues "Indexer" "indexer_node_names" "indexer_node_ip" "indexer_node_dns"
        cert_validateComponentSanValues "Manager" "manager_node_names" "manager_node_ip" "manager_node_dns"
        cert_validateComponentSanValues "Dashboard" "dashboard_node_names" "dashboard_node_ip" "dashboard_node_dns"
        cert_validateComponentSanValues "Load balancer" "lb_node_names" "lb_node_ip" "lb_node_dns"

        cert_sanitizeNodeName "Indexer" "indexer_node_names"
        cert_sanitizeNodeName "Manager" "manager_node_names"
        cert_sanitizeNodeName "Dashboard" "dashboard_node_names"
        cert_sanitizeNodeName "Load balancer" "lb_node_names"

        cert_validateComponentDuplicatedValues "Indexer" "indexer_node_names" "indexer_node_all_ips" "indexer_node_dns"
        cert_validateComponentDuplicatedValues "Wazuh manager" "manager_node_names" "manager_node_all_ips" "manager_node_dns"
        cert_validateComponentDuplicatedValues "Dashboard" "dashboard_node_names" "dashboard_node_all_ips" "dashboard_node_dns"

        cert_validateManagerNodeTypes

    else
        common_logger -e "No configuration file found."
        exit 1
    fi

}
function cert_rejectCAPaths() {

    if [[ -n "${2}" && "${2}" != -* ]]; then
        common_logger -e "${1} takes no argument, got '${2}': it issues the certificates of every node of config.yml it applies to, with the root CA of $(common_caGetDir 2>/dev/null). Set WAZUH_CA_DIR to use the root CA of another directory."
        exit 1
    fi

}
function cert_setpermisions() {
    # Validate cert_tmp_path before setting permissions
    if ! cert_validatePath "${cert_tmp_path}" "directory"; then
        common_logger -e "Invalid certificate temporary path."
        return 1
    fi

    # Private keys (every node/admin/remoted key) must stay
    # owner-only: the umask this tool sets at startup already creates them
    # at 0600, this only re-asserts it. Public certificates can be 0644 -
    # they carry no secret. The directory itself must stay 700 so the mode
    # on the files inside cannot be reached even if a later 'chmod 755' on
    # a copy of this directory relaxes traversal.
    if [ -n "${debugEnabled}" ]; then
        chmod 700 "${cert_tmp_path}"
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*-key.pem' -exec chmod 600 {} +
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*.pem' ! -name '*-key.pem' -exec chmod 644 {} +
    else
        chmod 700 "${cert_tmp_path}" > /dev/null 2>&1
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*-key.pem' -exec chmod 600 {} + > /dev/null 2>&1
        find "${cert_tmp_path}" -maxdepth 1 -type f -name '*.pem' ! -name '*-key.pem' -exec chmod 644 {} + > /dev/null 2>&1
    fi
}
function cert_convertCRLFtoLF() {
    local config_file_path="$1"
    local temp_file

    # Validate input file path
    if ! cert_validatePath "${config_file_path}" "file"; then
        common_logger -e "Invalid config file path."
        return 1
    fi

    # A new file with a random name, so no other user can create it or link it first.
    if ! temp_file="$(mktemp)"; then
        common_logger -e "Could not create a temporary file to convert ${config_file_path}."
        return 1
    fi

    # Convert CRLF to LF
    tr -d '\015' < "${config_file_path}" > "${temp_file}"

    # Move converted file back
    if [ -n "${debugEnabled}" ]; then
        mv "${temp_file}" "${config_file_path}"
    else
        mv "${temp_file}" "${config_file_path}" > /dev/null 2>&1
    fi
}

main "$@"
