diff --git a/agent_based/curl.py b/agent_based/curl.py new file mode 100644 index 0000000000000000000000000000000000000000..87881577030e718ffd3303ff825bb69bb8f6e199 --- /dev/null +++ b/agent_based/curl.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 + +# +# based on the work by Christian Wirtz doc[at]snowheaven[dot]de +# +# 2022-02-15: rewritten for cmk 2.0 +# 2022-02-20: removed url_effective from format file (bad output on linux: "url_effective":http://"https//checkmk.com") +# added num_connects, num_redirects, size_download, size_header, speed_download +# added redirect_url, remote_ip, scheme, http_version, http_connect +# 2022-02-22: added params for all values +# 2022-02-23: changed items on service info line +# 2022-02-28: added expected_strings and state_item_not_found option +# 2022-03-05: added support for search in header strings +# 2022-03-06: added support for tls/certificate information +# 2022-03-10: optimized code for perfdata +# 2022-03-12: added cafile and capath to Cert info section +# added max_age +# 2022-03-18: added regex pattern match +# 2022-03-22: added curl_error_code_to_ignore and http_error_code_to_ignore options +# 2022-04-06: removed .replace('://', ': //')) from json.loads() so service name is without space +# 2022-04-26: made http(s) URLs clickable +# 2022-05-15: added workaround for raise ValueError("Cannot render negative timespan") +# 2022-05-17: fixed wrong import path for _TIME_UNITS and _gen_timespan_chunks +# 2023-06-07: moved gui files to ~/local/lib/chek_mk/gui/plugins/... +# + +# Example output from agent: +# +# <<<curl>>> +# { +# "thl-cmk.hopto.org": { +# "url": "https://thl-cmk.hopto.org", +# "data": { +# "ssl_verify_result": "0", +# "http_return_code": "000", +# "time_namelookup": "0,000000", +# "time_connect": "0,000000", +# "time_appconnect": "0,000000", +# "time_pretransfer": "0,000000", +# "time_redirect": "0,000000", +# "time_starttransfer": "0,000000", +# "time_total": "0,000000", +# "http_connect": "000", +# "http_version": "0", +# "num_connects": "0", +# "num_redirects": "0", +# "redirect_url": "", +# "remote_ip": "", +# "scheme": "(nil)", +# "size_download": "0", +# "size_header": "0", +# "speed_download": "0,000", +# }, +# "error_level": "6" +# } +# } +# + +import json +import time +from typing import Dict, Any, List, Iterable +from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import ( + DiscoveryResult, + CheckResult, +) + +from cmk.base.plugins.agent_based.agent_based_api.v1 import ( + register, + Service, + State, + check_levels, + Result, + render, +) + +# +# start workaround for : raise ValueError("Cannot render negative timespan") +# +from cmk.base.api.agent_based.render import ( + _TIME_UNITS, + _gen_timespan_chunks, +) + + +def timespan(seconds: float) -> str: + """Render a time span in seconds + + Example: + >>> timespan(1606721) + '18 days 14 hours' + >>> timespan(0.0001) + '100 microseconds' + + """ + if seconds >= 0: + ts = " ".join(_gen_timespan_chunks(float(seconds), nchunks=2)) + else: + seconds = -1 * seconds + ts = " ".join(_gen_timespan_chunks(float(seconds), nchunks=2)) + ts = f"-{ts}" + if ts == "0 %s" % _TIME_UNITS[-1][0]: + ts = "0 seconds" + return ts + +# +# end workaround for : raise ValueError("Cannot render negative timespan") +# + + +_curl_error_codes = { + 1: 'Unsupported protocol. This build of curl has no support for this protocol.', + 2: 'Failed to initialize.', + 3: 'URL malformed. The syntax was not correct.', + 4: 'A feature or option that was needed to perform the desired request was not enabled or was explicitly ' + 'disabled at build-time. To make curl able to do this, you probably need another build of libcurl.', + 5: 'Could not resolve proxy. The given proxy host could not be resolved.', + 6: 'Could not resolve host. The given remote host could not be resolved.', + 7: 'Failed to connect to host.', + 8: 'Weird server reply. The server sent data curl could not parse.', + 9: 'FTP access denied. The server denied login or denied access to the particular resource or directory you ' + 'wanted to reach. Most often you tried to change to a directory that does not exist on the server.', + 10: 'FTP accept failed. While waiting for the server to connect back when an active FTP session is used, ' + 'an error code was sent over the control connection or similar.', + 11: 'FTP weird PASS reply. Curl could not parse the reply sent to the PASS request.', + 12: 'During an active FTP session while waiting for the server to connect back to curl, the timeout expired.', + 13: 'FTP weird PASV reply, Curl could not parse the reply sent to the PASV request.', + 14: 'FTP weird 227 format. Curl could not parse the 227-line the server sent.', + 15: 'FTP cannot use host. Could not resolve the host IP we got in the 227-line.', + 16: 'HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one ' + 'out of several problems, see the error message for details.', + 17: 'FTP could not set binary. Could not change transfer method to binary.', + 18: 'Partial file. Only a part of the file was transferred.', + 19: 'FTP could not download/access the given file, the RETR (or similar) command failed.', + 21: 'FTP quote error. A quote command returned error from the server.', + 22: 'HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error ' + 'code being 400 or above. This return code only appears if --fail is used.', + 23: 'Write error. Curl could not write data to a local filesystem or similar.', + 25: 'FTP could not STOR file. The server denied the STOR operation, used for FTP uploading.', + 26: 'Read error. Various reading problems.', + 27: 'Out of memory. A memory allocation request failed.', + 28: 'Operation timeout. The specified time-out period was reached according to the conditions.', + 30: 'FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, ' + 'try doing a transfer using PASV instead!', + 31: 'FTP could not use REST. The REST command failed. This command is used for resumed FTP transfers.', + 33: 'HTTP range error. The range "command" did not work.', + 34: 'HTTP post error. Internal post-request generation error.', + 35: 'SSL connect error. The SSL handshaking failed.', + 36: 'Bad download resume. Could not continue an earlier aborted download.', + 37: 'FILE could not read file. Failed to open the file. Permissions?', + 38: 'LDAP cannot bind. LDAP bind operation failed.', + 39: 'LDAP search failed.', + 41: 'Function not found. A required LDAP function was not found.', + 42: 'Aborted by callback. An application told curl to abort the operation.', + 43: 'Internal error. A function was called with a bad parameter.', + 45: 'Interface error. A specified outgoing interface could not be used.', + 47: 'Too many redirects. When following redirects, curl hit the maximum amount.', + 48: 'Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was' + ' passed on to libcurl and rejected. Read up in the manual!', + 49: 'Malformed telnet option.', + 51: 'The peer\'s SSL certificate or SSH MD5 fingerprint was not OK.', + 52: 'The server did not reply anything, which here is considered an error.', + 53: 'SSL crypto engine not found.', + 54: 'Cannot set SSL crypto engine as default.', + 55: 'Failed sending network data.', + 56: 'Failure in receiving network data.', + 58: 'Problem with the local certificate.', + 59: 'Could not use specified SSL cipher.', + 60: 'Peer certificate cannot be authenticated with known CA certificates.', + 61: 'Unrecognized transfer encoding.', + 62: 'Invalid LDAP URL.', + 63: 'Maximum file size exceeded.', + 64: 'Requested FTP SSL level failed.', + 65: 'Sending the data requires a rewind that failed.', + 66: 'Failed to initialise SSL Engine.', + 67: 'The user name, password, or similar was not accepted and curl failed to log in.', + 68: 'File not found on TFTP server.', + 69: 'Permission problem on TFTP server.', + 70: 'Out of disk space on TFTP server.', + 71: 'Illegal TFTP operation.', + 72: 'Unknown TFTP transfer ID.', + 73: 'File already exists (TFTP).', + 74: 'No such user (TFTP).', + 75: 'Character conversion failed.', + 76: 'Character conversion functions required.', + 77: 'Problem reading the SSL CA cert (path? access rights?).', + 78: 'The resource referenced in the URL does not exist.', + 79: 'An unspecified error occurred during the SSH session.', + 80: 'Failed to shut down the SSL connection.', + 82: 'Could not load CRL file, missing or wrong format.', + 83: 'Issuer check failed.', + 84: 'The FTP PRET command failed.', + 85: 'Mismatch of RTSP CSeq numbers.', + 86: 'Mismatch of RTSP Session Identifiers.', + 87: 'Unable to parse FTP file list.', + 88: 'FTP chunk callback reported error.', + 89: 'No connection available, the session will be queued.', + 90: 'SSL public key does not matched pinned public key.', + 91: 'Invalid SSL certificate status.', + 92: 'Stream error in HTTP/2 framing layer.', + 93: 'An API function was called from inside a callback.', + 94: 'An authentication function returned an error.', + 95: 'A problem was detected in the HTTP/3 layer. This is somewhat generic and can be one out of ' + 'several problems, see the error message for details.', + 96: 'QUIC connection error. This error may be caused by an SSL library error. QUIC is the ' + 'protocol used for HTTP/3 transfers.', +} + + +def _get_tls_info(tls_lines: List[str]): + tls_infos = {} + alpn_offerings = [] + for line in tls_lines: + line = line.strip() + if line.startswith('subject: '): + tls_infos.update({'subject': line[9:]}) + elif line.startswith('start date: '): + tls_infos.update({'start_date': line[12:]}) + elif line.startswith('expire date: '): + tls_infos.update({'expire_date': line[13:]}) + time_left = time.mktime( # 'Feb 16 10:18:08 2022 GMT' + time.strptime(tls_infos['expire_date'], '%b %d %H:%M:%S %Y %Z') + ) - time.mktime(time.gmtime()) + tls_infos.update({'time_left': time_left}) + elif line.startswith('subjectAltName: '): + tls_infos.update({'subject_alternate_name': line[21:]}) + elif line.startswith('issuer: '): + tls_infos.update({'issuer': line[8:]}) + elif line.startswith('ALPN, offering '): + alpn_offerings.append(line[15:]) + elif line.startswith('ALPN, server accepted to use '): + tls_infos.update({'alpn_accepted': line[29:]}) + elif line.startswith('SSL certificate verify result: '): + tls_infos.update({'ssl_verify_result': line[31:]}) + elif line.startswith('SSL connection using '): + tls_version, cipher = line[21:].split(' / ') + tls_infos.update({'tls_version_used': tls_version}) + tls_infos.update({'cipher_used': cipher}) + elif line.startswith('CAfile'): + tls_infos.update({'ca_file': line[8:]}) + elif line.startswith('CApath'): + tls_infos.update({'ca_path': line[8:]}) + + tls_infos.update({'alpn_offerings': alpn_offerings}) + + return tls_infos + + +def parse_curl(string_table): + try: + # section = json.loads(string_table[0][0].replace('://', ': //')) # removed so service name is without space + section = json.loads(string_table[0][0]) + except (IndexError, TypeError, json.JSONDecodeError): + return {} + if 'ERROR' not in section.keys(): + if len(string_table) > 1: + # insert splitter, split (and implicit remove splitter), avoid removal of '}{' + for line in string_table[1][0].replace('}{', '}|||{').split('|||'): + # '{'thl-cmk.hopto.org': {'expected_strings': [['string1', 1], ['string2', 1], ['html', 0]]}}' + try: + # line = line.replace('://', ': //') # removed so service name is without space + line = json.loads(line) + except json.JSONDecodeError: + continue + # get first key from line + key = list(line.keys())[0] + # update section data for key + section[key].update(line[key]) + for key in section.keys(): + if section[key].get('TLS_INFO'): + tls_info = _get_tls_info(section[key]['TLS_INFO']) + section[key].update({'tls_info': tls_info}) + # section[key].pop('TLS_INFO') + return section + + +def discovery_curl(section: Dict[str, Any]) -> DiscoveryResult: + for key in section.keys(): + yield Service(item=key) + + +def check_curl(item, params, section: Dict[str, Any]) -> CheckResult: + try: + _data = section[item] + except KeyError: + yield Result(state=State(params['state_item_not_found']), notice='Item not found in agent data') + return + + # url = _data['url'].replace('://', ': //') # ugly workaround to stop cmk from replacing the url + http_return_code = _data['data']['http_code'] + ssl_verify_result = _data['data']['ssl_verify_result'] + curl_error_code = int(_data['data']['exitcode']) + + # yield Result(state=State.OK, notice=f'URL from cfg: {url}') + for key, label in [ + ('url', 'URL'), + ('redirect_url', 'URL redirect'), + ('url_effective', 'URL effective'), + ('referer', 'Referer'), + ]: + if _data['data'].get(key): + value = _data['data'][key] + if value.startswith('http'): + if params.get('clickable_url'): + value = f'<a href={value} target="blank">{value}</a>' + else: + value = f'{value.replace("://", ": //")}' # ugly workaround to stop cmk from replacing the url + yield Result(state=State.OK, notice=f'{label}: {value}') + + if curl_error_code != 0: + if curl_error_code in params['curl_error_code_to_ignore']: + yield Result( + state=State.OK, + notice=f'curl error code: {curl_error_code}, {_curl_error_codes.get(curl_error_code, "N/A")}') + else: + yield Result( + state=State(params['state_curl_result_not_0']), + summary=f'curl error code: {curl_error_code} see details', + details=f'curl error code: {curl_error_code}, {_curl_error_codes.get(curl_error_code, "N/A")}' + ) + + if http_return_code < 400: # no connect, Ok, Redirect + yield Result(state=State.OK, notice=f'HTTP Return code: {http_return_code}') + else: + if http_return_code in params['http_error_code_to_ignore']: + yield Result( + state=State.OK, notice=f'HTTP Return code: {http_return_code}' + ) + else: + yield Result( + state=State(params['state_http_result_not_200']), + notice=f'HTTP Return code: {http_return_code}' + ) + + if ssl_verify_result == 0: + yield Result(state=State.OK, notice=f'SSL verify result: {ssl_verify_result}') + else: + yield Result(state=State(params['state_verify_sll_not_0']), summary=f'SSL verify result: {ssl_verify_result}') + + for key, label in [ + ('scheme', 'Scheme (protocol)'), + ('http_version', 'HTTP version'), + ('method', 'Method'), + ('content_type', 'Content type'), + ('remote_ip', 'Remote IP'), + ('http_connect', 'Proxy connect code'), + ('errormsg', 'Error message'), + ('curl_version', 'cURL version'), + ]: + if _data['data'].get(key): + yield Result(state=State.OK, notice=f'{label}: {_data["data"][key]}') + + if _data.get('curl_options'): + yield Result(state=State.OK, notice=f'cURL options: {_data["curl_options"]}') + + if _data.get('expected_response'): + for expected_string, result in _data['expected_response']: + if result == 0: + yield Result(state=State.OK, notice=f'Response string: "{expected_string}" found') + else: + yield Result( + state=State(params['state_expected_str_not_found']), + notice=f'Response string: "{expected_string}" not found' + ) + + if _data.get('expected_header'): + for header_strings, result in _data['expected_header']: + if result == 0: + yield Result(state=State.OK, notice=f'Header string: "{header_strings}" found') + else: + yield Result( + state=State(params['state_header_str_not_found']), + notice=f'Header string: "{header_strings}" not found' + ) + + regex_match, regex_no_match, regex_missing = params['state_for_regex'] + if _data.get('regex') == 0: # match + yield Result(state=State(regex_match), notice='Regex state: pattern matches') + elif _data.get('regex') == 1: # no match + yield Result(state=State(regex_no_match), notice='Regex state: pattern don\'t matches') + elif not _data.get('regex'): # missing info + yield Result(state=State(regex_missing), notice='Regex state: missing pattern match info') + + max_age_warn, max_age_crit, max_age_state = params['max_age'] + if max_age_warn: + max_age = None + if _data.get('RESPONSE_HEADER'): + for line in _data['RESPONSE_HEADER']: + if line.startswith('Last-Modified: '): + max_age = time.mktime(time.gmtime()) - time.mktime( + time.strptime(line[15:], '%a, %d %b %Y %H:%M:%S %Z')) # 'Fri, 18 Jun 2021 18:17:33 GMT' + if max_age: + yield from check_levels( + label='Last modified', + value=max_age, + render_func=render.timespan, + notice_only=True, + levels_upper=(max_age_warn, max_age_crit), + ) + else: + yield Result(state=State(max_age_state), notice='Last-Modified: Document modification date unknown') + + if _data.get('tls_info', {}).get('subject'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Certificate info:') + + for key, label in [ + ('subject', 'Subject'), + ('issuer', 'Issuer'), + ('subject_alternate_name', 'Subject alternate name'), + ('start_date', 'Start date'), + ('expire_date', 'Expire date'), + ('tls_version_used', 'TLS Version used'), + ('cipher_used', 'Cipher used'), + ('ssl_verify_result', 'SSL verify result'), + ('ca_file', 'CA file'), + ('ca_path', 'CA path'), + ]: + if _data['tls_info'].get(key): + yield Result(state=State.OK, notice=f'{label}: {_data["tls_info"][key]}') + + if _data['tls_info'].get('time_left'): + if params['cert_time_left'].get('upper'): + warn, crit = params['cert_time_left'].get('upper') + upper = (warn * 86400, crit * 86400) + else: + upper = None + + if params['cert_time_left'].get('lower'): + warn, crit = params['cert_time_left'].get('lower') + lower = (warn * 86400, crit * 86400) + else: + lower = None + + yield from check_levels( + label='Certificate time left', + value=_data['tls_info']['time_left'], + metric_name='cert_time_left', + render_func=timespan, + notice_only=True, + levels_upper=upper, + levels_lower=lower, + ) + + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Performance data:') + + def _render_string(v): + return f'{v}' + + for key, label, notice_only, render_func in [ + # times + ('time_namelookup', 'Time name lookup', True, render.timespan,), + ('time_connect', 'Time connect', True, render.timespan,), + ('time_appconnect', 'Time app connect', True, render.timespan,), + ('time_pretransfer', 'Time pre transfer', True, render.timespan,), + ('time_redirect', 'Time redirect', True, render.timespan,), + ('time_starttransfer', 'Time start transfer', True, render.timespan,), + ('time_total', 'Time total', False, render.timespan,), + ]: + if _data['data'].get(key): + # ms needs to be changed to s + value = _data['data'][key] / 1000 + if value != 0: + yield from check_levels( + label=label, + value=value, + metric_name=key, + notice_only=notice_only, + render_func=render_func, + levels_lower=params[key].get('lower'), + levels_upper=params[key].get('upper'), + ) + + for key, label, notice_only, render_func in [ + # counts + ('num_connects', '# of connects', True, _render_string, ), + ('num_redirects', '# of redirects', True, _render_string, ), + ('num_headers', '# of headers', True, _render_string, ), + # sizes + ('size_download', 'Size download', False, render.bytes,), + ('size_upload', 'Size upload', True, render.bytes,), + ('size_header', 'Size header', True, render.bytes,), + ('size_request', 'Size request', True, render.bytes,), + # speed + ('speed_download', 'Speed download', False, render.networkbandwidth,), + ('speed_upload', 'Speed upload', True, render.networkbandwidth,), + ]: + if _data['data'].get(key): + value = _data['data'][key] + if value != 0: + yield from check_levels( + label=label, + value=value, + metric_name=key, + notice_only=notice_only, + render_func=render_func, + levels_lower=params[key].get('lower'), + levels_upper=params[key].get('upper'), + ) + + show_request_header, show_response_headers, show_session_info, show_raw_data = params['show_additional_info'] + if show_request_header and _data.get('REQUEST_HEADER'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Request headers:') + for line in _data['REQUEST_HEADER']: + if line: + yield Result(state=State.OK, notice=f'{line}') + + if show_response_headers and _data.get('RESPONSE_HEADER'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'Response headers:') + for line in _data['RESPONSE_HEADER']: + if line: + yield Result(state=State.OK, notice=f'{line}') + + if show_session_info and _data.get('TLS_INFO'): + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'TLS/SSL/SSH info:') + for line in _data['TLS_INFO']: + if line: + yield Result(state=State.OK, notice=f'{line}') + + if show_raw_data: + yield Result(state=State.OK, notice=f' ') + yield Result(state=State.OK, notice=f'RAW data:') + yield Result(state=State.OK, notice=f'{_data}') + + +register.agent_section( + name="curl", + parse_function=parse_curl, +) + +register.check_plugin( + name='curl', + service_name='cURL %s', + discovery_function=discovery_curl, + check_function=check_curl, + check_default_parameters={ + 'show_additional_info': (None, None, None, None), + 'max_age': (None, None, None), + 'state_item_not_found': 3, + 'state_http_result_not_200': 1, + 'http_error_code_to_ignore': [], + 'state_curl_result_not_0': 1, + 'curl_error_code_to_ignore': [], + 'state_verify_sll_not_0': 1, + 'state_expected_str_not_found': 1, + 'state_header_str_not_found': 1, + 'state_for_regex': (0, 1, 0), + 'time_namelookup': {}, + 'time_connect': {}, + 'time_appconnect': {}, + 'time_pretransfer': {}, + 'time_redirect': {}, + 'time_starttransfer': {}, + 'time_total': {}, + 'num_connects': {}, + 'num_redirects': {}, + 'num_headers': {}, + 'size_download': {}, + 'size_upload': {}, + 'size_header': {}, + 'size_request': {}, + 'speed_download': {}, + 'speed_upload': {}, + 'cert_time_left': {}, + }, + check_ruleset_name='curl' +) diff --git a/agents/plugins/curl.ps1 b/agents/plugins/curl.ps1 new file mode 100755 index 0000000000000000000000000000000000000000..04727a27a02e41f4304b787fb6459f022c0a0e5e --- /dev/null +++ b/agents/plugins/curl.ps1 @@ -0,0 +1,218 @@ +<# +License: GNU General Public License v2 + +Author: thl-cmk[at]outlook[dot]com +URL : https://thl-cmk.hopto.org +Date : 2022-02-10 + +based on the work by Christian Wirtz doc[at]snowheaven[dot]de and Ingo Hambrock + +Wrapper around: https://curl.se/ + +2022-02-10: rewritten by thl-cmk[at]outlook.com to output json string +2022-02-16: added file checks for CURL_EXECUTABLE and CURL_FORMAT +2022-02-17: added ERRORLEVEL +2022-02-21: added curl_option to the output +2022-02-22: fixed search for curl.exe +2022-02-23: fixed handling of options from curl.cfg +2022-02-24: removed curl.format, replaced by -w %{json} +2022-02-25: removed separate url and error_level from output +2022-02-27: rewritten for powershell (from cmd) +2022-02-28: added support to search for strings in cURL output +2022-03-05: added support for search in header strings +2022-03-06: added support for ssl/tls session info +2022-03-11: added --verbose --stderr $TEMP_DIR\curl_session to CURL_OPTIONS +2022-03-12: fixed escapes for json format in TLS_INFO +2022-03-13: moved curl_item files to curl sub directory under $MK_CONFDIR +2022-03-15: moved curl options from curl.cfg to curl_item_#.options +2022-03-15: added regex pattern match +#> + +$TEMP_DIR="c:\windows\temp" + +# config file directory +$MK_CONFDIR = $env:MK_CONFDIR + +# Fallback if no MK_CONFDIR is set +if (!$MK_CONFDIR) { + $MK_CONFDIR= "$env:ProgramData\checkmk\agent\config" +} + +Write-Output "<<<curl:sep(0)>>>" + +if (Test-Path -Path $MK_CONFDIR\curl.cfg -PathType Leaf) { + $CURL_CONFIG="$MK_CONFDIR\curl.cfg" +} else { + Write-Output '{"ERROR":"config file $MK_CONFDIR\curl.cfg does not exist"}' + exit 0 +} + +# first check for curl.exe in cmk agent directory +if (Test-Path -Path "C:\ProgramData\checkmk\agent\bin\curl.exe" -PathType Leaf) { + $CURL_EXECUTABLE="C:\ProgramData\checkmk\agent\bin\curl.exe" + # if not found look for system provided file +} elseif (Test-Path -Path "C:\Windows\System32\curl.exe" -PathType Leaf) { + $CURL_EXECUTABLE="C:\Windows\System32\curl.exe" +} else { + Write-Output '{"ERROR":"executable file curl.exe not found"}' + exit 0 +} + +$CURL_OPTIONS="--disable --write-out %{json} --silent --verbose --include --stderr $TEMP_DIR\curl_session" +$CURL_OPTIONS=$CURL_OPTIONS.Substring(0,$CURL_OPTIONS.Length -0) +$CURL_RUN="FIRST" +$CURL_RESULT="" +$CURL_OUTPUT="$TEMP_DIR\curl_output" +$CURL_HEADER="$TEMP_DIR\curl_header" +$CURL_SESSION_FILE="$TEMP_DIR\curl_session" + +function Find-CurlStrings { + param ( + [string]$SEARCH_FILE, + [string]$SEARCH_EXTENSION, + [string]$JSON_KEY + ) + if (Test-Path -Path "$SEARCH_FILE" -PathType Leaf) { + if (Test-Path -Path "$MK_CONFDIR/curl/$CURL_ITEM.$SEARCH_EXTENSION" -PathType Leaf) { + $ROUND="FIRST" + $CURL_FILE=Get-Content $SEARCH_FILE -Raw + $CURL_RESULT="$CURL_RESULT{""$SERVICE_NAME"":{""$JSON_KEY"":[" + foreach($LINE in Get-Content $MK_CONFDIR/curl/$CURL_ITEM.$SEARCH_EXTENSION) { + if ( $ROUND -eq "SECOND" ) { + $CURL_RESULT="$CURL_RESULT," + } else { + $ROUND="SECOND" + } + $CURL_SEARCH=$CURL_FILE.IndexOf($LINE) + # change to match grep return codes + if ($CURL_SEARCH -ne -1){ + $CURL_SEARCH = 0 + } else { + $CURL_SEARCH = 1 + } + $CURL_RESULT="$CURL_RESULT[""$LINE"", $CURL_SEARCH]" + } + $CURL_RESULT="$CURL_RESULT]}}" + } + } + return $CURL_RESULT +} + +function Find-CurlSessionInfo { + param ( + [string]$SEARCH_MARKER, + [string]$JSON_KEY + ) + if (Test-Path -Path "$CURL_SESSION_FILE" -PathType Leaf) { + $CURL_RESULT = "$CURL_RESULT{""$SERVICE_NAME"":{""$JSON_KEY"":[" + $ROUND="FIRST" + foreach($LINE in Get-Content "$CURL_SESSION_FILE") { + if ($LINE[0] -eq "$SEARCH_MARKER") { + $LINE = $LINE.Substring(1, $LINE.Length -1).trim() -replace """", "\""" + if ( $ROUND -eq "SECOND" ) { + $CURL_RESULT="$CURL_RESULT," + } else { + $ROUND="SECOND" + } + $CURL_RESULT="$CURL_RESULT""$LINE""" + } + } + $CURL_RESULT="$CURL_RESULT]}}" + } + # return $CURL_RESULT even if it not altered + return $CURL_RESULT +} + +function Find-CurlRegex { + if (Test-Path -Path "$CURL_OUTPUT" -PathType Leaf) { + if (Test-Path -Path "$MK_CONFDIR/curl/$CURL_ITEM.regex" -PathType Leaf) { + if ($CURL_REG_MULTI -eq "multiline") { + $CURL_REG_MULTI = "(?sm)" + } else { + $CURL_REG_MULTI = "" + } + $CURL_FILE=Get-Content "$CURL_OUTPUT" -Raw + foreach($LINE in Get-Content "$MK_CONFDIR/curl/$CURL_ITEM.regex") { + if ($CURL_REG_CASE -eq "case") { + $CURL_REG_RESULT= $CURL_FILE -cmatch "$CURL_REG_MULTI$LINE" + } else { + $CURL_REG_RESULT= $CURL_FILE -match "$CURL_REG_MULTI$LINE" + } + } + if ("$CURL_REG_RESULT" -eq "True") { + $CURL_REG_RESULT = 0 + } else { + $CURL_REG_RESULT = 1 + } + $CURL_RESULT = "$CURL_RESULT{""$SERVICE_NAME"":{""regex"":$CURL_REG_RESULT}}" + } + } + return $CURL_RESULT +} + +function Cleanup { + if (Test-Path -Path $CURL_OUTPUT -PathType Leaf) { + Remove-Item -Path $CURL_OUTPUT + } + if (Test-Path -Path $CURL_HEADER -PathType Leaf) { + Remove-Item -Path $CURL_HEADER + } + if (Test-Path -Path $CURL_SESSION_FILE -PathType Leaf) { + Remove-Item -Path $CURL_SESSION_FILE + } +} + + +Write-Host -NoNewline "{" + +foreach($LINE in Get-Content $CURL_CONFIG) { + $SERVICE_NAME=$LINE.split("|")[0] + $CURL_ITEM=$LINE.split("|")[1] + $CURL_REGEX=$LINE.split("|")[2] + $CURL_REG_CASE=$CURL_REGEX.split("_")[0] + $CURL_REG_MULTI=$CURL_REGEX.split("_")[1] + + if ( $CURL_RUN -eq "SECOND") { + Write-Host -NoNewline "," + } else { + $CURL_RUN="SECOND" + } + + Cleanup + + Write-Host -NoNewline """$SERVICE_NAME"":{""data"":" + # $RUN="$CURL_EXECUTABLE $CURL_OPTIONS --url $URL $OPTIONS" + $RUN="$CURL_EXECUTABLE $CURL_OPTIONS --config $MK_CONFDIR/curl/$CURL_ITEM.options" + $RESULT = . cmd /c "$RUN" + Write-Host -NoNewline $RESULT + # Write-Host -NoNewline ",""curl_options"":""$CURL_OPTIONS $OPTIONS""" + Write-Host -NoNewline "}" + + # check for expected strings in response + $CURL_RESULT=Find-CurlStrings -SEARCH_FILE "$CURL_OUTPUT" -SEARCH_EXTENSION "search_response" -JSON_KEY "expected_response" + + # check for expected strings in headers + $CURL_RESULT=Find-CurlStrings -SEARCH_FILE "$CURL_HEADER" -SEARCH_EXTENSION "search_header" -JSON_KEY "expected_header" + + # collect for tls/ss session info + $CURL_RESULT=Find-CurlSessionInfo -SEARCH_MARKER "*" -JSON_KEY "TLS_INFO" + + # collect request header + $CURL_RESULT=Find-CurlSessionInfo -SEARCH_MARKER ">" -JSON_KEY "REQUEST_HEADER" + + # collect response header + $CURL_RESULT=Find-CurlSessionInfo -SEARCH_MARKER "<" -JSON_KEY "RESPONSE_HEADER" + + # check for regex match + $CURL_RESULT=Find-CurlRegex + +} + +Write-Output "}" + +Write-Output $CURL_RESULT + +# clean up +Cleanup + +exit 0 diff --git a/agents/plugins/curl.sh b/agents/plugins/curl.sh new file mode 100755 index 0000000000000000000000000000000000000000..5189a271ab268a3f1181fb7b272e2d535237cb73 --- /dev/null +++ b/agents/plugins/curl.sh @@ -0,0 +1,219 @@ +#!/bin/bash +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-20 +# +# Wrapper around: https://curl.se/ +# +# curl plugin for the check_mk linux agent +# +# 2022-02-21: added curl_option to the output +# 2022-02-24: changed to use -w %{json} instead of -w @$curl.format +# extended search for curl executable to agent dir +# 2022-02-25: removed separate url and error_level from output +# 2022-02-27: added support to search for strings in cURL output +# 2022-03-03: fixed handling of spaces in options string +# 2022-03-05: added support for search in header strings +# 2022-03-06: added support for ssl/tls session info +# 2022-03-11: added --verbose --stderr $TEMP_DIR/curl_session to CURL_OPTIONS +# 2022-03-13: moved curl_item files to curl sub directory under $MK_CONFDIR +# 2022-03-15: moved curl options from curl.cfg to curl_item_#.options +# 2022-03-17: added regex pattern match +# + +# ToDo: add plugin timeout enforcement + +CONF_DIR="/etc/check_mk" +LIB_DIR="/usr/lib/check_mk_agent" +TEMP_DIR="/var/tmp" + +CURL_OPTIONS="--disable --write-out %{json} --silent --verbose --include --stderr $TEMP_DIR/curl_session" +CURL_CONFIG=curl.cfg +CURL_RUN="FIRST" +CURL_OUTPUT="$TEMP_DIR/curl_output" +CURL_HEADER="$TEMP_DIR/curl_header" +CURL_SESSION_FILE="$TEMP_DIR/curl_session" +CURL_RESULT="" + +function find_curl_strings { + # usage: search_strings file_to_search_in extension_for_file_with_search_strings json_key + # check for expected strings in response + if [ -f "$1" ]; then + if [ -f "$CONF_DIR/curl/$CURL_ITEM.$2" ]; then + local ROUND="FIRST" + CURL_RESULT=$CURL_RESULT$(printf '{"%s":{"%s":[' "$SERVICE_NAME" "$3") + while read -r LINE; do + if [ "$ROUND" = "SECOND" ]; then + CURL_RESULT=$CURL_RESULT$(printf ',') + else + ROUND="SECOND" + fi + # ToDo: remove grep, read $CURL_OUTPUT to $CURL_FILE and search in there + grep -q -s "$LINE" "$1" + CURL_RESULT=$CURL_RESULT$(printf '["%s", %s]' "$LINE" $?) + done <"$CONF_DIR/curl/$CURL_ITEM.$2" + CURL_RESULT=$CURL_RESULT$(printf ']}}') + fi + # rm "$1" + fi +} + +function find_curl_session_info { + # usage: search_marker json_key replace_new_line + if [ -f "$CURL_SESSION_FILE" ]; then + CURL_RESULT="$CURL_RESULT"$(printf '{"%s":{"%s":[' "$SERVICE_NAME" "$2") + local ROUND="FIRST" + while read -r LINE; do + if [ "${LINE:0:1}" = "$1" ]; then + if [ "$ROUND" = "SECOND" ]; then + CURL_RESULT=$CURL_RESULT$(printf ',') + else + ROUND="SECOND" + fi + LINE=${LINE//"\""/"\\\""} + if [ "$3" = "YES" ]; then + local NEW_LINE + NEW_LINE=$(printf '"%s"' "${LINE:1:${#LINE}-2}") + CURL_RESULT="$CURL_RESULT$NEW_LINE" + else + CURL_RESULT=$CURL_RESULT$(printf '"%s"' "${LINE:1}") + fi + fi + done <"$CURL_SESSION_FILE" + CURL_RESULT="$CURL_RESULT]}}" + # rm "$CURL_SESSION_FILE" + fi +} + + +function find_curl_regex { + local GREP_OPTIONS="-q -s -o -z -P" + if [ -f "$CURL_OUTPUT" ]; then + if [ -f "$CONF_DIR/curl/$CURL_ITEM.regex" ]; then + if [ "$CURL_REG_MULTI" = "multiline" ]; then + CURL_REG_MULTI="(?sm)" + else + CURL_REG_MULTI="" + fi + while read -r LINE; do + if [ "$CURL_REG_CASE" = "case" ]; then + LINE="'$CURL_REG_MULTI$LINE'" + eval grep $GREP_OPTIONS $LINE "$CURL_OUTPUT" + CURL_REG_RESULT=$? + else + GREP_OPTIONS="$GREP_OPTIONS"" -i" + LINE="'$CURL_REG_MULTI$LINE'" + eval grep $GREP_OPTIONS $LINE $CURL_OUTPUT + CURL_REG_RESULT=$? + fi + CURL_RESULT=$CURL_RESULT$(printf '{"%s":{"regex":%s}}' "$SERVICE_NAME" "$CURL_REG_RESULT") + done < "$CONF_DIR/curl/$CURL_ITEM.regex" + fi + fi +} + +function cleanup { + if [ -f "$CURL_OUTPUT" ]; then + rm "$CURL_OUTPUT" + fi + + if [ -f "$CURL_HEADER" ]; then + rm "$CURL_HEADER" + fi + + if [ -f "$CURL_SESSION_FILE" ]; then + rm "$CURL_SESSION_FILE" + fi +} + +printf "<<<curl:sep(0)>>>\n" + +if [ -f "$MK_LIBDIR/bin/curl" ]; then + CURL_EXECUTABLE=$MK_LIBDIR/bin/curl +elif [ -f "$LIB_DIR/bin/curl" ]; then + CURL_EXECUTABLE=$LIB_DIR/bin/curl +elif [ -f "$(which curl)" ]; then + CURL_EXECUTABLE=$(which curl) +else + printf '{"ERROR":"executable file curl not found"}\n' + exit +fi + +if [ -f "$MK_CONFDIR/$CURL_CONFIG" ]; then + CURL_CONFIG=$MK_CONFDIR/$CURL_CONFIG + CONF_DIR="$MK_CONFDIR" +elif [ -f "$CONF_DIR/$CURL_CONFIG" ]; then + CURL_CONFIG=$CONF_DIR/$CURL_CONFIG +else + printf '{"ERROR":"config file %s not found"}\n' "$CURL_CONFIG" + exit +fi + +# start JSON +printf "{" + +while read -r LINE; do + SERVICE_NAME=$(echo "$LINE" | awk -F'|' '{print $1}') + CURL_ITEM=$(echo "$LINE" | awk -F'|' '{print $2}') + CURL_REGEX=$(echo "$LINE" | awk -F'|' '{print $3}') + CURL_REG_CASE=$(echo "$CURL_REGEX" | awk -F'_' '{print $1}') + CURL_REG_MULTI=$(echo "$CURL_REGEX" | awk -F'_' '{print $2}') + + # FIELDS=($(awk -F"|" '{$1=$1} 1' <<<"${LINE}")) + + if [ "$CURL_RUN" = "SECOND" ]; then + printf ',' + else + CURL_RUN="SECOND" + fi + + cleanup + + printf '"%s":{"data":' "$SERVICE_NAME" + eval "$CURL_EXECUTABLE" "$CURL_OPTIONS" --config "$CONF_DIR/curl/$CURL_ITEM.options" + printf '}' + + # check for expected strings in response + find_curl_strings "$CURL_OUTPUT" "search_response" "expected_response" + # check for expected strings in headers + find_curl_strings "$CURL_HEADER" "search_header" "expected_header" + + # retrieve SSL/TLS session info + find_curl_session_info "*" "TLS_INFO" "NO" + + # retrieve request header + find_curl_session_info ">" "REQUEST_HEADER" "YES" + + # retrieve response header + find_curl_session_info "<" "RESPONSE_HEADER" "YES" + + # check for regex match + find_curl_regex + +done <"$CURL_CONFIG" + +printf "}\n" +# end JSON + +echo "$CURL_RESULT" + +# clean up +cleanup + +unset CONF_DIR +unset LIB_DIR +unset CURL_EXECUTABLE +unset CURL_FORMAT +unset CURL_CONFIG +unset CURL_OPTIONS +unset CURL_RUN +unset CURL_OUTPUT +unset CURL_HEADER +unset CURL_SESSION_FILE +unset SERVICE_NAME +unset URL +unset CURL_REG_CASE +unset CURL_REG_MULTI + +exit 0 diff --git a/gui/metrics/curl.py b/gui/metrics/curl.py new file mode 100644 index 0000000000000000000000000000000000000000..bf186c5e436ba65459cd49fea5339465e8edd928 --- /dev/null +++ b/gui/metrics/curl.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# Metrics file for the curl plugin +# +# 2022-02-15: rewritten form cmk 2.0, based on the work by doc[at]snowheaven[dot]de +# 2022-02-20: added num_connects, num_redirects, size_download, size_header and speed_download +# 2022-03-13: moved cert time left graph to the end of graphs +# 2022-05-17: added scalars to cert_time_left + +from cmk.gui.i18n import _ + +from cmk.gui.plugins.metrics.utils import ( + metric_info, + graph_info, + perfometer_info, +) + +metric_info['time_namelookup'] = { + 'title': _('Time name lookup'), + 'unit': 's', + 'color': '11/a', +} +metric_info['time_connect'] = { + 'title': _('Time connect'), + 'unit': 's', + 'color': '21/a', +} +metric_info['time_appconnect'] = { + 'title': _('Time app connect'), + 'unit': 's', + 'color': '31/b', +} +metric_info['time_pretransfer'] = { + 'title': _('Time pre transfer'), + 'unit': 's', + 'color': '41/c', +} +metric_info['time_starttransfer'] = { + 'title': _('Time start transfer'), + 'unit': 's', + 'color': '13/b', +} +metric_info['time_total'] = { + 'title': _('Time Total'), + 'unit': 's', + 'color': '25/a', +} +metric_info['time_redirect'] = { + 'title': _('Time redirect'), + 'unit': 's', + 'color': '33/b', +} + +metric_info['num_connects'] = { + 'title': _('# of connects'), + 'unit': 'count', + 'color': '14/a', +} +metric_info['num_redirects'] = { + 'title': _('# of redirects'), + 'unit': 'count', + 'color': '24/b', +} +metric_info['num_headers'] = { + 'title': _('# of headers'), + 'unit': 'count', + 'color': '34/b', +} + +metric_info['size_download'] = { + 'title': _('Size download'), + 'unit': 'bytes', + 'color': '15/b', +} +metric_info['size_upload'] = { + 'title': _('Size upload'), + 'unit': 'bytes', + 'color': '25/b', +} + +metric_info['size_header'] = { + 'title': _('Size header'), + 'unit': 'bytes', + 'color': '35/b', +} +metric_info['size_request'] = { + 'title': _('Size request'), + 'unit': 'bytes', + 'color': '14/b', +} + +metric_info['speed_download'] = { + 'title': _('Speed download'), + 'unit': 'bytes/s', + 'color': '23/a', +} +metric_info['speed_upload'] = { + 'title': _('Speed upload'), + 'unit': 'bytes/s', + 'color': '13/a', +} + +metric_info['cert_time_left'] = { + 'title': _('Certificate Time left'), + 'unit': 's', + 'color': '33/b', +} + +graph_info['curl_times_total'] = { + 'title': _('Times total'), + 'metrics': [ + ('time_total', 'area'), + ], + 'scalars': [ + ('time_total:crit', _('crit')), + ('time_total:warn', _('warn')), + ], +} +graph_info['curl_times'] = { + 'title': _('Times'), + 'metrics': [ + ('time_redirect', 'line'), + ('time_starttransfer', 'line'), + ('time_pretransfer', 'line'), + ('time_appconnect', 'line'), + ('time_connect', 'line'), + ('time_namelookup', 'line'), + ], + 'optional_metrics': [ + 'time_redirect', + 'time_starttransfer', + 'time_pretransfer', + 'time_appconnect', + 'time_connect', + 'time_namelookup', + ], +} +graph_info['curl_speed'] = { + 'title': _('Speed'), + 'metrics': [ + ('speed_upload', '-area'), + ('speed_download', 'area'), + ], + 'optional_metrics': [ + 'speed_download', + 'speed_upload', + ], +} +graph_info['curl_size_download'] = { + 'title': _('Size download/upload'), + 'metrics': [ + ('size_upload', '-area'), + ('size_download', 'area'), + ], + 'optional_metrics': [ + 'size_upload', + 'size_download', + ], +} +graph_info['curl_size_header'] = { + 'title': _('Size header/request'), + 'metrics': [ + ('size_request', '-area'), + ('size_header', 'area'), + ], + 'optional_metrics': [ + 'size_request', + 'size_header', + ], +} +graph_info['curl_counts'] = { + 'title': _('Counts'), + 'metrics': [ + ('num_redirects', '-line'), + ('num_connects', 'line'), + ('num_headers', 'line'), + + ], + 'optional_metrics': [ + 'num_connects', + 'num_redirects', + 'num_headers', + ], +} +graph_info['curl_cert_time'] = { + 'title': _('Certificate time left'), + 'metrics': [ + ('cert_time_left', 'area'), + ], + 'scalars': [ + ('cert_time_left:crit', _('crit')), + ('cert_time_left:warn', _('warn')), + ], +} + +perfometer_info.append( + { + 'type': 'logarithmic', + 'metric': 'time_total', + 'half_value': 5.0, # 5 seconds -> bar half full + 'exponent': 2.0, # every double of 5 == 10% of bar more full + }, +) diff --git a/gui/wato/check_parameters/curl.py b/gui/wato/check_parameters/curl.py new file mode 100644 index 0000000000000000000000000000000000000000..e6c64b8f7086ad98fe01c6671f460ebf472cc469 --- /dev/null +++ b/gui/wato/check_parameters/curl.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# WATO file for curl plugin (bakery and check) +# +# 2022-02-19: moved global options under "Default settings" +# added per url settings +# added proxy settings (--proxy, --proxy-user, --proxy-digest, --proxy-basic, --proxy-ntlm, --proxy-anyauth) +# added follow redirects (--location) +# added don't verify certificates (--insecure) +# 2022-02-21: added tls/ssl version (--tlsv1.3, --tlsv1.2, --tlsv1.1, --tlsv1.0, --tlsv1, --sslv3, --sslv2) +# added http version (--http2, --http1.1, --http1.0) +# added header only (--head) +# added user authentication (--user, --basic, --digest, --ntlm, --ntlm-wb, --negotiate, --anyauth) +# added to proxy authentication (--proxy-negotiate) +# 2022-02-22: added option for cURL check plugin +# 2022-02-24: changed forbidden_chars from '<>| ' to '"<>#%{}|\^~[]` ' + "'" +# 2022-02-25: added plugin interval and timeout options +# added noproxy option to default settings +# 2022-02-26: added proxy protocol (--socks4, --socks4a, --socks4a, --socks5-hostname) +# 2022-02-27: added expected_strings option to default settings +# 2022-02-28: added expected_strings option to url settings +# added state_item_not_found option +# 2022-03-01: added options --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time, --connect-timeout +# added options --user-agent, --compressed, --no-alpn, --no-npn, --tcp-fastopen, --tcp-nodelay, +# added options --cert-status +# 2022-03-02: added options --referer -header and api_key_header (header from password store) +# 2022-03-05: added option --dump-header, state_header_str_not_found +# 2022-03-06: added option --verbose, --stderr cert_time_left +# fixed upper/lower levels +# 2022-03-10: optimized generation of upper/lower limit +# added transform for ms to s -> no need in th agent to change the value any more +# 2022-03-11: added check to avoid duplicate service names in url list +# added --location-trust, --max-redirs +# reworked redirect (--location, --location-trust, --max-redirs) +# reworked cert verify (-insecure, --cert-status, --ssl-no-revok) +# reworked advanced_settings (--no-alpn, --no-npn, --tcp-fastopen, -tcp-nodelay) +# removed get_session_data. moved to curl default options +# 2022-03-12: added --cacert +# added max_age +# 2022-03-13: added post data +# made some entries fordable (Default options/Per URL settings, CA Cert) +# changed url and service_name to curl_service tuple +# changed headers to list of tuple +# 2022-03-15: added regex pattern match for bakery +# 2022-03-18: added regex pattern match for check +# 2022-03-19: added options --dns-interface, -dns-ipv4-addr, --dns-ipv6-addr and --dns-servers +# reworked ip_address_resolution section +# added options --ftp-account, --ftp-alternative-to-user, --ftp-create-dirs, --ftp-method +# added options --ftp-pasv, --disable-epsv, --ftp-pret, --ftp-skip-pasv-ip +# added options --ftp-port, --disable-eprt +# added options --ftp-ssl-control, --ftp-ssl-ccc, --ftp-ssl-ccc-mode +# 2022-03-21: moved --connect-timeout, --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time +# to "limits" sub Directory +# 2022-03-22: added curl_error_code_to_ignore and http_error_code_to_ignore options +# 2022-03-24: added options --hostpubmd5, --hostpubsha256, --pubkey +# 2022-03-24: added options --key --passs +# reworked user_auth section +# 2022-03-25: added options --compressed-ssh, --list-only, --use-ascii +# added options --path-as-is, --ssl-allow-beast, --no-buffer, --no-keepalive, --no-sessionid +# 2022-03-28: added options --mail-auth, --mail-from, --mail-rcpt, --mail-rcpt-allowfails, --crlf, --upload-file (SMTP) +# 2022-04-10: added options to deploy cURL executable, no separate rules for curl and curl executable anymore +# windows/linux summarized under on option, no separate WATO rules per OS necessary anymore +# 2022-04-26: added check option clickable_url +# clarified option 'Don\'t verify certificate/pub key', 'Don\'t stop at verify errors (certificate/pub key)' +# 2023-10-21: split in service and bakery part +# modified for cmk 2.2.0 + +from cmk.gui.i18n import _ +from cmk.gui.valuespec import ( + Dictionary, + TextUnicode, + FixedValue, + Integer, + Tuple, + MonitoringState, + ListOfStrings, + Checkbox, + Age, +) +from cmk.gui.plugins.wato.utils import ( + rulespec_registry, + RulespecGroupCheckParametersOperatingSystem, + CheckParameterRulespecWithItem, +) + +bakery_plugin_version = '20220426.v0.0.8' + +# unsafe characters https://www.tutorialspoint.com/html/html_url_encoding.htm +forbidden_chars = '"<>#%{}|\^~[]` \'' + +_limits = [ + ('time_namelookup', 'Time name lookup', 'ms', ), + ('time_connect', 'Time connect', 'ms', ), + ('time_appconnect', 'Time app connect', 'ms', ), + ('time_pretransfer', 'Time pre transfer', 'ms', ), + ('time_redirect', 'Time redirect', 'ms', ), + ('time_starttransfer', 'Time start transfer', 'ms', ), + ('time_total', 'Time Total', 'ms', ), + ('cert_time_left', 'Certificate life time', 'Day(s)',), + ('num_connects', '# of connects', 'count',), + ('num_redirects', '# of redirects', 'count',), + ('num_headers', '# of headers', 'count',), + ('size_download', 'Size download', 'bytes',), + ('size_upload', 'Size upload', 'bytes',), + ('size_header', 'Size header', 'bytes',), + ('size_request', 'Size request', 'bytes',), + ('speed_download', 'Speed download', 'bytes/s',), + ('speed_upload', 'Speed upload', 'bytes/s',), +] + +_curl_check_elements = [ + ('clickable_url', + FixedValue( + True, + title=_('Render clickable URLs for HTTP/HTTPS (see Inline help)'), + totext=_(''), + help=_('Needs "Escape HTML in service output" rule or in Global settings enabled to work.') + )), + ('state_item_not_found', + MonitoringState( + default_value=1, + title=_('State on item not found'), + help=_('Monitoring state if the item is not found in the agent data. Default is UNKNOWN.') + )), + ('state_http_result_not_200', + MonitoringState( + default_value=1, + title=_('State on HTTP result not OK'), + help=_('Monitoring state if the HTTP return code is not in 2xx or 3xx. Default is WARN.') + )), + ('http_error_code_to_ignore', + ListOfStrings( + title=_('HTTP error codes to ignore'), + allow_empty=False, + orientation='horizontal', + # valuespec=Integer(size=3, minvalue=0, maxvalue=999), + )), + ('state_curl_result_not_0', + MonitoringState( + default_value=1, + title=_('State on cURL exit code not OK'), + help=_('Monitoring state if the exit code is not 0. Default is WARN.') + )), + ('curl_error_code_to_ignore', + ListOfStrings( + title=_('cURL error codes to ignore'), + allow_empty=False, + orientation='horizontal', + # valuespec=Integer(size=3, minvalue=0, maxvalue=999), + )), + ('state_verify_sll_not_0', + MonitoringState( + default_value=1, + title=_('State on SSL verify not OK'), + help=_('Monitoring state if the SSL verify code is not 0. Default is WARN.') + )), + ('state_expected_str_not_found', + MonitoringState( + default_value=1, + title=_('State on expected string not found'), + help=_('Monitoring state if one expected string is not found in the cURL output. Default is WARN.') + )), + ('state_header_str_not_found', + MonitoringState( + default_value=1, + title=_('State on expected header not found'), + help=_('Monitoring state if one expected header string is not found in the cURL output. Default is WARN.') + )), + ('state_for_regex', + Tuple( + title=_('State for regex pattern match'), + elements=[ + MonitoringState( + default_value=0, + title=_('State on regex pattern match'), + ), + MonitoringState( + default_value=1, + title=_('State on regex pattern don\'t match'), + ), + MonitoringState( + default_value=0, + title=_('State on regex pattern match info missing'), + ), + ], + )), + ('show_additional_info', + Tuple( + title=_('Show additional info'), + help=_('Shows RAW data from cURL in the service details.'), + elements=[ + Checkbox('Request headers'), + Checkbox('Response headers'), + Checkbox('(TLS/SSL) session info'), + Checkbox('RAW data'), + ], + )), + ('max_age', + Tuple( + title=_('Maximum age'), + elements=[ + Age( + title=_('Warning at'), + help=_('Warn, if the age of the page is older than this'), + default_value=3600 * 24, + ), + Age( + title=_('Critical at'), + help=_('Critical, if the age of the page is older than this'), + default_value=3600 * 24 * 4, + ), + MonitoringState( + default_value=0, + title=_('State if not available'), + help=_( + 'Monitoring state if the "Last-Modified" header is not available in the agent data. Default is OK.' + ) + ), + ] + )), +] + + +def _get_tuple_upper(_unit: str) -> Tuple: + return Tuple( + title=_('Upper limits'), + orientation='horizontal', + elements=[ + Integer(title=_('Warning at'), minvalue=0, unit=_(_unit), size=10), + Integer(title=_('Critical at'), minvalue=0, unit=_(_unit), size=10), + ], + ) + + +def _get_tuple_lower(_unit): + return Tuple( + title=_('Lower limits'), + orientation='horizontal', + elements=[ + Integer(title=_('Warning below'), minvalue=0, unit=_(_unit), size=10), + Integer(title=_('Critical below'), minvalue=0, unit=_(_unit), size=10), + ], + ) + + +for key, label, unit in _limits: + _curl_check_elements.append( + (key, + Dictionary( + title=_(label), + elements=[ + ('upper', _get_tuple_upper(unit),), + ('lower', _get_tuple_lower(unit),), + ], + )) + ) + + +def _valuespec_curl(): + return Dictionary( + elements=_curl_check_elements, + ) + + +rulespec_registry.register( + CheckParameterRulespecWithItem( + check_group_name='curl', + group=RulespecGroupCheckParametersOperatingSystem, + parameter_valuespec=_valuespec_curl, + title=lambda: _('cURL'), + match_type='dict', + item_spec=lambda: TextUnicode(title=_('cURL service name'), ), + )) diff --git a/gui/wato/check_parameters/curl_bakery.py b/gui/wato/check_parameters/curl_bakery.py new file mode 100644 index 0000000000000000000000000000000000000000..3c7c37d1df11b37541bff9538a50a13371adefd5 --- /dev/null +++ b/gui/wato/check_parameters/curl_bakery.py @@ -0,0 +1,1061 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# WATO file for curl plugin (bakery and check) +# +# 2022-02-19: moved global options under "Default settings" +# added per url settings +# added proxy settings (--proxy, --proxy-user, --proxy-digest, --proxy-basic, --proxy-ntlm, --proxy-anyauth) +# added follow redirects (--location) +# added don't verify certificates (--insecure) +# 2022-02-21: added tls/ssl version (--tlsv1.3, --tlsv1.2, --tlsv1.1, --tlsv1.0, --tlsv1, --sslv3, --sslv2) +# added http version (--http2, --http1.1, --http1.0) +# added header only (--head) +# added user authentication (--user, --basic, --digest, --ntlm, --ntlm-wb, --negotiate, --anyauth) +# added to proxy authentication (--proxy-negotiate) +# 2022-02-22: added option for cURL check plugin +# 2022-02-24: changed forbidden_chars from '<>| ' to '"<>#%{}|\^~[]` ' + "'" +# 2022-02-25: added plugin interval and timeout options +# added noproxy option to default settings +# 2022-02-26: added proxy protocol (--socks4, --socks4a, --socks4a, --socks5-hostname) +# 2022-02-27: added expected_strings option to default settings +# 2022-02-28: added expected_strings option to url settings +# added state_item_not_found option +# 2022-03-01: added options --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time, --connect-timeout +# added options --user-agent, --compressed, --no-alpn, --no-npn, --tcp-fastopen, --tcp-nodelay, +# added options --cert-status +# 2022-03-02: added options --referer -header and api_key_header (header from password store) +# 2022-03-05: added option --dump-header, state_header_str_not_found +# 2022-03-06: added option --verbose, --stderr cert_time_left +# fixed upper/lower levels +# 2022-03-10: optimized generation of upper/lower limit +# added transform for ms to s -> no need in th agent to change the value any more +# 2022-03-11: added check to avoid duplicate service names in url list +# added --location-trust, --max-redirs +# reworked redirect (--location, --location-trust, --max-redirs) +# reworked cert verify (-insecure, --cert-status, --ssl-no-revok) +# reworked advanced_settings (--no-alpn, --no-npn, --tcp-fastopen, -tcp-nodelay) +# removed get_session_data. moved to curl default options +# 2022-03-12: added --cacert +# added max_age +# 2022-03-13: added post data +# made some entries fordable (Default options/Per URL settings, CA Cert) +# changed url and service_name to curl_service tuple +# changed headers to list of tuple +# 2022-03-15: added regex pattern match for bakery +# 2022-03-18: added regex pattern match for check +# 2022-03-19: added options --dns-interface, -dns-ipv4-addr, --dns-ipv6-addr and --dns-servers +# reworked ip_address_resolution section +# added options --ftp-account, --ftp-alternative-to-user, --ftp-create-dirs, --ftp-method +# added options --ftp-pasv, --disable-epsv, --ftp-pret, --ftp-skip-pasv-ip +# added options --ftp-port, --disable-eprt +# added options --ftp-ssl-control, --ftp-ssl-ccc, --ftp-ssl-ccc-mode +# 2022-03-21: moved --connect-timeout, --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time +# to "limits" sub Directory +# 2022-03-22: added curl_error_code_to_ignore and http_error_code_to_ignore options +# 2022-03-24: added options --hostpubmd5, --hostpubsha256, --pubkey +# 2022-03-24: added options --key --passs +# reworked user_auth section +# 2022-03-25: added options --compressed-ssh, --list-only, --use-ascii +# added options --path-as-is, --ssl-allow-beast, --no-buffer, --no-keepalive, --no-sessionid +# 2022-03-28: added options --mail-auth, --mail-from, --mail-rcpt, --mail-rcpt-allowfails, --crlf, --upload-file (SMTP) +# 2022-04-10: added options to deploy cURL executable, no separate rules for curl and curl executable anymore +# windows/linux summarized under on option, no separate WATO rules per OS necessary anymore +# 2022-04-26: added check option clickable_url +# clarified option 'Don\'t verify certificate/pub key', 'Don\'t stop at verify errors (certificate/pub key)' +# 2023-10-21: changed for CMK 2.2.0 + +import ipaddress +from cmk.gui.i18n import _ +from cmk.gui.exceptions import MKUserError +from cmk.gui.valuespec import ( + Dictionary, + ListOf, + CascadingDropdown, + TextUnicode, + FixedValue, + Integer, + Tuple, + DropdownChoice, + ListOfStrings, + TextInput, + Checkbox, + CAorCAChain, + Optional, + Foldable, + UploadOrPasteTextFile, + Alternative, +) +from cmk.gui.plugins.wato.utils import ( + rulespec_registry, + HostRulespec, +) +from cmk.gui.plugins.wato.utils import ( + PasswordFromStore, +) + +from cmk.gui.cee.plugins.wato.agent_bakery.rulespecs.utils import ( + RulespecGroupMonitoringAgentsAgentPlugins, +) + +# unsafe characters https://www.tutorialspoint.com/html/html_url_encoding.htm +forbidden_chars = '"<>#%{}|\^~[]` \'' + +# ######################################################################################################### +# +# cURL bakery options +# +# ######################################################################################################### + +_option_curl_service = ('curl_service', + Tuple( + elements=[ + TextUnicode( + title=_('Service name'), + help=_('Name for the discovered service, for example www.example.com'), + allow_empty=False, + placeholder='your.service.name', + forbidden_chars=forbidden_chars, + ), + TextUnicode( # ToDo: change to URL? + title=_('URL to check'), + help=_('URL to check with cURL, for example https://www.example.com'), + allow_empty=False, + size=90, + placeholder='https://www.example.com', + forbidden_chars=forbidden_chars, + ), + ], + orientation='horizontal', + )) + +_option_verify_remote_host = ('cert_verify', + Tuple( + title='Configure verification of remote host (certificate/pub key)', + elements=[ + # Checkbox('Don\'t verify certificate/pub key'), + Checkbox('Don\'t stop at verify errors (certificate/pub key)'), + Checkbox('Use OCSP to check certificate status'), + Checkbox('Disable cert revocation checks (WinSSL)'), + Optional(Foldable(CAorCAChain()), label='Certificate to verify against', ), + Optional(TextUnicode(size=40, minlen=32, maxlen=32, regex='[0-9a-fA-F]', ), + label='Expected MD5 hash of pub key'), + Optional(TextUnicode(size=60, allow_empty=False), + label='Expected SHA256 hash of pub key'), + Optional(Foldable(UploadOrPasteTextFile(title='Public key'), ), + label='Expected public key'), + ] + )) + +_option_redirects = ('redirects', + Tuple( + title='Configure redirects', + elements=[ + Checkbox('Follow redirects'), + Checkbox('Use authentication on redirects'), + Optional(Integer(minvalue=-1, default_value=10), label='Max. redirects', sameline=True) + ] + )) + +_option_regex_response = ('regex_response', + Tuple( + title=_('Regular expression to expect in content'), + elements=[ + TextUnicode( + label=_('Regular expression'), + placeholder=_('If empty regex search will be disabled') + ), + Checkbox('Case insensitive'), + Checkbox('Multiline string matching'), + ] + )) + +_option_advanced_settings = ('advanced_settings', + Tuple( + title='Advanced settings', + elements=[ + Checkbox('Allow SSL beast security flaw to improve interoperability'), + Checkbox('Convert LF to CRLF in upload'), + Checkbox('Disable Application Layer Protocol Negotiation (ALPN)'), + Checkbox('Disable buffering of the output stream'), + Checkbox('Disable Next Protocol Negotiation (NPN)'), + Checkbox('Disable SSL session-ID reusing'), + Checkbox('Disable TCP keep alive on the connection'), + Checkbox('Do not squash .. sequences in URL path'), + Checkbox('Use TCP fast open option'), + Checkbox('Use TCP no delay option'), + ] + )) + +_options_get_header_only = ('get_header_only', + FixedValue( + '--head', + title=_('Get header only'), + totext=_('Only headers will be downloaded'), + help=_('cURL will will only download headers. Implements the "--head" option'), + )) +_url_get_header_only = ('get_header_only', + DropdownChoice( + title=_('Get Header only'), + choices=[ + ('--head', _('Get header only')), + ('', _('Get the hole document')), + ], + )) + +_option_auth_user = ('user_auth', + _('Username/Password'), + Tuple( + title=_('Configure user authentication'), + help=_( + 'The password entered here is stored in plain text within the monitored host. ' + 'This is needed because the agent plugin needs to have access to the unencrypted ' + 'password to authenticate with the server.' + ), + elements=[ + TextUnicode( + title=_('Username'), + allow_empty=False, + forbidden_chars=forbidden_chars, + placeholder='username', + ), + PasswordFromStore( + title=_('Password of the user'), + allow_empty=False, + ), + DropdownChoice( + title=_('Authentication method'), + choices=[ + ('', _('Use cURL default')), + ('--basic', _('Basic authentication')), + ('--digest', _('Digest authentication')), + ('--ntlm', _('NTLM authentication')), + ('--ntlm-wb', _('NTLM authentication with winbind')), + ('--negotiate', _('HTTP Negotiate (SPNEGO) authentication')), + ('--anyauth', _('Any authentication')), + ]), + ], + )) +_option_auth_priv_key = ('priv_key_auth', + _('Private/public key'), + Tuple( + elements=[ + TextUnicode( + title=_('Username'), + allow_empty=False, + forbidden_chars=forbidden_chars, + placeholder='username', + ), + PasswordFromStore( + title=_('Pass phrase'), + allow_empty=False, + ), + Foldable(UploadOrPasteTextFile(title='Private key', file_title='Private key (PEM)'), + title='Private key'), + ] + )) +_option_auth = ('user_auth', + CascadingDropdown( + title=_('Configure authentication'), + sorted=False, + choices=[ + _option_auth_user, + _option_auth_priv_key, + (None, _('No authentication')), + ], + )) + +_option_proxy_server = ('proxy_server', + Tuple( + title=_('Proxy server'), + show_titles=False, + elements=[ + DropdownChoice( + title=_('Protocol'), + choices=[ + ('--proxy', _('HTTP')), + ('--socks4', _('SOCKS4')), + ('--socks4a', _('SOCKS4a')), + ('--socks5', _('SOCKS5')), + ('--socks5-hostname', _('SOCKS5 hostname')), + ]), + TextUnicode( + label=_('Server'), + help=_('Name or IP-address of the proxy server.'), + allow_empty=False, + size=40, + placeholder='your.proxy.server', + forbidden_chars=forbidden_chars, + ), + Integer( + label=_('Port'), + default_value=3128, + minvalue=1, + maxvalue=65565, + ), + ], + orientation='horizontal', + )) + +_option_proxy_auth = ('proxy_auth', + Tuple( + title=_('Proxy authentication'), + help=_( + 'The password entered here is stored in plain text within the monitored host. ' + 'This is needed because the agent plugin needs to have access to the unencrypted ' + 'password to authenticate with the proxy.' + ), + elements=[ + TextUnicode( + title=_('Proxy username'), + allow_empty=False, + forbidden_chars=forbidden_chars, + placeholder='proxyusername', + ), + PasswordFromStore( + title=_('Password of the user'), + allow_empty=False, + ), + DropdownChoice( + title=_('Authentication method'), + choices=[ + ('--proxy-basic', _('Basic authentication')), + ('--proxy-digest', _('Digest authentication')), + ('--proxy-ntlm', _('NTLM authentication')), + ('--proxy-negotiate', _('HTTP Negotiate (SPNEGO) authentication')), + ('--proxy-anyauth', _('Any authentication')), + ('--socks5-basic', _('SOCKS5 basic authentication')), + ]), + ], + )) + +_options_proxy = ('http_proxy', + Alternative( + title=_('Configure proxy server'), + elements=[ + Dictionary( + title='Use proxy', + elements=[ + _option_proxy_server, + _option_proxy_auth, + ], + required_keys=['proxy_server'], + ), + FixedValue('--noproxy', title=_('Don\'t use any proxy'), totext=_('')), + FixedValue(None, title=_('Don\'t configure an proxy (use system settings)'), totext=_('')), + ], + )) + +_option_tls_ssl_version = ('tls_ssl_version', + DropdownChoice( + title=_('Use TLS/SSL version'), + choices=[ + ('', _('cURL default')), + ('--ssl', _('Try TLS/SSL')), + ('--ssl-reqd', _('Require TLS/SSL')), + ('--tlsv1.3', _('Use TLS 1.3')), + ('--tlsv1.2', _('Use TLS 1.2')), + ('--tlsv1.1', _('Use TLS 1.1')), + ('--tlsv1.0', _('Use TLS 1.0')), + ('--tlsv1', _('Use TLS1.0 or greater')), + ('--sslv3', _('Use SSLv3')), + ('--sslv2', _('Use SSLv2')), + ]),) + +_option_http_version = ('http_version', + DropdownChoice( + title=_('Use HTTP version'), + choices=[ + ('', _('cURL preferred version')), + ('--http2', _('Use HTTP/2')), + ('--http1.1', _('Use HTTP 1.1')), + ('--http1.0', _('Use HTTP 1.0')), + ]),) + + +def _validate_ipaddress(pattern, varprefix): + if pattern: + try: + ipaddress.ip_address(pattern) + except ValueError: + raise MKUserError(varprefix, _(f'{pattern} is not a valid IP address')) + + +_option_address_resolution = ('ip_address_resolution', + Foldable( + Dictionary( + title=_('DNS options'), + elements=[ + ('dns_resolve_names', DropdownChoice( + title=_('Resolve names'), + choices=[ + (None, _('IPv4/IPv6')), + ('--ipv4', _('IPv4 only')), + ('--ipv6', _('IPv6 only')), + ])), + ('dns_source_interface', TextUnicode( + title=_('Source interface'), + regex='[0-9a-zA-Z]', + size=15, + )), + ('dns_source_ipv4', TextUnicode( + title=_('IPv4 source address'), + validate=_validate_ipaddress, + size=15, + )), + ('dns_source_ipv6', TextUnicode( + title=_('IPv6 source address'), + validate=_validate_ipaddress, + size=42 + )), + ('dns_servers', ListOfStrings( + title=_('DNS servers'), + valuespec=TextInput( + size=42, + validate=_validate_ipaddress, + ), + orientation='horizontal', + )), + ], + ), + title=_('Set DNS options'), + )) + +_option_ftp_settings = ('ftp_settings', + Foldable( + Dictionary( + title=_('FTP/SCP/SFTP options'), + elements=[ + ('ftp_account', TextUnicode(title=_('Account data string')),), + ('ftp_alternate_to_user', TextUnicode(title=_('String to replace USER command'))), + ('ftp_create_dirs', FixedValue( + True, + title=_('Create remote dir(s)'), + totext=_('') + )), + ('ftp_change_cwd_method', DropdownChoice( + title=_('Change working directory method'), + choices=[ + ('multicwd', 'one CD for each directory'), + ('nocdw', 'No CD. Use full path in SIZE, RETR, STOR etc.'), + ('singlecwd', 'use one CD with full path') + ] + )), + ('ftp_mode', + CascadingDropdown( + title=_('Passive/Actrive mode'), + sorted=False, + choices=[ + ('ftp_pass', + _('FTP passive mode'), + Tuple( + elements=[ + Checkbox(label=_('Don\'t send EPSV command')), + Checkbox(label=_('Send PRET before PASV')), + Checkbox( + label=_('Use remote IP form control channel for data chanel')) + ] + )), + ('ftp_active', + _('FTP active mode'), + Tuple( + elements=[ + Checkbox(label=_('Don\'t send EPRT command')), + TextUnicode( + label=_('Address to use'), + help=_( + 'Can be the interface name ie "eth0", a exact ip address, a ' + 'hostname/FQDN or "-" to use the same address used for the ' + 'control connection' + ), + default_value='-', + regex='[0-9a-zA-Z\\.:\\-_]', + ), + ] + )) + ] + )), + ('ftp_ssl_control', + Tuple( + title=_('Require SSL/TLS for FTP login'), + elements=[ + Checkbox(label=_('Send CCC after authenticating')), + DropdownChoice( + label=_('Set CCC mode'), + choices=[ + ('active', 'Active'), + ('passive', 'Passive'), + ] + ) + ] + )), + ('compressed_ssh', FixedValue( + True, + title=_('Enable ssh compression'), + totext=_(''), + )), + ('list_only', FixedValue( + True, + title=_('Enable list only'), + totext=_(''), + )), + ('use_ascii', FixedValue( + True, + title=_('Enable ASCII transfer'), + totext=_(''), + )), + ], + ), + title=_('Set FTP/SCP/SFTP options'), + )) + +_option_expected_strings = ('expected_strings', + ListOfStrings( + title=_('Strings to expect in response'), + # orientation='horizontal', + allow_empty=False, + valuespec=TextInput(allow_empty=False, regex='[a-zA-Z0-9\.]'), + )) +_url_expected_strings = ('expected_strings', + Alternative( + title=_('Override strings to expect in response'), + elements=[ + _option_expected_strings[1], + FixedValue(None, title=_('Don\'t expect any strings in the response'), totext=_('')), + ], + )) + +_option_header_strings = ('header_strings', + ListOfStrings( + title=_('Strings to expect in header'), + # orientation='horizontal', + allow_empty=False, + valuespec=TextInput(allow_empty=False, regex='[a-zA-Z0-9\\.]'), + )) +_url_header_strings = ('header_strings', + Alternative( + title=_('Override strings to expect in header'), + elements=[ + _option_expected_strings[1], + FixedValue(None, title=_('Don\'t expect any strings in the header'), totext=_('')), + ], + )) + +_option_request_headers = ('request_headers', + ListOf( + Tuple( + title=_('Set headers'), + orientation='horizontal', + elements=[ + TextUnicode( + label=_('Header'), + allow_empty=False, + placeholder='X-your-header', + regex='[a-zA-Z0-9_\\-]', + # size=30, + ), + TextUnicode( + label=_('Value'), + # allow_empty=False, + placeholder='value of header', + # regex='[a-zA-Z0-9_ :;.,=<>#\\-@\\+\\*\'/\\?!\\(\\)\\{\\}\\[\\]\\$\\&~\\^%|"`\\]', + size=50, + empty_text=';' + ), + ] + ), + allow_empty=False, + title=_('Set headers'), + add_label=_('Add header'), + movable=True, + )) +_url_request_headers = ('request_headers', + Alternative( + title=_('Override default headers'), + elements=[ + _option_request_headers[1], + FixedValue(None, title=_('Don\'t configure request headers'), totext=_('')) + ] + )) + +_option_mail_settings = ('mail_settings', + Foldable( + Dictionary( + title=_('SMTP settings'), + elements=[ + ('mail_from', TextUnicode( + title=_('Mail from address'), + allow_empty=False, + )), + ('mail_rcpt', ListOfStrings( + title=_('Mail to address'), + allow_empty=False, + max_entries=5, + )), + ('mail_auth', TextUnicode( + title=_('Mail originator address'), + )), + # ('oauth2_header', TextUnicode( + # title=_('Oauth2 token'), + # )), + ('request', TextUnicode( + title=_('REQUEST command'), + help=_('Send this command instead of LIST (POP3/IMAP) or HELP/VRFY (SMTP).') + )), + _option_request_headers, + ('message', UploadOrPasteTextFile( + title=_('Message to send'), + )), + ('mail_rpct_allowfail', FixedValue( + True, + title=_('Allow some mail to addresses to fail'), + totext=_('') + )) + ] + ), + title=_('Set SMTP options'), + )) + +_option_api_key_header = ('api_key_header', + Tuple( + title=_('Set API key header'), + help=_( + 'The password entered here is stored in plain text within the monitored host. ' + 'This is needed because the agent plugin needs to have access to the unencrypted ' + 'password to authenticate with the server.' + ), + elements=[ + TextUnicode( + title=_('API Key header'), + allow_empty=False, + forbidden_chars='|"', + placeholder='X-API-Key: ', + ), + PasswordFromStore( + title=_('API Key'), + allow_empty=False, + ), + ], + )) +_url_api_key_header = ('api_key_header', + Alternative( + title=_('Set API key header'), + elements=[ + _option_api_key_header[1], + FixedValue(None, title=_('Don\'t configure an API key header'), totext=_('')), + ], + )) + +_option_limit_rate = ('limit_rate', + Tuple( + title=_('Maximum UP-/Download rate'), + # show_titles=False, + elements=[ + Integer( + label=_('Speed'), + # default_value=3128, + minvalue=1, + # maxvalue=65565, + ), + DropdownChoice( + default_value='M', + choices=[ + ('B', _('Byte/s')), + ('K', _('KByte/s')), + ('M', _('MByte/s')), + ('G', _('GByte/s')), + ]), + ], + orientation='horizontal', + )) +_url_limit_rate = ('limit_rate', + Alternative( + title=_('Maximum UP-/Download rate'), + elements=[ + _option_limit_rate[1], + FixedValue(None, title=_('Don\'t configure a rate limit'), totext=_('')), + ], + )) + +_option_max_file_size = ('max_file_size', + Tuple( + title=_('Maximum file size'), + # show_titles=False, + elements=[ + Integer( + label=_('Size'), + # default_value=3128, + minvalue=1, + # maxvalue=65565, + ), + DropdownChoice( + # title=_('Unit'), + choices=[ + ('B', _('Byte')), + ('K', _('KByte')), + ('M', _('MByte')), + ('G', _('GByte')), + ]), + ], + orientation='horizontal', + )) +_url_max_file_size = ('max_file_size', + Alternative( + title=_('Maximum file size'), + elements=[ + _option_max_file_size[1], + FixedValue(None, title=_('Don\'t configure a file size limit'), totext=_('')), + ], + )) + +_option_max_time = ('max_time', + Integer( + title=_('Maximum transfer time'), + default_value=10, + minvalue=1, + unit='s', + )) +_url_max_time = ('max_time', + Alternative( + title=_('Maximum transfer time'), + elements=[ + _option_max_time[1], + FixedValue(None, title=_('Don\'t configure a transfer time limit'), totext=_('')), + ], + )) + +_option_speed_limit = ('speed_limit', + Tuple( + title=_('Minimum speed'), + # show_titles=False, + elements=[ + Integer( + label=_('Speed'), + minvalue=1, + ), + DropdownChoice( + default_value=1024, + choices=[ + (1, _('Byte/s')), + (1024, _('KByte/s')), + (1048576, _('MByte/s')), + (1073741824, _('GByte/s')), + ]), + ], + orientation='horizontal', )) +_url_speed_limit = ('speed_limit', + Alternative( + title=_('Minimum speed'), + elements=[ + _option_speed_limit[1], + FixedValue(None, title=_('Don\'t configure a lower speed limit'), totext=_('')), + ], + )) + +_option_speed_time = ('speed_time', + Integer( + title=_('Minimum speed time'), + default_value=30, + minvalue=1, + unit='s', )) +_url_speed_time = ('speed_time', + Alternative( + title=_('Minimum speed time'), + elements=[ + _option_speed_time[1], + FixedValue(None, title=_('Don\'t configure a minimum speed time limit'), totext=_('')), + ], + )) + +_option_connect_timeout = ('connect_timeout', + Integer( + title=_('Maximum time to connect'), + default_value=1, + minvalue=1, + unit='s', )) +_url_connect_timeout = ('connect_timeout', + Alternative( + title=_('Maximum time to connect'), + elements=[ + _option_connect_timeout[1], + FixedValue(None, title=_('Don\'t configure a maximum time to connect'), totext=_('')), + ], + )) + +_option_limits = ('limits', + Foldable( + Dictionary( + title=_('Limits'), + elements=[ + _option_limit_rate, + _option_max_file_size, + _option_connect_timeout, + _option_max_time, + _option_speed_limit, + _option_speed_time, + ] + ), + title=_('Set connection limits') + )) +_url_limits = ('limits', + Foldable( + Dictionary( + title=_('Limits'), + elements=[ + _url_limit_rate, + _url_max_file_size, + _url_connect_timeout, + _url_max_time, + _url_speed_limit, + _url_speed_time, + ] + ), + title=_('Override connection limits') + )) + +_option_user_agent = ('user_agent', + TextUnicode( + title=_('Set user agent'), + allow_empty=False, + placeholder='your user agent', + # forbidden_chars=forbidden_chars, + forbidden_chars='"|' + )) +_url_user_agent = ('user_agent', + CascadingDropdown( + title=_('Set user agent'), + sorted=False, + choices=[ + ('user_agent', _('Override default user agent'), + _option_user_agent[1],), + ('', _('Don\'t configure a user agent')), + ], + )) + +_option_referer = ('referer', + TextUnicode( + title=_('Set referer'), + allow_empty=False, + placeholder='http://your.referer.url/', + # forbidden_chars=forbidden_chars, + forbidden_chars='|" ', + )) +_url_referer = ('referer', + Alternative( + title=_('Override default referer'), + elements=[ + _option_referer[1], + FixedValue(None, title=_('Don\'t configure a referer'), totext=_('')) + ] + )) + +_options_compressed = ('compressed', + FixedValue( + '--compressed', + title=_('Request compressed response'), + totext=_('Request compressed response enabled'), + )) +_url_compressed = ('compressed', + DropdownChoice( + title=_('Request compressed response'), + choices=[ + ('--compressed', _('Request compressed response')), + ('', _('Don\'t Request compressed response')), + ], + )) + +_option_post = ('post_binary', + Tuple( + title=_('Send HTTP POST data'), + elements=[ + TextUnicode( + label=_('Content-Type'), + allow_empty=False, + forbidden_chars=forbidden_chars, + default_value='text/html' + ), + UploadOrPasteTextFile( + # title=_('HTTP POST data'), + allow_empty=False, + default_value=_( + 'This posts data exactly as specified with no extra processing whatsoever.\n\n' + 'To disable HTTP POST data in per URL settings leave this empty.' + ), + ), + ] + )) + +_option_url_settings = ('url_settings', + Foldable( + Dictionary( + title=_('Per URL settings'), + elements=[ + _option_verify_remote_host, + _options_proxy, + _option_redirects, + _option_auth, + _url_get_header_only, + _option_regex_response, + _url_compressed, + _option_post, + _url_api_key_header, + _url_referer, + _url_request_headers, + _url_user_agent, + _url_header_strings, + _url_expected_strings, + _url_limits, + _option_address_resolution, + _option_ftp_settings, + _option_mail_settings, + _option_tls_ssl_version, + _option_http_version, + _option_advanced_settings, + ], + ), + title=_('Override default settings'), + )) + + +def _validate_service_names(pattern, varprefix): + service_names = [] + + for url in pattern: + service_names.append(url['curl_service'][0]) + duplicates = [service_name for service_name in service_names if service_names.count(service_name) > 1] + duplicates = list(set(duplicates)) + if duplicates: + raise MKUserError( + varprefix, + _(f'There are duplicate service names. Please check the following service names: {", ".join(duplicates)}') + ) + + +_option_url = ('url_list', + Foldable( + ListOf( + Dictionary( + elements=[ + _option_curl_service, + _option_url_settings, + ], + required_keys=['curl_service', ], + ), + title='Services/URLs', + add_label=_('Add URL'), + movable=True, + # title=_('URLs to check'), + allow_empty=False, + validate=_validate_service_names, + ), + title=_('URLs to check'), + )) + +_option_default_settings = ('default_settings', + Foldable( + Dictionary( + title=_('Plugin settings'), + elements=[ + _option_verify_remote_host, + _options_proxy, + _option_redirects, + _option_auth, + _options_get_header_only, + _option_regex_response, + _options_compressed, + _option_post, + _option_api_key_header, + _option_referer, + _option_request_headers, + _option_user_agent, + _option_header_strings, + _option_expected_strings, + _option_limits, + _option_address_resolution, + _option_ftp_settings, + _option_mail_settings, + _option_http_version, + _option_tls_ssl_version, + _option_advanced_settings, + ], + ), + title='Default setting', + )) + +_option_plugin_interval = ('interval', + Integer( + title=_('Plugin run interval'), + minvalue=1, + unit=_('min'), + # default_value=1, + help=_( + 'This is the interval at witch the plugin runs. If not set the plugin will ' + 'run with every cycle of the agent (By default every 1 minute).' + ), + ),) + +_option_plugin_timeout = ('timeout', + Integer( + title=_('Plugin timeout'), + minvalue=1, + unit=_('min'), + # default_value=300, + help=_( + 'This is the maximum run time for the plugin. If not set the timeout for ' + 'the plugin is 1 minute by default.' + ), + ),) + +_option_curl_executable = ('curl_executable', + DropdownChoice( + title=_('cURL executable to use'), + help=_( + 'By default this plugin will use the system provided cURL executable. You can ' + 'decide to deploy a separate cURL executable. If you do so you need to install the ' + 'optional CMK package curl_executable<version>.mkp.' + ), + choices=[ + (None, _('Use system provided cURL executable')), + ('64bit', _('Deploy 64bit cURL version')), + ('32bit', _('Deploy 32bit cURL version')), + ] + )) + + +def _valuespec_agent_config_curl(): + return Alternative( + title=_('cURL'), + elements=[ + Dictionary( + title=_('Deploy cURL agent plugin'), + elements=[ + _option_url, + _option_plugin_interval, + _option_plugin_timeout, + _option_curl_executable, + _option_default_settings, + ], + required_keys=['url_list'], + ), + FixedValue( + None, + title=_('Do not deploy the cURL agent plugin'), + totext=_('The cURL agent plugin will not be deployed') + ), + ], + ) + + +rulespec_registry.register( + HostRulespec( + group=RulespecGroupMonitoringAgentsAgentPlugins, + name='agent_config:curl', + valuespec=_valuespec_agent_config_curl, + ) +) diff --git a/lib/python3/cmk/base/cee/plugins/bakery/curl.py b/lib/python3/cmk/base/cee/plugins/bakery/curl.py new file mode 100644 index 0000000000000000000000000000000000000000..e9cd95dd5099b88b162b1b3f81d09f1fef3db156 --- /dev/null +++ b/lib/python3/cmk/base/cee/plugins/bakery/curl.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2022-02-15 +# +# bakery curl plugin +# +# 2022-02-19: integrated per url settings +# added proxy settings (--proxy, --proxy-user, --proxy-digest/--proxy-basic/--proxy-ntlm/--proxy-anyauth) +# moved the curl.exe deployment to curl_windows.mkp package +# 2022-02-23: fixed handling aof user_auth settings +# fixed options in curl.cfg for windows +# 2022-02-24: removed deployment of curl.format +# added noproxy option in per url settings +# 2022-02-25: optimized plugin file write code +# added double quotas to curl options for linux to be equal with windows +# added plugin interval and timeout +# added noproxy option to default settings +# 2022-02-26: removed 'http://' prefix from proxy server address +# added proxy protocol +# 2022-02-27: added expected_strings options +# 2022-03-01: added options --limit-rate, --max-filesize, --max-time, --speed-limit, --speed-time, +# --connect-timeout and --user-agent +# 2022-03-02: added options --referer -header and api_key_header (header from password store) +# 2022-03-04: fixed Requests headers per url settings +# 2022-03-05: added option --dump-header +# 2022-03-06: added option --stderr, --verbose +# 2022-03-11: added redirection (--location, --location-trust, --max-redirs) +# added cert_verify (--insecure, --cert-status, --ssl-no-revoke) +# added advanced_settings (--no-alpn, --no-npn, --tcp-fastopen, -tcp-nodelay) +# removed get_session_data. moved to curl default options +# 2022-03-12: added --cacert option +# fixed api-key-header handling +# 2022-03-13: moved curl_item files to curl sub directory under MK_CONFDIR +# changed url/service_name from separate dict entries to tuple +# changed headers to read from curl_item_x.header file +# 2022-03-15: moved curl options from curl.cfg to curl_item_#.options +# added regex pattern match +# 2022-03-20: added dns_options, ftp_options +# 2022-03-21: fixed handling of limits and sub directories from wato +# 2022-03-24: added options --hostpubmd5, --hostpubsha256, --pubkey +# 2022-03-24: added options --key --passs +# 2022-03-25: added options --compressed-ssh, --list-only, --use-ascii +# added options --path-as-is, --ssl-allow-beast, --no-buffer, --no-keepalive, --no-sessionid +# 2022-03-28: added option --crlf +# added SMTP settings: --mail-auth, --mail-from, --mail-rcpt, --mail-rcpt-allowfails, --upload-file (SMTP) +# 2022-04-10: added deployment of cURL executables +# no separate WATO rules per OS necessary anymore +# reworked to make scalable for multiple OSs (THX to andreas.doehler[at]gmail[dot]com) +# 2022-10-21: moved from ~/local/share/check_mk/agents_bakery to ~/local/lib/check_mk/base/cee/plugins/bakery + +from pathlib import Path +from typing import List, Tuple, Dict, Any +from dataclasses import dataclass + +from cmk.utils import ( + password_store, +) +from cmk.base.cee.plugins.bakery.bakery_api.v1 import ( + FileGenerator, + OS, + Plugin, + PluginConfig, + register +) + + +@dataclass +class CurlConfig: + base_os: OS + curl_output: str + temp_path: str + conf_path: str + traget_path_bin: str + plugin_name: str + curl_executable_src: Dict[str, str] + curl_executable_dest: str + + +CURL_CONFIGS: List[CurlConfig] = [ + CurlConfig( + base_os=OS.LINUX, + curl_output="--output /dev/null", + temp_path="/var/tmp/", + conf_path="/etc/check_mk/", + traget_path_bin='../bin/curl', + plugin_name='curl.sh', + curl_executable_src={'64bit': 'curl-amd64', '32bit': 'curl-i386'}, + curl_executable_dest='../bin/curl', + ), + CurlConfig( + base_os=OS.WINDOWS, + curl_output="--output NUL", + temp_path="c:/windows/temp/", + conf_path="C:/ProgramData/checkmk/agent/config/", + traget_path_bin='..\\bin\\curl.exe', + plugin_name='curl.ps1', + curl_executable_src={'64bit': 'curl.exe.64', '32bit': 'curl.exe.32'}, + curl_executable_dest='..\\bin\\curl.exe', + ), +] + +bakery_version = '20220410.v0.0.7' + + +def get_curl_files(conf) -> FileGenerator: + field_separator: str = '|' # needs matching separator in the shell scripts + + # catch pre 20220410 WATO format + options: Dict[str, Any] = conf[1].copy() if type(conf) == tuple else conf + + url_cfg_lines = [] + url_list = options['url_list'] + default_settings = options.get('default_settings', {}) + + interval = options['interval'] * 60 if options.get('interval') else None + timeout = options['timeout'] * 60 if options.get('timeout') else None + + if options.get('curl_executable'): + for curl_config in CURL_CONFIGS: + yield Plugin( + base_os=curl_config.base_os, + source=Path(curl_config.curl_executable_src[str(options['curl_executable'])]), + target=Path(curl_config.curl_executable_dest), + ) + yield Plugin(base_os=curl_config.base_os, source=Path('curl-ca-bundle.crt')) + + curl_item = 0 + for entry in url_list: + curl_item += 1 + regex_option = 'no_regex' + save_output = False + # get service name and url, first try new format, then old format + try: + service_name, url = entry['curl_service'] + except KeyError: + service_name = entry['service_name'] + url = entry['url'] + + url_settings = default_settings.copy() + + entry = entry.get('url_settings', {}).copy() + # merge subdirectories + for key in [ + 'ftp_settings', + 'ip_address_resolution', + 'limits', + 'mail_settings', + ]: + if (key in url_settings.keys()) and (key in entry.keys()): + url_settings[key].update(entry[key]) + entry.pop(key) + elif key in entry.keys(): + url_settings.update({key: entry[key]}) + entry.pop(key) + + # merge sub directory with url_settings for back ward compatibility + for key in [ + 'limits' + ]: + if key in url_settings.keys(): + url_settings.update(url_settings[key]) + url_settings.pop(key) + + # merge per url settings with default settings + url_settings.update(entry) + + for curl_config in CURL_CONFIGS: + _os = curl_config.base_os + _curl_output = curl_config.curl_output + _temp_path = curl_config.temp_path + _conf_path = curl_config.conf_path + + _options = [f'--url "{url}"'] + _headers = [] + + # filter options + _options.append(url_settings['get_header_only']) if url_settings.get('get_header_only') else None + _options.append(url_settings['compressed']) if url_settings.get('compressed') else None + _options.append(f'--max-time {url_settings["max_time"]}') if url_settings.get('max_time') else None + _options.append(f'--speed-time {url_settings["speed_time"]}') if url_settings.get('speed_time') else None + _options.append(f'--connect-timeout {url_settings["connect_timeout"]}') if url_settings.get('connect_timeout') else None + _options.append(f'--user-agent "{url_settings["user_agent"]}"') if url_settings.get('user_agent') else None + _options.append(f'--referer {url_settings["referer"]}') if url_settings.get('referer') else None + _options.append(f'{url_settings["tls_ssl_version"]}') if url_settings.get('tls_ssl_version') else None + _options.append(f'{url_settings["http_version"]}') if url_settings.get('http_version') else None + + if url_settings.get('max_file_size'): + max_size, unit = url_settings['max_file_size'] + _options.append(f'--max-filesize {max_size}{unit}') + + if url_settings.get('speed_limit'): + speed, unit = url_settings['speed_limit'] + _options.append(f'--speed-limit {speed * unit}') + + if url_settings.get('limit_rate'): + speed, unit = url_settings['limit_rate'] + _options.append(f'--limit-rate {speed}{unit}') + + if url_settings.get('http_proxy'): + if url_settings['http_proxy'] == '--noproxy': + _options.append("--noproxy '*'") + else: + proxy_protocol, proxy_server, proxy_port = url_settings['http_proxy']['proxy_server'] + _options.append(f'{proxy_protocol} {proxy_server}:{proxy_port}') + if url_settings['http_proxy'].get('proxy_auth'): + proxy_user, proxy_password, proxy_auth = url_settings['http_proxy']['proxy_auth'] + if proxy_password[0] == 'store': + pw = password_store.extract(proxy_password[1]) + else: + pw = proxy_password[1] + _options.append(f'--proxy-user {proxy_user}:{pw}') + _options.append(proxy_auth) + + if url_settings.get('request_headers'): + for header in url_settings['request_headers']: + key, value = header + _headers.append(f'{key}:{value}') + + if url_settings.get('api_key_header'): + api_header, api_key = url_settings['api_key_header'] + api_header = api_header.rstrip(':') + if api_key[0] == 'store': + api_key = password_store.extract(api_key[1]) + else: + api_key = api_key[1] + _headers.append(f'{api_header}:{api_key}') + + if url_settings.get('redirects'): + location, location_trusted, max_redirects = url_settings['redirects'] + _options.append(f'--location') if location else None + _options.append(f'--location-trusted') if location_trusted else None + _options.append(f'--max-redirs {max_redirects}') if max_redirects else None + + if url_settings.get('advanced_settings'): + allow_beast, cr2lf, no_apln, no_buffering, no_npn, no_sessionid, no_keepalive, \ + path_as_is, tcp_fastopen, tcp_nodelay = url_settings['advanced_settings'] + + _options.append(f'--ssl-allow-beast') if allow_beast else None + _options.append(f'--crlf') if cr2lf else None + _options.append(f'--no-alpn') if no_apln else None + _options.append(f'--no-buffer') if no_buffering else None + _options.append(f'--no-npn') if no_npn else None + _options.append(f'--no-sessionid') if no_sessionid else None + _options.append(f'--no-keepalive') if no_keepalive else None + _options.append(f'--path-as-is') if path_as_is else None + _options.append(f'--tcp-fastopen') if tcp_fastopen else None + _options.append(f'--tcp-nodelay') if tcp_nodelay else None + + if url_settings.get('ip_address_resolution'): + dns_options = url_settings['ip_address_resolution'] + _options.append(dns_options['dns_resolve_names']) if dns_options.get('dns_resolve_names') else None + _options.append(f'--dns-interface {dns_options["dns_source_interface"]}') if dns_options.get('dns_source_interface') else None + _options.append(f'--dns-ipv4-addr {dns_options["dns_source_ipv4"]}') if dns_options.get('dns_source_ipv4') else None + _options.append(f'--dns-ipv6-addr {dns_options["dns_source_ipv6"]}') if dns_options.get('dns_source_ipv6') else None + _options.append(f'--dns-servers {",".join(dns_options["dns_servers"])}') if dns_options.get('dns_servers') else None + + if url_settings.get('ftp_settings'): + ftp_options = url_settings['ftp_settings'] + _options.append(f'--compressed-ssh') if ftp_options.get('compressed_ssh') else None + _options.append(f'--list-only') if ftp_options.get('list_only') else None + _options.append(f'--use-ascii') if ftp_options.get('use_ascii') else None + _options.append(f'--ftp-create-dirs') if ftp_options.get('ftp_create_dirs') else None + _options.append(f'--ftp-account {ftp_options["ftp_account"]}') if ftp_options.get('ftp_account') else None + _options.append(f'--ftp-alternative-to-user {ftp_options["ftp_alternate_to_user"]}') if ftp_options.get('ftp_alternate_to_user') else None + _options.append(f'--ftp-method {ftp_options["ftp_change_cwd_method"]}') if ftp_options.get('ftp_change_cwd_method') else None + + if ftp_options.get('ftp_mode'): + ftp_mode = ftp_options['ftp_mode'] + if ftp_mode[0] == 'ftp_pass': + no_send_epsv, send_pret, skip_ip = ftp_mode[1] + _options.append(f'--ftp-pasv') + _options.append(f'--disable-epsv') if no_send_epsv else None + _options.append(f'--ftp-pret') if send_pret else None + _options.append(f'--ftp-skip-pasv-ip') if skip_ip else None + else: + no_send_eprt, active_address = ftp_options[1] + _options.append(f'--disable-eprt') if no_send_eprt else None + _options.append(f'--ftp-port {active_address}') if active_address else None + if ftp_options.get('ftp_ssl_control'): + _options.append(f'--ftp-ssl-control') + ftp_ssl_ccc, ftp_ssl_ccc_mode = ftp_options['ftp_ssl_control'] + if ftp_ssl_ccc: + _options.append(f'--ftp-ssl-ccc') + _options.append(f'--ftp-ssl-ccc-mode {ftp_ssl_ccc_mode}') + + if url_settings.get('user_auth'): + if url_settings['user_auth'][0] == 'user_auth': + user, user_password, user_auth = url_settings['user_auth'][1] + pw = password_store.extract(user_password[1]) if user_password[0] == 'store' else user_password[1] + _options.append(f'--user {user}:{pw}') + _options.append(user_auth) + elif url_settings['user_auth'][0] == 'priv_key_auth': + user, pass_phrase, priv_key = url_settings['user_auth'][1] + pw = password_store.extract(pass_phrase[1]) if pass_phrase[0] == 'store' else pass_phrase[1] + _options.append(f'--user {user}:') + _options.append(f'--key {_conf_path}curl/curl_item_{curl_item}.priv_key') + _options.append(f'--pass {pw}') + yield PluginConfig( + base_os=_os, + lines=[priv_key], + target=Path(f'curl/curl_item_{curl_item}.priv_key'), + include_header=False, + ) + + if url_settings.get('expected_strings'): + save_output = True + yield PluginConfig( + base_os=_os, + lines=url_settings['expected_strings'], + target=Path(f'curl/curl_item_{curl_item}.search_response'), + include_header=False, + ) + + if url_settings.get('header_strings'): + _options.append(f'--dump-header {_temp_path}curl_header') + yield PluginConfig( + base_os=_os, + lines=url_settings['header_strings'], + target=Path(f'curl/curl_item_{curl_item}.search_header'), + include_header=False, + ) + + if url_settings.get('cert_verify'): + pub_md5 = None + pub_sha256 = None + pub_key = None + try: # 2022-03-23: added ssh settings + insecure, ocsp, no_revoke, cert_chain = url_settings['cert_verify'] + except ValueError: # 2022-03-23: added ssh settings + insecure, ocsp, no_revoke, cert_chain, pub_md5, pub_sha256, pub_key = url_settings['cert_verify'] + + _options.append(f'--insecure') if insecure else None + _options.append(f'--cert-status') if ocsp else None + _options.append(f'--ssl-no-revoke') if no_revoke else None + _options.append(f'--hostpubmd5 {pub_md5}') if pub_md5 else None + _options.append(f'--hostpubsha256 {pub_sha256}') if pub_sha256 else None + + if cert_chain: + cert_chain = cert_chain[cert_chain.find('-----BEGIN CERTIFICATE-----'):] + yield PluginConfig( + base_os=_os, + lines=[cert_chain], + target=Path(f'curl/curl_item_{curl_item}.ca_cert'), + include_header=False, + ) + _options.append(f'--cacert {_conf_path}curl/curl_item_{curl_item}.ca_cert') + + if pub_key: + yield PluginConfig( + base_os=_os, + lines=[pub_key], + target=Path(f'curl/curl_item_{curl_item}.pub_key'), + include_header=False, + ) + _options.append(f'--pubkey {_conf_path}curl/curl_item_{curl_item}.pub_key') + + if url_settings.get('post_binary'): + _headers.append(f'content-type: {url_settings["post_binary"][0]}') + yield PluginConfig( + base_os=_os, + lines=[url_settings["post_binary"][1]], + target=Path(f'curl/curl_item_{curl_item}.post_binary'), + include_header=False, + ) + _options.append(f'--data-binary @{_conf_path}curl/curl_item_{curl_item}.post_binary') + + if url_settings.get('regex_response'): + regex_str, no_case, multi_line = url_settings['regex_response'] + if regex_str: + save_output = True + yield PluginConfig( + base_os=_os, + lines=[regex_str], + target=Path(f'curl/curl_item_{curl_item}.regex'), + include_header=False, + ) + regex_option = 'nocase' if no_case else 'case' + regex_option += '_multiline' if multi_line else '_nomultiline' + + if url_settings.get('mail_settings'): + mail_options = url_settings['mail_settings'] + + _options.append(f'--mail-from {mail_options["mail_from"]}') if mail_options.get('mail_from') else None + _options.append(f'--mail-auth {mail_options["mail_auth"]}') if mail_options.get('mail_auth') else None + _options.append(f'--request {mail_options["request"]}') if mail_options.get('request') else None + _options.append(f'--mail-rcpt-allowfails') if mail_options.get('mail_rpct_allowfail') else None + + if mail_options.get('mail_rcpt'): + for address in mail_options['mail_rcpt']: + _options.append(f'--mail-rcpt {address}') + + message = [] + message.append(mail_options['message']) if mail_options.get('message') else None + if mail_options.get('request_headers'): + for header, value in mail_options['request_headers']: + message.append(f'{header}: {value}') + if message: + _options.append(f'--upload-file {_conf_path}curl/curl_item_{curl_item}.message') + yield PluginConfig( + base_os=_os, + lines=message, + target=Path(f'curl/curl_item_{curl_item}.message'), + include_header=False, + ) + + _options.append(f'--output {_temp_path}curl_output') if save_output else _options.append(_curl_output) + + if _headers: + yield PluginConfig( + base_os=_os, + lines=_headers, + target=Path(f'curl/curl_item_{curl_item}.header'), + include_header=False, + ) + _options.append(f'--header @{_conf_path}curl/curl_item_{curl_item}.header') + + yield PluginConfig( + base_os=_os, + lines=_options, + target=Path(f'curl/curl_item_{curl_item}.options'), + include_header=True, + ) + + url_cfg_lines.append( + f'{service_name}{field_separator}' + f'curl_item_{curl_item}{field_separator}' + f'{regex_option}{field_separator}' + ) + + for curl_config in CURL_CONFIGS: + yield Plugin( + base_os=curl_config.base_os, + source=Path(curl_config.plugin_name), + interval=interval, + timeout=timeout + ) + yield PluginConfig( + base_os=curl_config.base_os, + lines=url_cfg_lines, + target=Path('curl.cfg'), + include_header=False + ) + + +register.bakery_plugin( + name='curl', + files_function=get_curl_files, +) diff --git a/mkp/curl-0.2.1-20231021.mkp b/mkp/curl-0.2.1-20231021.mkp index 6bc8509eab69b27e22d18886a67900e485b092b2..9cfd47c3df86d21918551f060dc0b7a256172062 100644 Binary files a/mkp/curl-0.2.1-20231021.mkp and b/mkp/curl-0.2.1-20231021.mkp differ diff --git a/packages/curl b/packages/curl new file mode 100644 index 0000000000000000000000000000000000000000..a429ea9d2ebf58e3276d8d5396a0419b65011c5c --- /dev/null +++ b/packages/curl @@ -0,0 +1,34 @@ +{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)', + 'description': 'cURL agent plugin\n' + '\n' + 'Monitor URLs from the perspective of your monitored hosts, ' + 'not the monitoring server ;-). \n' + '\n' + 'The plugin is based on a idea by Christian Wirtz ' + 'doc[at]snowheaven[dot]de and Ingo Hambrock.\n' + '\n' + 'This agent plugin for Linux and Windows is build around the ' + 'curl command line tool from https://curl.se.\n' + '\n' + 'The executable is not included in this package. The plugin ' + 'will use by default the system provided curl executable.\n' + '\n' + 'Deployment and configuration of the plugin is integrated in ' + 'the CMK bakery.\n' + '\n' + 'Note: this plugin needs at least curl version 7.70.0 from ' + 'April 29 2020 to work. You can use my CMK package ' + '"curl_executable.mkp" to deploy a compatible version.\n', + 'download_url': 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/curl', + 'files': {'agent_based': ['curl.py'], + 'agents': ['plugins/curl.sh', 'plugins/curl.ps1'], + 'gui': ['metrics/curl.py', + 'wato/check_parameters/curl.py', + 'wato/check_parameters/curl_bakery.py'], + 'lib': ['python3/cmk/base/cee/plugins/bakery/curl.py']}, + 'name': 'curl', + 'title': 'cURL agent plugin', + 'version': '0.2.1-20231021', + 'version.min_required': '2.2.0b1', + 'version.packaged': '2.2.0p11', + 'version.usable_until': '2.3.0b1'}