From 996fbb809c3f2f9f508e10ff1c76ea8d9fd5fa99 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Sat, 21 Oct 2023 19:33:31 +0200
Subject: [PATCH] update project

---
 agent_based/curl.py                           |  577 +++++++++
 agents/plugins/curl.ps1                       |  218 ++++
 agents/plugins/curl.sh                        |  219 ++++
 gui/metrics/curl.py                           |  210 ++++
 gui/wato/check_parameters/curl.py             |  280 +++++
 gui/wato/check_parameters/curl_bakery.py      | 1061 +++++++++++++++++
 .../cmk/base/cee/plugins/bakery/curl.py       |  453 +++++++
 mkp/curl-0.2.1-20231021.mkp                   |  Bin 26407 -> 26406 bytes
 packages/curl                                 |   34 +
 9 files changed, 3052 insertions(+)
 create mode 100644 agent_based/curl.py
 create mode 100755 agents/plugins/curl.ps1
 create mode 100755 agents/plugins/curl.sh
 create mode 100644 gui/metrics/curl.py
 create mode 100644 gui/wato/check_parameters/curl.py
 create mode 100644 gui/wato/check_parameters/curl_bakery.py
 create mode 100644 lib/python3/cmk/base/cee/plugins/bakery/curl.py
 create mode 100644 packages/curl

diff --git a/agent_based/curl.py b/agent_based/curl.py
new file mode 100644
index 0000000..8788157
--- /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 0000000..04727a2
--- /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 0000000..5189a27
--- /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 0000000..bf186c5
--- /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 0000000..e6c64b8
--- /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 0000000..3c7c37d
--- /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 0000000..e9cd95d
--- /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
GIT binary patch
delta 16267
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
Bs#5>}

delta 16334
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
Nw7nPv0xbgp`X8)H&@%u4

diff --git a/packages/curl b/packages/curl
new file mode 100644
index 0000000..a429ea9
--- /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'}
-- 
GitLab