#!/usr/bin/bash
#
# Copyright(C) 2021 ZENETYS
# This script is licensed under MIT License (http://opensource.org/licenses/MIT)
# License: License MIT
# Initial author: Julien THOMAS < jthomas @ zenetys.com >
#

set -f
PROGNAME=${0##*/}
NAGIOS_STATUS_TEXT=(
    [0]='OK'
    [1]='WARNING'
    [2]='CRITICAL'
    [3]='UNKNOWN'
)
NAGIOS_STATUS_PRIO_TR=( 0 2 3 1 )

DIR=
INDEX=index
WARNING=
CRITICAL=

IGNORE_EXPIRED=
VERBOSE=
SSH_HOST=
SSH_USER=
SSH_PORT=
TIMEOUT=5 # SSH ConnectTimeout
SSH_IDENTITY=
SUDO_USER=

function nagios_exit_usage() {
    echo "\
Usage: $PROGNAME [OPTION...] -d DIRECTORY -w WARNING -c CRITICAL
Nagios plugin to check for certificates expiration in an OpenSSL CA

Common options:
  -d, --directory DIR     OpenSSL CA base directory
  -i, --index     FNAME   OpenSSL CA database filename
  -w, --warning   INT     Days left before expiration, warning threshold
  -c, --critical  INT     Days left before expiration, critical threshold
  -E, --ignore-expired    Do not warn on expired certificates not revoked
  -v, --verbose           Increase verbose level (see below)
  -vv                     Shortcut for verbose level 2
  -S, --sudo-user USER    Read CA index with sudo -u USER cat
  -h, --help              Display this help

SSH options:
  -H, --host      HOST    SSH host, enable to retrieve CA index via SSH
  -u, --user      USER    SSH username
  -p, --port      INT     SSH port
  -i, --identity  FILE    SSH identity key file
  -t, --timeout   INT     SSH connect timeout

Verbosity level 1: detail of each certificate in alert in long plugin output
Verbosity level 2: add CN of certificates in alert in plugin output"
    exit 3
}

# $1: Nagios status code
# $2: Plugin output message
# $@: Optional lines of long plugin output
function nagios_die() {
    echo "${NAGIOS_STATUS_TEXT[$1]}: $2"
    (( $# > 2 )) && (IFS=$'\n'; echo "${*:3}")
    exit "$1"
}

# $1: x509 subject, parts separated by slash
function get_cn() {
    REPLY=
    local re='/CN=([^/]+)'
    [[ $1 =~ $re ]] || return 1
    REPLY=${BASH_REMATCH[1]}
}

# $1: current state
# $2: want state
function increase_prio() {
    REPLY=$1
    if (( NAGIOS_STATUS_PRIO_TR[$2] > NAGIOS_STATUS_PRIO_TR[$1] )); then
        REPLY=$2
    fi
}

function get_openssl_ca_index() {
    local cmd=( ${SUDO_USER:+sudo -u "$SUDO_USER"} cat "$DIR/$INDEX" )
    local sshopts sshargs
    if [[ -n $SSH_HOST ]]; then
        sshopts=(
            ${SSH_IDENTITY:+-i "$SSH_IDENTITY"} ${SSH_USER:+-l "$SSH_USER"}
            ${SSH_PORT:+-p "$SSH_PORT"} -q -C -T -o StrictHostKeyChecking=no
            -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=publickey
            ${TIMEOUT:+-o "ConnectTimeout=$TIMEOUT"}
        )
        printf -v sshargs "%q " "${cmd[@]}"
        cmd=( ssh "${sshopts[@]}" "$SSH_HOST" -- "$sshargs" )
    fi
    REPLY=$( "${cmd[@]}" )
}

while (( $# > 0 )); do
    case "$1" in
        # common
        -d|--directory) DIR=$2; shift ;;
        -i|--index) INDEX=$2; shift ;;
        -w|--warning) WARNING=$2; shift ;;
        -c|--critical) CRITICAL=$2; shift ;;
        -E|--ignore-expired) IGNORE_EXPIRED=1 ;;
        -v|--verbose) (( VERBOSE++ )) ;;
        -vv) (( VERBOSE += 2 )) ;;
        -S|--sudo-user) SUDO_USER=$2; shift ;;
        -h|--help) nagios_exit_usage 3 ;;
        # ssh
        -H|--host) SSH_HOST=$2; shift ;;
        -u|--user) SSH_USER=$2; shift ;;
        -p|--port) SSH_PORT=$2; shift ;;
        -i|--identity) SSH_IDENTITY=$2; shift ;;
        -t|--timeout) TIMEOUT=$2; shift ;;
    esac
    shift
done

if [[ -z $DIR ]]; then
    nagios_die 3 'Missing OpenSSL CA directory'
fi
if [[ -z $WARNING || -n ${WARNING//[0-9]} ]]; then
    nagios_die 3 'Invalid warning threshold, integer expected'
fi
if [[ -z $CRITICAL || -n ${CRITICAL//[0-9]} ]]; then
    nagios_die 3 'Invalid critical threshold, integer expected'
fi
if (( WARNING < CRITICAL )); then
    nagios_die 3 'Invalid thresholds, warning is lower than critical'
fi
if [[ -n $SSH_PORT && -n ${SSH_PORT//[0-9]} ]]; then
    nagios_die 3 'Invalid SSH port, integer expected'
fi
if [[ -n $TIMEOUT && -n ${TIMEOUT//[0-9]} ]]; then
    nagios_die 3 'Invalid SSH connect timeout, integer expected'
fi
if [[ -n $SSH_IDENTITY && ! -r $SSH_IDENTITY ]]; then
    nagios_die 3 'Cannot read SSH identity file'
fi

now=$(date +%s)
if [[ -z $now ]]; then
    nagios_die 3 'Failed to get current time'
fi

if ! get_openssl_ca_index; then
    nagios_die 3 "Failed to retrive CA database $DIR/index"
fi
openssl_ca_index=$REPLY

info_unknown=
info_warning=
info_critical=
info_expired=
detail_unknown=()
detail_warning=()
detail_critical=()
detail_expired=()
status=0
output=

# assume openssl ca index file is valid
# fields 0, 1, 3 and 5 are used
while IFS=$'\x16' read -r -a REPLY; do
    flag=${REPLY[0]}
    expire=${REPLY[1]}
    serial=${REPLY[3]}
    subject=${REPLY[5]}

    get_cn "$subject"; cn=$REPLY
    if [[ -n $cn ]]; then
        id="0x$serial, CN=$cn"
        name=$cn
    else
        id="0x$serial, CN=?"
        name=$serial
    fi

    # ignore revoked certificates
    [[ $flag == *R* ]] && continue

    # expire timestamp
    # 2 vs 4 digits year, eg: 271103103225Z vs 20271122230000Z
    (( ${#expire} == 13 )) && expire="20$expire"
    expire=$(date -d "${expire:0:4}-${expire:4:2}-${expire:6:2} ${expire:8:2}:${expire:10:2}:${expire:12}" +%s)
    if [[ -z $expire ]]; then
        info_unknown+="${info_critical:+, }$name"
        detail_unknown+=( "Certificate $id, cannot read expire date" )
        increase_prio "$status" 3; status=$REPLY
        continue
    fi

    days_before_expire=$(( (expire - now) / 3600 / 24 ))
    if (( days_before_expire >= 0 )); then
        if (( days_before_expire <= CRITICAL )); then
            info_critical+="${info_critical:+, }$name (${days_before_expire}d)"
            detail_critical+=( "Certificate $id, expire in ${days_before_expire}d" )
            increase_prio "$status" 2; status=$REPLY
        elif (( days_before_expire <= WARNING )); then
            info_warning+="${info_warning:+, }$name (${days_before_expire}d)"
            detail_warning+=( "Certificate $id, expire in ${days_before_expire}d" )
            increase_prio "$status" 1; status=$REPLY
        fi
    elif [[ -z $IGNORE_EXPIRED ]]; then # already expired
        info_expired+="${info_expired:+, }$name"
        detail_expired+=( "Certificate $id, expired $(( -days_before_expire ))d ago but not revoked" )
        increase_prio "$status" 1; status=$REPLY
    fi
done < <(echo "$openssl_ca_index" |sed -re 's,\t,\x16,g' |sort -t $'\x16' -k 2,2)

if (( ${#detail_critical[@]} > 0 )); then
    cert=cert; (( ${#detail_critical[@]} > 1 )) && cert+=s
    output+="${output:+ }${#detail_critical[@]} $cert expire <= ${CRITICAL}d"
    (( VERBOSE > 1 )) && output+=": $info_critical"
    output+=.
fi
if (( ${#detail_warning[@]} > 0 )); then
    cert=cert; (( ${#detail_warning[@]} > 1 )) && cert+=s
    output+="${output:+ }${#detail_warning[@]} $cert expire <= ${WARNING}d"
    (( VERBOSE > 1 )) && output+=": $info_warning"
    output+=.
fi
if (( ${#detail_expired[@]} > 0 )); then
    cert=cert; (( ${#detail_expired[@]} > 1 )) && cert+=s
    output+="${output:+ }${#detail_expired[@]} $cert expired not revoked"
    (( VERBOSE > 1 )) && output+=": $info_expired"
    output+=.
fi
if (( ${#detail_unknown[@]} > 0 )); then
    cert=cert; (( ${#detail_unknown[@]} > 1 )) && cert+=s
    output+="${output:+ }${#detail_unknown[@]} $cert unknown"
    (( VERBOSE > 1 )) && output+=": $info_unknown"
    output+=.
fi

if (( VERBOSE == 0 )); then
    nagios_die "$status" "${output:-No certificate in alert}"
else
    nagios_die "$status" "${output:-No certificate in alert}" \
        "${detail_critical[@]}" \
        "${detail_warning[@]}" \
        "${detail_expired[@]}" \
        "${detail_unknown[@]}"
fi
