From ef3fc41f51af53e4ed4dfd435f5adf4a4b345bed Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Thu, 2 May 2024 15:54:11 +0200
Subject: [PATCH] update project

---
 README.md                                     |   1 +
 agent_based/ssllabs_grade.py                  | 148 -------
 agent_ssllabs.mkp                             | Bin 6986 -> 0 bytes
 checkman/agent_ssllabs                        |  45 --
 checks/agent_ssllabs                          |  32 --
 {doc => img}/.gitkeep                         |   0
 {doc => img}/sample.png                       | Bin
 {doc => img}/wato-options-agent.png           | Bin
 {doc => img}/wato-options.png                 | Bin
 lib/check_mk/special_agent/agent_ssllabs.py   | 144 ------
 mkp/agent_ssllabs-2.0.2-20240105.mkp          | Bin 0 -> 10161 bytes
 source/agent_based/ssllabs_grade.py           | 412 ++++++++++++++++++
 .../agents}/special/agent_ssllabs             |   2 +-
 {checkman => source/checkman}/ssllabs_grade   |   0
 source/checks/agent_ssllabs                   |  37 ++
 .../wato/check_parameters/ssllabs_grade.py    | 126 ++++++
 .../cmk/special_agents/agent_ssllabs.py       | 251 +++++++++++
 {packages => source/packages}/agent_ssllabs   |  15 +-
 source/web/plugins/wato/agent_ssllabs.py      | 103 +++++
 web/plugins/wato/agent_ssllabs.py             |  82 ----
 web/plugins/wato/ssllabs_grade.py             |  76 ----
 21 files changed, 938 insertions(+), 536 deletions(-)
 delete mode 100644 agent_based/ssllabs_grade.py
 delete mode 100644 agent_ssllabs.mkp
 delete mode 100644 checkman/agent_ssllabs
 delete mode 100644 checks/agent_ssllabs
 rename {doc => img}/.gitkeep (100%)
 rename {doc => img}/sample.png (100%)
 rename {doc => img}/wato-options-agent.png (100%)
 rename {doc => img}/wato-options.png (100%)
 delete mode 100755 lib/check_mk/special_agent/agent_ssllabs.py
 create mode 100644 mkp/agent_ssllabs-2.0.2-20240105.mkp
 create mode 100644 source/agent_based/ssllabs_grade.py
 rename {agents => source/agents}/special/agent_ssllabs (90%)
 rename {checkman => source/checkman}/ssllabs_grade (100%)
 create mode 100644 source/checks/agent_ssllabs
 create mode 100644 source/gui/wato/check_parameters/ssllabs_grade.py
 create mode 100644 source/lib/python3/cmk/special_agents/agent_ssllabs.py
 rename {packages => source/packages}/agent_ssllabs (82%)
 create mode 100644 source/web/plugins/wato/agent_ssllabs.py
 delete mode 100644 web/plugins/wato/agent_ssllabs.py
 delete mode 100644 web/plugins/wato/ssllabs_grade.py

diff --git a/README.md b/README.md
index c7d46b7..67d5149 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 1a5e11e..0000000
--- 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
GIT binary patch
literal 0
HcmV?d00001

literal 6986
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

diff --git a/checkman/agent_ssllabs b/checkman/agent_ssllabs
deleted file mode 100644
index 08ef898..0000000
--- 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 325b21e..0000000
--- 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 a9bee53..0000000
--- 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
GIT binary patch
literal 10161
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

literal 0
HcmV?d00001

diff --git a/source/agent_based/ssllabs_grade.py b/source/agent_based/ssllabs_grade.py
new file mode 100644
index 0000000..f298545
--- /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 c28031c..d1dc725 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 0000000..57e1c5d
--- /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 0000000..67406f0
--- /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 0000000..62aaed5
--- /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 6f8dc48..698d35e 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 0000000..55fb883
--- /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 40f4d13..0000000
--- 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 9e9df0b..0000000
--- 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.'),
-    ))
-- 
GitLab