diff --git a/README.md b/README.md
index c7d46b782436c22b69aa894710dab8d44ba2be88..67d51496e8801381758d25918796db628bca03eb 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[PACKAGE]: ../../raw/master/mkp/agent_ssllabs-2.0.2-20240105.mkp "agent_ssllabs-2.0.2-20240105.mkp"
 # Qualys SSL Labs REST API special agent
 
 **Note: this package is for CheckMK version 2.x.**
diff --git a/agent_based/ssllabs_grade.py b/agent_based/ssllabs_grade.py
deleted file mode 100644
index 1a5e11e390dfe58ac0a943bc923ad3de53a8c8c0..0000000000000000000000000000000000000000
--- a/agent_based/ssllabs_grade.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-#
-# License: GNU General Public License v2
-#
-# 2015 Karsten Schoeke karsten.schoeke@geobasis-bb.de
-#
-# 2021-05-15: rewritten for CMK 2.0 by thl-cmk[at]outlook[dot]com
-#
-#
-# Example output from agent:
-# servername;status;time;agent_state;last_grade_result
-# <<<ssllabs_grade:sep(0)>>>
-# server1.de;A+;1435565830118;0;A+
-# server2.de;A;1435565830118;0;B
-# <<<<>>>>
-
-
-import time, re
-
-from typing import Dict, NamedTuple, Optional
-
-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,
-    Result,
-    get_value_store,
-)
-
-
-class SSLLabsGrade(NamedTuple):
-    lastcheck: str
-    time_diff: int
-    grade: Optional[str]
-    agent_state: Optional[int]
-    status_detail: str
-
-
-def parse_ssllabs_grade(string_table):
-    ssl_hosts = {}
-
-    for line in string_table:
-        line = line[0].split(';')
-        if len(line) == 5:
-            ssl_host, status_detail, lastcheck, agent_state, grade = line
-            lastcheck = time.strftime("%d.%m.%Y %H:%M", time.localtime(int(lastcheck[:10])))
-            time_diff = int(time.time() - int(line[2][:10]))
-            ssl_hosts.update({ssl_host: SSLLabsGrade(
-                lastcheck=lastcheck,
-                time_diff=time_diff,
-                grade=grade,
-                agent_state=int(agent_state),
-                status_detail=status_detail
-            )})
-        if len(line) == 4:
-            ssl_host, status_detail, lastcheck, agent_state = line
-            lastcheck = time.strftime("%d.%m.%Y %H:%M", time.localtime(int(lastcheck[:10])))
-            time_diff = int(time.time() - int(line[2][:10]))
-            ssl_hosts.update({ssl_host: SSLLabsGrade(
-                lastcheck=lastcheck,
-                time_diff=time_diff,
-                grade=None,
-                agent_state=None,
-                status_detail=status_detail
-            )})
-
-    return ssl_hosts
-
-
-def discovery_ssllabs_grade(section: Dict) -> DiscoveryResult:
-    for ssl_host in section.keys():
-        yield Service(item=ssl_host)
-
-
-def check_ssllabs_grade(item, params, section: Dict[str, SSLLabsGrade]) -> CheckResult:
-    #value_store = get_value_store()
-    #print(f'value_store: {value_store}')
-    #if not value_store[item][0] == 'last_run':
-    #    value_store[item] = ('last_run', {'grade': 'A+'})
-    #grade = value_store[item][1].get('grade')
-    #print(f'value_store: {grade}')
-
-    try:
-        ssllabsgrade = section.get(item)
-    except KeyError:
-        return None
-
-    ok, warn, crit = params["score"]
-    warn_last_run, crit_last_run = params["age"]
-    re_ok = re.compile(ok)
-    re_warn = re.compile(warn)
-    re_crit = re.compile(crit)
-    re_error = re.compile('(HTTP|JSON|unknow)')  # API Errors
-
-    if ssllabsgrade.agent_state == 0:  # test done
-        if re_crit.match(ssllabsgrade.grade):
-            state = State.CRIT
-        elif re_warn.match(ssllabsgrade.grade):
-            state = State.WARN
-        elif re_ok.match(ssllabsgrade.grade):
-            state = State.OK
-        else:
-            state = State.UNKNOWN
-        yield Result(state=state, summary=f'Grade "{ssllabsgrade.status_detail}"')
-
-        if ssllabsgrade.time_diff > crit_last_run:
-            state = State.CRIT
-        elif ssllabsgrade.time_diff > warn_last_run:
-            state = State.WARN
-        else:
-            state = State.OK
-        yield Result(state=state, summary=f'Last check at {ssllabsgrade.lastcheck}')
-    elif ssllabsgrade.agent_state == 1:  # test in progress
-        state = State.WARN
-        yield Result(state=state, summary=f'Server check is in progress, status was "{ssllabsgrade.status_detail}"')
-    elif ssllabsgrade.agent_state == 2:  # API error
-        state = State.CRIT
-        yield Result(state=state, summary=f'API error, status was "{ssllabsgrade.status_detail}"')
-    else:  # unknown error
-        state = State.UNKNOWN
-        yield Result(state=state,
-                     summary=f'Server check status was "{ssllabsgrade.status_detail}", last check at {ssllabsgrade.lastcheck}')
-
-    yield Result(state=State.OK, notice=f'For details go to https://www.ssllabs.com/ssltest/analyze.html?d={item}')
-
-
-register.agent_section(
-    name="ssllabs_grade",
-    parse_function=parse_ssllabs_grade,
-)
-
-register.check_plugin(
-    name='ssllabs_grade',
-    service_name='SSL Labs %s',
-    discovery_function=discovery_ssllabs_grade,
-    check_function=check_ssllabs_grade,
-    check_default_parameters={
-        "score": ("A", "B|C", "D|E|F|M|T"),
-        "age": (604800, 864000),
-    },
-    check_ruleset_name='ssllabs_grade'
-)
diff --git a/agent_ssllabs.mkp b/agent_ssllabs.mkp
deleted file mode 100644
index eadc333bcafc203183b78d15dee5c9f2b2fbf0f1..0000000000000000000000000000000000000000
Binary files a/agent_ssllabs.mkp and /dev/null differ
diff --git a/checkman/agent_ssllabs b/checkman/agent_ssllabs
deleted file mode 100644
index 08ef898bcf6eb7a7979de60090d54f9719bbbc5b..0000000000000000000000000000000000000000
--- a/checkman/agent_ssllabs
+++ /dev/null
@@ -1,45 +0,0 @@
-title: Dummy check man page - used as template for new check manuals
-agents: linux, windows, aix, solaris, hpux, vms, freebsd, snmp
-catalog: see modules/catalog.py for possible values
-license: GPL
-distribution: check_mk
-description:
- Describe here: (1) what the check actually does, (2) under which
- circumstances it goes warning/critical, (3) which devices are supported
- by the check, (4) if the check requires a separated plugin or
- tool or separate configuration on the target host.
-
-item:
- Describe the syntax and meaning of the check's item here. Provide all
- information one needs if coding a manual check with {checks +=} in {main.mk}.
- Give an example.  If the check uses {None} as sole item,
- then leave out this section.
-
-examples:
- # Give examples for configuration in {main.mk} here. If the check has
- # configuration variable, then give example for them here.
-
- # set default levels to 40 and 60 percent:
- foo_default_values = (40, 60)
-
- # another configuration variable here:
- inventory_foo_filter = [ "superfoo", "superfoo2" ]
-
-perfdata:
- Describe precisely the number and meaning of performance variables
- the check sends. If it outputs no performance data, then leave out this
- section.
-
-inventory:
- Describe how the inventory for the check works. Which items
- will it find? Describe the influence of check specific
- configuration parameters to the inventory.
-
-[parameters]
-foofirst(int): describe the first parameter here (if parameters are grouped
-        as tuple)
-fooother(string): describe another parameter here.
-
-[configuration]
-foo_default_levels(int, int): Describe global configuration variable of
-    foo here. Important: also tell the user how they are preset.
diff --git a/checks/agent_ssllabs b/checks/agent_ssllabs
deleted file mode 100644
index 325b21ea85348ea4842264e40cef9d549b9f5853..0000000000000000000000000000000000000000
--- a/checks/agent_ssllabs
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# {
-#     'sslhosts': ['ssl.test.com', 'sslserver2.test.com']
-#     'timeout': '180',
-# }
-
-# 'sslhost', timeout
-
-def agent_ssllabs_arguments(params, _hostname, _ipaddress):
-    args = []
-
-    if 'sslhosts' in params:
-        args += ['--sslhosts', ','.join(params['sslhosts'])]
-
-    if 'timeout' in params:
-        args += ['--timeout', params['timeout']]
-
-    if 'proxy' in params:
-        args += ['--proxy', params['proxy']]
-
-    if 'publishresults' in params:
-        args += ['--publish', params['publishresults']]
-
-    if 'maxage' in params:
-        args += ['--maxage', params['maxage']]
-
-    return args
-
-
-special_agent_info['ssllabs'] = agent_ssllabs_arguments
diff --git a/doc/.gitkeep b/img/.gitkeep
similarity index 100%
rename from doc/.gitkeep
rename to img/.gitkeep
diff --git a/doc/sample.png b/img/sample.png
similarity index 100%
rename from doc/sample.png
rename to img/sample.png
diff --git a/doc/wato-options-agent.png b/img/wato-options-agent.png
similarity index 100%
rename from doc/wato-options-agent.png
rename to img/wato-options-agent.png
diff --git a/doc/wato-options.png b/img/wato-options.png
similarity index 100%
rename from doc/wato-options.png
rename to img/wato-options.png
diff --git a/lib/check_mk/special_agent/agent_ssllabs.py b/lib/check_mk/special_agent/agent_ssllabs.py
deleted file mode 100755
index a9bee538f97a0c86ffd4ae85d21ebf1ce6e8f8eb..0000000000000000000000000000000000000000
--- a/lib/check_mk/special_agent/agent_ssllabs.py
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-#
-# Karsten Schoeke <karsten.schoeke@geobasis-bb.de>
-#
-# 2021-05-15: rewritten for CMK 2.0 by thl-cmk[at]outlook[dot]com
-#             changed cache file name form host_address to ssl_host_address
-# 2021-05-16: changed arguments to argparse
-#             added options for publish results and max cache age
-#
-# check_mk is free software;  you can redistribute it and/or modify it
-# under the  terms of the  GNU General Public License  as published by
-# the Free Software Foundation in version 2.  check_mk is  distributed
-# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
-# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
-# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
-# ails.  You should have  received  a copy of the  GNU  General Public
-# License along with GNU Make; see the file  COPYING.  If  not,  write
-# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
-# Boston, MA 02110-1301 USA.
-
-import argparse
-import json
-import os
-import requests
-import sys
-import time
-from typing import Optional, Sequence
-
-from cmk.utils.paths import tmp_dir
-
-
-def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
-    ''''Parse arguments needed to construct an URL and for connection conditions'''
-    parser = argparse.ArgumentParser()
-    parser.add_argument('--sslhosts', required=True, type=str, help='Comma separated list of FQDNs to test for')
-    parser.add_argument('--proxy', required=False, help='URL to HTTPS Proxy i.e.: https://192.168.1.1:3128')
-    parser.add_argument('--timeout', '-t', type=float, default=60, help='API call timeout in seconds', )
-    parser.add_argument('--publish', type=str, default='off', help='Publish test results on ssllabs.com', choices=['on', 'off'] )
-    parser.add_argument('--maxage', type=int, default=167, help='Maximum report age, in hours, if retrieving from "ssllabs.com" cache', )
-
-    return parser.parse_args(argv)
-
-
-def connect_ssllabs_api(ssl_host_address: str, host_cache: str, args: argparse.Namespace, ):
-    server = 'api.ssllabs.com'
-    uri = 'api/v3/analyze'
-    maxAge = args.maxage  # default 167 (1 week minus 1 hour)
-    publish = args.publish  # default off
-    fromCache = 'on'
-    now = time.time()
-
-    # url for request webservice (&startNew={startNew}&all={all})
-    url = f'https://{server}/{uri}?host={ssl_host_address}&publish={publish}&fromCache={fromCache}&maxAge={maxAge}'
-    proxies = {}
-    if args.proxy is not None:
-        proxies = {'https': args.proxy.split('/')[-1]}  # remove 'https://' from proxy string
-
-    try:
-        response = requests.get(
-            url=url,
-            timeout=args.timeout,
-            proxies=proxies,
-        )
-
-        jsonData = json.loads(response.text)
-
-    except Exception as err:
-        sys.stdout.write(f'{ssl_host_address} Connection error: {err} on {url}')
-        # print(f'{ssl_host_address};{err};{now};2')
-        return
-
-    print(response.text)
-
-    try:
-        if jsonData['status'] == 'READY':
-            # if test finish and json data ok --> write data in cache file
-            with open(host_cache, 'w') as outfile:
-                json.dump(jsonData, outfile)
-
-    except (ValueError, KeyError, TypeError):
-        print(f'{ssl_host_address};request JSON format error;{now};2')   # ;{grade_cache}
-
-
-def read_cache(host_cache: str):
-    # read cache file
-    with open(host_cache) as json_file:
-        # check if cache file contains valid json data
-        try:
-            jsonData = json.load(json_file)
-            if jsonData['status'] == 'READY':
-                print(json.dumps(jsonData))
-        except (ValueError, KeyError, TypeError):
-            # print(f'{ssl_host_address};cache JSON format error;{now};2;{grade_cache}')
-            return
-
-
-def main(argv: Optional[Sequence[str]] = None) -> None:
-    args = parse_arguments(argv)
-
-    VERSION = '2.0.1'
-    now = time.time()
-    cache_dir = tmp_dir + '/agents/agent_ssllabs'
-    cache_age = args.maxage
-    ssl_hosts = args.sslhosts.split(',')
-
-    # Output general information about the agent
-    sys.stdout.write('<<<check_mk>>>\n')
-    sys.stdout.write('Version: %s\n' % VERSION)
-    sys.stdout.write('AgentOS: linux\n')
-
-    # create cache directory, if not exists
-    if not os.path.exists(cache_dir):
-        os.makedirs(cache_dir)
-
-    print('<<<ssllabs_grade:sep(0)>>>')
-
-    for ssl_host_address in ssl_hosts:
-        # changed cache file form host_address to ssl_host_address
-        host_cache = '%s/%s' % (cache_dir, ssl_host_address)
-
-        # check if cache file exists and is not older as cache_age
-        if not os.path.exists(host_cache):
-            connect_ssllabs_api(
-                ssl_host_address=ssl_host_address,
-                host_cache=host_cache,
-                args=args)
-        else:
-            json_cache_file_age = os.path.getmtime(host_cache)
-            cache_age_sec = now - json_cache_file_age
-            if cache_age_sec < cache_age:
-                read_cache(
-                    host_cache=host_cache,
-                            )
-            else:
-                connect_ssllabs_api(
-                    ssl_host_address=ssl_host_address,
-                    host_cache=host_cache,
-                    args=args
-                )
-
-
-if __name__ == '__main__':
-    main()
diff --git a/mkp/agent_ssllabs-2.0.2-20240105.mkp b/mkp/agent_ssllabs-2.0.2-20240105.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..3f792b1793e82d82e5f993b29dcf8cf559bd0a40
Binary files /dev/null and b/mkp/agent_ssllabs-2.0.2-20240105.mkp differ
diff --git a/source/agent_based/ssllabs_grade.py b/source/agent_based/ssllabs_grade.py
new file mode 100644
index 0000000000000000000000000000000000000000..f2985459db5541d6c6d3428ffe8322cbbda3a771
--- /dev/null
+++ b/source/agent_based/ssllabs_grade.py
@@ -0,0 +1,412 @@
+#!/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  : 2024-04-29
+# File  : ssllabs_grade.py (check plugin)
+
+# based on the ssllabs plugin from Karsten Schoeke karsten.schoeke@geobasis-bb.de
+# see https://exchange.checkmk.com/p/ssllabs
+
+# 2021-05-15: rewritten for CMK 2.0 by thl-cmk[at]outlook[dot]com
+#             moved to ~/local/lib/check_mk/base/plugins/agent_based
+
+
+# sample string_table:
+# [
+#     {
+#         "host": "thl-cmk.hopto.org",
+#         "port": 443,
+#         "protocol": "http",
+#         "isPublic": false,
+#         "status": "READY",
+#         "startTime": 1714559152230,
+#         "testTime": 1714559237958,
+#         "engineVersion": "2.3.0",
+#         "criteriaVersion": "2009q",
+#         "endpoints": [
+#             {
+#                 "ipAddress": "91.4.75.201",
+#                 "serverName": "p5b044bc9.dip0.t-ipconnect.de",
+#                 "statusMessage": "Ready",
+#                 "grade": "A+",
+#                 "gradeTrustIgnored": "A+",
+#                 "hasWarnings": false,
+#                 "isExceptional": true,
+#                 "progress": 100,
+#                 "duration": 85530,
+#                 "delegation": 1
+#             }
+#         ]
+#     },
+#     {
+#         "host": "checkmk.com",
+#         "port": 443,
+#         "protocol": "http",
+#         "isPublic": false,
+#         "status": "IN_PROGRESS",
+#         "startTime": 1714563744895,
+#         "engineVersion": "2.3.0",
+#         "criteriaVersion": "2009q",
+#         "endpoints": [
+#             {
+#                 "ipAddress": "2a0a:51c1:0:5:0:0:0:4",
+#                 "serverName": "www.checkmk.com",
+#                 "statusMessage": "Ready",
+#                 "grade": "A+",
+#                 "gradeTrustIgnored": "A+",
+#                 "hasWarnings": false,
+#                 "isExceptional": true,
+#                 "progress": 100,
+#                 "duration": 72254,
+#                 "delegation": 1
+#             },
+#             {
+#                 "ipAddress": "45.133.11.28",
+#                 "serverName": "www.checkmk.com",
+#                 "statusMessage": "In progress",
+#                 "statusDetails": "TESTING_SESSION_RESUMPTION",
+#                 "statusDetailsMessage": "Testing session resumption", "delegation": 1
+#             }
+#         ]
+#     }
+# ]
+#
+
+
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+from json import loads as json_loads, JSONDecodeError
+from typing import Tuple
+from re import compile as re_compile, match as re_match
+from time import localtime, time as now_time, strftime
+
+from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
+    CheckResult,
+    DiscoveryResult,
+)
+
+from cmk.base.plugins.agent_based.agent_based_api.v1 import (
+    Result,
+    Service,
+    State,
+    check_levels,
+    register,
+    render,
+    get_value_store,
+)
+
+
+def get_str(field: str, data: Mapping[str: object]) -> str | None:
+    return str(data[field]) if data.get(field) is not None else None
+
+
+def get_bool(field: str, data: Mapping[str: object]) -> bool | None:
+    return bool(data[field]) if data.get(field) is not None else None
+
+
+def get_int(field: str, data: Mapping[str: object]) -> bool | None:
+    return int(data[field]) if data.get(field) is not None else None
+
+
+@dataclass(frozen=True)
+class SSLLabsEndpoint:
+    ip_address: str | None
+    server_name: str | None
+    status_message: str | None
+    grade: str | None
+    grade_trust_ignored: str | None
+    has_warnings: bool | None
+    is_exceptional: bool | None
+    progress: int | None
+    duration: int | None
+    delegation: int | None
+    statusDetails: str | None
+    statusDetailsMessage: str | None
+
+    @classmethod
+    def parse(cls, end_point: Mapping):
+        return cls(
+            ip_address=get_str('ipAddress', end_point),
+            server_name=get_str('serverName', end_point),
+            status_message=get_str('statusMessage', end_point),
+            grade=get_str('grade', end_point),
+            grade_trust_ignored=get_str('gradeTrustIgnored', end_point),
+            has_warnings=get_bool('hasWarnings', end_point),
+            is_exceptional=get_bool('isExceptional', end_point),
+            progress=get_int('progress', end_point),
+            duration=get_int('duration', end_point),
+            delegation=get_int('delegation', end_point),
+            statusDetails=get_str('statusDetails', end_point),
+            statusDetailsMessage=get_str('statusDetailsMessage', end_point),
+        )
+
+
+@dataclass(frozen=True)
+class SSLLabsHost:
+    host: str
+    port: int
+    protocol: str
+    is_public: bool
+    status: str
+    start_time: int
+    test_time: int | None
+    engine_version: str
+    criteria_version: str
+    status_message: str | None
+    cache_expiry_time: int | None
+    from_agent_cache: bool | None
+    end_points: Sequence[SSLLabsEndpoint]
+
+    @classmethod
+    def parse(cls, ssl_host):
+        return cls(
+            host=get_str('host', ssl_host),
+            port=get_int('port', ssl_host),
+            protocol=get_str('protocol', ssl_host),
+            is_public=get_bool('isPublic', ssl_host),
+            status=get_str('status', ssl_host),
+            start_time=get_int('startTime', ssl_host),
+            test_time=get_int('testTime', ssl_host),
+            engine_version=get_str('engineVersion', ssl_host),
+            criteria_version=get_str('criteriaVersion', ssl_host),
+            status_message=get_str('statusMessage', ssl_host),
+            cache_expiry_time=get_int('cacheExpiryTime', ssl_host),
+            from_agent_cache=get_bool('from_agent_cache', ssl_host),
+            end_points=[SSLLabsEndpoint.parse(endpoint) for endpoint in ssl_host.get('endpoints', [])]
+        )
+
+
+SECTION = Mapping[str: SSLLabsHost]
+
+
+# _CMK_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%m %Z'
+
+
+def parse_ssllabs_grade(string_table) -> SECTION | None:
+    try:
+        data = json_loads(string_table[0][0])
+    except JSONDecodeError:
+        return
+
+    ssl_hosts = {host['host']: SSLLabsHost.parse(host) for host in data if host.get('host') is not None}
+    return ssl_hosts
+
+
+def discovery_ssllabs_grade(section: SECTION) -> DiscoveryResult:
+    for ssl_host in section:
+        yield Service(item=ssl_host)
+
+
+def collect_has_warnings(end_points: Sequence[SSLLabsEndpoint]) -> Sequence[bool]:
+    return list(set([end_point.has_warnings for end_point in end_points if end_point.has_warnings is not None]))
+
+
+def collect_is_exceptional(end_points: Sequence[SSLLabsEndpoint]) -> Sequence[bool]:
+    return list(set([end_point.is_exceptional for end_point in end_points if end_point.is_exceptional is not None]))
+
+
+def check_grade(score: Tuple, grade: str, name: str, notice_only: bool) -> Result:
+    re_ok = re_compile(score[0])
+    re_warn = re_compile(score[1])
+    re_crit = re_compile(score[2])
+
+    if re_match(re_ok, grade):
+        state = State.OK
+    elif re_match(re_warn, grade):
+        state = State.WARN
+    elif re_match(re_crit, grade):
+        state = State.CRIT
+    else:
+        state = State.UNKNOWN
+
+    message = f'{name} Grade: {grade}'.strip()
+    if notice_only:
+        yield Result(state=state, notice=message)
+    else:
+        yield Result(state=state, summary=message)
+
+
+def check_grades(params: Mapping[str: any], ssl_host: SSLLabsHost, value_store):
+    grades = list(set([end_point.grade for end_point in ssl_host.end_points if end_point.grade is not None]))
+    if len(grades) == 1:
+        yield from check_grade(score=params['score'], name='', grade=grades[0], notice_only=False)
+        if (last_grade := value_store.get(ssl_host.host)) is not None:
+            yield Result(state=State.OK, summary=f'Last grade: {last_grade}')
+        value_store[ssl_host.host] = grades[0]
+    elif len(grades) == 0:
+        yield Result(state=State(params.get('no_grade', 1)), notice=f'No grade information found')
+    else:
+        end_points: Sequence[SSLLabsEndpoint] = ssl_host.end_points
+        for end_point in end_points:
+            name = f'{end_point.server_name}/{end_point.ip_address}'
+            last_grade = value_store.get(name)
+            if end_point.grade is not None:
+                yield from check_grade(
+                    score=params['score'],
+                    name=name,
+                    grade=end_point.grade,
+                    notice_only=True,
+                )
+                yield Result(state=State.OK, notice=f'{name} last grade: {last_grade}')
+                value_store[name] = end_point.grade
+            elif last_grade is not None:
+                yield from check_grade(
+                    score=params['score'],
+                    name=f'{name} Last',
+                    grade=last_grade,
+                    notice_only=True,
+                )
+
+
+def check_has_warning(params: Mapping[str: any], end_points: Sequence[SSLLabsEndpoint]):
+    has_warnings = list(set([
+        end_point.has_warnings for end_point in end_points if end_point.has_warnings is not None
+    ]))
+    if len(has_warnings) == 1 and has_warnings[0] is True:
+        yield Result(state=State(params.get('has_warnings', 1)), notice=f'Has warnings')
+    else:
+        for end_point in end_points:
+            name = f'{end_point.server_name}/{end_point.ip_address}'
+            if end_point.has_warnings is True:
+                yield Result(state=State(params.get('has_warnings', 1)), notice=f'{name}: has warnings')
+
+
+def check_is_exceptional(params: Mapping[str: any], end_points: Sequence[SSLLabsEndpoint]):
+    is_exceptional = list(set([
+        end_point.is_exceptional for end_point in end_points if end_point.is_exceptional is not None
+    ]))
+    if len(is_exceptional) == 1 and is_exceptional[0] is not True:
+        yield Result(state=State(params.get('is_exceptional', 1)), notice=f'Is not exceptional')
+    else:
+        for end_point in end_points:
+            name = f'{end_point.server_name}/{end_point.ip_address}'
+            if end_point.has_warnings is True:
+                yield Result(state=State(params.get('is_exceptional', 1)), notice=f'{name}: is not exceptional')
+
+
+def check_status(params: Mapping[str: any], end_points: Sequence[SSLLabsEndpoint]):
+    for end_point in end_points:
+        name = f'{end_point.server_name}/{end_point.ip_address}'
+
+        if end_point.status_message.lower() not in ['ready', 'in progress']:
+            yield Result(state=State.WARN, notice=f'Status {name}: {end_point.status_message}')
+
+
+def check_ssllabs_grade(item: str, params: Mapping[str: any], section: SECTION) -> CheckResult:
+    try:
+        ssl_host: SSLLabsHost = section[item]
+    except KeyError:
+        return None
+
+    value_store = get_value_store()
+
+    match ssl_host.status:
+        case 'READY':
+            levels_upper = None
+            if params.get('age') is not None:
+                warn, crit = params.get('age')
+                levels_upper = (warn * 86400, crit * 86400)  # change to days
+
+            yield from check_levels(
+                value=now_time() - (ssl_host.test_time / 1000),
+                label='Last tested',
+                render_func=render.timespan,
+                levels_upper=levels_upper,
+                # notice_only=True,
+            )
+            yield from check_grades(params, ssl_host, value_store)
+            yield from check_has_warning(params, ssl_host.end_points)
+            yield from check_is_exceptional(params, ssl_host.end_points)
+            yield from check_status(params, ssl_host.end_points)
+
+        case 'DNS':
+            yield Result(state=State(params.get('state_dns', 0)), summary=f'DNS: {ssl_host.status_message}')
+            yield Result(
+                state=State.OK,
+                summary=f'Started {render.timespan(now_time() - (ssl_host.start_time / 1000))} before'
+            )
+        case 'ERROR':
+            yield Result(state=State(params.get('state_error'), 1), notice=f'Error: {ssl_host.status_message}')
+            if ssl_host.cache_expiry_time:
+                yield Result(
+                    state=State.OK,
+                    notice=f'Cache expiry time: {render.datetime(ssl_host.cache_expiry_time / 1000)}'
+                )
+        case 'IN_PROGRESS':
+            yield Result(
+                state=State(params.get('state_in_progress', 0)),
+                summary=f'Test is in progress, started '
+                        f'{render.timespan(now_time() - (ssl_host.start_time / 1000))} before'
+            )
+            yield from check_grades(params, ssl_host, value_store)
+            yield from check_has_warning(params, ssl_host.end_points)
+            yield from check_is_exceptional(params, ssl_host.end_points)
+            yield from check_status(params, ssl_host.end_points)
+        case _:
+            yield Result(state=State.UNKNOWN, notice=f'Unknown test status: {ssl_host.status}')
+
+    yield Result(state=State.OK, notice=f'For full details go to https://www.ssllabs.com/ssltest/analyze.html?d={item}')
+
+    if params.get('details'):
+        yield Result(state=State.OK, notice=f'\nHost details')
+        yield Result(state=State.OK, notice=f'Host: {ssl_host.host}')
+        yield Result(state=State.OK, notice=f'Port: {ssl_host.port}')
+        yield Result(state=State.OK, notice=f'Protocol: {ssl_host.protocol}')
+        yield Result(state=State.OK, notice=f'Start Time: {render.datetime(ssl_host.start_time / 1000)}')
+        if ssl_host.test_time is not None:
+            yield Result(state=State.OK, notice=f'Test Time: {render.datetime(ssl_host.test_time / 1000)}')
+        yield Result(state=State.OK, notice=f'Engine version: {ssl_host.engine_version}')
+        yield Result(state=State.OK, notice=f'Criteria version: {ssl_host.criteria_version}')
+        yield Result(state=State.OK, notice=f'Status: {ssl_host.status}')
+        if ssl_host.from_agent_cache is not None:
+            yield Result(state=State.OK, notice=f'From agent cache: {ssl_host.from_agent_cache}')
+        else:
+            yield Result(state=State.WARN, notice=f'Live data')
+
+        if ssl_host.end_points:
+            yield Result(state=State.OK, notice=f'\nEndpoints')
+        for end_point in ssl_host.end_points:
+            yield Result(state=State.OK, notice=f'Server name: {end_point.server_name}')
+            yield Result(state=State.OK, notice=f'IP-Address: {end_point.ip_address}')
+            yield Result(state=State.OK, notice=f'Status Message: {end_point.status_message}')
+            if end_point.grade is not None:
+                yield Result(state=State.OK, notice=f'Grade: {end_point.grade}')
+
+            name = f'{end_point.server_name}/{end_point.ip_address}'
+            if (last_grade := value_store.get(name)) is not None:
+                yield Result(state=State.OK, notice=f'Last grade: {last_grade}')
+
+            if end_point.grade_trust_ignored is not None:
+                yield Result(state=State.OK, notice=f'Grade Trust Ignored: {end_point.grade_trust_ignored}')
+            if end_point.has_warnings is not None:
+                yield Result(state=State.OK, notice=f'has warnings: {end_point.has_warnings}')
+            if end_point.is_exceptional is not None:
+                yield Result(state=State.OK, notice=f'is exceptional: {end_point.is_exceptional}')
+            if end_point.progress is not None:
+                yield Result(state=State.OK, notice=f'progress: {end_point.progress}')
+            if end_point.duration is not None:
+                yield Result(state=State.OK, notice=f'duration: {render.timespan(end_point.duration / 1000)}s')
+            yield Result(state=State.OK, notice=f'delegation: {end_point.delegation}')
+            yield Result(state=State.OK, notice=f'\n')
+
+
+register.agent_section(
+    name="ssllabs_grade",
+    parse_function=parse_ssllabs_grade,
+)
+
+register.check_plugin(
+    name='ssllabs_grade',
+    service_name='SSL Labs %s',
+    discovery_function=discovery_ssllabs_grade,
+    check_function=check_ssllabs_grade,
+    check_default_parameters={
+        "score": ("A", "B|C", "D|E|F|M|T"),
+    },
+    check_ruleset_name='ssllabs_grade'
+)
diff --git a/agents/special/agent_ssllabs b/source/agents/special/agent_ssllabs
similarity index 90%
rename from agents/special/agent_ssllabs
rename to source/agents/special/agent_ssllabs
index c28031c21a3972d6d4cb9584627d2e501dfed988..d1dc7252b84697634f648041a4e8c6fadb5574f9 100755
--- a/agents/special/agent_ssllabs
+++ b/source/agents/special/agent_ssllabs
@@ -8,7 +8,7 @@
 # if the file in special_agents it will not work, don't now why, it looks in the wrong directory
 #
 # from cmk.special_agents.agent_cisco_ise import main
-from cmk.special_agent.agent_ssllabs import main
+from cmk.special_agents.agent_ssllabs import main
 
 if __name__ == '__main__':
     main()
\ No newline at end of file
diff --git a/checkman/ssllabs_grade b/source/checkman/ssllabs_grade
similarity index 100%
rename from checkman/ssllabs_grade
rename to source/checkman/ssllabs_grade
diff --git a/source/checks/agent_ssllabs b/source/checks/agent_ssllabs
new file mode 100644
index 0000000000000000000000000000000000000000..57e1c5dcc237062abb1bcb50bd71e8c022b6571c
--- /dev/null
+++ b/source/checks/agent_ssllabs
@@ -0,0 +1,37 @@
+#!/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  : 2024-04-29
+# File  : agent_ssllabs.py (params stub)
+#
+
+# based on the ssllabs plugin from Karsten Schoeke karsten.schoeke@geobasis-bb.de
+# see https://exchange.checkmk.com/p/ssllabs
+
+def agent_ssllabs_arguments(params, _hostname, _ipaddress):
+    args = []
+
+    if (ssl_hosts := params.get('ssl_hosts')) is not None:
+        args += ['--ssl-hosts', ','.join(ssl_hosts)]
+
+    if (timeout := params.get('timeout')) is not None:
+        args += ['--timeout', timeout]
+
+    if (proxy := params.get('proxy')) is not None:
+        args += ['--proxy', proxy]
+
+    if (publish_results := params.get('publish_results')) is not None:
+        args += ['--publish', publish_results]
+
+    if (max_age := params.get('max_age')) is not None:
+        args += ['--max-age', max_age]
+
+    return args
+
+
+special_agent_info['ssllabs'] = agent_ssllabs_arguments
diff --git a/source/gui/wato/check_parameters/ssllabs_grade.py b/source/gui/wato/check_parameters/ssllabs_grade.py
new file mode 100644
index 0000000000000000000000000000000000000000..67406f028739219e41db3591a825f2eee2f6e73e
--- /dev/null
+++ b/source/gui/wato/check_parameters/ssllabs_grade.py
@@ -0,0 +1,126 @@
+#!/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  : 2024-04-29
+# File  : ssllabs_grade.py (wato check plugin)
+
+# based on the ssllabs plugin from Karsten Schoeke karsten.schoeke@geobasis-bb.de
+# see https://exchange.checkmk.com/p/ssllabs
+
+#
+# 2021-05-15: rewritten for CMK 2.0 by thl-cmk[at]outlook[dot]com
+# 2024-05-01: modified for CMK 2.2.x
+#             moved to ~/local/lib/check_mk/gui/plugins/wato/check_parameters
+# 2024-05-01: changed age to days
+
+from cmk.gui.i18n import _
+from cmk.gui.valuespec import (
+    Dictionary,
+    FixedValue,
+    Integer,
+    RegExp,
+    RegExpUnicode,
+    TextAscii,
+    Tuple,
+    MonitoringState,
+)
+
+from cmk.gui.plugins.wato.utils import (
+    CheckParameterRulespecWithItem,
+    rulespec_registry,
+    RulespecGroupCheckParametersNetworking,
+)
+
+
+def _parameter_valuespec_ssllabs_grade():
+    return Dictionary(elements=[
+        ('age',
+         Tuple(
+             title=_('Maximum age of ssllabs scan'),
+             help=_('The maximum age of the last ssllabs check.'),
+             elements=[
+                 Integer(title=_('Warning at'), default_value=2, minvalue=1),
+                 Integer(title=_('Critical at'), default_value=3, minvalue=1),
+             ])),
+        ('score',
+         Tuple(
+             title=_('grade level for ssllabs scan'),
+             help=_('Put here the Integerttern (regex) for ssllabs grade check level.'),
+             elements=[
+                 RegExpUnicode(
+                     title=_('Pattern (regex) Ok level'),
+                     mode=RegExp.prefix,
+                     default_value='A',
+                 ),
+                 RegExpUnicode(
+                     title=_('Pattern (regex) Warning level'),
+                     mode=RegExp.prefix,
+                     default_value='B|C',
+                 ),
+                 RegExpUnicode(
+                     title=_('Pattern (regex) Critical level'),
+                     mode=RegExp.prefix,
+                     default_value='D|E|F|M|T',
+                 ),
+             ])),
+        ('no_grade',
+         MonitoringState(
+             title=_('Monitoring state if no grade was found'),
+             default_value=1,
+             help=_('Set the monitoring state no grade information was found the result. Default is WARN.'),
+         )),
+        ('has_warnings',
+         MonitoringState(
+             title=_('Monitoring state if host has warnings'),
+             default_value=1,
+             help=_('Set the monitoring state if "hasWarnings" in the result is true. Default is WARN.'),
+         )),
+        ('is_exceptional',
+         MonitoringState(
+             title=_('Monitoring state if host is not exceptional'),
+             default_value=1,
+             help=_('Set the monitoring state if "isExceptional" in the result is not true. Default is WARN.'),
+         )),
+        ('state_dns',
+         MonitoringState(
+             title=_('Monitoring state if the check is in "DNS resolving" state'),
+             default_value=0,
+             help=_('Set the monitoring state if the ssllabs scan is in "DNS resolving" state. Default is OK.'),
+         )),
+        ('state_error',
+         MonitoringState(
+             title=_('Monitoring state if the check is in "ERROR" state'),
+             default_value=1,
+             help=_('Set the monitoring state if the ssllabs scan is reporting an "ERROR". Default is WARN.'),
+         )),
+        ('state_in_progress',
+         MonitoringState(
+             title=_('Monitoring state if the check is in "IN_PROGRESS" state'),
+             default_value=0,
+             help=_('Set the monitoring state if the ssllabs scan is "IN_PROGRESS". Default is OK.'),
+         )),
+        ('details',
+         FixedValue(
+             value=True,
+             title=_('Show result detail in the service details'),
+             totext='',
+         ))
+    ],
+        title=_('SSL Server check via ssllabs API'),
+    )
+
+
+rulespec_registry.register(
+    CheckParameterRulespecWithItem(
+        check_group_name='ssllabs_grade',
+        group=RulespecGroupCheckParametersNetworking,
+        item_spec=lambda: TextAscii(title=_('The FQDN on ssl server to check'), ),
+        match_type='dict',
+        parameter_valuespec=_parameter_valuespec_ssllabs_grade,
+        title=lambda: _('SSL Server via ssllabs API.'),
+    ))
diff --git a/source/lib/python3/cmk/special_agents/agent_ssllabs.py b/source/lib/python3/cmk/special_agents/agent_ssllabs.py
new file mode 100644
index 0000000000000000000000000000000000000000..62aaed5ca32b4bed6ab6dacfcc7f4824a6331910
--- /dev/null
+++ b/source/lib/python3/cmk/special_agents/agent_ssllabs.py
@@ -0,0 +1,251 @@
+#!/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  : 2024-04-29
+# File  : ssllabs_grade.py (wato check plugin)
+#
+# based on the ssllabs plugin from Karsten Schoeke karsten.schoeke@geobasis-bb.de
+# see https://exchange.checkmk.com/p/ssllabs
+#
+
+# 2021-05-15: rewritten for CMK 2.0 by thl-cmk[at]outlook[dot]com
+#             changed cache file name form host_address to ssl_host_address
+# 2021-05-16: changed arguments to argparse
+#             added options for publish results and max cache age
+# 2024-05-01: refactoring
+
+# sample agent output (formatted)
+# <<<check_mk>>>
+# Version: 2.0.2
+# AgentOS: linux
+#
+# <<<ssllabs_grade:sep(0)>>>
+# [
+#     {
+#         "host": "thl-cmk.hopto.org",
+#         "port": 443,
+#         "protocol": "http",
+#         "isPublic": false,
+#         "status": "READY",
+#         "startTime": 1714559152230,
+#         "testTime": 1714559237958,
+#         "engineVersion": "2.3.0",
+#         "criteriaVersion": "2009q",
+#         "endpoints": [
+#             {"ipAddress": "91.4.75.201",
+#              "serverName": "p5b044bc9.dip0.t-ipconnect.de",
+#              "statusMessage": "Ready",
+#              "grade": "A+",
+#              "gradeTrustIgnored": "A+",
+#              "hasWarnings": false,
+#              "isExceptional": true,
+#              "progress": 100,
+#              "duration": 85530,
+#              "delegation": 1
+#              }
+#         ]
+#     },
+#     {
+#         "host": "checkmk.com",
+#         "port": 443,
+#         "protocol": "http",
+#         "isPublic": false,
+#         "status": "IN_PROGRESS",
+#         "startTime": 1714563744895,
+#         "engineVersion": "2.3.0",
+#         "criteriaVersion": "2009q",
+#         "endpoints": [
+#             {
+#                 "ipAddress": "2a0a:51c1:0:5:0:0:0:4",
+#                 "serverName": "www.checkmk.com",
+#                 "statusMessage": "Ready",
+#                 "grade": "A+",
+#                 "gradeTrustIgnored": "A+",
+#                 "hasWarnings": false,
+#                 "isExceptional": true,
+#                 "progress": 100,
+#                 "duration": 72254,
+#                 "delegation": 1
+#             },
+#             {
+#                 "ipAddress": "45.133.11.28",
+#                 "serverName": "www.checkmk.com",
+#                 "statusMessage": "In progress",
+#                 "statusDetails": "TESTING_SESSION_RESUMPTION",
+#                 "statusDetailsMessage": "Testing session resumption", "delegation": 1
+#             }
+#         ]
+#     }
+# ]
+# <<<>>>
+
+from argparse import Namespace
+from collections.abc import Sequence
+from json import dumps as json_dumps, loads as json_loads, JSONDecodeError
+from pathlib import Path
+from requests import get
+from requests.exceptions import ConnectionError
+from sys import stdout as sys_stdout
+from time import time as now_time
+
+from cmk.special_agents.utils.agent_common import special_agent_main
+from cmk.special_agents.utils.argument_parsing import create_default_argument_parser
+from cmk.utils.paths import tmp_dir
+
+VERSION = '2.0.2'
+
+
+class Args(Namespace):
+    max_age: int
+    proxy: str
+    publish: str
+    ssl_hosts: str
+    timeout: float
+
+
+def write_section(section: dict):
+    sys_stdout.write('\n<<<ssllabs_grade:sep(0)>>>\n')
+    sys_stdout.write(json_dumps(section))
+    sys_stdout.write('\n<<<>>>\n')
+
+
+def parse_arguments(argv: Sequence[str] | None) -> Args:
+    """'Parse arguments needed to construct a URL and for connection conditions"""
+    parser = create_default_argument_parser(__doc__)
+    parser.description = 'This is a CKK special agent for the Qualys SSL Labs API to monitor SSL Certificate status'
+    parser.add_argument(
+        '--ssl-hosts', required=True, type=str,
+        help='Comma separated list of FQDNs to test for',
+    )
+    parser.add_argument(
+        '--proxy', required=False,
+        help='URL to HTTPS Proxy i.e.: https://192.168.1.1:3128',
+    )
+    parser.add_argument(
+        '--timeout', '-t', type=float, default=60,
+        help='API call timeout in seconds',
+    )
+    parser.add_argument(
+        '--publish', type=str, default='off', choices=['on', 'off'],
+        help='Publish test results on ssllabs.com',
+    )
+    parser.add_argument(
+        '--max-age', type=int, default=167,
+        help='Maximum report age, in hours, if retrieving from "ssllabs.com" cache',
+    )
+    parser.epilog = (
+        '\n\nAcnowlegement:\n'
+        ' This agent is based on the work by Karsten Schoeke karsten[dot]schoeke[at]geobasis-bb[dot]de\n'
+        ' see https://exchange.checkmk.com/p/ssllabs\n\n'
+        f'written by thl-cmk[at]outlook[dot], Version: {VERSION}, '
+        f'For more information see: https://thl-cmk.hopto.org\n'
+    )
+    return parser.parse_args(argv)
+
+
+def connect_ssllabs_api(ssl_host_address: str, host_cache: str, args: Args, ) -> dict | None:
+    #
+    # https://github.com/ssllabs/ssllabs-scan
+    #
+    server = 'api.ssllabs.com'
+    uri = 'api/v3/analyze'  # change to api v4 (?)
+    max_age = args.max_age * 24  # default 1 day (1 week minus 1 hour)
+    publish = args.publish  # default off
+    from_cache = 'on'  # on | off
+    # all_data = 'done'  # on | done
+    ignore_mismatch = 'on'  # on | off
+    start_new = 'on'
+
+    # url for request webservice (&startNew={startNew}&all={all})
+    url = (
+        f'https://{server}/{uri}'
+        f'?host={ssl_host_address}'
+        f'&publish={publish}'
+        f'&fromCache={from_cache}'
+        f'&maxAge={max_age}'
+        # f'&all={all_data}'
+        f'&ignoreMismatch={ignore_mismatch}'
+        # f'&startNew={start_new}'
+    )
+    proxies = {}
+    if args.proxy is not None:
+        proxies = {'https': args.proxy.split('/')[-1]}  # remove 'https://' from proxy string
+
+    try:
+        response = get(
+            url=url,
+            timeout=args.timeout,
+            proxies=proxies,
+            headers={
+                'User-Agent': f'CMK SSL Labs special agent {VERSION}',
+            },
+        )
+    except ConnectionError as e:
+        host_data = {'host': ssl_host_address, 'status': 'ConnectionError', 'error': str(e)}
+    else:
+        try:
+            host_data = response.json()
+        except JSONDecodeError as e:
+            host_data = {'host': ssl_host_address, 'status': 'JSONDecodeError', 'error': str(e)}
+        if host_data.get('status') == 'READY':
+            Path(host_cache).write_text(response.text)
+
+    return host_data
+
+
+def read_cache(host_cache: str) -> dict | None:
+    try:
+        data: dict = json_loads(Path(host_cache).read_text())
+    except JSONDecodeError:
+        return
+
+    data.update({'from_agent_cache': True})
+    return data
+
+
+def agent_ssllsbs_main(args: Args) -> int:
+    now = now_time()
+    cache_dir = f'{tmp_dir}/agents/agent_ssllabs'
+    cache_age = args.max_age + 86400
+    ssl_hosts = args.ssl_hosts.split(',')
+
+    # Output general information about the agent
+    sys_stdout.write('<<<check_mk>>>\n')
+    sys_stdout.write(f'Version: {VERSION}\n')
+    sys_stdout.write('AgentOS: linux\n')
+
+    # create cache directory, if it not exists
+    Path(cache_dir).mkdir(parents=True, exist_ok=True)
+
+    data = []
+    for ssl_host_address in ssl_hosts:
+        host_cache = f'{cache_dir}/{ssl_host_address}'
+
+        # check if cache file exists and is not older as cache_age
+        if Path(host_cache).exists() and now - Path(host_cache).stat().st_mtime < cache_age:
+            if host_data := read_cache(host_cache=host_cache):
+                data.append(host_data)
+        else:
+            if host_data := connect_ssllabs_api(
+                    ssl_host_address=ssl_host_address,
+                    host_cache=host_cache,
+                    args=args,
+            ):
+                data.append(host_data)
+
+    if data:
+        write_section(data)
+    return 0
+
+
+def main() -> int:
+    return special_agent_main(parse_arguments, agent_ssllsbs_main)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/packages/agent_ssllabs b/source/packages/agent_ssllabs
similarity index 82%
rename from packages/agent_ssllabs
rename to source/packages/agent_ssllabs
index 6f8dc48bbcc48a3b800ec7847d2c7fb62f61d05e..698d35e3fcdfbac89f36c685ac2d87c980d1bf11 100644
--- a/packages/agent_ssllabs
+++ b/source/packages/agent_ssllabs
@@ -26,13 +26,12 @@
            'agents': ['special/agent_ssllabs'],
            'checkman': ['ssllabs_grade'],
            'checks': ['agent_ssllabs'],
-           'lib': ['check_mk/special_agent/agent_ssllabs.py'],
-           'web': ['plugins/wato/agent_ssllabs.py',
-                   'plugins/wato/ssllabs_grade.py']},
+           'gui': ['wato/check_parameters/ssllabs_grade.py'],
+           'lib': ['python3/cmk/special_agents/agent_ssllabs.py'],
+           'web': ['plugins/wato/agent_ssllabs.py']},
  'name': 'agent_ssllabs',
- 'num_files': 7,
  'title': 'ssllabs api check',
- 'version': '2.0.1',
- 'version.min_required': '2.0.0',
- 'version.packaged': '2021.04.10',
- 'version.usable_until': None}
\ No newline at end of file
+ 'version': '2.0.2-20240105',
+ 'version.min_required': '2.2.0b1',
+ 'version.packaged': '2.2.0p24',
+ 'version.usable_until': None}
diff --git a/source/web/plugins/wato/agent_ssllabs.py b/source/web/plugins/wato/agent_ssllabs.py
new file mode 100644
index 0000000000000000000000000000000000000000..55fb88332d720994d7ea6610cba7860956f06d09
--- /dev/null
+++ b/source/web/plugins/wato/agent_ssllabs.py
@@ -0,0 +1,103 @@
+#!/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  : 2024-04-29
+# File  : ssllabs.py (wato special agent)
+#
+
+# based on the ssllabs plugin from Karsten Schoeke karsten.schoeke@geobasis-bb.de
+# see https://exchange.checkmk.com/p/ssllabs
+
+# 2024-05-01: modified for CMK 2.2.x
+
+from cmk.gui.i18n import _
+from cmk.gui.plugins.wato.utils import (
+    HostRulespec,
+    rulespec_registry,
+)
+from cmk.gui.valuespec import (
+    Dictionary,
+    Integer,
+    TextAscii,
+    ListOfStrings,
+    FixedValue,
+)
+
+from cmk.gui.plugins.wato.special_agents.common import RulespecGroupDatasourceProgramsOS
+
+
+def _valuespec_special_agents_ssllabs():
+    return Dictionary(
+        elements=[
+            ('ssl_hosts',
+             ListOfStrings(
+                 title=_('SSL hosts to check'),
+                 orientation='vertical',
+                 allow_empty=False,
+                 size=50,
+                 empty_text='www.checkmk.com',
+                 max_entries=10,
+                 help=_(
+                     'List of server names to check. Add the host names without "http(s)://". Ie: www.checkmk.com. '
+                     'The list is limited to 10 entries. If you need more than 10 entries create another rule.'
+                 ),
+             )),
+            ('timeout',
+             Integer(
+                 title=_('Connect Timeout'),
+                 help=_(
+                     'The network timeout in seconds when communicating via HTTPS. The default is 30 seconds.'
+                 ),
+                 default_value=30,
+                 minvalue=1,
+                 unit=_('seconds')
+             )),
+            ('proxy',
+             TextAscii(
+                 title=_('proxy server, if required'),
+                 help=_('proxy in the format: https://ip-addres|servername:port'),
+             )),
+            ('publish_results',
+             FixedValue(
+                 value='on',
+                 title=_('Publish results'),
+                 totext=_('Results will be published'),
+                 help=_(
+                     'By default test results will not be published. If you enable this option the test'
+                     ' results will by public visible on https://www.ssllabs.com/ssltest'
+                 ),
+                 default_value='off',
+             )),
+            ('max_age',
+             Integer(
+                 title=_('Max Age for ssllbas.com cache'),
+                 help=_(
+                     'Maximum report age, in hours, if retrieving from "ssllabs.com" cache. '
+                     'After this time a new test will by initiated. The default (and minimum) is 1 Day'
+                 ),
+                 default_value=1,
+                 minvalue=1,
+                 unit=_('Days')
+             )),
+        ],
+        title=_('Qualys SSL Labs server test'),
+        help=_(
+            'This rule selects the ssllabs agent, which fetches SSL Server status from api.ssllabs.com.'
+            'For more details about the SSL server check see https://www.ssllabs.com/ssltest/index.html.'
+            'For mor information about the SSL Labs API see: '
+            'https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md.'
+        ),
+    )
+
+
+rulespec_registry.register(
+    HostRulespec(
+        group=RulespecGroupDatasourceProgramsOS,
+        name='special_agents:ssllabs',
+        valuespec=_valuespec_special_agents_ssllabs,
+    ))
diff --git a/web/plugins/wato/agent_ssllabs.py b/web/plugins/wato/agent_ssllabs.py
deleted file mode 100644
index 40f4d13fe7956ae18f0ca505298ab3f6ad02d9d7..0000000000000000000000000000000000000000
--- a/web/plugins/wato/agent_ssllabs.py
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-#
-#
-
-
-from cmk.gui.i18n import _
-from cmk.gui.plugins.wato import (
-    HostRulespec,
-    rulespec_registry,
-)
-from cmk.gui.valuespec import (
-    Dictionary,
-    Integer,
-    TextAscii,
-    ListOfStrings,
-    FixedValue,
-)
-
-from cmk.gui.plugins.wato.datasource_programs import (
-    RulespecGroupDatasourceProgramsOS,
-)
-
-
-def _valuespec_special_agents_ssllabs():
-    return Dictionary(
-        elements=[
-            ('sslhosts',
-             ListOfStrings(
-                 title=_('SSL hosts to check'),
-                 orientation='vertical',
-                 allow_empty=False,
-             )
-             ),
-            ('timeout',
-             Integer(
-                 title=_('Connect Timeout'),
-                 help=_('The network timeout in seconds when communicating via HTTPS. '
-                        'The default is 60 seconds.'),
-                 default_value=60,
-                 minvalue=1,
-                 unit=_('seconds')
-             )
-             ),
-            ('proxy',
-             TextAscii(
-                 title=_('proxy server, if required'),
-                 help=_('proxy in the format: https://ip-addres|servername:port'),
-             ),
-             ),
-            ('publishresults',
-             FixedValue(
-                 'on',
-                 title=_('Publish results'),
-                 totext=_('Results will be published'),
-                 help=_('By default test results will not be published. If you enable this option the test'
-                        ' results will by public visible on https://www.ssllabs.com/ssltest'),
-                 default_value='off',
-             )),
-            ('maxage',
-             Integer(
-                 title=_('Max Age for ssllbas.com cache'),
-                 help=_('Maximum report age, in hours, if retrieving from "ssllabs.com" cache. '
-                        'After this time a new test will by initiated. The default (and minimum) is 167 hours'),
-                 default_value=167,
-                 minvalue=167,
-             )),
-        ],
-        title=_('Qualys SSL Labs server test'),
-        help=_('This rule selects the ssllabs agent, which fetches SSL Server status from api.ssllabs.com.'
-               'For more details about the SSL server check see https://www.ssllabs.com/ssltest/index.html.'
-               'For mor information about the SSL Labs API see: '
-               'https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md.'),
-    )
-
-
-rulespec_registry.register(
-    HostRulespec(
-        group=RulespecGroupDatasourceProgramsOS,
-        name='special_agents:ssllabs',
-        valuespec=_valuespec_special_agents_ssllabs,
-    ))
diff --git a/web/plugins/wato/ssllabs_grade.py b/web/plugins/wato/ssllabs_grade.py
deleted file mode 100644
index 9e9df0b7a25e9080e2bba4efb0d37b1e89b51e02..0000000000000000000000000000000000000000
--- a/web/plugins/wato/ssllabs_grade.py
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-#
-#
-#
-# 2015 Karsten Schoeke karsten.schoeke@geobasis-bb.de
-#
-# 2021-05-15: rewritten for CMK 2.0 by thl-cmk[at]outlook[dot]com
-#
-
-from cmk.gui.i18n import _
-from cmk.gui.valuespec import (
-    Dictionary,
-    TextAscii,
-    Age,
-    Tuple,
-    RegExpUnicode,
-    RegExp,
-)
-
-from cmk.gui.plugins.wato import (
-    CheckParameterRulespecWithItem,
-    rulespec_registry,
-    RulespecGroupCheckParametersNetworking,
-)
-
-
-def _parameter_valuespec_ssllabs_grade():
-    return Dictionary(elements=[
-        ('age',
-         Tuple(
-             title=_('Maximum age of ssllabs scan'),
-             help=_('The maximum age of the last ssllabs check.'),
-             elements=[
-                 Age(title=_('Warning at'), default_value=604800, minvalue=604800),
-                 Age(title=_('Critical at'), default_value=864000, minvalue=691200),
-             ]
-         ),
-         ),
-        ('score',
-         Tuple(
-             title=_('grade level for ssllabs scan'),
-             help=_('Put here the Integerttern (regex) for ssllabs grade check level.'),
-             elements=[
-                 RegExpUnicode(
-                     title=_('Pattern (regex) Ok level'),
-                     mode=RegExp.prefix,
-                     default_value='A',
-                 ),
-                 RegExpUnicode(
-                     title=_('Pattern (regex) Warning level'),
-                     mode=RegExp.prefix,
-                     default_value='B|C',
-                 ),
-                 RegExpUnicode(
-                     title=_('Pattern (regex) Critical level'),
-                     mode=RegExp.prefix,
-                     default_value='D|E|F|M|T',
-                 ),
-             ]
-         ),
-         ),
-    ],
-        title=_('SSL Server check via ssllabs API'),
-    )
-
-
-rulespec_registry.register(
-    CheckParameterRulespecWithItem(
-        check_group_name='ssllabs_grade',
-        group=RulespecGroupCheckParametersNetworking,
-        item_spec=lambda: TextAscii(title=_('The FQDN on ssl server to check'), ),
-        match_type='dict',
-        parameter_valuespec=_parameter_valuespec_ssllabs_grade,
-        title=lambda: _('SSL Server via ssllabs API.'),
-    ))