diff --git a/doc/hmdl.schema.json b/doc/hmdl.schema.json index 93a0190..4a2393f 100644 --- a/doc/hmdl.schema.json +++ b/doc/hmdl.schema.json @@ -571,6 +571,49 @@ "parameters" ] }, + { + "title": "check 'tls_certificate'", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "tls_certificate" + ] + }, + "parameters": { + "type": "object", + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "strict": { + "description": "whether a violation of this check shall be leveled as critical instead of concerning", + "type": "boolean", + "default": true + }, + "expiry_threshold": { + "description": "in days; allowed amount of valid days before the certificate expires", + "type": [ + "null", + "integer" + ], + "default": 7, + "minimum": 0 + } + }, + "required": [ + "host" + ] + } + }, + "required": [ + "kind", + "parameters" + ] + }, { "title": "check 'http_request'", "type": "object", diff --git a/examples/main.hmdl.json b/examples/main.hmdl.json index bdbc795..99c1cf3 100644 --- a/examples/main.hmdl.json +++ b/examples/main.hmdl.json @@ -15,8 +15,6 @@ ] }, "includes": [ - "script.hmdl.json", - "file_state.hmdl.json", - "generic_remote.hmdl.json" + "tls_certificate.hmdl.json" ] } diff --git a/examples/tls_certificate.hmdl.json b/examples/tls_certificate.hmdl.json new file mode 100644 index 0000000..69c786d --- /dev/null +++ b/examples/tls_certificate.hmdl.json @@ -0,0 +1,19 @@ +{ + "checks": [ + { + "name": "test1", + "kind": "tls_certificate", + "parameters": { + "host": "greenscale.de", + "expiry_threshold": 50 + } + }, + { + "name": "test2", + "kind": "tls_certificate", + "parameters": { + "host": "chemnitz-gesundheit.de" + } + } + ] +} diff --git a/source/localization/de.json b/source/localization/de.json index af7b4f2..f650cdb 100644 --- a/source/localization/de.json +++ b/source/localization/de.json @@ -17,6 +17,8 @@ "checks.file_state.timestamp_implausible": "Datei ist scheinbar aus der Zukunft", "checks.file_state.too_old": "Datei ist zu alt", "checks.file_state.too_big": "Datei ist zu groß", + "checks.tls_certificate.not_obtainable": "TLS-Zertifikat nicht abrufbar; evtl. bereits ausgelaufen", + "checks.tls_certificate.expires_soon": "TLS-Zertifikat läuft bald aus", "checks.generic_remote.overflow": "Laufwerk fast voll", "checks.http_request.request_failed": "HTTP-Abfrage fehlgeschlagen", "checks.http_request.status_code_mismatch": "Status-Code {{status_code_actual}} stimmt nicht mit dem erwarteten Wert {{status_code_expected}} überein", diff --git a/source/localization/en.json b/source/localization/en.json index 9ace217..54d0442 100644 --- a/source/localization/en.json +++ b/source/localization/en.json @@ -17,6 +17,8 @@ "checks.file_state.timestamp_implausible": "file is apparently from the future", "checks.file_state.too_old": "file is too old", "checks.file_state.too_big": "file is too big", + "checks.tls_certificate.not_obtainable": "TLS certificate not obtainable; maybe already expired", + "checks.tls_certificate.expires_soon": "TLS certificate will expire soon", "checks.generic_remote.overflow": "disk drive almost full", "checks.http_request.request_failed": "HTTP request failed", "checks.http_request.status_code_mismatch": "actual status code {{status_code_actual}} does not match expected value {{status_code_expected}}", diff --git a/source/logic/checks/tls_certificate.py b/source/logic/checks/tls_certificate.py new file mode 100644 index 0000000..0548f07 --- /dev/null +++ b/source/logic/checks/tls_certificate.py @@ -0,0 +1,121 @@ +''' +todo: allow_self_signed +todo: allow_bad_domain +todo: +''' +class implementation_check_kind_tls_certificate(interface_check_kind): + + ''' + [implementation] + ''' + def parameters_schema(self): + return { + "type": "object", + "additionalProperties": False, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "default": 443 + }, + "strict": { + "description": "whether a violation of this check shall be leveled as critical instead of concerning", + "type": "boolean", + "default": True + }, + "expiry_threshold": { + "description": "in days; allowed amount of valid days before the certificate expires", + "type": ["null", "integer"], + "default": 7, + "minimum": 0 + } + }, + "required": [ + "host" + ] + } + + + ''' + [implementation] + ''' + def normalize_conf_node(self, node): + if (not "host" in node): + raise ValueError("missing mandatory field 'host'") + else: + return dict_merge( + { + "strict": True, + "port": 443, + "expiry_threshold": 7, + # "allow_self_signed": False, + # "allow_bad_domain": False, + }, + node + ) + return node + + + ''' + [implementation] + ''' + def run(self, parameters): + context = _ssl.create_default_context() + try: + socket = _socket.create_connection((parameters["host"], parameters["port"], )) + socket_wrapped = context.wrap_socket(socket, server_hostname = parameters["host"]) + version = socket_wrapped.version() + data = socket_wrapped.getpeercert(False) + except _ssl.SSLCertVerificationError as error: + version = None + data = None + if (data is None): + return { + "condition": ( + enum_condition.critical + if parameters["strict"] else + enum_condition.concerning + ), + "info": { + "host": parameters["host"], + "port": parameters["port"], + "faults": [ + translation_get("checks.tls_certificate.not_obtainable"), + ], + "data": { + }, + } + } + else: + # version == "TLSv1.3" + expiry_timestamp = _ssl.cert_time_to_seconds(data["notAfter"]) + current_timestamp = get_current_timestamp() + days = _math.ceil((expiry_timestamp - current_timestamp) / (60 * 60 * 24)) + if (days <= parameters["expiry_threshold"]): + return { + "condition": ( + enum_condition.critical + if parameters["strict"] else + enum_condition.concerning + ), + "info": { + "host": parameters["host"], + "port": parameters["port"], + "faults": [ + translation_get("checks.tls_certificate.expires_soon"), + ], + "data": { + "expiry_timestamp": expiry_timestamp, + "days": days, + }, + } + } + else: + return { + "condition": enum_condition.ok, + "info": { + } + } + diff --git a/source/logic/main.py b/source/logic/main.py index a6a7a44..6c1e3c3 100644 --- a/source/logic/main.py +++ b/source/logic/main.py @@ -121,6 +121,7 @@ def main(): check_kind_implementations = { "script": implementation_check_kind_script(), "file_state": implementation_check_kind_file_state(), + "tls_certificate": implementation_check_kind_tls_certificate(), "http_request": implementation_check_kind_http_request(), "generic_remote" : implementation_check_kind_generic_remote(), } diff --git a/source/logic/packages.py b/source/logic/packages.py index 38ef464..cb23ce6 100644 --- a/source/logic/packages.py +++ b/source/logic/packages.py @@ -1,6 +1,7 @@ import sys as _sys import os as _os import subprocess as _subprocess +import math as _math import hashlib as _hashlib import tempfile as _tempfile import argparse as _argparse @@ -11,3 +12,5 @@ import time as _time import datetime as _datetime import smtplib as _smtplib from email.mime.text import MIMEText +import ssl as _ssl +import socket as _socket diff --git a/tools/build b/tools/build index 255c85b..a9cb503 100755 --- a/tools/build +++ b/tools/build @@ -44,6 +44,7 @@ def main(): _os.path.join(dir_source, "logic", "checks", "_interface.py"), _os.path.join(dir_source, "logic", "checks", "script.py"), _os.path.join(dir_source, "logic", "checks", "file_state.py"), + _os.path.join(dir_source, "logic", "checks", "tls_certificate.py"), _os.path.join(dir_source, "logic", "checks", "http_request.py"), _os.path.join(dir_source, "logic", "checks", "generic_remote.py"), _os.path.join(dir_source, "logic", "channels", "_interface.py"),