diff --git a/source/localization/de.json b/source/localization/de.json index 8ae818d..5537080 100644 --- a/source/localization/de.json +++ b/source/localization/de.json @@ -26,7 +26,7 @@ "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.request_failed": "HTTP-Abfrage fehlgeschlagen: {{reason}}", "checks.http_request.status_code_mismatch": "Status-Code {{status_code_actual}} stimmt nicht mit dem erwarteten Wert {{status_code_expected}} überein", "checks.http_request.header_missing": "Header '{{key}}' ist nicht gesetzt und hat somit nicht den erwarteten Wert '{{value_expected}}'", "checks.http_request.header_value_mismatch": "Header-Wert für Schlüssel '{{key}}' '{{value_actual}}' stimmt nicht mit erwartetem Wert '{{value_expected}}' überein", diff --git a/source/localization/en.json b/source/localization/en.json index 318dcc5..3c78bf9 100644 --- a/source/localization/en.json +++ b/source/localization/en.json @@ -26,7 +26,7 @@ "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.request_failed": "HTTP request failed: {{reason}}", "checks.http_request.status_code_mismatch": "actual status code {{status_code_actual}} does not match expected value {{status_code_expected}}", "checks.http_request.header_missing": "header '{{key}}' is unset and hence does not match the expected value '{{value_expected}}'", "checks.http_request.header_value_mismatch": "actual header value for key '{{key}}' '{{value_actual}}' does not match the expected value {{value_expected}}", diff --git a/source/logic.old/check_kinds/file_state.py b/source/logic.old/check_kinds/file_state.py deleted file mode 100644 index 406b875..0000000 --- a/source/logic.old/check_kinds/file_state.py +++ /dev/null @@ -1,254 +0,0 @@ -class implementation_check_kind_file_state(interface_check_kind): - - ''' - [implementation] - ''' - def parameters_schema(self): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - }, - "exist_mode": { - "description": "whether the file is supposed to exist or not", - "type": "boolean", - "default": True, - }, - "exist_critical": { - "description": "whether a violation of the extist state (parameter 'exist_mode') shall be considered as critical (true) or concerning (false)", - "type": "boolean", - "default": True, - }, - "age_threshold_concerning": { - "description": "in seconds; ignored if 'exist_mode' is set to false", - "type": ["null", "integer"], - "exclusiveMinimum": 0, - "default": None, - }, - "age_threshold_critical": { - "description": "in seconds; ignored if 'exist_mode' is set to false", - "type": ["null", "integer"], - "exclusiveMinimum": 0, - "default": None, - }, - "size_threshold_concerning": { - "description": "in bytes; ignored if 'exist_mode' is set to false", - "type": ["null", "integer"], - "exclusiveMinimum": 0, - "default": None, - }, - "size_threshold_critical": { - "description": "in bytes; ignored if 'exist_mode' is set to false", - "type": ["null", "integer"], - "exclusiveMinimum": 0, - "default": None, - }, - - # deprecated - "strict": { - "deprecated": True, - "description": "", - "type": "boolean", - "default": True, - }, - "exist": { - "deprecated": True, - "description": "", - "type": "boolean", - "default": True, - }, - "age_threshold": { - "deprecated": True, - "description": "", - "type": ["null", "integer"], - "exclusiveMinimum": 0, - "default": None, - }, - "size_threshold": { - "deprecated": True, - "description": "", - "type": ["null", "integer"], - "exclusiveMinimum": 0, - "default": None, - }, - }, - "required": [ - "path", - ] - } - - - ''' - [implementation] - ''' - def normalize_order_node(self, node): - version = ( - "v1" - if (not ("exist_mode" in node)) else - "v2" - ) - - if (version == "v1"): - if ("path" not in node): - raise ValueError("missing mandatory field 'path'") - else: - node_ = dict_merge( - { - "critical": True, - "exist": True, - "age_threshold": None, - "size_threshold": None, - }, - node - ) - return { - "exist_mode": node_["exist"], - "exist_critical": node_["strict"], - "age_threshold_concerning": ( - None - if node_["strict"] else - node_["age_threshold"] - ), - "age_threshold_critical": ( - node_["age_threshold"] - if node_["strict"] else - None - ), - "size_threshold_concerning": ( - None - if node_["strict"] else - node_["age_threshold"] - ), - "size_threshold_critical": ( - node_["age_threshold"] - if node_["strict"] else - None - ), - } - elif (version == "v2"): - if ("path" not in node): - raise ValueError("missing mandatory field 'path'") - else: - node_ = dict_merge( - { - "exist_mode": True, - "exist_critical": True, - "age_threshold_concerning": None, - "age_threshold_critical": None, - "size_threshold_concerning": None, - "size_threshold_critical": None, - }, - node - ) - return node_ - else: - raise ValueError("unhandled") - - - - ''' - [implementation] - ''' - def run(self, parameters): - condition = enum_condition.ok - faults = [] - data = {} - exists = _os.path.exists(parameters["path"]) - if (not parameters["exist_mode"]): - if (exists): - condition = ( - enum_condition.critical - if parameters["exist_critical"] else - enum_condition.concerning - ) - faults.append(translation_get("checks.file_state.exists")) - else: - pass - else: - if (not exists): - condition = ( - enum_condition.critical - if parameters["exist_critical"] else - enum_condition.concerning - ) - faults.append(translation_get("checks.file_state.missing")) - else: - stat = _os.stat(parameters["path"]) - ## age - if True: - timestamp_this = get_current_timestamp() - timestamp_that = int(stat.st_atime) - age = (timestamp_this - timestamp_that) - if (age < 0): - condition = enum_condition.critical - faults.append(translation_get("checks.file_state.timestamp_implausible")) - else: - if ( - (parameters["age_threshold_critical"] is not None) - and - (age > parameters["age_threshold_critical"]) - ): - condition = enum_condition.critical - faults.append(translation_get("checks.file_state.too_old")) - else: - if ( - (parameters["age_threshold_concerning"] is not None) - and - (age > parameters["age_threshold_concerning"]) - ): - condition = enum_condition.concerning - faults.append(translation_get("checks.file_state.too_old")) - else: - pass - data = dict_merge( - data, - { - "timestamp_of_checking_instance": timestamp_this, - "timestamp_of_file": timestamp_that, - "age_value_in_seconds": age, - "age_threshold_in_seconds_concerning": parameters["age_threshold_concerning"], - "age_threshold_in_seconds_concerning": parameters["age_threshold_critical"], - } - ) - ## size - if True: - size = stat.st_size - if (size < 0): - condition = enum_condition.critical - faults.append(translation_get("checks.file_state.size_implausible")) - else: - if ( - (parameters["size_threshold_critical"] is not None) - and - (size > parameters["size_threshold_critical"]) - ): - condition = enum_condition.critical - faults.append(translation_get("checks.file_state.too_big")) - else: - if ( - (parameters["size_threshold_concerning"] is not None) - and - (size > parameters["size_threshold_concerning"]) - ): - condition = enum_condition.concerning - faults.append(translation_get("checks.file_state.too_big")) - else: - pass - data = dict_merge( - data, - { - "size_value_in_bytes": size, - "size_threshold_in_bytes": parameters["size_threshold"], - } - ) - return { - "condition": condition, - "info": { - "path": parameters["path"], - "faults": faults, - "data": data, - } - } - diff --git a/source/logic.old/check_kinds/script.py b/source/logic.old/check_kinds/script.py deleted file mode 100644 index b819151..0000000 --- a/source/logic.old/check_kinds/script.py +++ /dev/null @@ -1,63 +0,0 @@ -class implementation_check_kind_script(interface_check_kind): - - ''' - [implementation] - ''' - def parameters_schema(self): - return { - "type": "object", - "additionalProperties": False, - "properties": { - "path": { - "type": "string" - }, - "arguments": { - "type": "array", - "item": { - "type": "string" - } - }, - }, - "required": [ - "path", - ] - } - - - ''' - [implementation] - ''' - def normalize_order_node(self, node): - return dict_merge( - { - }, - node - ) - - - ''' - [implementation] - ''' - def run(self, parameters): - result = shell_command( - " ".join([parameters["path"]] + parameters["arguments"]) - ) - if (result["return_code"] == 0): - condition = enum_condition.ok - elif (result["return_code"] == 1): - condition = enum_condition.unknown - elif (result["return_code"] == 2): - condition = enum_condition.concerning - elif (result["return_code"] == 3): - condition = enum_condition.critical - else: - # raise ValueError("invalid exit code: %i" % result.returncode) - condition = enum_condition.unknown - return { - "condition": condition, - "info": { - "stdout": result["stdout"], - "stderr": result["stderr"], - }, - } - diff --git a/source/logic.old/check_kinds/tls_certificate.py b/source/logic.old/check_kinds/tls_certificate.py deleted file mode 100644 index 92fe9b0..0000000 --- a/source/logic.old/check_kinds/tls_certificate.py +++ /dev/null @@ -1,172 +0,0 @@ -''' -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 - }, - "expiry_threshold_concerning": { - "description": "in days; allowed amount of valid days before the certificate expires; threshold for condition 'concerning'; 'null' means 'report at no value'", - "type": ["null", "integer"], - "default": 7, - "minimum": 0 - }, - "expiry_threshold_critical": { - "description": "in days; allowed amount of valid days before the certificate expires; threshold for condition 'critical'; 'null' means 'report at no value'", - "type": ["null", "integer"], - "default": 1, - "minimum": 0 - }, - "expiry_threshold": { - "deprecated": True, - "description": "", - "type": ["null", "integer"], - "minimum": 0, - "default": None, - }, - "strict": { - "deprecated": True, - "description": "", - "type": ["null", "boolean"], - "default": None, - }, - }, - "required": [ - "host", - ] - } - - - ''' - [implementation] - ''' - def normalize_order_node(self, node): - version = ( - "v1" - if ( - (not ("expiry_threshold_concerning" in node)) - and - (not ("expiry_threshold_critical" in node)) - ) else - "v2" - ) - - if (version == "v1"): - if (not "host" in node): - raise ValueError("missing mandatory field 'host'") - else: - node_ = dict_merge( - { - "port": 443, - "expiry_threshold": 7, - "strict": True, - # "allow_self_signed": False, - # "allow_bad_domain": False, - }, - node - ) - return { - "port": node_["port"], - "expiry_threshold_concerning": ( - None - if node_["strict"] else - node_["expiry_threshold"] - ), - "expiry_threshold_critical": ( - node_["expiry_threshold"] - if node_["strict"] else - None - ), - } - elif (version == "v2"): - if (not "host" in node): - raise ValueError("missing mandatory field 'host'") - else: - node_ = dict_merge( - { - "port": 443, - "expiry_threshold_concerning": 7, - "expiry_threshold_critical": 1, - # "allow_self_signed": False, - # "allow_bad_domain": False, - }, - node - ) - return node_ - else: - raise ValueError("unhandled") - - - ''' - [implementation] - ''' - def run(self, parameters): - faults = [] - data = {} - context = _ssl.create_default_context() - condition = enum_condition.ok - try: - socket = _socket.create_connection((parameters["host"], parameters["port"], )) - socket_wrapped = context.wrap_socket(socket, server_hostname = parameters["host"]) - version = socket_wrapped.version() - stuff = socket_wrapped.getpeercert(False) - except _ssl.SSLCertVerificationError as error: - version = None - stuff = None - if (stuff is None): - faults.append(translation_get("checks.tls_certificate.not_obtainable")) - condition = enum_condition.critical - else: - # version == "TLSv1.3" - expiry_timestamp = _ssl.cert_time_to_seconds(stuff["notAfter"]) - current_timestamp = get_current_timestamp() - days = _math.ceil((expiry_timestamp - current_timestamp) / (60 * 60 * 24)) - data = dict_merge( - data, - { - "expiry_timestamp": expiry_timestamp, - "days": days, - }, - ) - if ( - (parameters["expiry_threshold_critical"] is not None) - and - (days <= parameters["expiry_threshold_critical"]) - ): - faults.append(translation_get("checks.tls_certificate.expires_soon")) - condition = enum_condition.critical - else: - if ( - (parameters["expiry_threshold_concerning"] is not None) - and - (days <= parameters["expiry_threshold_concerning"]) - ): - faults.append(translation_get("checks.tls_certificate.expires_soon")) - condition = enum_condition.concerning - else: - pass - return { - "condition": condition, - "info": { - "host": parameters["host"], - "port": parameters["port"], - "faults": faults, - "data": data, - } - } - diff --git a/source/logic/check_kinds/generic_remote.ts b/source/logic/check_kinds/generic_remote.ts new file mode 100644 index 0000000..2c51b3d --- /dev/null +++ b/source/logic/check_kinds/generic_remote.ts @@ -0,0 +1,282 @@ +namespace _heimdall.check_kinds.generic_remote +{ + + /** + */ + function parameters_schema( + ) : _heimdall.helpers.json_schema.type_schema + { + return { + "type": "object", + "additionalProperties": false, + "properties": { + "host" : { + "type" : "string" + }, + "ssh_port": { + "anyOf": [ + { + "type": "null", + "default": null + }, + { + "type": "integer", + "default": null + }, + ] + }, + "ssh_user" : { + "anyOf": [ + { + "type": "null", + "default": null + }, + { + "type": "integer", + "default": null + }, + ] + }, + "ssh_key" : { + "anyOf": [ + { + "type": "null", + "default": null + }, + { + "type": "integer", + "default": null + }, + ] + }, + "mount_point" : { + "type" : "string", + "default" : "/" + }, + "threshold" : { + "type" : "integer", + "default" : 95, + "description" : "maximaler Füllstand in Prozent" + }, + "critical": { + "description": "whether a violation of this check shall be leveled as critical instead of concerning", + "type": "boolean", + "default": true + }, + "strict": { + "deprecated": true, + "description": "alias for 'critical'", + "type": "boolean", + "default": true + }, + }, + "required": [ + "host", + ] + }; + } + + + /** + */ + function normalize_order_node( + node : any + ) : any + { + const version : string = ( + (! ("critical" in node)) + ? "v1" + : "v2" + ); + switch (version) { + default: { + throw (new Error("unhandled version")); + break; + } + case "v1": { + if (! ("host" in node)) { + throw (new Error("mandatory parameter \"host\" missing")); + } + else { + const node_ = lib_plankton.object.patched( + { + "ssh_port": null, + "ssh_user": null, + "ssh_key": null, + "mount_point": "/", + "threshold": 95, + "strict": false, + }, + node, + true + ); + return { + "ssh_port": node_["ssh_port"], + "ssh_user": node_["ssh_user"], + "ssh_key": node_["ssh_key"], + "mount_point": node_["ssh_path"], + "threshold": node_["ssh_threshold"], + "critical": node_["strict"], + }; + } + break; + } + case "v2": { + if (! ("host" in node)) { + throw (new Error("mandatory parameter \"host\" missing")); + } + else { + const node_ = lib_plankton.object.patched( + { + "ssh_port": null, + "ssh_user": null, + "ssh_key": null, + "mount_point": "/", + "threshold": 95, + "critical": false, + }, + node, + true + ); + return node_; + } + break; + } + } + } + + + /** + */ + async function run( + parameters + ) : Promise<_heimdall.type_result> + { + const nm_child_process = require("child_process"); + + const inner_command : string = lib_plankton.string.coin( + "df {{mount_point}} | tr -s \" \"", + { + "mount_point": parameters["mount_point"], + } + ); + + let outer_command_parts : Array = []; + if (true) { + outer_command_parts.push("ssh"); + } + if (true) { + outer_command_parts.push(lib_plankton.string.coin("{{host}}", {"host": parameters["host"]})); + } + if (parameters["ssh_port"] !== null) { + outer_command_parts.push(lib_plankton.string.coin("-p {{port}}", {"port": parameters["ssh_port"].toFixed(0)})); + } + if (parameters["ssh_user"] !== null) { + outer_command_parts.push(lib_plankton.string.coin("-l {{user}}", {"user": parameters["ssh_user"]})); + } + if (parameters["ssh_key"] !== null) { + outer_command_parts.push(lib_plankton.string.coin("-i {{key}}", {"key": parameters["ssh_key"]})); + } + if (true) { + outer_command_parts.push(lib_plankton.string.coin("-o BatchMode=yes", {})); + } + if (true) { + outer_command_parts.push(lib_plankton.string.coin("'{{inner_command}}'", {"inner_command": inner_command})); + } + const outer_command : string = outer_command_parts.join(" "); + + type type_result = { + return_code : int; + stdout : string; + stderr : string; + }; + /** + * @see https://nodejs.org/api/child_process.html#child_processspawncommand-args-options + */ + const result : type_result = await new Promise( + (resolve, reject) => { + nm_child_process.spawnSync( + outer_command, + [], + { + }, + (result) => { + if (result.error) { + reject(result.error); + } + else { + resolve( + { + "return_code": result.status, + "stdout": result.stdout, + "stderr": result.stdin, + } + ); + } + } + ); + } + ); + + if (result.return_code > 0) { + return { + "condition": _heimdall.enum_condition.unknown, + "info": { + "error": result.stderr, + } + }; + } + else { + const stuff : Array = result.stdout.split("\n").slice(-2)[0].split(" "); + const data = { + "device": stuff[0], + "used": parseInt(stuff[2]), + "avail": parseInt(stuff[3]), + "perc": parseInt(stuff[4].slice(-1)), + }; + let faults : Array = []; + if (data["perc"] > parameters["threshold"]) { + faults.push(lib_plankton.translate.get("checks.generic_remote.overflow")); + } + else { + // do nothing + } + return { + "condition": ( + (faults.length <= 0) + ? _heimdall.enum_condition.ok + : ( + parameters["critical"] + ? _heimdall.enum_condition.critical + : _heimdall.enum_condition.concerning + ) + ), + "info": { + "data": { + "host": parameters["host"], + "device": data["device"], + "mount_point": parameters["mount_point"], + "used": _heimdall.helpers.misc.format_bytes(data["used"]), + "available": _heimdall.helpers.misc.format_bytes(data["avail"]), + "percentage": (data["perc"].toFixed(0) + "%"), + }, + "faults": faults, + }, + }; + } + } + + + /** + */ + export function check_kind_implementation( + ) : type_check_kind + { + return { + "parameters_schema": parameters_schema, + "normalize_order_node": normalize_order_node, + "run": run, + }; + } + +} diff --git a/source/logic/check_kinds/http_request.ts b/source/logic/check_kinds/http_request.ts index 9c240f5..da55c4f 100644 --- a/source/logic/check_kinds/http_request.ts +++ b/source/logic/check_kinds/http_request.ts @@ -177,17 +177,29 @@ namespace _heimdall.check_kinds.http_request /** */ async function run( - parameters + parameters : { + request : { + target : string; + method : string; + }; + timeout : float; + follow_redirects : boolean; + response : { + status_code : int; + headers ?: Record; + body_part ?: string; + }; + critical : boolean; + } ) : Promise<_heimdall.type_result> { let error : (null | Error); const http_request : lib_plankton.http.type_request = { - "host": parameters["request"]["target"], - "query": "", + "target": parameters.request.target, "method": { "GET": lib_plankton.http.enum_method.get, "POST": lib_plankton.http.enum_method.post, - }[parameters["request"]["method"]], + }[parameters.request.method], "headers": {}, "body": "", }; @@ -196,8 +208,9 @@ namespace _heimdall.check_kinds.http_request http_response = await lib_plankton.http.call( http_request, { - "timeout": parameters["timeout"], - "follow_redirects": parameters["follow_redirects"], + "timeout": /*parameters.timeout*/20.0, + "follow_redirects": parameters.follow_redirects, + "implementation": "http_module", } ); error = null; @@ -209,14 +222,14 @@ namespace _heimdall.check_kinds.http_request if (http_response === null) { return { "condition": ( - parameters["strict"] + parameters.critical ? _heimdall.enum_condition.critical : _heimdall.enum_condition.concerning ), "info": { - "request": parameters["request"], + "request": parameters.request, "faults": [ - lib_plankton.translate.get("checks.http_request.request_failed"), + lib_plankton.translate.get("checks.http_request.request_failed", {"reason": error.toString()}), ], }, }; @@ -226,15 +239,15 @@ namespace _heimdall.check_kinds.http_request // status code { if ( - (! ("status_code" in parameters["response"])) + (! ("status_code" in parameters.response)) || - (parameters["response"]["status_code"] === null) + (parameters.response.status_code === null) ) { // do nothing } else { - const status_code_expected : int = (parameters["response"]["status_code"] as int); - if (http_response.statuscode === status_code_expected) { + const status_code_expected : int = (parameters.response.status_code as int); + if (! (http_response.statuscode === status_code_expected)) { faults.push( lib_plankton.translate.get( "checks.http_request.status_code_mismatch", @@ -253,17 +266,21 @@ namespace _heimdall.check_kinds.http_request // headers { if ( - (! ("headers" in parameters["response"])) + (! ("headers" in parameters.response)) || - (parameters["response"]["headers"] === null) + (parameters.response.headers === null) ) { // do nothing } else { - const headers_expected : Record = (parameters["response"]["headers"] as Record); + const headers_expected : Record = (parameters.response.headers as Record); Object.entries(headers_expected).forEach( ([header_key, header_value]) => { - if (! (header_key in http_response.headers)) { + if ( + (! (header_key in http_response.headers)) + && + (! (header_key.toLowerCase() in http_response.headers)) + ) { faults.push( lib_plankton.translate.get( "checks.http_request.header_missing", @@ -275,7 +292,11 @@ namespace _heimdall.check_kinds.http_request ); } else { - if (! (http_response.headers[header_key] === header_value)) { + if ( + (! (http_response.headers[header_key] === header_value)) + && + (! (http_response.headers[header_key.toLowerCase()].toLowerCase() === header_value.toLowerCase())) + ) { faults.push( lib_plankton.translate.get( "checks.http_request.header_value_mismatch", @@ -298,14 +319,14 @@ namespace _heimdall.check_kinds.http_request // body { if ( - (! ("body_part" in parameters["response"])) + (! ("body_part" in parameters.response)) || - (parameters["response"]["body_part"] === null) + (parameters.response.body_part === null) ) { // do nothing } else { - const body_part : string = (parameters["response"]["body_part"] as string); + const body_part : string = (parameters.response.body_part as string); if (! http_response.body.includes(body_part)) { faults.push( lib_plankton.translate.get( @@ -327,13 +348,13 @@ namespace _heimdall.check_kinds.http_request (faults.length <= 0) ? _heimdall.enum_condition.ok : ( - parameters["critical"] + parameters.critical ? _heimdall.enum_condition.critical : _heimdall.enum_condition.concerning ) ), "info": { - "request": parameters["request"], + "request": parameters.request, "response": { "status_code": http_response.statuscode, "headers": http_response.headers, diff --git a/source/logic/check_kinds/script.ts b/source/logic/check_kinds/script.ts index 38f7356..5e801f6 100644 --- a/source/logic/check_kinds/script.ts +++ b/source/logic/check_kinds/script.ts @@ -122,7 +122,7 @@ namespace _heimdall.check_kinds.script "info": { "result": result, }, - } + }; } diff --git a/source/logic/check_kinds/tls_certificate.ts b/source/logic/check_kinds/tls_certificate.ts new file mode 100644 index 0000000..2f27637 --- /dev/null +++ b/source/logic/check_kinds/tls_certificate.ts @@ -0,0 +1,250 @@ +namespace _heimdall.check_kinds.tls_certificate +{ + + /** + */ + function parameters_schema( + ) : _heimdall.helpers.json_schema.type_schema + { + return { + "type": "object", + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "default": 443 + }, + "expiry_threshold_concerning": { + "description": "in days; allowed amount of valid days before the certificate expires; threshold for condition 'concerning'; 'null' means 'report at no value'", + "anyOf": [ + { + "type": "null", + }, + { + "type": "integer", + "minimum": 0 + }, + ], + "default": 7, + }, + "expiry_threshold_critical": { + "description": "in days; allowed amount of valid days before the certificate expires; threshold for condition 'critical'; 'null' means 'report at no value'", + "anyOf": [ + { + "type": "null", + }, + { + "type": "integer", + "minimum": 0 + }, + ], + "default": 1, + }, + "expiry_threshold": { + "deprecated": true, + "description": "", + "anyOf": [ + { + "type": "null", + }, + { + "type": "integer", + "minimum": 0 + }, + ], + "default": null, + }, + "strict": { + "deprecated": true, + "description": "", + "anyOf": [ + { + "type": "null", + }, + { + "type": "boolean", + }, + ], + "default": null, + }, + }, + "required": [ + "host", + ] + }; + } + + + /** + */ + function normalize_order_node( + node : any + ) : any + { + const version : string = ( + ( + (! ("expiry_threshold_concerning" in node)) + && + (! ("expiry_threshold_critical" in node)) + ) + ? "v1" + : "v2" + ); + + switch (version) { + default: { + throw (new Error("unhandled version")); + break; + } + case "v1": { + if (! ("host" in node)) { + throw new Error("missing mandatory field 'host'"); + } + else { + const node_ = Object.assign( + { + "port": 443, + "expiry_threshold": 7, + "strict": true, + }, + node + ); + return { + "host": node_["host"], + "port": node_["port"], + "expiry_threshold_concerning": ( + node_["strict"] + ? null + : node_["expiry_threshold"] + ), + "expiry_threshold_critical": ( + node_["strict"] + ? node_["expiry_threshold"] + : null + ), + }; + } + break; + } + case "v2": { + if (! ("host" in node)) { + throw new Error("missing mandatory field 'host'"); + } + else { + const node_ = Object.assign( + { + "port": 443, + "expiry_threshold_concerning": 7, + "expiry_threshold_critical": 1, + }, + node + ); + return node_; + } + break; + } + } + } + + + /** + */ + async function run( + parameters + ) : Promise<_heimdall.type_result> + { + // TODO: outsource to parameters + const timeout : float = 5.0; + + type type_stuff = { + valid_from : int; + valid_to : int; + }; + + // const nm_child_process = require("x509"); + const nm_tls = require("tls"); + const nm_ssl_checker = require("ssl-checker"); + + let faults : Array = []; + let data : Record = {}; + let condition : _heimdall.enum_condition = _heimdall.enum_condition.ok; + let version : (null | string); + + const stuff : (null | type_stuff) = await ( + nm_ssl_checker(parameters["host"], {"port": parameters["port"]}) + .then( + x => ({ + "valid_from": Math.floor((new Date(x["validFrom"])).getTime() / 1000), + "valid_to": Math.floor((new Date(x["validTo"])).getTime() / 1000), + }) + ) + ); + + if (stuff === null) { + faults.push(lib_plankton.translate.get("checks.tls_certificate.not_obtainable")); + condition = _heimdall.enum_condition.critical; + version = null; + } + else { + version = "TLSv1.3"; + const current_timestamp : int = _heimdall.get_current_timestamp(); + const expiry_timestamp = stuff.valid_to; + const days : int = Math.ceil((expiry_timestamp - current_timestamp) / (60 * 60 * 24)); + data = Object.assign( + data, + { + "expiry_timestamp": expiry_timestamp, + "days": days, + } + ); + if ( + (parameters["expiry_threshold_critical"] !== null) + && + (days <= parameters["expiry_threshold_critical"]) + ) { + faults.push(lib_plankton.translate.get("checks.tls_certificate.expires_soon")); + condition = _heimdall.enum_condition.critical; + } + else { + if ( + (parameters["expiry_threshold_concerning"] !== null) + && + (days <= parameters["expiry_threshold_concerning"]) + ) { + faults.push(lib_plankton.translate.get("checks.tls_certificate.expires_soon")); + condition = _heimdall.enum_condition.concerning; + } + else { + // no nothing + } + } + } + + return Promise.resolve({ + "condition": condition, + "info": { + "host": parameters["host"], + "port": parameters["port"], + "faults": faults, + "data": data, + } + }); + } + + + /** + */ + export function check_kind_implementation( + ) : type_check_kind + { + return { + "parameters_schema": parameters_schema, + "normalize_order_node": normalize_order_node, + "run": run, + }; + } + +} diff --git a/source/logic/helpers/misc.ts b/source/logic/helpers/misc.ts new file mode 100644 index 0000000..d3feaf3 --- /dev/null +++ b/source/logic/helpers/misc.ts @@ -0,0 +1,32 @@ +namespace _heimdall.helpers.misc +{ + + /** + */ + export function format_bytes( + bytes : int + ) : string + { + const units : Array<{label : string; digits : int;}> = [ + {"label": "B", "digits": 0}, + {"label": "KB", "digits": 1}, + {"label": "MB", "digits": 1}, + {"label": "GB", "digits": 1}, + {"label": "TB", "digits": 1}, + {"label": "PB", "digits": 1}, + ] + let number_ : int = bytes; + let index : int = 0; + while ((number_ >= 1000) && (index < (units.length - 1))) { + number_ /= 1000; + index += 1; + } + return lib_plankton.string.coin( + "{{number}} {{label}}", + { + "number": number_.toFixed(units[index].digits), + "label": units[index].label, + } + ); + } +} diff --git a/source/logic/main.ts b/source/logic/main.ts index 49ac0dc..190ea7c 100644 --- a/source/logic/main.ts +++ b/source/logic/main.ts @@ -248,6 +248,8 @@ async function main( "script": _heimdall.check_kinds.script.check_kind_implementation(), "http_request": _heimdall.check_kinds.http_request.check_kind_implementation(), "file_state": _heimdall.check_kinds.file_state.check_kind_implementation(), + "tls_certificate": _heimdall.check_kinds.tls_certificate.check_kind_implementation(), + "generic_remote": _heimdall.check_kinds.generic_remote.check_kind_implementation(), }; if (args["show_schema"]) { process.stdout.write( diff --git a/tools/heimdall.prj.json b/tools/heimdall.prj.json index 2a1bd00..2293b39 100644 --- a/tools/heimdall.prj.json +++ b/tools/heimdall.prj.json @@ -22,12 +22,15 @@ "source/logic/base.ts", "source/logic/helpers/json_schema.ts", "source/logic/helpers/sqlite.ts", + "source/logic/helpers/misc.ts", "source/logic/notification_kinds/_abstract.ts", "source/logic/notification_kinds/console.ts", "source/logic/check_kinds/_abstract.ts", "source/logic/check_kinds/script.ts", "source/logic/check_kinds/http_request.ts", "source/logic/check_kinds/file_state.ts", + "source/logic/check_kinds/tls_certificate.ts", + "source/logic/check_kinds/generic_remote.ts", "source/logic/state_repository.ts", "source/logic/order.ts", "source/logic/main.ts"