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 1/3] [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"), From 3d4843439d4ac0016506fea066fb90dfd85e7663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Wed, 22 Mar 2023 14:25:21 +0100 Subject: [PATCH 2/3] [upd] doc:schema --- doc/hmdl.schema.json | 111 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/doc/hmdl.schema.json b/doc/hmdl.schema.json index 2804d2b..4a2393f 100644 --- a/doc/hmdl.schema.json +++ b/doc/hmdl.schema.json @@ -19,7 +19,7 @@ "default": 3 }, "annoy": { - "description": "whether notifications shall be kept sending after the threshold has been surpassed", + "description": "whether notifications about non-ok states shall be kept sending after the threshold has been surpassed", "type": "boolean", "default": false }, @@ -31,7 +31,10 @@ "anyOf": [ { "description": "in seconds", - "type": "integer", + "type": [ + "null", + "integer" + ], "exclusiveMinimum": 0 }, { @@ -51,7 +54,10 @@ "anyOf": [ { "description": "in seconds", - "type": "integer", + "type": [ + "null", + "integer" + ], "exclusiveMinimum": 0 }, { @@ -66,6 +72,26 @@ } ], "default": 120 + }, + "reminding_interval": { + "anyOf": [ + { + "description": "in seconds", + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "description": "as text", + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week" + ] + } + ], + "default": 86400 } }, "required": [] @@ -240,7 +266,7 @@ "default": 3 }, "annoy": { - "description": "whether notifications shall be kept sending after the threshold has been surpassed", + "description": "whether notifications about non-ok states shall be kept sending after the threshold has been surpassed", "type": "boolean", "default": false }, @@ -252,7 +278,10 @@ "anyOf": [ { "description": "in seconds", - "type": "integer", + "type": [ + "null", + "integer" + ], "exclusiveMinimum": 0 }, { @@ -272,7 +301,10 @@ "anyOf": [ { "description": "in seconds", - "type": "integer", + "type": [ + "null", + "integer" + ], "exclusiveMinimum": 0 }, { @@ -287,6 +319,26 @@ } ], "default": 120 + }, + "reminding_interval": { + "anyOf": [ + { + "description": "in seconds", + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "description": "as text", + "type": "string", + "enum": [ + "minute", + "hour", + "day", + "week" + ] + } + ], + "default": 86400 } }, "required": [] @@ -519,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", @@ -621,7 +716,7 @@ "type": "object", "additionalProperties": false, "properties": { - "ssh_host": { + "host": { "type": "string" }, "ssh_port": { @@ -660,7 +755,7 @@ } }, "required": [ - "ssh_host" + "host" ] } }, From 88d12eb88f5f4bb0098f6204a91059dea06e53f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Wed, 22 Mar 2023 15:19:36 +0100 Subject: [PATCH 3/3] [issue-4] [add] parameter:port --- source/localization/de.json | 2 +- source/logic/checks/tls_certificate.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/source/localization/de.json b/source/localization/de.json index 692a1cd..f650cdb 100644 --- a/source/localization/de.json +++ b/source/localization/de.json @@ -17,7 +17,7 @@ "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.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", diff --git a/source/logic/checks/tls_certificate.py b/source/logic/checks/tls_certificate.py index 007a658..0548f07 100644 --- a/source/logic/checks/tls_certificate.py +++ b/source/logic/checks/tls_certificate.py @@ -1,6 +1,7 @@ ''' todo: allow_self_signed todo: allow_bad_domain +todo: ''' class implementation_check_kind_tls_certificate(interface_check_kind): @@ -15,6 +16,10 @@ class implementation_check_kind_tls_certificate(interface_check_kind): "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", @@ -43,6 +48,7 @@ class implementation_check_kind_tls_certificate(interface_check_kind): return dict_merge( { "strict": True, + "port": 443, "expiry_threshold": 7, # "allow_self_signed": False, # "allow_bad_domain": False, @@ -58,7 +64,7 @@ class implementation_check_kind_tls_certificate(interface_check_kind): def run(self, parameters): context = _ssl.create_default_context() try: - socket = _socket.create_connection((parameters["host"], 443, )) + 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) @@ -74,6 +80,7 @@ class implementation_check_kind_tls_certificate(interface_check_kind): ), "info": { "host": parameters["host"], + "port": parameters["port"], "faults": [ translation_get("checks.tls_certificate.not_obtainable"), ], @@ -95,6 +102,7 @@ class implementation_check_kind_tls_certificate(interface_check_kind): ), "info": { "host": parameters["host"], + "port": parameters["port"], "faults": [ translation_get("checks.tls_certificate.expires_soon"), ],