#!/bin/bash

# Copyright (C) 2018  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.1"

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

ARTICLE="https://access.redhat.com/security/vulnerabilities/3422241"

# 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.


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


compare() {
    # Compares two versions strings, such as 2.6.32-131, using comparison operators.
    #
    # Args:
    #     left - left version string to compare
    #     expression - valid comparison operator, or their combination, e.g. >, >=, =, <=, <, <>
    #     right - left version string to compare
    #
    # Returns:
    #     0 if comparison passes, 1 otherwise

    local left=( $( tr ".-" "  " <<< "$1" ) )
    local right=( $( tr ".-" "  " <<< "$3" ) )
    local expression="$2"

    # Find longer version string
    local nr
    if (( ${#left[@]} > ${#right[@]} )); then
        nr=${#left[@]}
    else
        nr=${#right[@]}
    fi

    # Balance shorter version string
    while (( ${#left[@]} < nr )); do
        left+=( "0" )
    done
    while (( ${#right[@]} < nr )); do
        right+=( "0" )
    done

    if [[ "$expression" == *">"* ]]; then
        for (( i = 0; i < nr; i++ )); do
            (( left[i] < right[i] )) && break
            (( left[i] > right[i] )) && return 0
        done
    fi

    if [[ "$expression" == *"<"* ]]; then
        for (( i = 0; i < nr; i++ )); do
            (( left[i] > right[i] )) && break
            (( left[i] < right[i] )) && return 0
        done
    fi

    if [[ "$expression" == *"="* ]]; then
        for (( i = 0; i < nr; i++ )); do
            (( left[i] != right[i] )) && break
            (( i == nr - 1 )) && return 0
        done
    fi

    return 1
}


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
}


unregex() {
    # Escapes characters which are special in `sed` when used in ERE mode (`sed -r`).
    # http://stackoverflow.com/a/2705678/120999
    # It does not handle newlines in the string.
    #
    # Args:
    #     string - string to be escaped
    #
    # Prints:
    #     Escaped string.

    string="$1"

    sed 's/[]\/()$*.^|[]/\\&/g' <<< "$string"
}


split_array() {
    # Splits a string to array of strings based on separator which can be multi-character.
    # It does not handle newlines in the string.
    #
    # Args:
    #     array_name - name of the global array
    #     separator - string to be used as a separator, may be multi-character
    #     string - input string
    #
    # Side effects:
    #     Overwrites content of the array 'array_name' with split strings

    local array_name="$1"
    local separator
    separator=$( unregex "$2" )
    local string="$3"

    read_array "$array_name" <<< "$( sed -r 's/'"$separator"'/\n/g' <<< "$string" )"
}


in_array() {
    # Checks if an element is contained in the array.
    #
    # Args:
    #    array_name - name of the global array
    #
    # Returns:
    #    0 if element is found, 1 otherwise.

    local array_name="$1[@]"
    local array=( "${!array_name}" )
    local element="$2"

    for (( i = 0; i < "${#array[@]}"; i++ )); do
        if [[ "$element" == "${array[i]}" ]]; then
            return 0
        fi
    done
    return 1
}


print_array() {
    # Prints array, one element per line.
    #
    # Args:
    #     array_name - name of the global array
    #
    # Prints:
    #     Content of the array, one element per line.

    local array_name="$1[@]"
    local array=( "${!array_name}" )

    for (( i = 0; i < "${#array[@]}"; i++ )); do
        echo "${array[i]}"
    done
}

VULNERABLE_IDS_3_3_1_46_39=(
    "cee523a4e55c"
    "f7d84766e13c"
)

VULNERABLE_IDS_3_2_1_34=(
    "7cd7c46dc84d"
    "ec8307ed9be4"
    "4768f643de64"
    "a216c1590daf"
    "62003a98a0d4"
)

VULNERABLE_IDS_3_1_1_11=(
    '1fc064813495'
    '9fba84c724bf'
    '0116d79b95b0'
    '0d120d98fc4a'
    'e94f83b2044b'
    '30facd2f7b7a'
    'e9cb0e88e37c'
    '9d960bd3767e'
)

check_openshift() {
    # Checks if there are 'ose-sti-builder' docker images with version which is vulnerable.
    #
    # Prints:
    #     List of vulnerable 'ose-sti-builder' docker images with hashes.
    #     If there is no such image it does not print anything.
    #
    # Note:
    #     Command 'docker images' needs to be available
    VERSION_STRING_PATTERN='^v[[:digit:]]+(\.[[:digit:]]+)*$'

    while read -r data; do
        read -ra data_array <<< "$data"

        # Ignore any images which do not have version string as a tag
        if [[ ! "${data_array[0]}" =~ ${VERSION_STRING_PATTERN} ]]; then
            continue
        fi

        version_string=$( tr -d "v" <<< "${data_array[0]}" )
        hash="${data_array[1]}"


        version_array=()
        split_array version_array '.' "$version_string"

        # Fixed versions:
        # v3.9.25
        # v3.8.37
        # v3.7.44
        # v3.6.173.0.113
        # v3.5.5.31.67
        # v3.4.1.44.53

        if (( version_array[0] == 3 && version_array[1] == 9 )); then
            if compare "$version_string" "<" "3.9.25"; then
                echo "$version_string $hash"
            fi
        elif (( version_array[0] == 3 && version_array[1] == 8 )); then
            if compare "$version_string" "<" "3.8.37"; then
                echo "$version_string $hash"
            fi
        elif (( version_array[0] == 3 && version_array[1] == 7 )); then
            if compare "$version_string" "<" "3.7.44"; then
                echo "$version_string $hash"
            fi
        elif (( version_array[0] == 3 && version_array[1] == 6 )); then
            if compare "$version_string" "<" "3.6.173.0.113"; then
                echo "$version_string $hash"
            fi
        elif (( version_array[0] == 3 && version_array[1] == 5 )); then
            if compare "$version_string" "<" "3.5.5.31.67"; then
                echo "$version_string $hash"
            fi
        elif (( version_array[0] == 3 && version_array[1] == 4 )); then
            if compare "$version_string" "<" "3.4.1.44.53"; then
                echo "$version_string $hash"
            fi

        # Non-hot-fixed versions, based on hash

        elif (( version_array[0] == 3 && version_array[1] == 3 )); then
            if compare "$version_string" "<" "3.3.1.46.39"; then
                echo "$version_string $hash"
            elif compare "$version_string" "=" "3.3.1.46.39"; then
                if in_array VULNERABLE_IDS_3_3_1_46_39 "$hash"; then
                    echo "$version_string $hash"
                fi
            fi
        elif (( version_array[0] == 3 && version_array[1] == 2 )); then
            if compare "$version_string" "<" "3.2.1.34"; then
                echo "$version_string $hash"
            elif compare "$version_string" "=" "3.2.1.34"; then
                if in_array VULNERABLE_IDS_3_2_1_34 "$hash"; then
                    echo "$version_string $hash"
                fi
            fi
        elif (( version_array[0] == 3 && version_array[1] == 1 )); then
            if compare "$version_string" "<" "3.1.1.11"; then
                echo "$version_string $hash"
            elif compare "$version_string" "=" "3.1.1.11"; then
                if in_array VULNERABLE_IDS_3_1_1_11 "$hash"; then
                    echo "$version_string $hash"
                fi
            fi

        # Versions bigger than 3.9.25, like 3.10.x are fixed
        elif compare "$version_string" ">=" "3.9.25"; then
            :
        else
            # Unknown version is reported as vulnerable
            echo "$version_string $hash"
        fi

    done <<< "$( docker images | awk '/\/ose-sti-builder /{ print $2" "$3 }' )"
}


if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    basic_args "$@"
    basic_reqs "CVE-2018-1102"
    running_kernel=$( uname -r )
    check_supported_kernel "$running_kernel"

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

    # Checks
    if ! docker images &> /dev/null; then
        echo "Cannot run command 'docker images'. Execute script with necessary privileges."
        exit 1
    fi
    list_of_vulnerable_versions=()
    read_array list_of_vulnerable_versions <<< "$( check_openshift )"

    # Debug prints
    if [[ "$debug" ]]; then
        echo "** DEBUG **"
        docker images
        echo "********************"
        declare -p list_of_vulnerable_versions
        echo "** DEBUG END **"
        echo
    fi

    # Results
    if [[ "${list_of_vulnerable_versions[0]}" ]]; then
        echo -e "${RED}The following 'ose-sti-builder' image versions are vulnerable:${RESET}"
        print_array list_of_vulnerable_versions
        echo
        echo -e "Follow $ARTICLE for advice."
        exit 2
    else
        echo -e "${GREEN}All detected versions of 'ose-sti-builder' image are not vulnerable.${RESET}"
        exit 0
    fi
fi
