From 24ec4d53edf8adaf40ffff6fcf462d3ee125b2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Wed, 22 Mar 2023 14:22:54 +0100 Subject: [PATCH] [issue-4] [add] check:tls_certificate --- examples/main.hmdl.json | 4 +- examples/tls_certificate.hmdl.json | 19 +++++ source/localization/de.json | 2 + source/localization/en.json | 2 + source/logic/checks/tls_certificate.py | 113 +++++++++++++++++++++++++ source/logic/conf.py | 6 +- source/logic/main.py | 7 +- source/logic/packages.py | 3 + tools/build | 1 + 9 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 examples/tls_certificate.hmdl.json create mode 100644 source/logic/checks/tls_certificate.py 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..692a1cd 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 augelaufen", + "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..007a658 --- /dev/null +++ b/source/logic/checks/tls_certificate.py @@ -0,0 +1,113 @@ +''' +todo: allow_self_signed +todo: allow_bad_domain +''' +class implementation_check_kind_tls_certificate(interface_check_kind): + + ''' + [implementation] + ''' + def parameters_schema(self): + return { + "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" + ] + } + + + ''' + [implementation] + ''' + def normalize_conf_node(self, node): + if (not "host" in node): + raise ValueError("missing mandatory field 'host'") + else: + return dict_merge( + { + "strict": True, + "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"], 443, )) + 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"], + "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"], + "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/conf.py b/source/logic/conf.py index 19e407b..bd38314 100644 --- a/source/logic/conf.py +++ b/source/logic/conf.py @@ -227,9 +227,9 @@ def conf_normalize_schedule( node ) return { - "regular_interval": conf_normalize_interval(node["regular_interval"]), - "attentive_interval": conf_normalize_interval(node["attentive_interval"]), - "reminding_interval": conf_normalize_interval(node["reminding_interval"]), + "regular_interval": conf_normalize_interval(node_["regular_interval"]), + "attentive_interval": conf_normalize_interval(node_["attentive_interval"]), + "reminding_interval": conf_normalize_interval(node_["reminding_interval"]), } diff --git a/source/logic/main.py b/source/logic/main.py index 4e08bc8..6c1e3c3 100644 --- a/source/logic/main.py +++ b/source/logic/main.py @@ -12,7 +12,11 @@ def state_decode(state_encoded): "timestamp": state_encoded["timestamp"], "condition": condition_decode(state_encoded["condition"]), "count": state_encoded["count"], - "last_notification_timestamp": state_encoded["last_notification_timestamp"], + "last_notification_timestamp": ( + state_encoded["last_notification_timestamp"] + if ("last_notification_timestamp" in state_encoded) else + None + ), } @@ -117,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"),