First version

This commit is contained in:
Stephane Bortzmeyer 2021-07-05 20:49:49 +02:00
parent 887e30d86a
commit ab3feeec4a
5 changed files with 426 additions and 1 deletions

View File

@ -1,3 +1,32 @@
# check_expire
A Nagios (and compatible) plugin to check the impending expiration of a domain name. It relies on RDAP exclusively (no whois) and works with every top-level domain with RDAP (which includes all the ICANN ones).
A Nagios (and compatible) plugin to check the impending expiration of a domain name. It relies on RDAP exclusively (no whois) and works with every top-level domain with RDAP (which includes all the ICANN ones).
## Usage
check_expire follows the usual Nagios rules. The options are:
* -H: domain name to monitor
* -c: critical threshold in days
* -w: warning threshold in days
* -v: verbose details in output
## Installation
You need Python 3 and [Requests](http://python-requests.org/). You can install Requests, for instance, with pip `pip3 install requests`.
Then, copy the script `check_expire` and the file `ianardap.py`to the directory of local plugins.
## License
GPL. See LICENSE.
## Authors
Stéphane Bortzmeyer <stephane+chapril@bortzmeyer.org>.
## Reference site
https://forge.chapril.org/bortzmeyer/check_expire Use the Gitea issue tracker to report bugs or wishes.

3
TODO Normal file
View File

@ -0,0 +1,3 @@
Icinga template

150
check_expire Executable file
View File

@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Monitoring plugin (Nagios-compatible) to check the impeding
expiration of a domain name, through RDAP.
The monitoring plugin API is documented at
<https://www.monitoring-plugins.org/doc/guidelines.html>.
"""
import sys
import json
import datetime
import getopt
import re
# http://python-requests.org/ for easier HTTPS retrieval
import requests
# Local module
import ianardap
VERSION = "0.0"
# Funny diversity in RDAP servers' output...
RFC3339 = "%Y-%m-%dT%H:%M:%SZ%z"
RFC3339TZ = "%Y-%m-%dT%H:%M:%S%z" # We cannot use it directly, Python uses HHMM as offset, not HH:MM as RFC 3339 does :-(
RFC3339LONG = "%Y-%m-%dT%H:%M:%S.%fZ%z" # Microseconds support (%f) does not seem documented
UTCINFO = datetime.timezone(offset=datetime.timedelta(seconds=0))
# Do not touch
# https://www.monitoring-plugins.org/doc/guidelines.html#AEN78
STATE_OK = 0
STATE_WARNING = 1
STATE_CRITICAL = 2
STATE_UNKNOWN = 3
STATE_DEPENDENT = 4
# Can be changed on the command-line
verbose = False
domain = None
critical_t = datetime.timedelta(days=2)
warning_t = datetime.timedelta(days=7)
# TODO implement timeout for HTTPS requests. -t option. Does Requests allow it?
def usage(msg=None):
print("Usage: %s -H domain-name [-c critical -w warning]" % sys.argv[0], end="")
if msg is not None and msg != "":
print(" (%s)" % msg)
else:
print("")
def details():
if verbose:
print(" RDAP database \"%s\", version %s published on %s, RDAP server is %s" % \
(database.description, database.version, database.publication, server))
else:
print("")
def error(msg=None):
if msg is None:
msg = "Unknown error"
print("%s CRITICAL: %s" % (domain, msg), end="")
details()
sys.exit(STATE_CRITICAL)
def warning(msg=None):
if msg is None:
msg = "Unknown warning"
print("%s WARNING: %s" % (domain, msg), end="")
details()
sys.exit(STATE_WARNING)
def ok(msg=None):
if msg is None:
msg = "Unknown message but everything is OK"
print("%s OK: %s" % (domain, msg), end="")
details()
sys.exit(STATE_OK)
try:
optlist, args = getopt.getopt (sys.argv[1:], "c:hH:vVw:",
["critical=", "expiration", "help", "verbose", "version", "warning="])
for option, value in optlist:
if option == "--critical" or option == "-c":
critical_t = datetime.timedelta(days=int(value))
elif option == "--help" or option == "-h":
usage()
sys.exit(STATE_OK)
elif option == "--hostname" or option == "-H":
domain = value
elif option == "--verbose" or option == "-v":
verbose = True
elif option == "--version" or option == "-V":
print("%s version %s" % (sys.argv[0], VERSION))
sys.exit(STATE_OK)
elif option == "--warning" or option == "-w":
warning_t = datetime.timedelta(days=int(value))
else:
# Should never occur, it is trapped by getopt
print("Unknown option %s" % option)
sys.exit(STATE_UNKNOWN)
except getopt.error as reason:
usage(reason)
sys.exit(STATE_UNKNOWN)
if len(args) != 0:
usage("Extraneous arguments")
sys.exit(STATE_UNKNOWN)
if domain is None:
usage("-H is mandatory")
sys.exit(STATE_UNKNOWN)
database = ianardap.IanaRDAPDatabase()
server = database.find(domain)
if server is None:
error("No RDAP server found for %s" % domain)
if server.endswith("/"):
server = server[:-1] # Donuts RDAP server balks when there are two slashes and reply 404
response = requests.get("%s/domain/%s" % (server, domain))
if response.status_code != 200:
error("Invalid RDAP return code: %s" % response.status_code)
rdap = json.loads(response.content)
for event in rdap["events"]:
if event["eventAction"] == "expiration":
try:
expiration = datetime.datetime.strptime(event["eventDate"] + "+0000", RFC3339LONG)
except ValueError:
try:
expiration = datetime.datetime.strptime(event["eventDate"] + "+0000", RFC3339)
except ValueError:
# Python standard library offers no way to parse all RFC3339-compliant datetimes, so we have to hack.
if re.search(r"\+[0-9]{2}:[0-9]{2}$", event["eventDate"]):
dt = re.sub(r"\+([0-9]{2}):([0-9]{2})$", r"+\1\2", event["eventDate"])
expiration = datetime.datetime.strptime(dt, RFC3339TZ)
else:
print("No recognized format for datetime \"%s\"" % event["eventDate"])
sys.exit(STATE_UNKNOWN)
now = datetime.datetime.now(tz=UTCINFO)
if expiration < now:
error("domain %s is already expired (%s ago)" % (domain, (now-expiration)))
rest = expiration-now
if rest < critical_t:
error("expires in %s, HURRY UP!!!" % (rest))
elif rest < warning_t:
warning("expires in %s, renew now" % (rest))
else:
ok("expires in %s." % (rest))
error("No expiration found")

68
ianardap.py Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""A simple module to get the RDAP server for a given domain name,
from the IANA database specified in RFC 7484.
"""
# http://python-requests.org/ for easier HTTPS retrieval
import requests
import datetime
import json
import os
import sys
IANABASE = "https://data.iana.org/rdap/dns.json"
CACHE = os.environ["HOME"] + "/.ianardapcache.json"
MAXAGE = 24 # Hours
class IanaRDAPDatabase():
def __init__(self, maxage=MAXAGE, cachefile=CACHE):
""" Retrieves the IANA databse, if not already cached. maxage is in hours. """
cache_valid = False
# TODO we should lock it
if os.path.exists(cachefile) and \
datetime.datetime.fromtimestamp(os.path.getmtime(cachefile)) >= \
(datetime.datetime.utcnow() - datetime.timedelta(hours = maxage)):
cache = open(cachefile, "rb")
content = cache.read()
cache.close()
cache_valid = True
else:
response = requests.get(IANABASE)
if response.status_code != 200:
raise Exception("Invalid HTTPS return code when trying to get %s: %s" % (IANABASE, response.status_code))
content = response.content
database = json.loads(content)
self.description = database["description"]
self.publication = database["publication"]
self.version = database["version"]
self.services = {}
for service in database["services"]:
for tld in service[0]:
for server in service[1]:
self.services[tld] = server
if not cache_valid:
# TODO we should lock it
cache = open(cachefile, "wb")
cache.write(content)
cache.close()
def find(self, domain):
""" Get the RDAP server for a given domain name. None if there is none."""
labels = domain.split(".")
tld = labels[len(labels)-1]
if tld in self.services:
return self.services[tld]
else:
return None
if __name__ == "__main__":
rdap = IanaRDAPDatabase(maxage=1)
print("Database \"%s\", version %s published on %s, %i services" % \
(rdap.description, rdap.version, rdap.publication, len(rdap.services)))
for domain in sys.argv[1:]:
print("%s -> %s" % (domain, rdap.find(domain)))

175
tests.yaml Normal file
View File

@ -0,0 +1,175 @@
# Run these tests from test_exe_matrix (reference Web site is
# <https://framagit.org/feth/test_exe_matrix>). You can install it
# with 'pip3 install test_exe_matrix'.
---
config:
timeout: 20
tests:
- exe: './check_expire'
args:
- '-h'
retcode: 0
partstdout: '-H domain-name'
- exe: './check_expire'
args:
- '--zzz'
retcode: 3
partstdout: 'option --zzz not recognized'
- exe: './check_expire'
args:
- '-c'
- 'foobar'
- '-H'
- 'bortzmeyer.org'
retcode: 1
partstderr: 'ValueError'
# 2021-07-05: no RDAP server for this TLD
- exe: './check_expire'
args:
- '-H'
- 'bie.re'
retcode: 2
partstdout: 'No RDAP server'
# 2021-07-05: no expiration in RDAP for this TLD (but there is one
# in whois)
- exe: './check_expire'
args:
- '-H'
- 'hopkaup.is'
retcode: 2
partstdout: 'No expiration found'
- exe: './check_expire'
args:
- '-H'
- 'bortzmeyer.org'
retcode: 0
stderr: ''
partstdout: 'OK'
- exe: './check_expire'
args:
- '-v'
- '-H'
- 'bortzmeyer.org'
retcode: 0
stderr: ''
partstdout: 'RDAP server is'
- exe: './check_expire'
args:
- '-c'
- '1000'
- '-H'
- 'sources.org'
retcode: 2
stderr: ''
partstdout: 'HURRY UP'
- exe: './check_expire'
args:
- '-w'
- '1000'
- '-H'
- 'sources.org'
retcode: 1
stderr: ''
partstdout: 'renew now'
# Now, test various RDAP servers
# Verisign
- exe: './check_expire'
args:
- '-H'
- 'nagios.com'
retcode: 0
stderr: ''
# CentralNIC
- exe: './check_expire'
args:
- '-H'
- 'botsin.space'
retcode: 0
stderr: ''
# Fred
- exe: './check_expire'
args:
- '-H'
- 'turris.cz'
retcode: 0
stderr: ''
# Afnic
- exe: './check_expire'
args:
- '-H'
- 'brest.bzh'
retcode: 0
stderr: ''
# Donuts
- exe: './check_expire'
args:
- '-H'
- 'mastodon.social'
retcode: 0
stderr: ''
# Iran, expiration date in the past
- exe: './check_expire'
args:
- '-H'
- 'nic.pars'
retcode: 2
partstdout: "already expired"
stderr: ''
# Brazil
- exe: './check_expire'
args:
- '-H'
- 'nic.globo'
retcode: 0
stderr: ''
# Fury (Canada)
- exe: './check_expire'
args:
- '-H'
- 'blackbox.kiwi'
retcode: 0
stderr: ''
# JPRS
- exe: './check_expire'
args:
- '-H'
- 'nic.jprs'
retcode: 0
stderr: ''
# Nominet
- exe: './check_expire'
args:
- '-H'
- 'nic.abogado'
retcode: 0
stderr: ''
# .at
- exe: './check_expire'
args:
- '-H'
- 'nic.wien'
retcode: 0
stderr: ''