#!/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 base_path="$(dirname "$(readlink -f "$0")")"
readonly config_file="${base_path}/config.yml"
readonly logfile="${base_path}/wazuh-certificates-tool.log"
cert_tmp_path=""
debug=">> ${logfile} 2>&1"
readonly cert_tool_script_name=".*certs.*\.sh"

# ------------ certFunctions.sh ------------ 
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
}

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

    echo -e ""
    echo -e "NAME"
    echo -e "        wazuh-certs-tool.sh - Manages the creation of certificates of the Wazuh components."
    echo -e ""
    echo -e "SYNOPSIS"
    echo -e "        wazuh-certs-tool.sh [OPTIONS]"
    echo -e ""
    echo -e "DESCRIPTION"
    echo -e "        -a,  --admin-certificates"
    echo -e "                Creates the admin certificates."
    echo -e ""
    echo -e "        -as, --agent-san <ip|dns>"
    echo -e "                Adds an extra address to the subject alternative name of every"
    echo -e "                agent listener certificate, on top of the ip and dns entries of"
    echo -e "                each manager node in config.yml. Repeat it for more than one."
    echo -e "                It is how to name an address agents dial that no single node"
    echo -e "                owns, such as a load balancer shared by every node of a cluster."
    echo -e "                Must be used along with one of these options: -A, -wm"
    echo -e ""
    echo -e "        -ap, --api-san <ip|dns>"
    echo -e "                Adds an extra address to the subject alternative name of every"
    echo -e "                Server API certificate, on top of the ip and dns entries of each"
    echo -e "                manager node in config.yml and loopback. Repeat it for more than"
    echo -e "                one. Use it for the address API clients dial, such as a published"
    echo -e "                name or a load balancer in front of the Server API."
    echo -e "                Must be used along with one of these options: -A, -wm"
    echo -e ""
    echo -e "        -A, --all"
    echo -e "                Creates certificates specified in config.yml and admin certificates."
    echo -e "                If there is no root CA in the CA directory, a new one is created there."
    echo -e ""
    echo -e "        -ca, --root-ca-certificates"
    echo -e "                Creates the root CA in the CA directory, if it does not exist yet."
    echo -e ""
    echo -e "        -lb, --load-balancer-certificates"
    echo -e "                Creates the certificates of the load_balancer entries of config.yml."
    echo -e "                Only needed by a proxy that terminates"
    echo -e "                TLS: it is then the certificate agents validate, signed by the same"
    echo -e "                root-ca they pin. A layer 4 passthrough load balancer terminates"
    echo -e "                nothing and needs -as|--agent-san instead."
    echo -e ""
    echo -e "        -v,  --verbose"
    echo -e "                Enables verbose mode."
    echo -e ""
    echo -e "        -wd,  --wazuh-dashboard-certificates"
    echo -e "                Creates the Wazuh dashboard certificates."
    echo -e ""
    echo -e "        -wi,  --wazuh-indexer-certificates"
    echo -e "                Creates the Wazuh indexer certificates."
    echo -e ""
    echo -e "        -wm,  --wazuh-manager-certificates"
    echo -e "                Creates the Wazuh manager certificates."
    echo -e "                Each manager node also gets <name>-remoted.pem and <name>-remoted-key.pem,"
    echo -e "                the certificate of the agent listener (remoted and authd), and"
    echo -e "                <name>-apid.pem and <name>-apid-key.pem, the certificate of the Server API."
    echo -e ""
    echo -e "        -tmp,  --cert_tmp_path </path/to/tmp_dir>"
    echo -e "                Uses this directory to create the certificates, instead of a new one in /tmp."
    echo -e "                The directory must not exist, or must be an empty directory owned by root."
    echo -e "                Must be used along with one of these options: -a, -A, -ca, -wi, -wd, -wm, -lb"
    echo -e ""
    echo -e "ROOT CA"
    echo -e "        The root CA is read from $(common_caGetDir 2>/dev/null), or from the directory set in WAZUH_CA_DIR."
    echo -e "        Its private key, root-ca.key, stays there and is never copied to the wazuh-certificates directory."
    echo -e "        The tool must be run as root."
    echo -e ""

    exit "${1:-1}"

}
function main() {

    # Set a restrictive umask so new regular files default to 600 and directories to 700,
    # limiting access to the current user.
    umask 0077

    # The root CA is created and read as root.
    common_checkRoot
    cert_checkOpenSSL

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

    if [ -n "${1}" ]; then
        while [ -n "${1}" ]
        do
            case "${1}" in
            "-a"|"--admin-certificates")
                cert_rejectCAPaths "-a|--admin-certificates" "${2}"
                cadmin=1
                shift 1
                ;;
            "-A"|"--all")
                cert_rejectCAPaths "-A|--all" "${2}"
                all=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
                ;;
            "-ca"|"--root-ca-certificates"|"--root-ca-certificate")
                ca=1
                shift 1
                ;;
            "-lb"|"--load-balancer-certificates")
                cert_rejectCAPaths "-lb|--load-balancer-certificates" "${2}"
                clb=1
                shift 1
                ;;
            "-h"|"--help")
                getHelp 0
                ;;
            "-v"|"--verbose")
                debugEnabled=1
                shift 1
                ;;
            "-wd"|"--wazuh-dashboard-certificates")
                cert_rejectCAPaths "-wd|--wazuh-dashboard-certificates" "${2}"
                cdashboard=1
                shift 1
                ;;
            "-wi"|"--wazuh-indexer-certificates")
                cert_rejectCAPaths "-wi|--wazuh-indexer-certificates" "${2}"
                cindexer=1
                shift 1
                ;;
            "-wm"|"--wazuh-manager-certificates")
                cert_rejectCAPaths "-wm|--wazuh-manager-certificates" "${2}"
                cmanager=1
                shift 1
                ;;
            "-tmp"|"--cert_tmp_path")
                if [[ -n "${3}" || ( "${cadmin}" == 1 || "${all}" == 1 || "${ca}" == 1 || "${cdashboard}" == 1 || "${cindexer}" == 1 || "${cmanager}" == 1 || "${clb}" == 1 ) ]]; then
                    if [[ -z "${2}" || ! "${2}" == /* ]]; then
                        common_logger -e "Error on arguments. Probably missing </path/to/tmp_dir> or path does not start with '/'."
                        getHelp
                        exit 1
                    else
                        cert_tmp_path="${2}"
                        shift 2
                    fi
                else
                    common_logger -e "Error: -tmp must be used along with one of these options: -a, -A, -ca, -wi, -wd, -wm, -lb"
                    getHelp
                    exit 1
                fi
                ;;
            *)
                echo "Unknown option: ${1}"
                getHelp
            esac
        done

        common_logger "Verbose logging redirected to ${logfile}"

        cert_validateAgentSan
        cert_validateApiSan

        if [[ -d "${base_path}"/wazuh-certificates ]]; then
            if [ -n "$(ls -A "${base_path}"/wazuh-certificates)" ]; then
                common_logger -e "Directory wazuh-certificates already exists in the same path as the script. Please, remove the certs directory to create new certificates."
                exit 1
            fi
            # Removed so the temporary directory is moved to this name and not inside it.
            rmdir "${base_path}"/wazuh-certificates
        fi

        cert_createTmpDir

        cert_readConfig

        if [[ -n "${all}" || -n "${cmanager}" ]]; then
            cert_checkListenerReachability "warning"
        fi

        if [ -n "${debugEnabled}" ]; then
            debug="2>&1 | tee -a ${logfile}"
        fi

        if [[ -n "${cadmin}" ]]; then
            cert_checkRootCA
            cert_generateAdmincertificate
            common_logger "Admin certificates created."
        fi

        if [[ -n "${all}" ]]; then
            cert_checkRootCA "create"
            cert_generateAdmincertificate
            common_logger "Admin certificates created."
            if cert_generateIndexercertificates; then
                common_logger "Wazuh indexer certificates created."
            fi
            if cert_generateManagercertificates; then
                common_logger "Wazuh manager certificates created."
            fi
            if cert_generateDashboardcertificates; then
                common_logger "Wazuh dashboard certificates created."
            fi
            # Only when config.yml carries a load_balancer section: it is optional and
            # its absence is not an error.
            if cert_generateLoadbalancercertificates; then
                common_logger "Load balancer certificates created."
            fi
        fi

        if [[ -n "${ca}" ]]; then
            cert_generateRootCAcertificate
            cp "${cert_ca_dir}/root-ca.pem" "${cert_tmp_path}/root-ca.pem"
            common_logger "The root CA is in ${cert_ca_dir}. Its private key, root-ca.key, stays there."
        fi

        if [[ -n "${cindexer}" ]]; then
            if [ ${#indexer_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateIndexercertificates
                common_logger "Wazuh indexer certificates created."
            else
                common_logger -e "Indexer node not present in config.yml."
                exit 1
            fi
        fi

        if [[ -n "${cmanager}" ]]; then
            if [ ${#manager_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateManagercertificates
                common_logger "Wazuh manager certificates created."
            else
                common_logger -e "Manager node not present in config.yml."
                exit 1
            fi
        fi

        if [[ -n "${clb}" ]]; then
            if [ ${#lb_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateLoadbalancercertificates
                common_logger "Load balancer certificates created."
            else
                common_logger -e "Load balancer section not present in config.yml."
                exit 1
            fi
        fi

        if [[ -n "${cdashboard}" ]]; then
            if [ ${#dashboard_node_names[@]} -gt 0 ]; then
                cert_checkRootCA
                cert_generateDashboardcertificates
                common_logger "Wazuh dashboard certificates created."
            else
                common_logger -e "Dashboard node not present in config.yml."
                exit 1
            fi
        fi

        # Every requested option writes to the same temporary directory, which is moved
        # once, so options can be combined in one run (-wi -wd, -wi -wd -wm, ...).
        cert_cleanFiles
        cert_setpermisions
        if [ -n "${debugEnabled}" ]; then
            mv "${cert_tmp_path}" "${base_path}/wazuh-certificates"
        else
            mv "${cert_tmp_path}" "${base_path}/wazuh-certificates" > /dev/null 2>&1
        fi
        if [[ -n "${all}" || -n "${cmanager}" ]]; then
            cert_verifyRemotedcertificates "${base_path}/wazuh-certificates"
        fi
        if [[ -n "${all}" || -n "${clb}" ]]; then
            cert_verifyLoadbalancercertificates "${base_path}/wazuh-certificates"
        fi

    else
        getHelp
    fi

}
# ------------ certVariables.sh ------------ 

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}"

}

main "$@"
