2024-07-19 00:46:09 +02:00
#!/usr/bin/env python
2024-07-19 09:16:20 +02:00
# Copyright 2024 Alarig Le Lay <alarig@swordarmor.fr>
# Distributed under the terms of the GNU General Public License v3
2024-07-19 00:46:09 +02:00
import argparse
import datetime
import logging
2024-07-27 13:19:29 +02:00
import os
2024-07-19 00:46:09 +02:00
import requests
import nagiosplugin
import requests_cache
2024-07-27 13:19:29 +02:00
__script__ = os . path . basename ( __file__ )
__version__ = ' 0.1 '
2024-07-19 00:46:09 +02:00
_log = logging . getLogger ( ' nagiosplugin ' )
2024-07-24 14:38:08 +02:00
# cache session for json and csv storage
session = requests_cache . CachedSession (
' /tmp/iana_rdap_cache ' ,
cache_control = True
)
2024-07-19 00:46:09 +02:00
2024-07-24 14:38:08 +02:00
def find_rdap_server ( domain ) :
""" Find the TLD rdap server. """
import pandas
list2dict = [ ]
2024-07-19 12:04:13 +02:00
req = session . get ( ' https://data.iana.org/rdap/dns.json ' , timeout = 120 )
2024-07-19 11:49:31 +02:00
for k , v in req . json ( ) [ ' services ' ] :
2024-07-19 10:45:21 +02:00
for x in k :
list2dict . append ( { ' name ' : x , ' url ' : v [ 0 ] } )
2024-07-19 00:46:09 +02:00
2024-07-19 10:45:21 +02:00
df = pandas . DataFrame ( list2dict )
2024-07-19 00:46:09 +02:00
tld = domain . split ( ' . ' ) [ - 1 ]
try :
url = df [ df . name == ( tld ) ] . iloc [ 0 ] . url
# no rdap on tld
except IndexError :
raise nagiosplugin . CheckError (
2024-07-27 14:28:43 +02:00
f ' The TLD { tld } does not have an RDAP server, try forcing the registrar server with --server. It can be found on https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml '
2024-07-19 00:46:09 +02:00
)
2024-07-19 11:38:55 +02:00
_log . debug ( f ' The used RDAP server is { url } ' )
2024-07-24 14:38:08 +02:00
return url
def parse_ldap ( domain , rdap_server ) :
req_rdap = requests . get ( f ' { rdap_server } domain/ { domain } ' )
2024-07-19 00:46:09 +02:00
2024-07-19 11:38:55 +02:00
match req_rdap . status_code :
case 403 :
raise nagiosplugin . CheckError (
2024-07-24 14:38:08 +02:00
f ' Got { req_rdap . status_code } , the RDAP server { rdap_server } refused to reply '
2024-07-19 11:38:55 +02:00
)
case 404 :
raise nagiosplugin . CheckError (
f ' Got { req_rdap . status_code } , the domain { domain } has not been found '
)
2024-07-24 14:38:08 +02:00
case 409 :
raise nagiosplugin . CheckError (
f ' Got { req_rdap . status_code } , the RDAP server { rdap_server } has too many requests '
)
2024-07-19 11:38:55 +02:00
case 503 :
raise nagiosplugin . CheckError (
2024-07-24 14:38:08 +02:00
f ' Got { req_rdap . status_code } , the RDAP server { rdap_server } seems broken '
2024-07-19 11:38:55 +02:00
)
case _ :
pass
2024-07-19 12:04:13 +02:00
2024-07-24 14:38:08 +02:00
_log . debug ( f ' The used RDAP JSON from { req_rdap . url } is { req_rdap . json ( ) } ' )
2024-07-19 11:38:55 +02:00
2024-07-19 00:46:09 +02:00
raw_expiration = [
event . get ( ' eventDate ' , False )
for event in req_rdap . json ( ) . get ( ' events ' , { } )
if event . get ( ' eventAction ' , { } ) == ' expiration '
]
2024-07-24 14:38:08 +02:00
# if we have not found the field expiration in the list eventAction
if len ( raw_expiration ) == 0 :
_log . debug ( f ' The domain JSON for { domain } does not have " eventAction " . " expiration " field, run with -vvv or --debug to have the JSON dump ' )
raw_registrar = [
entity . get ( ' vcardArray ' , False )
for entity in req_rdap . json ( ) . get ( ' entities ' , { } )
if ' registrar ' in entity . get ( ' roles ' )
]
# I hope that order of the fields is consistent
# and I do not know at all what fn means
# We try to find the registrar here
for line in raw_registrar [ 0 ] [ 1 ] :
if ' fn ' in line :
raw_expiration . append ( line [ 3 ] )
elif len ( raw_expiration ) == 1 :
2024-07-19 00:46:09 +02:00
fecha = raw_expiration [ 0 ] . split ( ' T ' ) [ 0 ]
2024-07-24 14:38:08 +02:00
today = datetime . datetime . now ( )
delta = datetime . datetime . strptime ( fecha , ' % Y- % m- %d ' ) - today
raw_expiration [ 0 ] = delta . days
else :
2024-07-19 00:46:09 +02:00
raise nagiosplugin . CheckError (
2024-07-24 14:38:08 +02:00
f ' { raw_expiration } is too long '
2024-07-19 00:46:09 +02:00
)
2024-07-24 14:38:08 +02:00
return raw_expiration
2024-07-26 00:22:08 +02:00
def expiration ( domain , server ) :
2024-07-24 14:38:08 +02:00
""" Find the expiration date for the domain. """
2024-07-26 00:22:08 +02:00
if server is None :
raw_expiration = parse_ldap ( domain , find_rdap_server ( domain ) )
else :
raw_expiration = parse_ldap ( domain , server )
2024-07-24 14:38:08 +02:00
# we have parsed the eventAction field about expiration
if isinstance ( raw_expiration [ 0 ] , int ) :
return raw_expiration [ 0 ]
# we have not, so we try to fall back to registrar ldap
elif isinstance ( raw_expiration [ 0 ] , str ) :
import csv
# fetch csv
2024-07-27 14:28:43 +02:00
iana_registrars_req = session . get (
2024-07-24 14:38:08 +02:00
' https://www.iana.org/assignments/registrar-ids/registrar-ids-1.csv ' ,
timeout = 120
2024-07-27 14:28:43 +02:00
)
iana_registrars_csv = iana_registrars_req . content . decode ( ' utf-8 ' )
2024-07-24 14:38:08 +02:00
# parse csv
registrar_rdap_found = False
for registrar_row in csv . reader (
iana_registrars_csv . splitlines ( ) ,
delimiter = ' , '
) :
# lower case comparaison just in case (haha)
if registrar_row [ 1 ] . lower ( ) == raw_expiration [ 0 ] . lower ( ) :
# re-query
_log . debug ( f ' Falling back to registrar RDAP: { registrar_row [ 3 ] } ' )
registrar_rdap_found = True
registrar_expiration = parse_ldap ( domain , registrar_row [ 3 ] )
if isinstance ( registrar_expiration [ 0 ] , int ) :
return registrar_expiration [ 0 ]
else :
raise nagiosplugin . CheckError (
f ' Neither TLD or { registrar_row [ 3 ] } have expiration data '
)
if not ( registrar_rdap_found ) :
raise nagiosplugin . CheckError (
2024-07-27 14:28:43 +02:00
f ' The registrar { raw_expiration [ 0 ] } is not found from { iana_registrars_req . url } '
2024-07-24 14:38:08 +02:00
)
else :
raise nagiosplugin . CheckError (
f ' Error while parsing the JSON, { raw_expiration [ 0 ] } does not have an expected format '
)
2024-07-19 00:46:09 +02:00
# data acquisition
class Expiration ( nagiosplugin . Resource ) :
""" Domain model: domain expiration
Get the expiration date from RDAP .
The RDAP server is extracted from https : / / data . iana . org / rdap / dns . json which
2024-07-26 00:07:08 +02:00
cached to avoid useless fetching ; but the JSON from the registry RDAP is
not cached because we can not presume of the data lifetime .
2024-07-19 00:46:09 +02:00
"""
2024-07-26 00:22:08 +02:00
def __init__ ( self , domain , server ) :
2024-07-19 00:46:09 +02:00
self . domain = domain
2024-07-26 00:22:08 +02:00
self . server = server
2024-07-19 00:46:09 +02:00
def probe ( self ) :
try :
2024-07-26 00:22:08 +02:00
days_to_expiration = expiration ( self . domain , self . server )
2024-07-19 00:46:09 +02:00
except requests . exceptions . ConnectionError as err :
raise nagiosplugin . CheckError (
f ' The connection to the RDAP server failed: { err } '
)
return [ nagiosplugin . Metric (
' daystoexpiration ' ,
days_to_expiration ,
uom = ' d '
) ]
# data presentation
class ExpirationSummary ( nagiosplugin . Summary ) :
""" Status line conveying expiration information.
"""
def __init__ ( self , domain ) :
self . domain = domain
pass
# runtime environment and data evaluation
@nagiosplugin.guarded
def main ( ) :
2024-07-24 14:38:08 +02:00
import pyunycode
2024-07-19 00:46:09 +02:00
argp = argparse . ArgumentParser ( description = __doc__ )
argp . add_argument (
' -w ' , ' --warning ' , metavar = ' int ' , default = ' 30 ' ,
help = ' warning expiration max days. Default=30 '
)
argp . add_argument (
' -c ' , ' --critical ' , metavar = ' range ' , default = ' 15 ' ,
help = ' critical expiration max days. Default=15 '
)
2024-07-26 00:22:08 +02:00
argp . add_argument (
' -s ' , ' --server ' , default = None , help = ' Specify the RDAP base URL (eg. https://rdap.nic.bzh/) '
)
2024-07-19 00:46:09 +02:00
argp . add_argument (
2024-07-27 13:19:29 +02:00
' -v ' , ' --verbose ' , action = ' count ' , default = 0 , help = ' Be more verbose, can go up to -vvv '
)
argp . add_argument (
' --version ' , action = ' version ' , version = f ' { __script__ } { __version__ } ' , help = ' Print version '
2024-07-19 00:46:09 +02:00
)
2024-07-19 11:38:55 +02:00
argp . add_argument (
' -d ' , ' --debug ' , action = ' count ' , default = 0 ,
help = ' debug logging to /tmp/nagios-check_domain_expiration_rdap.log '
)
2024-07-19 00:46:09 +02:00
argp . add_argument ( ' domain ' )
args = argp . parse_args ( )
wrange = f ' @ { args . critical } : { args . warning } '
crange = f ' @~: { args . critical } '
fmetric = ' {value} days until domain expires '
2024-07-19 11:38:55 +02:00
if ( args . debug ) :
logging . basicConfig (
filename = ' /tmp/nagios-check_domain_expiration_rdap.log ' ,
encoding = ' utf-8 ' ,
format = ' %(asctime)s %(message)s ' ,
level = logging . DEBUG
)
2024-07-24 14:38:08 +02:00
domain = pyunycode . convert ( args . domain )
2024-07-19 00:46:09 +02:00
check = nagiosplugin . Check (
2024-07-26 00:22:08 +02:00
Expiration ( domain , args . server ) ,
2024-07-19 00:46:09 +02:00
nagiosplugin . ScalarContext (
' daystoexpiration ' ,
warning = wrange ,
critical = crange ,
fmt_metric = fmetric
) ,
ExpirationSummary ( args . domain )
)
check . main ( verbose = args . verbose )
if __name__ == ' __main__ ' :
main ( )