#!/bin/bash

set -e

# Nagios-plugin to warn if _any_ certificates in the chain are close
# to expiration.
#
# Requires these programs:
#   - bash
#   - openssl
#   - sed
#   - date

VERSION="check_cert_chain_expiration v1.2"

# ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ###
progress () {
    if [ "$VERBOSE" == "1" ]; then
      echo "$1"
    fi
}

exit_with_error () {
    local msg
    msg="$1"

    local exit_code
    exit_code="$2"

    if [[ -z "$exit_code" ]]; then
        exit_code="1"
    fi

    echo "error: $msg"
    exit "$exit_code"
}

seconds_to_text () {
    local seconds
    seconds=$1

    local subject
    subject=$2

    local is_root
    is_root=$3

    if [[ "$is_root" == "yes" ]]; then
        MSG1="the root certificate"
    else
        MSG1="one of the certificates in the chain"
    fi

    if [[ "$seconds" -lt "0" ]]; then
        MSG2="has expired"
    elif [[ "$seconds" -lt "3600" ]]; then
        MSG2="is expiring in less than one hour"
    elif [[ "$seconds" -lt "86400" ]]; then
        MSG2="is expiring in less than one day"
    else
        days=$((seconds / 86400))
        MSG2="is expiring in $days days"
    fi

    echo "${MSG1} ${MSG2} (subject: $subject)"
}

show_help () {
cat <<EOF
$VERSION
This plugin tests the expiration of ALL certificates in the chain
from a live site.

Usage:
   check_cert_chain_expiration -H <host> [-p <port>] [-w <warn time>] [-c <critical time>]

Options:
   -h, --help
      Print detailed help screen
   -V, --version
       Print version information
   -H, --host
      Hostname to query (REQUIRED)
   -p, --port
       SSL port to use (defaults to 443)
   -c, --critical
       Maximum age before sending a CRITICAL (in seconds) (defaults to one month)
   -w, --warn
       Maximum age before sending a WARN (in seconds) (defaults to two months)

Note:
   1. If no certificates are returned this test returns UNKNOWN.
   2. The server MUST support SNI.

Examples:

   # Send WARNING if any certificate in the certificate chain is within
   # 3 weeks of expiring, and send CRITICAL if any certificate in the
   # certificate chain is within 1 week of expiring.
   #
   # 3 weeks = 1,814,400 seconds
   # 1 week  =   604,800 seconds
   check_cert_chain_expiration -H example.com -w 1814400 -c 604800

   # Use defalts for WARNING and CRITICAL but use port 8443 rather than the
   # default of 443.
   check_cert_chain_expiration -H example.com -p 8443
EOF
   exit 0
}
# ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ###

# Nagios codes
EXIT_OK=0
EXIT_WARNING=1
EXIT_CRITICAL=2
EXIT_UNKNOWN=3

SHOW_VERSION=0
SHOW_HELP=0
VERBOSE=0
PORT=443

# Default warn to two months, critical to one month
WARNING_SECS=$(( 60 * 60 * 24 * 60))
CRITICAL_SECS=$(( 60 * 60 * 24 * 30))

# The following option parsing code taken from:
# https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f
while (( "$#" )); do
    case "$1" in
        -v|--verbose)
            VERBOSE=1
            shift
            ;;
        -V|--version)
            SHOW_VERSION=1
            shift
            ;;
        -h|--help)
            SHOW_HELP=1
            shift
            ;;
        -p|--port)
            if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
                PORT=$2
                shift 2
            else
              exit_with_error "argument for $1 is missing" "$EXIT_UNKNOWN"
            fi
            ;;
        -H|--host)
            if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
                HOST_NAME=$2
                shift 2
            else
              exit_with_error "argument for $1 is missing" "$EXIT_UNKNOWN"
            fi
            ;;
        -w|--warn)
            if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
                WARNING_SECS=$2
                shift 2
            else
              exit_with_error "argument for $1 is missing" "$EXIT_UNKNOWN"
            fi
            ;;
        -c|--critical)
            if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
                CRITICAL_SECS=$2
                shift 2
            else
              exit_with_error "argument for $1 is missing" "$EXIT_UNKNOWN"
            fi
            ;;
        -*) # unsupported flags
            exit_with_error "unsupported flag $1" "$EXIT_UNKNOWN"
            ;;
        *) # preserve positional arguments
            PARAMS="$PARAMS $1"
            shift
            ;;
    esac
done

# Grab positional arguments
eval set -- "$PARAMS"

# The message is everything else past the status:
MESSAGE="${@:2}"

if [[ "$SHOW_VERSION" == "1" ]]; then
    echo "$VERSION"
    exit "$EXIT_UNKNOWN"
fi

if [[ "$SHOW_HELP" == "1" ]]; then
    show_help
    exit "$EXIT_UNKNOWN"
fi

if [[ -z "$HOST_NAME" ]]; then
    show_help
    exit "$EXIT_UNKNOWN"
fi

progress "warn threshhold (seconds): $WARNING_SECS"

progress "extracting cert chain from $HOST_NAME:$PORT"
CERTS=$(echo "" | openssl s_client -showcerts -servername "$HOST_NAME" -connect "$HOST_NAME":"$PORT" 2> /dev/null \
  |     sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p')


MINIMUM_EXP=""
MINIMUM_SUBJECT=""
MINIMUM_ISROOT="no"

while [[ ! -z $CERTS ]]; do
    CERT=$(echo "$CERTS" | sed -n '/^-----BEGIN CERTIFICATE-----/,/^-----END CERTIFICATE-----/p;/^-----END CERTIFICATE-----/q')

    CERTS=$(echo "$CERTS" | sed -e '1,/^-----END CERTIFICATE-----/ d')

    # Get the expiration date
    notAfter=$(echo "$CERT" | openssl x509 -noout -dates | sed -n 's/^notAfter=\(.*\)$/\1/p')
    notAfter_epoch=$(date --date="$notAfter" "+%s")
    current_epoch=$(date "+%s")

    subject=$(echo "$CERT" | openssl x509 -noout -subject | sed -n 's/^subject=\(.*\)$/\1/p')
    issuer=$(echo "$CERT" | openssl x509 -noout -issuer | sed -n 's/^issuer=\(.*\)$/\1/p')
    if [[ "$issuer" == "$subject" ]] ; then
      is_root="yes"
    else
      is_root="no"
    fi

    progress "extracted a certificate:"
    progress "  subject:  $subject"
    progress "  issuer:   $issuer"
    progress "  notAfter: $notAfter"
    progress "  is_root:  $is_root"

    date_diff=$((notAfter_epoch - current_epoch))

    if [[ -z "$MINIMUM_EXP" ]]; then
        MINIMUM_EXP="$date_diff"
        MINIMUM_SUBJECT="$subject"
        MINIMUM_ISROOT="$is_root"
    elif [[ "$date_diff" -lt "$MINIMUM_EXP" ]]; then
        MINIMUM_EXP="$date_diff"
        MINIMUM_SUBJECT="$subject"
        MINIMUM_ISROOT="$is_root"
    elif [[ "$date_diff" -eq "$MINIMUM_EXP" ]]; then
        # If the date_diff is equal and this is a root certificate,
        # use this one as we want to show expiring root certificates.
        if [[ "$is_root" == "yes" ]]; then
            MINIMUM_EXP="$date_diff"
            MINIMUM_SUBJECT="$subject"
            MINIMUM_ISROOT="$is_root"
        fi
    fi
done

# What if nothing came back? In this case we send unknown
if [[ -z "$MINIMUM_EXP" ]]; then
    echo "UNKNOWN - No certificates returned"
    exit "$EXIT_UNKNOWN"
else
    MESSAGE=$(seconds_to_text "$MINIMUM_EXP" "$MINIMUM_SUBJECT" "$MINIMUM_ISROOT" )

    if [[ "$MINIMUM_EXP" -lt "$CRITICAL_SECS" ]]; then
        echo "CRITICAL - $MESSAGE"
        exit "$EXIT_CRITICAL"
    elif [[ "$MINIMUM_EXP" -lt "$WARNING_SECS" ]]; then
        echo "WARNING - $MESSAGE"
        exit "$EXIT_WARNING"
    else
        echo "OK"
        exit "$EXIT_OK"
    fi
fi
