#!/bin/sh
: << =cut
=head1 NAME

certificate_file_expiry - check the certificate validity of your certificates

= head1 CONFIGURATION

Installing: Add list of your certificates prefixed by the type in munin plugin-conf.d

For openvpn ca.crt and crl.pem

 [certificate_file_expiry]
 user root
 env.CERTS crl:/etc/openvpn/easy-rsa/keys/crl.pem x509:/etc/openvpn/easy-rsa/keys/ca.crt
 env.LOGARITHMIC yes

For openvpn inline <ca> and <cert> certificates, as described here
https://community.openvpn.net/openvpn/wiki/Openvpn23ManPage#lbAV

 [certificate_file_expiry]
 user root
 env.CERTS openvpn_inline:/etc/openvpn/client.conf
 env.LOGARITHMIC yes

For letsencrypt certificates

 [certificate_file_expiry]
 user root
 env.CERTS x509:/etc/letsencrypt/live/*/cert.pem

Warning and Critical levels can also be configured with env variables like this:

 [certificate_file_expiry]
 ...
 # warn when certificate will be invalid within 5 days
 env.warning 5:
 # for this certificate warn us 10 days before because it takes longer to renew
 env._etc_letsencrypt_live_example_com_cert_pem_warning 10:
 # critical when certificate will be invalid within 1 day
 env.critical 1:

env.CERTS should be a space separated list of patterns prefixed by the type of certificate to check and a colon. All
types of certificates that openssl supports as standard commands and have a validity output are supported
(e.g. x509, crl).
A special type is openvpn_inline where the plugin gets the certificates directly from the openvpn conf file in between
the <ca>\n...\n</ca> and <cert>\n...\n</cert> lines and checks those with openssl x509.
File patterns can be a single file (e.g. /etc/openvpn/easy-rsa/keys/crl.pem) or a pattern that matches multiple files
(e.g. /etc/letsencrypt/live/*/cert.pem).

env.warning and env.critical are configurable values for the warning and critical levels according to
http://guide.munin-monitoring.org/en/latest/tutorial/alert.html?highlight=warning#syntax-of-warning-and-critical

env.LOGARITHMIC "yes" enables the logarithmic display of values which is useful if some of your certs are relatively
long lived in respect to the warning level. e.g. a ca.crt that is valid for 10 years together with a crl.pem that is
valid for only a few months combined with warning levels of 5 days. default is "yes" to disable set it to "no".

env.IGNORE_UNEXPANDED_PATTERNS "yes" ignores patterns that did not expand to any files. this is useful to define one
config that handles multiple types of certs where only one pattern is used. default is "no".

=head1 Dependencies

Dependencies: openssl

=head1 AUTHOR

andreas perhab - andreas.perhab@wt-io-it.at (https://www.wt-io-it.at/)

=head1 LICENSE

GPLv2

=cut

. "$MUNIN_LIBDIR/plugins/plugin.sh"

LOGARITHMIC=${LOGARITHMIC:-yes}
IGNORE_UNEXPANDED_PATTERNS=${IGNORE_UNEXPANDED_PATTERNS:-no}

if [ "$1" = "config" ] ; then
  echo "graph_title Certificate validity"
  if [ "$LOGARITHMIC" = "yes" ] ; then
    graph_args="--logarithmic --units=si"
  fi
  echo "graph_args --base 1000 $graph_args"
  echo "graph_vlabel days"
  echo "graph_category security"
fi

# by default when certificate is only valid for 5 days or less emit a warning
warning=${warning:-5:}
# by default when certificate is only valid for 1 day or less emit a critical
critical=${critical:-1:}

now=$(date +%s)
get_validity() {
  local file
  local openssl_type
  local validity_line
  local validity_str_value
  local validity_timestamp
  local validity_seconds
  openssl_type=$1
  file=$2
  if [ "$file" != "-" ] ; then
    validity_line=$(/usr/bin/openssl "$openssl_type" -text -noout -in "$file" | grep -E '(Next Update|Not After)')
  else
    # when file is set to -- read from stdin
    validity_line=$(/usr/bin/openssl "$openssl_type" -text -noout | grep -E '(Next Update|Not After)')
  fi
  validity_str_value=${validity_line#*:}
  validity_timestamp=$(date --date="$validity_str_value" +%s)
  validity_seconds=$((validity_timestamp - now))
  echo "$validity_seconds" | awk '{ print ($1 / 86400) }'
}
print_config_lines() {
  name=$1
  label=$2
  echo "${name}.label ${label}"
  print_warning "$name"
  print_critical "$name"
}
get_openvpn_inline_cert() {
  file=$1
  type=$2
  # print content between <type> and </type> lines (ca and cert)
  awk 'BEGIN{content=0}/^<\/'"$type"'>$/{content=0}(content==1){ print $0 }/^<'"$type"'>$/{content=1}' < "$file"
}

for cert in ${CERTS}; do
  cert_type=${cert%:*}
  cert_pattern=${cert#*:}
  for cert_file in $cert_pattern; do
    # note: if file contains a * (e.g. /etc/letsencrypt/live/*/cert.pem) it might be an unexpanded pattern
    # to supress errors see IGNORE_UNEXPANDED_PATTERNS above
    # shellcheck disable=SC2063
    if [ "$IGNORE_UNEXPANDED_PATTERNS" = "yes" ] \
      && [ "$cert_file" = "$cert_pattern" ] \
      && ! [ -e "$cert_file" ] \
      && echo "$cert_file" | grep -q "*" ; then
        # skip unexpanded patterns when IGNORE_UNEXPANDED_PATTERNS is set to yes
        continue
    fi
    if [ "$cert_type" = "openvpn_inline" ] ; then
      for type in "ca" "cert"; do
        cert_name=$(clean_fieldname "$cert_file-$type")
        if [ "$1" = "config" ] ; then
          print_config_lines "$cert_name" "${cert_file} ${type}"
        elif [ "$1" = "" ] ; then
          echo "${cert_name}.value $(get_openvpn_inline_cert "$cert_file" "$type" | get_validity "x509" "-")"
        fi
      done
    else
      cert_name=$(clean_fieldname "$cert_file")
      if [ "$1" = "config" ] ; then
        print_config_lines "$cert_name" "${cert_file}"
      elif [ "$1" = "" ] ; then
        echo "${cert_name}.value $(get_validity "$cert_type" "$cert_file")"
      fi
    fi
  done
done
