#!/bin/bash
# shellcheck disable=SC1001,SC1090,SC1091,SC2001,SC2002,SC2012,SC2013,SC2016,SC2034,SC2045,SC2046,SC2086,SC2143,SC2153,SC2154,SC2181,SC2206,SC2219,SC2236

export LC_ALL=C
export FULL_IFS=$' \t\n'
export NL_IFS=$'\n'

conf=/etc/patchman/patchman-client.conf
protocol=1
verbose=false
debug=false
report=false
local_updates=false
repo_check=true
dry_run=false
tags=''
api_key=''

usage() {
    echo "${0} [-v] [-d] [-n] [-u] [-y] [-r] [-s SERVER] [-c FILE] [-t TAGS] [-H HOSTNAME] [-p PROTOCOL] [-k API_KEY]"
    echo "-v: verbose output (default is silent)"
    echo "-d: debug output"
    echo "-n: no repo check (required when used as an apt or yum plugin)"
    echo "-u: find updates locally (e.g. for RHEL)"
    echo "-r: request a report from the server (default is no report)"
    echo "-s SERVER: web server address, e.g. https://patchman.example.com"
    echo "-c FILE: config file location (default is /etc/patchman/patchman-client.conf)"
    echo "-t TAGS: comma-separated list of tags, e.g. -t www,dev"
    echo "-H HOSTNAME: specify the hostname of the local host"
    echo "-p PROTOCOL: protocol version (1 or 2, default is 1)"
    echo "-k API_KEY: API key for protocol 2 authentication"
    echo "-y: dry run (collect data but do not submit)"
    echo
    echo "Command line options override config file options."
    exit 0
}

parseopts() {
    while getopts "vdnuyrs:c:t:h:H:p:k:" opt; do
        case ${opt} in
        v)
            verbose=true
            ;;
        d)
            debug=true
            verbose=true
            ;;
        n)
            repo_check=false
            ;;
        u)
            local_updates=true
            ;;
        y)
            dry_run=true
            ;;
        r)
            cli_report=true
            ;;
        s)
            cli_server=${OPTARG}
            ;;
        c)
            cli_conf=${OPTARG}
            ;;
        t)
            cli_tags="${OPTARG}"
            ;;
        h|H)
            cli_hostname=${OPTARG}
            ;;
        p)
            cli_protocol=${OPTARG}
            ;;
        k)
            cli_api_key=${OPTARG}
            ;;
        *)
            usage
            ;;
        esac
    done
}

cleanup() {
    if ${verbose} && ${debug} ; then
        echo "Debug: not deleting ${tmpfile_pkg} (packages)"
        echo "Debug: not deleting ${tmpfile_rep} (repos)"
        echo "Debug: not deleting ${tmpfile_sec} (security updates)"
        echo "Debug: not deleting ${tmpfile_bug} (updates)"
        echo "Debug: not deleting ${tmpfile_mod} (modules)"
        if [ ! -z "${tmpfile_packages_json}" ] ; then
            echo "Debug: not deleting ${tmpfile_packages_json} (packages json)"
            echo "Debug: not deleting ${tmpfile_repos_json} (repos json)"
            echo "Debug: not deleting ${tmpfile_modules_json} (modules json)"
            echo "Debug: not deleting ${tmpfile_sec_json} (security updates json)"
            echo "Debug: not deleting ${tmpfile_bug_json} (bug updates json)"
            echo "Debug: not deleting ${tmpfile_report_json} (full report json)"
        fi
    elif ${verbose} && ! ${debug} ; then
        echo "Deleting ${tmpfile_pkg}"
        echo "Deleting ${tmpfile_rep}"
        echo "Deleting ${tmpfile_sec}"
        echo "Deleting ${tmpfile_bug}"
        echo "Deleting ${tmpfile_mod}"
    fi
    if ! ${debug} ; then
        rm -fr "${tmpfile_pkg}"
        rm -fr "${tmpfile_rep}"
        rm -fr "${tmpfile_sec}"
        rm -fr "${tmpfile_bug}"
        rm -fr "${tmpfile_mod}"
        rm -fr "${tmpfile_packages_json}"
        rm -fr "${tmpfile_repos_json}"
        rm -fr "${tmpfile_modules_json}"
        rm -fr "${tmpfile_sec_json}"
        rm -fr "${tmpfile_bug_json}"
        rm -fr "${tmpfile_report_json}"
    fi
    flock -u 200
    rm -fr "${lock_dir}/patchman.lock"
}

check_conf() {
    if [ ! -z "${cli_conf}" ] ; then
        conf=${cli_conf}
    fi

    if [ -z "${conf}" ] || [ ! -f "${conf}" ] ; then
        echo "patchman-client: config file not found: ${conf}" >&2
        echo "  Create the config file and set server= to your patchman server." >&2
        exit 1
    fi
    source "${conf}"

    conf_dir=$(dirname "${conf}")/conf.d
    if [ -d "${conf_dir}" ] ; then
        let f=$(find "${conf_dir}" -maxdepth 1 -type f | wc -l)
        if [ ${f} -gt 0 ] ; then
            source "${conf_dir}"/*
        fi
    fi

    # check server is configured and not the example placeholder
    if ! ${dry_run} ; then
        if [ -z "${server}" ] && [ -z "${cli_server}" ] ; then
            echo "patchman-client: server not configured." >&2
            echo "  Edit ${conf} and set server= to your patchman server URL." >&2
            exit 1
        fi
        if [ ! -z "${cli_server}" ] ; then
            server=${cli_server}
        fi
        if echo "${server}" | grep -qE 'patchman\.example\.com' ; then
            echo "patchman-client: server not configured." >&2
            echo "  Edit ${conf} and set server= to your patchman server URL." >&2
            exit 1
        fi
    else
        if [ ! -z "${cli_server}" ] ; then
            server=${cli_server}
        fi
    fi

    if [ ! -z "${cli_report}" ] ; then
        report=${cli_report}
    fi

    if [ ! -z "${cli_tags}" ] ; then
        tags="${cli_tags}"
    fi

    if [ ! -z "${cli_protocol}" ] ; then
        protocol=${cli_protocol}
    fi

    if [ ! -z "${cli_api_key}" ] ; then
        api_key=${cli_api_key}
    fi

    if [ -z "${hostname}" ] && [ -z "${cli_hostname}" ] ; then
        get_hostname
    else
        if [ ! -z "${cli_hostname}" ] ; then
            hostname=${cli_hostname}
        fi
    fi

    check_booleans

    # Check if protocol 2 is requested but jq is not available
    if [ "${protocol}" == "2" ] ; then
        if ! check_command_exists jq ; then
            echo "Warning: jq not found, falling back to protocol 1"
            protocol=1
        fi
    fi

    if ${verbose} ; then
        echo "Patchman configuration seems OK:"
        if [ -f ${conf} ] ; then
            echo "Using configuration file: ${conf}"
        fi
        echo "Patchman Server: ${server}"
        echo "Hostname: ${hostname}"
        echo "Tags: ${tags}"
        echo "Protocol: ${protocol}"
        if [ ! -z "${api_key}" ] ; then
            echo "API Key: ${api_key:0:12}..."
        fi
        for var in report local_updates repo_check dry_run verbose debug ; do
            eval val=\$${var}
            echo "${var}: ${val}"
        done
    fi
}

check_booleans() {
    for var in report local_updates repo_check dry_run verbose debug ; do
        eval val=\$${var}
        if [ -z ${val} ] || [ "${val}" == "0" ] || [ "${val,,}" == "false" ] ; then
            eval ${var}=false
        elif [ "${val}" == "1" ] || [ "${val,,}" == "true" ] ; then
            eval ${var}=true
        fi
    done
}

check_command_exists() {
    command -v "${1}" >/dev/null 2>&1
}

check_for_modularity() {
    modularity=false
    if check_command_exists yum ; then
        if ${verbose} ; then
            echo 'Checking for modularity...'
        fi
        if yum module 2>&1 | grep -q 'No such command' ; then
            modularity=false
        else
            modularity=true
        fi
    fi
}

parse_module() {
    module_info=$(echo -e ${1})
    module_arch=$(echo "${module_info}" | grep ^Architecture | cut -d : -f 2 | awk '{$1=$1};1')
    module_repo=$(echo "${module_info}" | grep ^Repo | cut -d : -f 2 | awk '{$1=$1};1')
    module_stream=$(echo "${module_info}" | grep ^Stream | cut -d : -f 2 | sed -e 's/ \[.*//g' | awk '{$1=$1};1')
    module_version=$(echo "${module_info}" | grep ^Version | cut -d : -f 2 | awk '{$1=$1};1')
    module_context=$(echo "${module_info}" | grep ^Context | cut -d : -f 2 | awk '{$1=$1};1')
    module_packages=$(echo "${module_info}" | sed -n '/Artifacts/,$p' | sed -e 's/Artifacts //' | sed -e 's/^: *//g' | grep -v ^$)
    IFS=${NL_IFS}
    module_package_str=""
    for package in ${module_packages} ; do
        module_package_str="${module_package_str} '${package}'"
    done
    IFS=${FULL_IFS}
    if [ ! -z ${CPE_NAME} ] ; then
        module_repo="${CPE_NAME}-${module_repo}"
    fi
    echo "'${module}' '${module_stream}' '${module_version}' '${module_context}' '${module_arch}' '${module_repo}'${module_package_str}" >> ${tmpfile_mod}
}

get_enabled_modules() {
    if ${verbose} ; then
        echo 'Finding enabled yum modules...'
    fi

    enabled_modules=$(yum module list --enabled \
        | grep "\[e\]" \
        | grep -v ^Hint \
        | awk '{print $1}')

    for module in ${enabled_modules} ; do
        modules_info=$(yum module info ${module} | grep -v ^Hint)
        unset x
        while read -r line ; do
            if [ -z "${line}" ] ; then
                echo -e "${x}" | grep -q -E "^Stream.*\[e\]" && parse_module "${x}"
                unset x
            else
                x="${x}${line}\n"
            fi
        done < <(echo "${modules_info}")
    done

    if ${debug} ; then
        cat "${tmpfile_mod}"
    fi
}

get_installed_rpm_packages() {
    if check_command_exists rpm ; then
        if ${verbose} ; then
            echo 'Finding installed rpms...'
        fi

        rpm -qa --queryformat "'%{NAME}' '%{EPOCH}' '%{VERSION}' '%{RELEASE}' '%{ARCH}' 'rpm'\n" 2>/dev/null \
        | sed -e 's/(none)//g' \
        | sed -e 's/\+/%2b/g' >> "${tmpfile_pkg}"

        if ${debug} ; then
            cat "${tmpfile_pkg}"
        fi
    fi
}

get_installed_deb_packages() {
    if check_command_exists dpkg-query ; then
        if ${verbose} ; then
            echo 'Finding installed debs...'
        fi
        dpkg-query -W --showformat='${Status}|${Package}|${Version}|${Architecture}\n' |
            awk -f <(cat <<'AWK'
                /^(install|hold) ok installed\|/ {
                    split($0, parts, "|");
                    package = parts[2];
                    version = parts[3];
                    architecture = parts[4];
                    epoch = "";
                    release = "";

                    if (split(version, epoch_t, ":") > 1) {
                        epoch = epoch_t[1];
                        version = substr(version, length(epoch) + 2);
                    }
                    n = split(version, version_t, "-");
                    if (n > 1) {
                        release = version_t[n]
                        version = substr(version, 1, length(version) - length(release) - 1);
                    }

                    print "'" package "'", "'" epoch "'", "'" version "'", "'" release "'", "'" architecture "'", "'deb'"
                }
AWK
            ) >> "${tmpfile_pkg}"
        if ${debug} ; then
            echo "'name' 'epoch' 'version' 'release' 'arch' 'type'"
            cat "${tmpfile_pkg}"
        fi
    fi
}

get_installed_archlinux_packages() {
    if check_command_exists pacman ; then
        IFS=${NL_IFS}
        pacman -Q -i | awk -v q="'" -v s=" " '/^Name/{n=$3} /^Version/{l=split($3, e, ":"); if (l==2) {ep=e[1]; v=e[2]} else {ep=""; v=$3}; split(v, r, "-")} /^Architecture/{a=$3} /^$/{print q n q s q ep q s q r[1] q s q r[2] q s q a q s q"arch"q}' >> "${tmpfile_pkg}"
        IFS=${FULL_IFS}
    fi
}

get_installed_gentoo_packages() {
    if check_command_exists qkeyword ; then
        gentoo_package_arch=$(qkeyword -A)
    fi
    if check_command_exists qlist ; then
        qlist -Ic -F "'%{PN}' '%{SLOT}' '%{PV}' REL'%{PR}' '${gentoo_package_arch}' 'gentoo' '%{CAT}' '%{REPO}'" | sed -e "s/REL'r/'/g"  >> "${tmpfile_pkg}"
    fi
}

get_packages() {
    get_installed_rpm_packages
    get_installed_deb_packages
    get_installed_archlinux_packages
    get_installed_gentoo_packages
}

get_modules() {
    check_for_modularity
    if ${modularity} ; then
        get_enabled_modules
    fi
}

get_hostname() {
    hostname=$(hostname -f)
    if [ -z "${hostname}" ] ; then
        short_hostname=$(hostname)
        if [ -z "${short_hostname}" ] ; then
            short_hostname=$(cat /etc/hostname)
        fi
        domainname=$(dnsdomainname)
        if [ ! -z "${domainname}" ] ; then
            hostname=${short_hostname}.${domainname}
        else
            hostname=${short_hostname}
        fi
    fi
}

get_host_data() {
    host_kernel=$(uname -r | sed -e 's/\+/%2b/g')
    host_arch=$(uname -m)
    os='unknown'
    if [ -f /etc/os-release ] ; then
        . /etc/os-release
        if [ "${ID}" == "debian" ] ; then
            os="Debian $(cat /etc/debian_version | sed -e 's/\//-/')"
        elif [ "${ID}" == "raspbian" ] ; then
            os="Raspbian $(cat /etc/debian_version)"
        elif [ "${ID}" == "ubuntu" ] ; then
            os="${PRETTY_NAME}"
        elif [ "${ID}" == "centos" ] ; then
            os="$(cat /etc/centos-release)"
        elif [ "${ID}" == "rhel" ] ; then
            os="$(cat /etc/redhat-release)"
        elif [ "${ID}" == "fedora" ] ; then
            os="${PRETTY_NAME}"
        elif [ "${ID}" == "arch" ] ; then
            os="${NAME}"
        elif [ "${ID}" == "gentoo" ] ; then
            os="${PRETTY_NAME} ${VERSION_ID}"
        elif [[ "${ID_LIKE}" =~ "suse" ]] ; then
            os="${PRETTY_NAME}"
        elif [ "${ID}" == "astra" ] ; then
            os="${NAME} $(cat /etc/astra_version)"
        else
            os="${NAME} ${VERSION}"
        fi
    else
        releases="/etc/SuSE-release /etc/lsb-release /etc/debian_version /etc/fermi-release /etc/redhat-release /etc/fedora-release /etc/centos-release"
        for i in ${releases} ; do
            if [ -f ${i} ] ; then
                case "${i}" in
                /etc/SuSE-release)
                    os=$(grep -i suse ${i})
                    break
                    ;;
                /etc/lsb-release)
                    tmp_os=$(grep DISTRIB_DESCRIPTION ${i})
                    os=$(echo ${tmp_os} | sed -e 's/DISTRIB_DESCRIPTION="\(.*\)"/\1/')
                    if [ -z "${os}" ] ; then
                        tmp_os=$(grep  DISTRIB_DESC ${i})
                        os=$(echo ${tmp_os} | sed -e 's/DISTRIB_DESC="\(.*\)"/\1/')
                    fi
                    if [ -z "${os}" ] ; then
                        continue
                    fi
                    break
                    ;;
                /etc/debian_version)
                    os="Debian $(cat ${i})"
                    break
                    ;;
                /etc/fermi-release|/etc/redhat-release|/etc/fedora-release|/etc/centos-release)
                    os=$(cat ${i})
                    break
                    ;;
                esac
            fi
        done
    fi
    if [ ! -z "${CPE_NAME}" ] ; then
        os="${os} [${CPE_NAME}]"
    fi
    if ${verbose} ; then
        echo "Kernel:   ${host_kernel}"
        echo "Arch:     ${host_arch}"
        echo "OS:       ${os}"
    fi
}

get_yum_updates() {
    yum -q makecache 2>/dev/null
    let yum_major_version=$(yum --version | head -n 1 | cut -d "." -f 1)
    if [ ${yum_major_version} -gt 3 ] ; then
        yum -q -C --security list updates --disablerepo="*" --enablerepo="${1}" 2>&1 \
        | tr "\n" "#" | sed -e 's/# / /g' | tr "#" "\n" \
        | grep -v ': ' \
        | grep -v 'Limiting package lists to security relevant ones' \
        | grep -v 'Available Upgrades' \
        | grep -v 'Updated Packages' \
        | grep -v 'excluded' \
        | grep -v 'Last metadata expiration check' \
        | grep -v 'needed for security' \
        | grep -v 'Loaded plugins' \
        | grep -v 'Subscription Management' \
        | grep -v 'Failed to set locale' \
        >> "${tmpfile_sec}"
        sed -i '/^$/d' "${tmpfile_sec}"
    fi
    yum -q -C list updates --disablerepo="*" --enablerepo="${1}" 2>&1 \
    | tr "\n" "#" | sed -e 's/# / /g' | tr "#" "\n" \
    | grep -v ': ' \
    | grep -v 'Available Upgrades' \
    | grep -v 'Updated Packages' \
    | grep -v 'excluded' \
    | grep -v 'Last metadata expiration check' \
    | grep -v 'needed for security' \
    | grep -v 'Loaded plugins' \
    | grep -v 'Subscription Management' \
    | grep -v 'Failed to set locale' \
    >> "${tmpfile_bug}"
    sed -i '/^$/d' "${tmpfile_bug}"
    if [ ! -z ${CPE_NAME} ] ; then
        sed -i -e "s#${1}#${CPE_NAME}-${1}#g" "${tmpfile_sec}"
        sed -i -e "s#${1}#${CPE_NAME}-${1}#g" "${tmpfile_bug}"
    fi
}

get_zypper_updates() {
    zypper -q -n ref
    # can't differentiate between security and bugfix updates yet
    zypper -q -n -s11 lu -r ${1} | grep ^v | awk '{print $2"."$5,$4}' | sed -e "s/$/ ${1}/" >> "${tmpfile_bug}"
}

get_apt_updates() {
    if ! check_command_exists apt ; then
        return
    fi
    if ${verbose} ; then
        echo 'Finding apt updates...'
    fi
    apt list --upgradable 2>/dev/null | grep -v '^Listing' | while IFS= read -r line ; do
        if [ -z "${line}" ] ; then
            continue
        fi
        # Format: package/suite version arch [upgradable from: old-version]
        pkg=$(echo "${line}" | cut -d '/' -f 1)
        suite=$(echo "${line}" | cut -d '/' -f 2 | cut -d ' ' -f 1)
        version=$(echo "${line}" | awk '{print $2}')
        arch=$(echo "${line}" | awk '{print $3}')
        if echo "${suite}" | grep -qi 'security' ; then
            echo "${pkg}.${arch} ${version}" >> "${tmpfile_sec}"
        else
            echo "${pkg}.${arch} ${version}" >> "${tmpfile_bug}"
        fi
    done
}

get_repos() {
    IFS=${NL_IFS}

    # Red Hat / CentOS
    if check_command_exists yum ; then
        if ${verbose} ; then
            echo 'Finding yum repos...'
        fi
        releasever=$(rpm -q --qf "%{version}\n" --whatprovides redhat-release | sort -u)
        let numrepos=$(ls /etc/yum.repos.d/*.repo | wc -l)
        if [ ${numrepos} -gt 0 ] ; then
            priorities=$(sed -n -e "/^name/h; /priority *=/{ G; s/\n/ /; s/ity *= *\(.*\)/ity=\1/ ; s/\$releasever/${releasever}/ ; s/name *= *\(.*\)/'\1 ${host_arch}'/ ; p }" /etc/yum.repos.d/*.repo)
        fi
        # replace this with a dedicated awk or simple python script?
        yum_repolist=$(yum repolist enabled --verbose 2>/dev/null | sed -e "s/:\? *([0-9]\+ more)$//g" -e "s/ ([0-9]\+$//g" -e "s/:\? more)$//g" -e "s/'//g" -e "s/%/%%/g")
        for i in $(echo "${yum_repolist}" | awk 'BEGIN{n=0} { if ($1=="Repo-id") {if(n>0){print ""} n++; url=0; printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'"'"' "} if ($1=="Repo-name") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'" ${host_arch}'"' "} if ($1=="Repo-mirrors" || $1=="Repo-metalink") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'"'"' "} if ($1=="Repo-baseurl" || $1=="Repo-baseurl:") { url=1; comma=match($NF,","); if (comma) out=substr($NF,1,comma-1); else out=$NF; printf "'"'"'"out"'"'"' "; } else { if (url==1) { if ($1==":") { comma=match($NF,","); if (comma) out=substr($NF,1,comma-1); else out=$NF; printf "'"'"'"out"'"'"' "; } else {url=0} } } } END{if(n>0) print ""}' | sed -e "s/\/'/'/g" | sed -e "s/ ' /' /") ; do
            full_id=$(echo ${i} | cut -d \' -f 2)
            id=$(echo ${i} | cut -d \' -f 2 | cut -d \/ -f 1)
            orig_name=$(echo ${i} | cut -d \' -f 4)
            # Strip " - arch arch" suffix pattern to avoid duplicates like "EPEL - x86_64 x86_64"
            name=$(echo "${orig_name}" | sed -e "s/ - ${host_arch} ${host_arch}$/ ${host_arch}/")
            if [ "${priorities}" != "" ] ; then
                priority=$(echo "${priorities}" | grep "'${name}'" | sed -e "s/priority=\(.*\) '${name}'/\1/")
            fi
            # default yum priority is 99
            if [ "${priority}" == "" ] ; then
                priority=99
            fi
            redhat_repo=$(echo ${i} | grep -e "https://.*/XMLRPC.*\|https://cdn[-[a-z]*]*.redhat.com/.*")
            if [ ${?} == 0 ] || ${local_updates} ; then
                if ${verbose} ; then
                    echo "Finding updates locally for ${id}"
                fi
                get_yum_updates ${id}
            fi
            if [ ! -z ${CPE_NAME} ] ; then
                id="${CPE_NAME}-${id}"
            fi
            j=$(echo ${i} | sed -e "s#'${full_id}' '${orig_name}'#'${name}' '${id}' '${priority}'#" | sed -e "s/'\[/'/g" -e "s/\]'/'/g")
            echo "'rpm' ${j}" >> "${tmpfile_rep}"
            unset priority
        done
    fi

    # Debian
    if check_command_exists apt-cache ; then
        if ${verbose} ; then
            echo 'Finding apt repos...'
        fi
        osname=$(echo ${os} | cut -d " " -f 1)
        shortversion=${VERSION_ID}
        repo_string="'deb\' \'${osname} ${shortversion} ${host_arch} repo at"
        repos=$(apt-cache policy | grep -v Translation | grep -E "^ *[0-9]{1,5}" | grep -E " mirror\+file|http(s)?:" | sed -e "s/^ *//g" -e "s/ *$//g" | cut -d " " -f 1,2,3,4)
        non_mirror_repos=$(echo "${repos}" | grep -Ev "mirror\+file")
        dist_repos=$(echo "${non_mirror_repos}" | grep -v -e "Packages$")
        nondist_repos=$(echo "${non_mirror_repos}" | grep -e "Packages$")
        mirror_repos=$(echo "${repos}" | grep -E "mirror\+file")
        for mirror_repo in ${mirror_repos} ; do
            mirror_file=$(echo "${mirror_repo}" | sed -e "s/.* mirror+file://g" | cut -d " " -f 1)
            if [ -f "${mirror_file}" ] ; then
                for url in $(cat ${mirror_file}) ; do
                    dist_repo=$(echo "${mirror_repo}" | sed -e "s#mirror+file:${mirror_file}#${url}#g")
                    dist_repos="${dist_repos}"$'\n'"${dist_repo}"
                done
            fi
        done
        echo "${dist_repos}" | sed -e "s/\([0-9]*\) \(http:.*\|https:.*\)[\/]\? \(.*\/.*\) \(.*\)/${repo_string} \2\/dists\/\3\/binary-\4' '\1' '\2\/dists\/\3\/binary-\4'/" >> "${tmpfile_rep}"
        echo "${nondist_repos}" | sed -e "s/\([0-9]*\) \(http:.*\|https:.*\)[\/]\? \(.*\/\?.*\) Packages/${repo_string} \2\/\3' '\1' '\2\/\3'/" >> "${tmpfile_rep}"
    fi

    # SUSE
    if check_command_exists zypper ; then
        if ${verbose} ; then
            echo 'Finding zypper repos...'
        fi
        if [ $(zypper -q --no-refresh lr --details | head -n 1 | grep Keep) ] ; then
            zypper_lr_cols='{print "${os}" $3 "|" $2 "|" $8 "|" $10}'
        else
            zypper_lr_cols='{print "${os}" $3 "|" $2 "|" $7 "|" $9}'
        fi
        for i in $(zypper -q --no-refresh lr -E -u --details | grep -v ^$ | tail -n +3 | awk -F"|" "${zypper_lr_cols}" | sed -e "s/\${os}/${PRETTY_NAME}/" -e "s/ *|/ ${host_arch} |/" -e "s/\?[a-zA-Z0-9_-]* *$//" -e "s/^/'/g" -e "s/ *| */' '/g" -e "s/ *$/'/g") ; do
            echo \'rpm\' ${i} >> "${tmpfile_rep}"
            id=$(echo ${i} | cut -d \' -f 4)
            suse_repo=$(echo ${i} | grep -e "https://updates.suse.com/.*")
            if [ ${?} == 0 ] || ${local_updates} ; then
                if ${verbose} ; then
                    echo "Finding updates locally for ${id}"
                fi
                get_zypper_updates ${id}
            fi
        done
    fi

    # Arch
    if check_command_exists pacman ; then
        if ${verbose} ; then
            echo 'Finding pacman repos...'
        fi
        declare -A repos
        pacman_conf=$(awk '/\[/{prefix=$0; next} $1{print prefix $0}' /etc/pacman.conf | grep -v '\[options\]' | grep '^\[')
        for stanza in ${pacman_conf} ; do
            repo=$(echo ${stanza} | cut -d ']' -f 1 | sed -e 's/\[//')
            rhs=$(echo ${stanza} | cut -d ']' -f 2 | grep -v '^#')
            if [[ ${rhs} =~ "Include" ]] ; then
                include=$(echo ${rhs} | sed -e 's/^ *Include *= *//')
                for f in $(ls ${include} 2>/dev/null) ; do
                    if [ -f ${f} ] ; then
                        servers=$(cat ${f} | grep Server | sed -e 's/^ *Server *= *//')
                        for s in ${servers} ; do
                            repos[${repo}]+="'$(eval echo ${s})' "
                        done
                    fi
                done
            elif [[ ${rhs} =~ "Server" ]] ; then
                s=$(echo ${rhs} | sed -e 's/^ *Server *= *//')
                repos[${repo}]+="'$(eval echo ${s})' "
            fi
        done
        for r in "${!repos[@]}"; do
            echo "'arch' 'Arch Linux ${r} ${host_arch}' '${r}' ${repos[${r}]}" >> "${tmpfile_rep}"
        done
    fi

    # Gentoo
    if [[ "${os}" =~ "Gentoo" ]] ; then
        if [ ${verbose} == 1 ] ; then
            echo 'Finding portage repos...'
        fi
        declare -A repo_info
        repos_output=$(portageq repos_config /)
        repo_name=""
        priority=""
        sync_uri=""

        while IFS= read -r line; do
            # if the line starts with a section header (e.g., [gentoo], [guru]), it's the repo name
            if [[ "${line}" =~ ^\[(.*)\] ]]; then
                # if we already have a repo_name, save the previous entry
                if [[ -n "${repo_name}" && -n "${sync_uri}" ]]; then
                    repo_info["${repo_name}"]="${priority},${sync_uri}"
                fi
                # else start new repo parsing, resetting vars
                repo_name="${BASH_REMATCH[1]}"
                priority=""
                sync_uri=""
            fi

            # if the line contains "priority", extract the value, 0 if it doesnt exist
            if [[ "${line}" =~ "priority" ]]; then
                priority=$(echo "${line}" | cut -d'=' -f2 | xargs)
            fi

            # if the line contains "sync-uri", extract the value
            if [[ "${line}" =~ "sync-uri" ]]; then
                sync_uri=$(echo "${line}" | cut -d'=' -f2 | xargs)
            fi
        done <<< "${repos_output}"

        # save the last repository entry if it's available
        if [[ -n "${repo_name}" && -n "${sync_uri}" ]]; then
            repo_info["${repo_name}"]="${priority},${sync_uri}"
        fi

        for repo in "${!repo_info[@]}"; do
            priority=$(echo ${repo_info[$repo]} | cut -d',' -f1)
            sync_uri=$(echo ${repo_info[$repo]} | cut -d',' -f2)
            if [ "${priority}" == "" ] ; then
                priority=0
            fi
            echo "'gentoo' 'Gentoo Linux ${repo} Repo ${host_arch}' '${repo}' '${priority}' '${sync_uri}'" >> "${tmpfile_rep}"
        done
    fi

    IFS=${FULL_IFS}

    sed -i -e '/^$/d' "${tmpfile_rep}"

    if ${debug} ; then
        cat "${tmpfile_rep}"
    fi
}

reboot_required() {
    # Debian/Ubuntu: update-notifier-common sets this file
    if [ -e /var/run/reboot-required ] ; then
        reboot=True
        return
    fi

    # Compare running vs installed kernel via /boot/vmlinuz symlink
    if [ -e /proc/sys/kernel/osrelease ] && [ -L /boot/vmlinuz ] ; then
        running_kernel=$(cat /proc/sys/kernel/osrelease)
        installed_kernel=$(readlink /boot/vmlinuz | sed -e 's/^vmlinuz-//')
        if [ "${running_kernel}" != "${installed_kernel}" ] ; then
            reboot=True
            return
        fi
    fi

    reboot=ServerCheck
}

build_packages_json() {
    # Convert packages file to JSON array, writing to temp file
    local outfile="${1}"
    echo "[" > "${outfile}"
    local first=true
    while IFS= read -r line ; do
        if [ -z "${line}" ] ; then
            continue
        fi
        # Parse: 'name' 'epoch' 'version' 'release' 'arch' 'type' ['category'] ['repo']
        # Extract quoted fields properly
        local parts=()
        while IFS= read -r part ; do
            parts+=("${part}")
        done < <(echo "${line}" | grep -oP "'[^']*'" | tr -d "'")

        local name="${parts[0]:-}"
        local epoch="${parts[1]:-}"
        local version="${parts[2]:-}"
        local release="${parts[3]:-}"
        local arch="${parts[4]:-}"
        local ptype="${parts[5]:-}"
        local category="${parts[6]:-}"
        local repo="${parts[7]:-}"

        # Skip packages without a valid type
        if [ -z "${ptype}" ] ; then
            continue
        fi

        # Skip gpg-pubkey entries (they're not real packages)
        if [ "${name}" == "gpg-pubkey" ] ; then
            continue
        fi

        if ! ${first} ; then
            echo "," >> "${outfile}"
        fi
        first=false

        jq -n -c \
            --arg name "${name}" \
            --arg epoch "${epoch}" \
            --arg version "${version}" \
            --arg release "${release}" \
            --arg arch "${arch}" \
            --arg type "${ptype}" \
            --arg category "${category}" \
            --arg repo "${repo}" \
            '{name: $name, epoch: $epoch, version: $version, release: $release, arch: $arch, type: $type, category: $category, repo: $repo}' >> "${outfile}"
    done < "${tmpfile_pkg}"
    echo "]" >> "${outfile}"
}

build_repos_json() {
    # Convert repos file to JSON array, writing to temp file
    local outfile="${1}"
    echo "[" > "${outfile}"
    local first=true
    while IFS= read -r line ; do
        if [ -z "${line}" ] ; then
            continue
        fi
        # Parse quoted fields from line
        local parts=()
        while IFS= read -r part ; do
            parts+=("${part}")
        done < <(echo "${line}" | grep -oP "'[^']*'" | tr -d "'")

        local rtype="${parts[0]}"
        local name="${parts[1]}"
        local rid=""
        local priority=""
        local url_start=0

        # APT repos have 4 fields: 'type' 'name' 'priority' 'url'
        # Other repos have 5+ fields: 'type' 'name' 'id' 'priority' 'url1' ...
        if [ "${rtype}" == "deb" ] ; then
            rid=""
            priority="${parts[2]}"
            url_start=3
        else
            rid="${parts[2]}"
            priority="${parts[3]}"
            url_start=4
        fi

        # Collect URLs (all remaining parts)
        local urls_json="["
        local url_first=true
        for ((i=${url_start}; i<${#parts[@]}; i++)); do
            if ! ${url_first} ; then
                urls_json="${urls_json},"
            fi
            url_first=false
            urls_json="${urls_json}\"${parts[i]}\""
        done
        urls_json="${urls_json}]"

        if ! ${first} ; then
            echo "," >> "${outfile}"
        fi
        first=false

        jq -n -c \
            --arg type "${rtype}" \
            --arg name "${name}" \
            --arg id "${rid}" \
            --argjson priority "${priority:-0}" \
            --argjson urls "${urls_json}" \
            '{type: $type, name: $name, id: $id, priority: $priority, urls: $urls}' >> "${outfile}"
    done < "${tmpfile_rep}"
    echo "]" >> "${outfile}"
}

build_modules_json() {
    # Convert modules file to JSON array, writing to temp file
    local outfile="${1}"
    echo "[" > "${outfile}"
    local first=true
    while IFS= read -r line ; do
        if [ -z "${line}" ] ; then
            continue
        fi
        # Parse: 'name' 'stream' 'version' 'context' 'arch' 'repo' ['pkg1' 'pkg2' ...]
        local parts=()
        while IFS= read -r part ; do
            parts+=("${part}")
        done < <(echo "${line}" | grep -oP "'[^']*'" | tr -d "'")

        local name="${parts[0]}"
        local stream="${parts[1]}"
        local version="${parts[2]}"
        local context="${parts[3]}"
        local arch="${parts[4]}"
        local repo="${parts[5]}"

        # Collect packages (all remaining parts)
        local pkgs_json="["
        local pkg_first=true
        for ((i=6; i<${#parts[@]}; i++)); do
            if ! ${pkg_first} ; then
                pkgs_json="${pkgs_json},"
            fi
            pkg_first=false
            pkgs_json="${pkgs_json}\"${parts[i]}\""
        done
        pkgs_json="${pkgs_json}]"

        if ! ${first} ; then
            echo "," >> "${outfile}"
        fi
        first=false

        jq -n -c \
            --arg name "${name}" \
            --arg stream "${stream}" \
            --arg version "${version}" \
            --arg context "${context}" \
            --arg arch "${arch}" \
            --arg repo "${repo}" \
            --argjson packages "${pkgs_json}" \
            '{name: $name, stream: $stream, version: $version, context: $context, arch: $arch, repo: $repo, packages: $packages}' >> "${outfile}"
    done < "${tmpfile_mod}"
    echo "]" >> "${outfile}"
}

build_updates_json() {
    # Convert updates file to JSON array, writing to temp file
    local updates_file="${1}"
    local outfile="${2}"
    echo "[" > "${outfile}"
    local first=true
    while IFS= read -r line ; do
        if [ -z "${line}" ] ; then
            continue
        fi
        # Parse: name.arch version repo
        local parts=(${line})
        local name_arch="${parts[0]}"
        local version="${parts[1]}"
        local repo="${parts[2]}"

        # Split name.arch
        local name="${name_arch%.*}"
        local arch="${name_arch##*.}"

        if ! ${first} ; then
            echo "," >> "${outfile}"
        fi
        first=false

        jq -n -c \
            --arg name "${name}" \
            --arg version "${version}" \
            --arg arch "${arch}" \
            --arg repo "${repo}" \
            '{name: $name, version: $version, arch: $arch, repo: $repo}' >> "${outfile}"
    done < "${updates_file}"
    echo "]" >> "${outfile}"
}

build_json_report() {
    # Build complete JSON report using temp files
    # Temp files are created by post_json_data before calling this function

    build_packages_json "${tmpfile_packages_json}"
    build_repos_json "${tmpfile_repos_json}"
    build_modules_json "${tmpfile_modules_json}"
    build_updates_json "${tmpfile_sec}" "${tmpfile_sec_json}"
    build_updates_json "${tmpfile_bug}" "${tmpfile_bug_json}"

    # Convert tags to JSON array
    local tags_json="[]"
    if [ ! -z "${tags}" ] && [ "${tags}" != "Default" ] ; then
        tags_json=$(echo "${tags}" | tr ',' '\n' | jq -R . | jq -s .)
    fi

    # Convert reboot to boolean
    local reboot_bool="false"
    if [ "${reboot}" == "True" ] ; then
        reboot_bool="true"
    fi

    jq -n \
        --argjson protocol 2 \
        --arg hostname "${hostname}" \
        --arg arch "${host_arch}" \
        --arg kernel "${host_kernel}" \
        --arg os "${os}" \
        --argjson tags "${tags_json}" \
        --argjson reboot_required "${reboot_bool}" \
        --slurpfile packages "${tmpfile_packages_json}" \
        --slurpfile repos "${tmpfile_repos_json}" \
        --slurpfile modules "${tmpfile_modules_json}" \
        --slurpfile sec_updates "${tmpfile_sec_json}" \
        --slurpfile bug_updates "${tmpfile_bug_json}" \
        '{
            protocol: $protocol,
            hostname: $hostname,
            arch: $arch,
            kernel: $kernel,
            os: $os,
            tags: $tags,
            reboot_required: $reboot_required,
            packages: $packages[0],
            repos: $repos[0],
            modules: $modules[0],
            sec_updates: $sec_updates[0],
            bug_updates: $bug_updates[0]
        }'
}

post_json_data() {
    # Post data using protocol 2 (JSON)
    curl_opts=${curl_options}

    if ${verbose} ; then
        curl_opts="${curl_opts} -i"
        echo "Sending JSON data to ${server} with curl (protocol 2):"
    else
        curl_opts="${curl_opts} -s -S"
    fi

    # Create temp files for JSON (global so cleanup can handle them)
    tmpfile_packages_json=$(mktemp)
    tmpfile_repos_json=$(mktemp)
    tmpfile_modules_json=$(mktemp)
    tmpfile_sec_json=$(mktemp)
    tmpfile_bug_json=$(mktemp)
    tmpfile_report_json=$(mktemp)

    # Build JSON report to temp file
    build_json_report > "${tmpfile_report_json}"

    if ${debug} ; then
        echo "JSON Report:"
        jq . "${tmpfile_report_json}"
    fi

    curl_opts="${curl_opts} -H 'Content-Type: application/json'"
    if [ ! -z "${api_key}" ] ; then
        curl_opts="${curl_opts} -H 'Authorization: Api-Key ${api_key}'"
    fi
    curl_opts="${curl_opts} -d @${tmpfile_report_json}"

    post_command="curl ${curl_opts} ${server%/}/api/report/"

    if ${verbose} ; then
        echo "${post_command}"
    fi

    result=$(eval "${post_command}")
    retval=${?}

    if [ ! ${retval} -eq 0 ] ; then
        echo 'Failed to upload report.'
        exit ${retval}
    fi

    if ${report} || ${verbose} ; then
        if [ ! -z "${result}" ] ; then
            echo "${result}" | jq . 2>/dev/null || echo "${result}"
        else
            echo "No output returned."
        fi
    fi
}

post_data() {
    curl_opts=${curl_options}

    if ${verbose} ; then
        curl_opts="${curl_opts} -i"
        echo "Sending data to ${server} with curl:"
    else
        curl_opts="${curl_opts} -s -S";
    fi

    if [ -z "${tags}" ] ; then
        tags='Default'
    fi

    sed -i -e 's/%2b/\+/g' "${tmpfile_pkg}"

    curl_opts="${curl_opts} -F host=\"${hostname}\""
    curl_opts="${curl_opts} -F tags=\"${tags}\""
    curl_opts="${curl_opts} -F kernel=\"${host_kernel}\""
    curl_opts="${curl_opts} -F arch=\"${host_arch}\""
    curl_opts="${curl_opts} -F protocol=\"${protocol}\""
    curl_opts="${curl_opts} -F os=\"${os}\""
    curl_opts="${curl_opts} -F report=\"${report}\""
    curl_opts="${curl_opts} -F packages=\<${tmpfile_pkg}"
    curl_opts="${curl_opts} -F repos=\<${tmpfile_rep}"
    curl_opts="${curl_opts} -F sec_updates=\<${tmpfile_sec}"
    curl_opts="${curl_opts} -F bug_updates=\<${tmpfile_bug}"
    curl_opts="${curl_opts} -F modules=\<${tmpfile_mod}"
    curl_opts="${curl_opts} -F reboot=\"${reboot}\""
    post_command="curl ${curl_opts} ${server%/}/reports/upload/"

    if ${verbose} ; then
        echo "${post_command}"
    fi

    result=$(eval "${post_command}")
    retval=${?}

    if [ ! ${retval} -eq 0 ] ; then
        echo 'Failed to upload report.'
        exit ${retval}
    fi

    if ${report} || ${verbose} ; then
        if [ ! -z "${result}" ] ; then
            echo "${result}"
        else
            echo "No output returned."
        fi
    fi
}

if ! check_command_exists awk || \
   ! check_command_exists mktemp || \
   ! check_command_exists curl || \
   ! check_command_exists flock ; then
    echo "awk, mktemp, flock or curl was not found, exiting."
    exit 1
fi

os_lock_dir=/var/lock
lock_dir=$(dirname $(readlink -f ${os_lock_dir}))/patchman
mkdir -p "${lock_dir}"
if [ ! -d "${lock_dir}" ] ; then
    echo "Lock directory does not exist, exiting: ${lock_dir}"
    exit 1
fi

parseopts "$@"

if ${verbose} ; then
    echo "Attempting to obtain lock: ${lock_dir}/patchman.lock"
fi

exec 200>"${lock_dir}/patchman.lock"
flock -xn 200 || exit 1

check_conf

trap cleanup EXIT
tmpfile_pkg=$(mktemp)
tmpfile_rep=$(mktemp)
tmpfile_sec=$(mktemp)
tmpfile_bug=$(mktemp)
tmpfile_mod=$(mktemp)

get_host_data
get_packages
get_modules
if ${repo_check} ; then
    get_repos
fi
if ${local_updates} ; then
    get_apt_updates
fi
reboot_required

if ${dry_run} ; then
    echo
    echo "=== Dry Run Summary ==="
    echo "Hostname: ${hostname}"
    echo "OS: ${os}"
    echo "Arch: ${host_arch}"
    echo "Kernel: ${host_kernel}"
    echo "Packages: $(wc -l < ${tmpfile_pkg})"
    echo "Repos: $(wc -l < ${tmpfile_rep})"
    echo "Modules: $(wc -l < ${tmpfile_mod})"
    echo "Security updates: $(wc -l < ${tmpfile_sec})"
    echo "Bugfix updates: $(wc -l < ${tmpfile_bug})"
    echo "Reboot required: ${reboot}"
    if [ ! -z "${tags}" ] ; then
        echo "Tags: ${tags}"
    fi
    if ${verbose} ; then
        echo
        echo "=== Packages ==="
        cut -d \' -f 2 "${tmpfile_pkg}" | sort
        echo
        echo "=== Repos ==="
        awk -F\' '{printf "  [%s] %s\n", $2, $4}' "${tmpfile_rep}"
        if [ -s "${tmpfile_sec}" ] ; then
            echo
            echo "=== Security Updates ==="
            awk -F\' '{printf "  %s %s\n", $2, $4}' "${tmpfile_sec}"
        fi
        if [ -s "${tmpfile_bug}" ] ; then
            echo
            echo "=== Bugfix Updates ==="
            awk -F\' '{printf "  %s %s\n", $2, $4}' "${tmpfile_bug}"
        fi
        if [ -s "${tmpfile_mod}" ] ; then
            echo
            echo "=== Modules ==="
            awk -F\' '{printf "  %s\n", $2}' "${tmpfile_mod}"
        fi
    fi
    if ${debug} ; then
        if [ "${protocol}" == "2" ] && check_command_exists jq ; then
            tmpfile_packages_json=$(mktemp)
            tmpfile_repos_json=$(mktemp)
            tmpfile_modules_json=$(mktemp)
            tmpfile_sec_json=$(mktemp)
            tmpfile_bug_json=$(mktemp)
            echo
            echo "=== Full JSON Report ==="
            build_json_report
        else
            echo
            echo "=== Raw Data Files ==="
            echo "--- ${tmpfile_pkg} ---"
            cat "${tmpfile_pkg}"
            echo "--- ${tmpfile_rep} ---"
            cat "${tmpfile_rep}"
            echo "--- ${tmpfile_sec} ---"
            cat "${tmpfile_sec}"
            echo "--- ${tmpfile_bug} ---"
            cat "${tmpfile_bug}"
            echo "--- ${tmpfile_mod} ---"
            cat "${tmpfile_mod}"
        fi
    fi
    exit 0
fi

# Use protocol 2 (JSON) or protocol 1 (form data) based on config
if [ "${protocol}" == "2" ] ; then
    post_json_data
else
    post_data
fi
