#!/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="cert-chain-expiration v1.1"

# ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ### # ###
progress () {
    if [ "$VERBOSE" == "1" ]; then
      echo "progress: $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

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

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
   -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)
   -v, --verbose
       Show extra information while running
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)
            PORT="$1"
            shift
            ;;
        -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 -connect "$HOST_NAME":"$PORT" 2> /dev/null \
  |     sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p')


MINIMUM_EXP=""

cert_count=0
while [[ -n $CERTS ]]; do
    cert_count=$((cert_count+1))

    CERT=$(echo "$CERTS" | sed -n '/^-----BEGIN CERTIFICATE-----/,/^-----END CERTIFICATE-----/p;/^-----END CERTIFICATE-----/q')

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

    # Get subject (not because we need the subject, but just so we can see the
    # subject during verbose output).
    subject=$(echo "$CERT" | openssl x509 -noout -subject)
    progress "certificate $cert_count: extracted ($subject)"

    # 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")
    progress "certificate $cert_count: expires on $notAfter"

    date_diff=$((notAfter_epoch - current_epoch))
    progress "certificate $cert_count: expiration in $date_diff seconds"

    if [[ -z "$MINIMUM_EXP" ]]; then
        MINIMUM_EXP="$date_diff"
        MINIMUM_EXP_SUBJECT="$subject"
    elif [[ "$date_diff" -lt "$MINIMUM_EXP" ]]; then
        MINIMUM_EXP="$date_diff"
        MINIMUM_EXP_SUBJECT="$subject"
    fi
    progress "minimum expiration seconds found so far: $MINIMUM_EXP"
done

MESSAGE=$(seconds_to_text "$MINIMUM_EXP")
MESSAGE="${MESSAGE} ($MINIMUM_EXP_SUBJECT)"

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

# Documentation.  Use a hack to hide this from the shell.  Because of the
# above exit line, this should never be executed.
DOCS=<<__END_OF_DOCS__

=head1 NAME

check_cert_chain_expiration - 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>]

=head1 SYNOPSIS

check_cert_chain_expiration [B<-hVpcw>]

=head1 DESCRIPTION

B<check_cert_chain_expiration> Script to tests the expiration of ALL certificates in the chain from
a live site

=head1 OPTIONS

=over 4

=item B<-h>, B<--help>

Print detailed help screen

=item B<-V>, B<--version>

Print version information

=item B<-p>, B<--port>

SSL port to use (defaults to 443)

=item B<-c>, B<--critical>

Maximum age before sending a CRITICAL (in seconds) (defaults to one month)

=item B<-w>, B<--warn>

Maximum age before sending a WARN (in seconds) (defaults to two months)

=back

=head1 AUTHOR

Adam H Lewenberg <adamhl@stanford.edu>

=cut

__END_OF_DOCS__
