#!/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/security/vulnerabilities/runcescape"

# 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

# Checking against the list of vulnerable packages is necessary because of the way how features
# are back-ported to older versions of packages in various channels.


VULNERABLE_VERSIONS=(
    'runc-0.0.8-1.git4155b68.el7'
    'runc-0.1.0-3.el7'
    'runc-0.1.1-4.el7'
    'runc-0.1.1-5.el7'
    'runc-1.0.0-1.rc2.el7'
    'runc-1.0.0-3.rc2.el7'
    'runc-1.0.0-6.gite800860.el7'
    'runc-1.0.0-12.1.gitf8ce01d.el7'
    'runc-1.0.0-14.rc4dev.git84a082b.el7'
    'runc-1.0.0-21.rc4.dev.gitaea4f21.el7'
    'runc-1.0.0-23.rc4.dev.git1d3ab6d.el7'
    'runc-1.0.0-24.rc4.dev.gitc6e4a1e.el7'
    'runc-1.0.0-26.rc4.dev.git9f9c962.el7'
    'runc-1.0.0-26.rc4.dev.git9f9c962.el7'
    'runc-1.0.0-27.rc5.dev.git4bb1fe4.el7'
    'runc-1.0.0-37.rc5.dev.gitad0f525.el7'
    'runc-1.0.0-52.dev.git70ca035.el7_5'
    'runc-1.0.0-52.dev.git70ca035.el7_5'
    'runc-1.0.0-54.dev.git2abd837.el7'
    'runc-1.0.0-57.dev.git2abd837.el7'
    'runc-1.0.0-52.rc5.dev.git2abd837.el8+1884+9fee228c'
    'docker-1.12.5-14.el7'
    'docker-1.12.6-11.el7'
    'docker-1.12.6-16.el7'
    'docker-1.12.6-28.git1398f24.el7'
    'docker-1.12.6-32.git88a4867.el7'
    'docker-1.12.6-48.git0fdc778.el7'
    'docker-1.12.6-55.gitc4618fb.el7'
    'docker-1.12.6-61.git85d7426.el7'
    'docker-1.12.6-68.gitec8512b.el7'
    'docker-1.12.6-71.git3e8e77d.el7'
    'docker-1.13.1-53.git774336d.el7'
    'docker-1.13.1-58.git87f2fab.el7'
    'docker-1.13.1-63.git94f4240.el7'
    'docker-1.13.1-68.gitdded712.el7'
    'docker-1.13.1-74.git6e3bb8e.el7'
    'docker-1.13.1-75.git8633870.el7_5'
    'docker-1.13.1-75.git8633870.el7_5'
    'docker-1.13.1-84.git07f3374.el7'
    'docker-1.13.1-88.git07f3374.el7'
    'docker-1.13.1-90.git07f3374.el7'
    'docker-latest-1.12.1-2.el7'
    'docker-latest-1.12.1-3.el7'
    'docker-latest-1.12.3-10.el7'
    'docker-latest-1.12.5-14.el7'
    'docker-latest-1.12.6-11.el7'
    'docker-latest-1.13.1-4.el7'
    'docker-latest-1.13.1-11.git3a17ad5.el7'
    'docker-latest-1.13.1-13.gitb303bf6.el7'
    'docker-latest-1.13.1-21.1.gitcd75c68.el7'
    'docker-latest-1.13.1-23.git28ae36d.el7'
    'docker-latest-1.13.1-26.git1faa135.el7'
    'docker-latest-1.13.1-36.git9a813fa.el7'
    'docker-latest-1.13.1-37.git9a813fa.el7'
    'docker-latest-1.13.1-53.git774336d.el7'
    'docker-latest-1.13.1-58.git87f2fab.el7'
)


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"
    YELLOW="\\033[1;33m"
    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=""
            YELLOW=""
            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-7]* ]]; then
        echo -e "${RED}This script is meant to be used only on RHEL 5, 6 and 7.${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', or '7'

    local running_kernel="$1"

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


check_package() {
    # Checks if installed package is in list of vulnerable packages.
    #
    # Args:
    #     installed_packages - installed packages string as returned by 'rpm -qa package'
    #                          (may be multi-line)
    #     vulnerable_versions - an array of vulnerable versions
    #
    # Prints:
    #     First vulnerable package string as returned by 'rpm -qa package', or nothing

    # shellcheck disable=SC2206
    local installed_packages=( $1 )  # Convert to array, use word splitting
    shift
    local vulnerable_versions=( "$@" )

    for tested_package in "${vulnerable_versions[@]}"; do
        for installed_package in "${installed_packages[@]}"; do
            installed_package_without_arch="${installed_package%.*}"
            if [[ "$installed_package_without_arch" == "$tested_package" ]]; then
                echo "$installed_package"
                return 0
            fi
        done
    done
}


get_installed_packages() {
    # Checks for installed packages of a 'package_name'. Compatible with RHEL5.
    #
    # Args:
    #     package_name - package name string
    #
    # Prints:
    #     Lines with N-V-R.A strings of all installed packages.

    local package_name="$1"

    rpm -qa --queryformat="%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n" "$package_name"
}


get_selinux_mode() {
    # Checks SELinux mode.
    #
    # Returns:
    #     0 when getenforce command is available, 1 otherwise
    #
    # Prints:
    #     SELinux mode in lowercase.

    if command -v getenforce &> /dev/null 2>&1; then
        getenforce | awk '{ print tolower($0) }'
        return 0
    fi
    return 1
}


get_selinux_policy() {
    # Checks SELinux policy.
    #
    # Returns:
    #     0 when sestatus command is available, 1 otherwise
    #
    # Prints:
    #     SELinux policy in lowercase.

    local policy

    if command -v sestatus &> /dev/null 2>&1; then
        policy=$( sestatus | awk '/Loaded policy name:/ { print $4 }' )
        if [[ ! "$policy" ]]; then
            # RHEL5
            policy=$( sestatus | awk '/Policy from config file:/ { print $5 }' )
        fi
        echo "$policy"
        return 0
    fi
    return 1
}


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

    installed_runc=$( get_installed_packages "runc" )
    installed_docker=$( get_installed_packages "docker" )
    installed_docker_latest=$( get_installed_packages "docker-latest" )

    selinux_mode=$( get_selinux_mode )
    selinux_policy=$( get_selinux_policy )
}

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

    vulnerable_runc=$( check_package "$installed_runc" "${VULNERABLE_VERSIONS[@]}" )
    vulnerable_docker=$( check_package "$installed_docker" "${VULNERABLE_VERSIONS[@]}" )
    vulnerable_docker_latest=$( check_package "$installed_docker_latest" "${VULNERABLE_VERSIONS[@]}" )

    selinux_mitigates=0
    if [[ "$selinux_mode" == "enforcing" && "$selinux_policy" == "targeted" ]]; then
        selinux_mitigates=1
    fi

    result=0
    if [[ "$vulnerable_runc" ]]; then
        (( result |= 2 ))
    fi
    if [[ "$vulnerable_docker" ]]; then
        (( result |= 4 ))
    fi
    if [[ "$vulnerable_docker_latest" ]]; then
        (( result |= 8 ))
    fi
    if (( selinux_mitigates )); then
        result=0
    fi
}


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

    variables=( installed_runc installed_docker installed_docker_latest
                vulnerable_runc vulnerable_docker vulnerable_docker_latest
                selinux_mode selinux_policy selinux_mitigates result )
    for variable in "${variables[@]}"; do
        echo "$variable = *${!variable}*"
    done
    echo
}


if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    basic_args "$@"
    basic_reqs "CVE-2019-5736"
    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

    parse_facts
    draw_conclusions

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

    # Outputs
    echo -e "Detected 'runc' package: ${BOLD}$installed_runc${RESET}"
    echo -e "Detected 'docker' package: ${BOLD}$installed_docker${RESET}"
    echo -e "Detected 'docker-latest' package: ${BOLD}$installed_docker_latest${RESET}"
    echo -e "SELinux mode: ${BOLD}$selinux_mode${RESET}"
    echo -e "SELinux policy: ${BOLD}$selinux_policy${RESET}"
    echo

    if (( selinux_mitigates )); then
        vulnerable_color="$YELLOW"
    else
        vulnerable_color="$RED"
    fi

    if [[ "$installed_runc" ]]; then
        if [[ "$vulnerable_runc" ]]; then
            echo -e "'runc' package is ${vulnerable_color}vulnerable${RESET}"
        else
            echo -e "'runc' package is ${GREEN}NOT vulnerable${RESET}"
        fi
    fi

    if [[ "$installed_docker" ]]; then
        if [[ "$vulnerable_docker" ]]; then
            echo -e "'docker' package is ${vulnerable_color}vulnerable${RESET}"
        else
            echo -e "'docker' package is ${GREEN}NOT vulnerable${RESET}"
        fi
    fi

    if [[ "$installed_docker_latest" ]]; then
        if [[ "$vulnerable_docker_latest" ]]; then
            echo -e "'docker-latest' package is ${vulnerable_color}vulnerable${RESET}"
        else
            echo -e "'docker-latest' package is ${GREEN}NOT vulnerable${RESET}"
        fi
    fi

    if [[ ! "$vulnerable_runc" && ! "$vulnerable_docker" && ! "$vulnerable_docker_latest" ]]; then
        echo -e "There are ${GREEN}NO vulnerable packages${RESET} installed."
    elif (( selinux_mitigates )); then
        echo -e "Vulnerability is ${GREEN}mitigated${RESET} by SELinux and cannot be exploited."
    else
        echo -e "SELinux in current state does ${RED}NOT mitigate${RESET} the vulnerability."
    fi

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

    exit "$result"
fi
