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