#!/bin/bash

# Copyright (c) 2019  Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

VERSION="1.0"

# Warning! Be sure to download the latest version of this script from its primary source:

ARTICLE="https://access.redhat.com/solutions/tsx-asynchronousabort"

# DO NOT blindly trust any internet sources and NEVER do `curl something | bash`!

# This script is meant for simple detection of the vulnerability. Feel free to modify it for your
# environment or needs. For more advanced detection, consider Red Hat Insights:
# https://access.redhat.com/products/red-hat-insights#getstarted


basic_args() {
    # Parses basic commandline arguments and sets basic environment.
    #
    # Args:
    #     parameters - an array of commandline arguments
    #
    # Side effects:
    #     Exits if --help parameters is used
    #     Sets COLOR constants and debug variable

    local parameters=( "$@" )

    RED="\\033[1;31m"
    GREEN="\\033[1;32m"
    BOLD="\\033[1m"
    RESET="\\033[0m"
    for parameter in "${parameters[@]}"; do
        if [[ "$parameter" == "-h" || "$parameter" == "--help" ]]; then
            echo "Usage: $( basename "$0" ) [-n | --no-colors] [-d | --debug]"
            exit 1
        elif [[ "$parameter" == "-n" || "$parameter" == "--no-colors" ]]; then
            RED=""
            GREEN=""
            BOLD=""
            RESET=""
        elif [[ "$parameter" == "-d" || "$parameter" == "--debug" ]]; then
            debug=true
        fi
    done
}


basic_reqs() {
    # Prints common disclaimer and checks basic requirements.
    #
    # Args:
    #     CVE - string printed in the disclaimer
    #
    # Side effects:
    #     Exits when 'rpm' command is not available

    local CVE="$1"

    # Disclaimer
    echo
    echo -e "${BOLD}This script (v$VERSION) is primarily designed to detect $CVE on supported"
    echo -e "Red Hat Enterprise Linux systems and kernel packages."
    echo -e "Result may be inaccurate for other RPM based systems.${RESET}"
    echo

    # RPM is required
    if ! command -v rpm &> /dev/null; then
        echo "'rpm' command is required, but not installed. Exiting."
        exit 1
    fi
}


check_supported_kernel() {
    # Checks if running kernel is supported.
    #
    # Args:
    #     running_kernel - kernel string as returned by 'uname -r'
    #
    # Side effects:
    #     Exits when running kernel is obviously not supported

    local running_kernel="$1"

    # Check supported platform
    if [[ "$running_kernel" != *".el"[5-8]* ]]; then
        echo -e "${RED}This script is meant to be used only on RHEL 5-8.${RESET}"
        exit 1
    fi
}


get_rhel() {
    # Gets RHEL number.
    #
    # Args:
    #     running_kernel - kernel string as returned by 'uname -r'
    #
    # Prints:
    #     RHEL number, e.g. '5', '6', '7', or '8'

    local running_kernel="$1"

    local rhel
    rhel=$( sed -r -n 's/^.*el([[:digit:]]).*$/\1/p' <<< "$running_kernel" )
    echo "$rhel"
}


get_virtualization() {
    # Gets virtualization type.
    #
    # Prints:
    #     Virtualization type, "None", or "virt-what not available"

    local virt

    if command -v virt-what &> /dev/null; then
        virt=$( virt-what 2>&1 | tr '\n' ' ' )
        if [[ "$virt" ]]; then
            echo "$virt"
        else
            echo "None"
        fi
    else
        echo "virt-what not available"
    fi
}


set_default_values() {
    avail_vuln_file=0
    vuln_file_value=""
    cpu_vendor="Unknown"
    cpu_model_name=""
    cpu_model=""
    cpu_flags=()
    arch=""

    vulnerable_taa=0
    update_microcode=0
    vulnerable_cpu_flag_hle=""
    vulnerable_cpu_flag_rtm=""
    affected_cpu=0
    vulnerable=0
    result=0
}


check_cpu_flag() {
    # Checks CPU flag if it is in list of cpu flags from /proc/cpuinfo.
    #
    # Args:
    #     flag - cpu flag string to check
    #     cpuinfo_flags - an array of cpu flags as parsed from /proc/cpuinfo
    #
    # Prints:
    #     Vulnerable cpu flag string as parsed from /proc/cpuinfo, or nothing
    local flag="$1"
    shift
    local cpuinfo_flags=( "$@" )

    for tested_flag in "${cpuinfo_flags[@]}"; do
        if [[ "$flag" == "$tested_flag" ]]; then
            echo "$flag"
            break
        fi
    done
}


read_array() {
    # Reads lines from stdin and saves them in a global array referenced by a name.
    # It is a poor man's readarray compatible with Bash 3.1.
    #
    # Args:
    #     array_name - name of the global array
    #
    # Side effects:
    #     Overwrites content of the array 'array_name' with lines from stdin
    local array_name="$1"

    local i=0
    while IFS= read -r line; do
        read -r "${array_name}[$(( i++ ))]" <<< "$line"
    done
}


parse_facts() {
    # Gathers all available information and stores it in global variables. Only store facts and
    # do not draw conclusion in this function for better maintainability.
    #
    # Side effects:
    #     Sets many global boolean flags and content variables
    #
    # Notes:
    #     MOCK_VULN_FILE_PATH can be used to mock /sys/devices/system/cpu/vulnerabilities/tsx_async_abort file
    #     MOCK_CPU_INFO_PATH can be used to mock /proc/cpuinfo file
    #     MOCK_ARCH can be used to mock `uname -r` command output

    local vuln_file_path=${MOCK_VULN_FILE_PATH:-/sys/devices/system/cpu/vulnerabilities/tsx_async_abort}
    local cpuinfo_path=${MOCK_CPU_INFO_PATH:-/proc/cpuinfo}

    # Parse vulnerability file
    if [[ -r "${vuln_file_path}" ]]; then
        avail_vuln_file=1
        vuln_file_value=$( <"${vuln_file_path}" )
    fi

    # Parse CPU info for vendor data
    cpuinfo_value=$( <"${cpuinfo_path}" )
    if [[ "$cpuinfo_value" =~ "GenuineIntel" ]]; then
        cpu_vendor="Intel"
    elif [[ "$cpuinfo_value" =~ "AuthenticAMD" ]]; then
        cpu_vendor="AMD"
    elif [[ "$cpuinfo_value" =~ "POWER" ]]; then
        cpu_vendor="POWER"
    fi

    # Parse CPU info for model data
    if [[ "$cpu_vendor" == "Intel" || "$cpu_vendor" == "AMD" ]]; then
        cpu_model_name="$( awk '/model name/ { for(i = 4; i < NF; i++) printf "%s ", $i; print $i; exit }' <<< "$cpuinfo_value" )"
        cpu_model="$( awk '/model/ && NF == 3 { print $3; exit }' <<< "$cpuinfo_value" )"
    elif [[ "$cpu_vendor" == "POWER" ]]; then
        cpu_model_name="$( awk '/cpu/ { for(i = 3; i < NF; i++) printf "%s ", $i; print $i; exit }' <<< "$cpuinfo_value" )"
    else
        # Fallback
        cpu_model_name="$( awk '/model name/ { for(i = 4; i < NF; i++) printf "%s ", $i; print $i; exit }' <<< "$cpuinfo_value" )"
    fi

    # Parse CPU info cpu flags
    if [[ "$cpu_vendor" == "Intel" || "$cpu_vendor" == "AMD" ]]; then
        read_array cpu_flags <<< "$( awk '/flags/ { for(i = 3; i < NF; i++) printf "%s\n", $i; print $i; exit }' <<< "$cpuinfo_value" )"
    fi

    # Check vulnerable cpu flags
    if [[ "$cpu_vendor" == "Intel" ]]; then
        vulnerable_cpu_flag_hle=$( check_cpu_flag "hle" "${cpu_flags[@]}" )
        vulnerable_cpu_flag_rtm=$( check_cpu_flag "rtm" "${cpu_flags[@]}" )
    fi

    # Store architecture as `uname -r` does not contain it on RHEL5

    arch=${MOCK_ARCH:-$( uname -m )}
}


draw_conclusions() {
    # Draws conclusions based on available system data.
    #
    # Side effects:
    #     Sets many global boolean flags and content variables

    # Check vulnerability
    if (( avail_vuln_file )); then
        if [[ "$vuln_file_value" =~ "Vulnerable" ]]; then
            vulnerable_taa=1
        fi
        if [[ "$vuln_file_value" =~ "microcode" ]]; then
            update_microcode=1
        fi
    else
        vulnerable_taa=1
    fi

    if [[ "$vulnerable_cpu_flag_hle" || "$vulnerable_cpu_flag_rtm" ]]; then
        affected_cpu=1
    fi

    if (( affected_cpu && vulnerable_taa )); then
        vulnerable=1
    fi

    # Result values
    if (( ! vulnerable )); then
        result=0
    elif (( ! avail_vuln_file )); then
        result=2
    elif (( vulnerable_taa )); then
        result=3
    else
        result=4
    fi
}


debug_print() {
    # Prints selected variables when debugging is enabled.

    variables=( running_kernel rhel virtualization
                avail_vuln_file vuln_file_value
                cpu_vendor cpu_model_name cpu_model arch
                vulnerable_taa update_microcode 
                vulnerable_cpu_flag_hle vulnerable_cpu_flag_rtm
                affected_cpu vulnerable result
               )

    for variable in "${variables[@]}"; do
        echo "$variable = *${!variable}*"
    done
    declare -p cpu_flags
    echo
}


require_root() {
    # Checks if user is root.
    #
    # Side effects:
    #     Exits when user is not root.
    #
    # Notes:
    #     MOCK_EUID can be used to mock EUID variable

    local euid=${MOCK_EUID:-$EUID}

    # Am I root?
    if (( euid != 0 )); then
        echo "This script must run with elevated privileges (e.g. as root)"
        exit 1
    fi
}



if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    require_root  # Needed for virt-what
    basic_args "$@"
    basic_reqs "CVE-2019-11135"
    running_kernel=$( uname -r )
    check_supported_kernel "$running_kernel"

    rhel=$( get_rhel "$running_kernel" )
    if (( rhel == 5 )); then
        export PATH="/sbin:/usr/sbin:$PATH"
    fi

    virtualization=$( get_virtualization )

    set_default_values
    parse_facts
    draw_conclusions

    # Debug prints
    if [[ "$debug" ]]; then
        debug_print
    fi

    # Outputs
    echo -e "Detected CPU vendor: ${BOLD}$cpu_vendor${RESET}"
    echo -e "CPU: ${BOLD}$cpu_model_name${RESET}"
    if [[ "$cpu_model" ]]; then
        printf "CPU model: ${BOLD}%d${RESET} (0x%x)\\n" "$cpu_model" "$cpu_model"
    fi
    echo -e "Running kernel: ${BOLD}$running_kernel${RESET}"
    echo -e "Architecture: ${BOLD}$arch${RESET}"
    echo -e "Virtualization: ${BOLD}$virtualization${RESET}"
    echo

    # Results
    if (( ! avail_vuln_file )); then
        if (( ! affected_cpu )); then
            echo -e "${GREEN}This CPU is not affected.${RESET}"
        else
            echo -e "${RED}Vulnerable${RESET}"
            echo
            echo -e "* The running kernel does not include mitigations"
        fi
    elif (( avail_vuln_file )); then
        if (( vulnerable )); then
            echo -e "${RED}${vuln_file_value}${RESET}"
            echo
            if (( update_microcode )); then
                if [[ "$virtualization" =~ "None" ]]; then
                    echo -e "* CPU microcode update is not detected"
                fi
            fi
        else
            echo -e "${GREEN}${vuln_file_value}${RESET}"
        fi
    fi
    echo

    # Additional information
    echo -e "For more information about the vulnerability see:"
    echo -e "* $ARTICLE"
    echo

    # Additional conditional notes
    if (( result != 0 )); then  # Only if there is still a problem
        if [[ "$virtualization" != "None" ]]; then
            echo -e "For more information about correctly enabling mitigations in VMs, see:"
            echo -e "* https://access.redhat.com/articles/3331571"
            echo -e "(This is Spectre related article, but the steps are the same)"
            echo
        fi
        if [[ "$virtualization" =~ "vmware" ]]; then
            echo -e "For more information about correctly enabling mitigations in VMWare VMs, see:"
            echo -e "* https://kb.vmware.com/s/article/52085"
            echo -e "(This is Spectre related article, but the steps should be similar)"
            echo
        fi
    fi

    exit "$result"
fi

