From 52746fd98039148774a07c0a4684dca5978130b6 Mon Sep 17 00:00:00 2001 From: Chl Date: Sun, 27 Apr 2025 21:31:39 +0200 Subject: [PATCH] nagios/check_expiration_domain: first draft --- ...check_expiration_domain_name_rdap_whois.sh | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100755 nagios/check_expiration_domain_name_rdap_whois.sh diff --git a/nagios/check_expiration_domain_name_rdap_whois.sh b/nagios/check_expiration_domain_name_rdap_whois.sh new file mode 100755 index 0000000..ac5273a --- /dev/null +++ b/nagios/check_expiration_domain_name_rdap_whois.sh @@ -0,0 +1,369 @@ +#!/bin/sh + +# Monitoring script to check the expiration of domain names via RDAP and WHOIS. +# +# Copyright: chl-dev@bugness.org 2025 https://code.bugness.org/ +# Licence: WTFPL + +# Stop at first uncaught error +set -e + +PROGPATH=$( echo $0 | sed -e 's,[\\/][^\\/][^\\/]*$,,' ) +REVISION="0.2" + +# Default values +IANA_URL_DOMAINS="https://data.iana.org/rdap/dns.json" +IANA_CACHE_FILE="/tmp/.check_expiration_domain_name.cache.json" +IANA_CACHE_DURATION=$(( 86400 * 30 )) +THRESHOLD_WARNING="2592000" # 30 days +THRESHOLD_CRITICAL="691200" # 8 days +TIMESTAMP_NOW="$( date +%s )" +OPTION_RDAP_QUERYING="yes" +OPTION_WHOIS_QUERYING="yes" + +# Include check_range() +# Not needed at the moment +#. $PROGPATH/utils.sh +STATE_OK=0 +STATE_WARNING=1 +STATE_CRITICAL=2 +STATE_UNKNOWN=3 +STATE_DEPENDENT=4 + +# Output +OUTPUT_EXIT_STATUS=0 +OUTPUT_DETAIL_WARNING="" +OUTPUT_DETAIL_CRITICAL="" +OUTPUT_PERFDATA="" + +# +# Help function +# +usage() { + cat </dev/null 2>&1; then + wget -q -O - "$1" + elif which "curl" >/dev/null 2>&1 ; then + curl -s --fail "$1" + else + echo "No wget/curl/whatever available to make HTTP queries." >&2 + return 1 + fi +} + +# +# Echo the root IANA database +# +cat_iana_dns() { + local PREVIOUS_NOCLOBBER_STATE_CMD="$( set +o | grep noclobber )" + + # Check that our cache file exists, has not been emptily created by mistake, + # and is sufficiently recent. We'll also check that the file belongs to us + # afterwards. + if [ ! -f "$IANA_CACHE_FILE" ] || [ ! -s "$IANA_CACHE_FILE" ] || [ "$( stat -c %Y "$IANA_CACHE_FILE" )" -lt "$(( $( date +%s ) - $IANA_CACHE_DURATION ))" ]; then + rm -f "$IANA_CACHE_FILE" + # For security, we activate the 'noclobber' : if a smartass raced us in + # creating the file (or a symlink to a dangerous file), the '>' will simply + # fail. + set -C + if ! ERROR_FETCH="$( fetch_with_curl_wget_or_whatever "$IANA_URL_DOMAINS" 2>&1 >"$IANA_CACHE_FILE" )"; then + echo "failed to retrieve IANA database ($ERROR_FETCH)" + $PREVIOUS_NOCLOBBER_STATE_CMD + return 1 + fi + $PREVIOUS_NOCLOBBER_STATE_CMD + fi + + # Check that the file belongs to us + if [ ! -O "$IANA_CACHE_FILE" ]; then + echo "Cache file ($IANA_CACHE_FILE) belongs to another user: stopping for security risk." + return 1 + fi + + # Output + cat "$IANA_CACHE_FILE" +} + +# +# Get expiration date from RDAP +# (output the date as seconds since Unix epoch) +# (error when exit status is != 0 or empty output) +# +get_expiration_date_rdap() { + local DOMAIN="$1" + local ROOT="$1" + + # We truncate and loop until we find a suitable RDAP server (wikipedia.co.uk -> co.uk -> uk) + while true; do + # Truncate the first part of the domain name + ROOT="$( echo "$ROOT" | sed 's/[^.]\+.\?//' )" + test -n "$ROOT" || break + + # Search for the RDAP servers + if ! LIST_SERVERS="$( cat_iana_dns )" || ! LIST_SERVERS="$( echo "$LIST_SERVERS" | jq -r ".services[] | select(.[][] == \"$ROOT\") | .[1][]" )"; then + echo "Error getting server list: $LIST_SERVERS" + return 1 + fi + # Try every server (usually only one ?) + for SERVER in $LIST_SERVERS; do + # Query the server + # (small regexp to avoid some double '/' in the URL) + if OUTPUT="$( fetch_with_curl_wget_or_whatever "$( echo "$SERVER" | sed 's#/$##' )/domain/$DOMAIN" 2>&1 )"; then + EXPIRATION_DATE="$( echo "$OUTPUT" | jq -r '.events[] | select(."eventAction" == "expiration") | .eventDate' )" + # Small protection against injection + if [ -n "$( echo "$EXPIRATION_DATE" | LANG=C sed -n '/^[a-zA-Z0-9:.\-]\{1,64\}$/p' )" ]; then + # We directly output the date as seconds from epoch. + date --date="$EXPIRATION_DATE" +%s + return + fi + fi + done + # If we have a non-empty list of servers but didn't manage to get an + # expiration date, we return an error. + if [ -n "$LIST_SERVERS" ]; then + echo "All servers ($LIST_SERVERS) failed for $DOMAIN / $ROOT (last output: $OUTPUT)" + return 1 + fi + done + + echo "No RDAP server for $DOMAIN" + return 1 +} + + +# +# Get expiration date from WHOIS +# (output the date as seconds since Unix epoch) +# (error when exit status is != 0 or empty output) +# +get_expiration_date_whois() { + local DOMAIN="$1" + local EXPIRATION_DATE + + # WHOIS query outside of the sed pipe to catch erroneous exit value. + if ! WHOIS_OUTPUT="$( whois "$DOMAIN" )"; then + return 1 + fi + + # We put each format on a dedicated line + # + filter only the first line + # Note : some sed implementations seems not to offer + # case-insensitive option, so we try not to + # use it for the moment. + # 1 - kernel.org, icann.org + # 2 - coca-cola.com (we try to ignore lines starting with space...) + # 3 - facebook.com (...but if there's nothing else, take it anyway) + # 4 - some databases with simple display (.se, .si) + # 5 - .it + # 6 - .ru + # 7 - .jp + # Add your own filter here :) + EXPIRATION_DATE="$( echo "$WHOIS_OUTPUT" | sed -n \ + -e 's/^Registry Expiry Date:[[:space:]]\+//p' \ + -e '/^[^[:space:]]/s/.*\(xpiry\|xpiration\) Date:[[:space:]]\+//p' \ + -e 's/.*\(xpiry\|xpiration\) Date:[[:space:]]\+//p' \ + -e 's/^expires\?:[[:space:]]*//p' \ + -e 's/^Expire Date:[[:space:]]*//p' \ + -e 's/^paid-till:[[:space:]]*//p' \ + -e 's/^\[Expires on\][[:space:]]*//p' \ + | sed 'q' + )" + + # Small protection against injection + if [ -n "$( echo "$EXPIRATION_DATE" | LANG=C sed -n '/^[a-zA-Z0-9:.\-]\{1,64\}$/p' )" ]; then + date --date="$EXPIRATION_DATE" +%s + fi +} + + +# +# Check if arg is an integer +# (copied from jilles @ http://stackoverflow.com/questions/806906/how-do-i-test-if-a-variable-is-a-number-in-bash ) +# +is_int() { + case "$1" in + ''|*[!0-9]*) return 1;; + *) return 0;; + esac +} + + +# Some early checks +#if ! which jq >/dev/null 2>&1; then +# echo "UNKNOWN: 'jq' command not found." +# exit 1 +#fi + + +# +# Parameters management +# +while [ "$#" -gt 0 ]; do + while getopts hw:c:iIrR f; do + case "$f" in + 'h') + usage + exit + ;; + + 'w') + THRESHOLD_WARNING="$( convert_to_seconds "$OPTARG" )" + ;; + + 'c') + THRESHOLD_CRITICAL="$( convert_to_seconds "$OPTARG" )" + ;; + + 'i') + OPTION_WHOIS_QUERYING="yes" + ;; + + 'I') + OPTION_WHOIS_QUERYING="" + ;; + + 'r') + OPTION_RDAP_QUERYING="yes" + ;; + + 'R') + OPTION_RDAP_QUERYING="" + ;; + + \?) + usage + exit 1 + ;; + esac + done + shift $( expr $OPTIND - 1 ) + + # Little checks + if ! is_int "$THRESHOLD_WARNING" || ! is_int "$THRESHOLD_CRITICAL"; then + echo "UNKNOWN invalid parameter : one of the threshold is not an integer." + exit $STATE_UNKNOWN + fi + + # End of the options, we get the domain name + if [ -z "$1" ]; then + # No more options but no domain specified ? Weird but well... + break + fi + DOMAIN="$1" + shift + + # Sleeping a bit to add some throttle if we already looped : we try not to + # flood the rdap/whois servers. + test -n "$EXPIRATION_DATE" && sleep 2 + EXPIRATION_DATE="" + + # Get the expiration date via RDAP + if [ -n "$OPTION_RDAP_QUERYING" ]; then + if OUTPUT_RDAP="$( get_expiration_date_rdap "$DOMAIN" )" && [ -n "$OUTPUT_RDAP" ]; then + EXPIRATION_DATE="$OUTPUT_RDAP" + fi + fi + # If RDAP failed to get an expiration date, we try via WHOIS. + if [ -z "$EXPIRATION_DATE" ] && [ -n "$OPTION_WHOIS_QUERYING" ]; then + if OUTPUT_WHOIS="$( get_expiration_date_whois "$DOMAIN" )" && [ -n "$OUTPUT_WHOIS" ]; then + EXPIRATION_DATE="$OUTPUT_WHOIS" + fi + fi + + if [ -z "$EXPIRATION_DATE" ]; then + # We couldn't get the date :-( + if [ "$OUTPUT_EXIT_STATUS" -eq "$STATE_OK" ]; then + OUTPUT_EXIT_STATUS="$STATE_UNKNOWN" + fi + OUTPUT_DETAIL_UNKNOWN="$OUTPUT_DETAIL_UNKNOWN $DOMAIN:could_not_get_date (output_rdap: $OUTPUT_RDAP)(output_whois: $OUTPUT_WHOIS)" + break + fi + + # Dispatch in the OK/Warning/Critical boxes + OUTPUT_DOMAIN_DETAIL="$DOMAIN:$( date --date=@$EXPIRATION_DATE +%FT%T%z)" + if [ "$EXPIRATION_DATE" -le "$( expr "$TIMESTAMP_NOW" + "$THRESHOLD_CRITICAL" )" ]; then + # Domain is critical + OUTPUT_EXIT_STATUS="$STATE_CRITICAL" + OUTPUT_DETAIL_CRITICAL="$OUTPUT_DETAIL_CRITICAL $OUTPUT_DOMAIN_DETAIL" + elif [ "$EXPIRATION_DATE" -le "$( expr "$TIMESTAMP_NOW" + "$THRESHOLD_WARNING" )" ]; then + # Domain is warning + OUTPUT_DETAIL_WARNING="$OUTPUT_DETAIL_WARNING $OUTPUT_DOMAIN_DETAIL" + # we don't change if the status is already Critical + # (but we take precedence over Unknown) + if [ "$OUTPUT_EXIT_STATUS" -ne "$STATE_CRITICAL" ]; then + OUTPUT_EXIT_STATUS="$STATE_WARNING" + fi + else + # Domain is Ok + OUTPUT_DETAIL_OK="$OUTPUT_DETAIL_OK $OUTPUT_DOMAIN_DETAIL" + fi +done + +# final output +case "$OUTPUT_EXIT_STATUS" in + "$STATE_OK") + echo "OK $OUTPUT_DETAIL_OK" + ;; + + "$STATE_WARNING") + echo "WARNING $OUTPUT_DETAIL_WARNING" + ;; + + "$STATE_CRITICAL") + echo "CRITICAL $OUTPUT_DETAIL_CRITICAL" + ;; + + "$STATE_UNKNOWN") + echo "UNKNOWN $OUTPUT_DETAIL_UNKNOWN" + ;; + + *) + echo "WTF" + ;; +esac + +exit "$OUTPUT_EXIT_STATUS"