diff --git a/agent_based/inv_cisco_bug.py b/agent_based/inv_cisco_bug.py
index 111928277f8a754d98955722f3a7abe933b7e56e..65d869bdddf1bfec15b971a1bcb67169ed6de638 100644
--- a/agent_based/inv_cisco_bug.py
+++ b/agent_based/inv_cisco_bug.py
@@ -11,12 +11,15 @@
 #                    #bug['known_fixed_releases'] = bug['known_fixed_releases'].split(' ')
 #                    #bug['known_affected_releases'] = bug['known_affected_releases'].split(' ')
 # 2021-07-24: rewritten for CMK 2.0
+# 2021-07-26: some cleanup
+#
 
 import os
 import json
 import logging
 import time
-from typing import List
+from typing import List, Optional
+from dataclasses import dataclass
 from cmk.base.plugins.agent_based.agent_based_api.v1 import (
     register,
     SNMPTree,
@@ -33,187 +36,150 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
     InventoryResult,
 )
 
-# from cmk.base.plugins.agent_based.utils import ciscoapi as utils
 from cmk.base.plugins.agent_based.utils.ciscoapi import (
     set_loglevel,
     expand_path,
-    compact_info_by_serial,
-    check_bad_serial,
-    pid_on_black_list,
-    sn_on_black_list,
-    pid_on_bad_list,
-    set_PID_black_list,
-    set_PID_bad_list,
-    set_SN_black_list,
+    get_base_path,
 )
 
-def parse_inv_cisco_bug(string_table: List[StringTable]):
-    section = string_table
-    return section
-
-def inventory_cisco_bug(params, section) -> InventoryResult:
-
-    def get_OS_Version(cw_version, sysdescription, inventory):
-        version = None
-        pid = ''
-        # cw_version = None
-        sys_version = None
-        phy_version = None
-
-        if str(cw_version).find('CW_VERSION$') == 0:
-            cw_version = cw_version.split('$')[1]  # .upper()
-
-        sysdescription = sysdescription.split(',')
-        # sys description from N5K-C5672UP (has two version entrys :-( )
-        # Cisco NX-OS(tm) n6000, Software (n6000-uk9), Version 7.0(8)N1(1), RELEASE SOFTWARE Copyright (c) 2002-2012 \
-        # by Cisco Systems, Inc. Device Manager Version 6.0(2)N1(1), Compiled 2/20/2016 21:00:00
-        for entry in sysdescription:
-            if not sys_version and 'version' in entry.lower():  # get only the first 'version' entry
-                sys_version = entry[entry.lower().find('version') + 7:].strip()
-                if sys_version.startswith(':'):  # AsyncOS
-                    sys_version = sys_version[1:].strip()
-                sys_version = sys_version.split(' ')[0].strip()
-                sys_version = sys_version  # .upper()
-
-        low_index = 1000000
-        for phyentry in inventory:
-            phyindex, physoftwarerev, phymodelname, physicalclass = phyentry
-            if physicalclass == '3':  # chassis
-                if int(phyindex) <= low_index:
-                    if phymodelname != '':  # PID at all
-                        if phymodelname.find(' ') == -1:  # no spaces in PID
-                            low_index = int(phyindex)
-                            pid = phymodelname
-                            phy_version = physoftwarerev.split(',')[0]
-
-        version = cw_version
-        if version == '' and sys_version:
-            version = sys_version
-        if version == '' and phy_version:
-            version = phy_version
-
-        # remove leading '0' in IOSXE version numbers
-        if version and version[0] == '0':
-            version = version.replace('.0', '.')
-            version = version.lstrip('0')
-        version = version.rstrip(';')
-
-        # todo: cut intermediate for ASA version as option
-
-        return version, pid
-
-    def get_bug_status(bug_status):
-        _bug_status = {
-            'F': 'Fixed',
-            'O': 'Open',
-            'T': 'Terminated'
-        }
-        return _bug_status.get(bug_status, bug_status)
-
-    def get_bug_behavior_changed(behavior_changed):
-        _behavior_changed = {
-            'Y': 'yes',
-            'N': 'no',
-        }
-        return _behavior_changed.get(behavior_changed, behavior_changed)
 
+@dataclass
+class SnmpBug:
+    version: str
+    pid: str
+
+
+def _get_os_version(cw_version, sysdescription, inventory):
+    pid = ''
+    sys_version = None
+    phy_version = None
+
+    if str(cw_version).find('CW_VERSION$') == 0:
+        cw_version = cw_version.split('$')[1]  # .upper()
+
+    sysdescription = sysdescription.split(',')
+    # sys description from N5K-C5672UP (has two version entries :-( )
+    # Cisco NX-OS(tm) n6000, Software (n6000-uk9), Version 7.0(8)N1(1), RELEASE SOFTWARE Copyright (c) 2002-2012 \
+    # by Cisco Systems, Inc. Device Manager Version 6.0(2)N1(1), Compiled 2/20/2016 21:00:00
+    for entry in sysdescription:
+        if not sys_version and 'version' in entry.lower():  # get only the first 'version' entry
+            sys_version = entry[entry.lower().find('version') + 7:].strip()
+            if sys_version.startswith(':'):  # AsyncOS
+                sys_version = sys_version[1:].strip()
+            sys_version = sys_version.split(' ')[0].strip()
+            sys_version = sys_version  # .upper()
+
+    low_index = 1000000
+    for phyentry in inventory:
+        phyindex, physoftwarerev, phymodelname, physicalclass = phyentry
+        if physicalclass == '3':  # chassis
+            if int(phyindex) <= low_index:
+                if phymodelname != '':  # PID at all
+                    if phymodelname.find(' ') == -1:  # no spaces in PID
+                        low_index = int(phyindex)
+                        pid = phymodelname
+                        phy_version = physoftwarerev.split(',')[0]
+
+    version = cw_version
+    if version == '' and sys_version:
+        version = sys_version
+    if version == '' and phy_version:
+        version = phy_version
+
+    # remove leading '0' in IOSXE version numbers
+    if version and version[0] == '0':
+        version = version.replace('.0', '.')
+        version = version.lstrip('0')
+    version = version.rstrip(';')
+
+    # todo: cut intermediate for ASA version as option
+
+    return version, pid
+
+
+def _get_bug_status(bug_status):
+    _bug_status = {
+        'F': 'Fixed',
+        'O': 'Open',
+        'T': 'Terminated'
+    }
+    return _bug_status.get(bug_status, bug_status)
+
+
+def _get_bug_behavior_changed(behavior_changed):
+    _behavior_changed = {
+        'Y': 'yes',
+        'N': 'no',
+    }
+    return _behavior_changed.get(behavior_changed, behavior_changed)
+
+
+def parse_inv_cisco_bug(string_table: List[StringTable]) -> Optional[SnmpBug]:
+    cw_version, sysdescription = string_table[0][0]
+    pids = string_table[1]
+
+    # get OS version
+    version, pid = _get_os_version(cw_version, sysdescription, pids)
+
+    if version == '' or pid == '':
+        return
+    else:
+        return SnmpBug(
+            version=version,
+            pid=pid
+        )
+
+
+def inventory_cisco_bug(params, section: SnmpBug) -> InventoryResult:
     set_loglevel()
 
-    # disable_bug = False
     optionalcolumns = [
         'base_pid',
-        # 'bug_id',
-        # 'behavior_changed',
         'description',
-        # 'headline',
         'id',
         'known_affected_releases',
-        # 'known_fixed_releases',
-        # 'last_modified_date',
         'product',
-        # 'severity',
-        # 'status',
-        # 'support_case_count',
     ]
 
-    # version = ''
-    not_updated = 10000000
-    dont_show_older_then = '0000-00-00'
-    conf_file = '~/etc/ciscoapi/ciscoapi.json'
-    conf_file = os.path.expanduser(conf_file)
-    bug_status = {}
-    # prepare path variables (expand homedir and add '/')
-    base_path = '~/var/ciscoapi'
-    cw_version, sysdescription = section[0][0]
-    pids = section[1]
-
     # get parameters from wato
     if params:
         optionalcolumns = params.get('removecolumns', optionalcolumns)
 
-    # get OS version
-    version, pid = get_OS_Version(cw_version, sysdescription, pids)
-
-    if version == '' or pid == '':
-        return
-
-    # check for conf_file and read parameters
-    if os.path.isfile(conf_file):
-        with open(conf_file) as f:
-            try:
-                config = json.load(f)
-                base_path = config['global']['base_dir']
-            except ValueError as e:
-                logging.warning(f'snmp_cisco_eox:status:JSON load error: {e}')
-
+    base_path = get_base_path()
     bug_dir = base_path + '/bug'
     path_found = expand_path(bug_dir + '/found/')
     path_not_found = expand_path(bug_dir + '/not_found/')
     path_request = expand_path(bug_dir + '/request/')
-    status_path = expand_path(base_path + '/status')
 
-    # node = inv_tree_list('software.support.cisco_bug.')
-    path = ['software','support','cisco_bug']
-    yield Attributes(
-        path=path,
-        inventory_attributes={
-            'os_version': version,
-            'PID': pid,
-        })
+    path = ['software', 'support', 'cisco_bug']
+    attribute = {
+        'os_version': section.version,
+        'PID': section.pid,
+    }
 
-    pid = pid.replace('/', '_')
+    pid = section.pid.replace('/', '_')
 
     # bug report found
-    if os.path.isfile(path_found + pid + '/' + version):
-        logging.info('snmp_cisco_bug:report found: %s, %s' % (pid, version))
-        modifytime = os.path.getmtime(path_found + pid + '/' + version)
-        yield Attributes(
-        path=path,
-        inventory_attributes={
+    if os.path.isfile(path_found + pid + '/' + section.version):
+        modifytime = os.path.getmtime(path_found + pid + '/' + section.version)
+        attribute.update({
             'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
             'status': 'found'
         })
 
         bug_record = ''
-        with open(path_found + pid + '/' + version) as f:
+        with open(path_found + pid + '/' + section.version) as f:
             try:
                 bug_record = json.load(f)
             except ValueError as e:
-                logging.warning(f'{host_name}:snmp_cisco_bug:bug_found:JSON load error: [e]')
+                logging.warning(f'inv_cisco_bug:bug_found:JSON load error: {e}')
+                exit()
         if bug_record != '':
-            yield Attributes(
-                path=path,
-                inventory_attributes={
-                    'Total_records': bug_record.get('total_records', 0),
-                })
+            attribute['Total_records'] = bug_record.get('total_records', 0)
 
             missing = bug_record.get('missing', {})
             if len(missing.keys()) != 0:
-                yield Attributes(
-                    path=path,
-                    inventory_attributes={
-                        'missing_records': len(missing.keys()) * 10,
-                    })
+                attribute['missing_records'] = len(missing.keys()) * 10
 
             bugs = bug_record.get('bugs')
             bug_ids = []
@@ -223,12 +189,10 @@ def inventory_cisco_bug(params, section) -> InventoryResult:
                     bug_ids.append(bug.get('bug_id'))
                     bug['known_fixed_releases'] = ', '.join(bug['known_fixed_releases'].split(' '))
                     bug['known_affected_releases'] = ', '.join(bug['known_affected_releases'].split(' '))
-                    bug['status'] = get_bug_status(bug['status'])
-                    bug['behavior_changed'] = get_bug_behavior_changed(bug['behavior_changed'])
-                    logging.info('remove columns: %s' % optionalcolumns)
+                    bug['status'] = _get_bug_status(bug['status'])
+                    bug['behavior_changed'] = _get_bug_behavior_changed(bug['behavior_changed'])
                     if optionalcolumns is not None:
                         for column in optionalcolumns:
-                            logging.info('remove column: %s' % column)
                             bug.pop(column, None)
                     clean_bugs.append(bug)
                     key_columns = {'bug_id': bug.get('bug_id')}
@@ -238,91 +202,31 @@ def inventory_cisco_bug(params, section) -> InventoryResult:
                         key_columns=key_columns,
                         inventory_columns=bug,
                     )
-            yield Attributes(
-                path=path,
-                inventory_attributes={
-                    'duplicate_records':len(bugs) - len(clean_bugs),
-                })
-
+            attribute['duplicate_records'] = len(bugs) - len(clean_bugs)
 
     # bug report not found
-    elif os.path.isfile(path_not_found + pid + '/' + version):
-        logging.info('snmp_cisco_bug:report not found: %s, %s' % (pid, version))
-        modifytime = os.path.getmtime(path_not_found + pid + '/' + version)
-        yield Attributes(
-            path=path,
-            inventory_attributes={
-                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                'status': 'Not found',
-            })
+    elif os.path.isfile(path_not_found + pid + '/' + section.version):
+        modifytime = os.path.getmtime(path_not_found + pid + '/' + section.version)
+        attribute.update({
+            'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+            'status': 'Not found',
+        })
     else:
         # create new request
         path_request = expand_path(path_request + pid)
-        if not os.path.isfile(path_request + '/' + version):
-            logging.info('snmp_cisco_bug:create request: %s, %s' % (pid, version))
-            with open(path_request + '/' + version, 'w+') as f:
+        if not os.path.isfile(path_request + '/' + section.version):
+            with open(path_request + '/' + section.version, 'w+') as f:  # only create file (touch)
                 pass
-        modifytime = os.path.getmtime(path_request + '/' + version)
-        yield Attributes(
-            path=path,
-            inventory_attributes={
-                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                'status': 'requested',
-            })
-
-    # # create and write back api status, will be used for active check cisco_api_status
-    # apistatus = {}
-    # lastrun = {}
-    # laststatus = {}
-    # thisrun = []
-    # apiname = 'bug'
-    # rewrite = False
-    # use_keys = ['sir', 'advisoryTitle', 'lastUpdated']
-    #
-    # if bug_status != {}:
-    #
-    #     # in CMK v1.5.x global variable g_hostname was replaced by API call host_name()
-    #     try:
-    #         host_name = host_name()
-    #     except NameError:
-    #         host_name = g_hostname
-    #
-    #     if os.path.isfile(status_path + host_name):
-    #         with open(status_path + host_name) as f:
-    #             try:
-    #                 apistatus = json.load(f)
-    #             except ValueError, e:
-    #                 logging.warning('snmp_cisco_bug:status:JSON load error: %s' % e)
-    #
-    #     if apistatus.get(apiname, None) is not None:
-    #         lastrun = apistatus.get(apiname).get('lastrun', {})
-    #         laststatus.update({'status': apistatus.get(apiname).get('status', '')})
-    #         laststatus.update({'product_family': apistatus.get(apiname).get('product_family', '')})
-    #         laststatus.update({'Last_checked': apistatus.get(apiname).get('Last_checked', '')})
-    #     else:
-    #         apistatus.update({apiname: {}})
-    #
-    #     if bug_status != laststatus:
-    #         apistatus.get(apiname).update(bug_status)
-    #         rewrite = True
-    #
-    #     if bug_status != node:
-    #         # cleanup bug infos for status
-    #         advisories = node.get('advisories', {})
-    #         if advisories != {}:
-    #             for entry in advisories:
-    #                 this_entry = {}
-    #                 for key in entry.keys():
-    #                     if key in use_keys:
-    #                         this_entry.update({key: entry.get(key)})
-    #                 thisrun.append(this_entry)
-    #
-    #         if lastrun != thisrun:
-    #             apistatus.get(apiname).update({'lastrun': thisrun})
-    #             rewrite = True
-    #     if rewrite:
-    #         with open(status_path + host_name, 'w') as f:
-    #             json.dump(apistatus, f)
+        modifytime = os.path.getmtime(path_request + '/' + section.version)
+        attribute.update({
+            'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+            'status': 'requested',
+        })
+
+    yield Attributes(
+        path=path,
+        inventory_attributes=attribute
+    )
 
 
 register.snmp_section(
@@ -354,7 +258,6 @@ register.snmp_section(
 register.inventory_plugin(
     name='inv_cisco_bug',
     inventory_function=inventory_cisco_bug,
-    inventory_default_parameters={
-    },
+    inventory_default_parameters={},
     inventory_ruleset_name='inv_cisco_bug',
 )
diff --git a/agent_based/inv_cisco_contract.py b/agent_based/inv_cisco_contract.py
index 1bff9836cfd2da7336cc9d878cc0b4b6a5b76dc3..52d543f02209b5451ef4db550f3f199def93ff57 100644
--- a/agent_based/inv_cisco_contract.py
+++ b/agent_based/inv_cisco_contract.py
@@ -18,12 +18,22 @@
 # 2020-08-04: moved node tree from hardware.system.support to hardware.system
 #             cleanup create_sn2info_record
 # 2021-07-24: rewritten for CMK 2.0
+# 2021-07-25: some cleanup
+#
+#  ToDo: parse function and SNMP section can be shared between inv_cisco_eox and inv_cisco_contract
+#        pro: only one section/parse function
+#        con: with only one pare function these plugins can not disabled separately
+#        solution: separate SNMP section, common parse function
+# ToDo: error handling for json.load -> in eox/contract pass, in psirt/bug -> exit
+# ToDo: exit codes ??
 #
-import logging
 import os
 import json
 import time
 
+from typing import Dict
+from dataclasses import dataclass
+
 from cmk.base.plugins.agent_based.agent_based_api.v1 import (
     register,
     SNMPTree,
@@ -37,20 +47,21 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
     InventoryResult,
 )
 
-# from cmk.base.plugins.agent_based.utils import ciscoapi as utils
 from cmk.base.plugins.agent_based.utils.ciscoapi import (
-    set_loglevel,
     expand_path,
-    compact_info_by_serial,
     check_bad_serial,
     pid_on_black_list,
-    sn_on_black_list,
-    pid_on_bad_list,
-    set_PID_black_list,
-    set_PID_bad_list,
-    set_SN_black_list,
+    set_pid_black_list,
+    get_base_path,
 )
 
+
+@dataclass
+class SnmpContractEntry:
+    phydescr: str
+    pid: str
+
+
 def _create_sn2info_record(sn2infofile, optional_columns):
     sn2info = {}
     if os.path.isfile(sn2infofile):
@@ -58,10 +69,9 @@ def _create_sn2info_record(sn2infofile, optional_columns):
             sn2inforecord = json.load(f)
 
         modifytime = os.path.getmtime(sn2infofile)
-        sn2info.update({'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime))
-                        })
+        sn2info.update({'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime))})
 
-        # add nonempty values to sn2inforecord
+        # add nonempty values to sn2info record
         for key, value in [
             ('serial_number', sn2inforecord.get('sr_no')),
             ('is_covered', sn2inforecord.get('is_covered')),
@@ -77,200 +87,93 @@ def _create_sn2info_record(sn2infofile, optional_columns):
             ('warranty_type', sn2inforecord.get('warranty_type')),
             ('warranty_type_description', sn2inforecord.get('warranty_type_description')),
             ('warranty_end_date', sn2inforecord.get('warranty_end_date')),
-        ]: 
+        ]:
             value = value.strip()
             if value != '' and key not in optional_columns:
                 sn2info[key] = value
 
-    logging.info('create_sn2info_record sn2info: %s' % sn2info)
-    # print(sn2info)
     return sn2info
 
-def parse_inv_cisco_contract(string_table: StringTable):
-    section = string_table
-    return section
-
-def inventory_cisco_contract(params, section) -> InventoryResult:
-    set_loglevel()
 
-    disable_contract = False
-    optionalcolumns = ['contract_site_state_province',
-                       'warranty_type_description',
-                       'warranty_end_date',
-                       'warranty_type',
-                       ]
-    base_path = '~/var/ciscoapi'
-    conf_file = '~/etc/ciscoapi/ciscoapi.json'
-    conf_file = os.path.expanduser(conf_file)
+def parse_inv_cisco_contract(string_table: StringTable) -> Dict[str, SnmpContractEntry]:
+    serials: Dict[str, SnmpContractEntry] = {}
+    for phydescr, physerialnum, phymodelname in string_table:
+        if not check_bad_serial(physerialnum):
+            if not pid_on_black_list(phymodelname):
+                phymodelname = phymodelname.strip()
+                physerialnum = physerialnum.strip()
+                if phymodelname == '':
+                    phymodelname = 'UNSPECIFIED'
+                pid = phymodelname.split()[0].upper()
+                serial = physerialnum.split()[0].upper()
+                if serial not in serials.keys():
+                    serials.update({serial: SnmpContractEntry(
+                        phydescr=phydescr,
+                        pid=pid
+                    )})
+                else:
+                    temp = serials[serial]
+                    temp.phydescr = ', '.join(list(set(temp.phydescr.split(', ') + [phydescr])))
+                    temp.pid = ', '.join(list(set(temp.pid.split(', )') + [pid])))
+                    serials[serial] = temp
+
+    return serials
+
+
+def inventory_cisco_contract(params, section: Dict[str, SnmpContractEntry]) -> InventoryResult:
+    optionalcolumns = [
+        'contract_site_state_province',
+        'warranty_type_description',
+        'warranty_end_date',
+        'warranty_type',
+    ]
 
     # get parameters from wato
     if params:
         optionalcolumns = params.get('removecolumns', optionalcolumns)
-        set_PID_black_list(params.get('PID_black_list'))
-
-    # check for conf_file and read parameters
-    if os.path.isfile(conf_file):
-        with open(conf_file) as f:
-            try:
-                config = json.load(f)
-                base_path = config['global']['base_dir']
-            except ValueError as e:
-                logging.warning(f'snmp_cisco_eox:status:JSON load error: {e}')
-                # exit()
+        set_pid_black_list(params.get('PID_black_list'))
 
+    base_path = get_base_path()
     sn2info_dir = base_path + '/sn2info'
     path_found = expand_path(sn2info_dir + '/found/')
     path_not_found = expand_path(sn2info_dir + '/not_found/')
     path_request = expand_path(sn2info_dir + '/request/')
-    status_path = expand_path(base_path + '/status')
 
     path = ['hardware', 'support', 'cisco_contract']
 
-    serials = []
-    serialscontract = []
-    nodes ={}
-
-    logging.info('raw info: %s' % section)
-
-    # if no serial or no PID no service ;-)
-    # remove all empty serials
-    info = [i for i in section if not check_bad_serial(i[1])]
-    # remove all empty PIDs
-    #info = [i for i in info if i[2] != '']
-
-    logging.info('info no empty pid/serail: %s' % info)
-
-    info = compact_info_by_serial(info)
-
-    for phydescr, physerialnum, phymodelname in info:
-
-        logging.info('raw pid  : %s' % phymodelname)
-        logging.info('raw sn   : %s' % physerialnum)
-        logging.info('raw desc : %s' % phydescr)
-
-        # cut PID/SN at first space sign, change to all Uppercase
-        pid = phymodelname.split()[0].upper()
-        physerialnum = physerialnum.split()[0].upper()
-
-        # drop all PIDs on Black List, create default node entry for SN
-        if not pid_on_black_list(pid):
-
-            logging.info('PID not on blacklist: %s' % pid)
-            if phydescr == "":
-                phydescr = 'requested'
-
-            # create list of serial numbers to request contract info for
-            serials.append(physerialnum)
-            key_columns = {'serial_number': physerialnum}
+    for serial in section.keys():
+        if not pid_on_black_list(section[serial].pid):
+            key_columns = {'serial_number': serial}
+            serialscontract = ({
+                'pid': section[serial].pid,
+                'ProductIDDescription': section[serial].phydescr,
+                'is_covered': 'requested',
+            })
+            # find serial in SER-sn2info-covered
+            if os.path.isfile(path_found + serial):
+                serialscontract.update(_create_sn2info_record(path_found + serial, optionalcolumns))
+                serialscontract.pop('serial_number')
+            # find serial in SER-sn2info-notcovered
+            elif os.path.isfile(path_not_found + serial):
+                modifytime = os.path.getmtime(path_not_found + serial)
+                serialscontract.update({
+                    'is_covered': 'NO',
+                    'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+                })
+            else:
+                # create new request in SER-sn2info-request
+                if not os.path.isfile(path_request + serial):
+                    with open(path_request + serial, 'w+') as f:  # only create file (touch)
+                        pass
+                modifytime = os.path.getmtime(path_request + serial)
+                serialscontract.update({
+                    'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+                })
             yield TableRow(
                 path=path,
                 key_columns=key_columns,
-                inventory_columns={
-                    'pid': pid,
-                    'ProductIDDescription': phydescr,
-                }
+                inventory_columns=serialscontract
             )
-            nodes[physerialnum] = {
-                'pid': pid,
-                'ProductIDDescription': phydescr,
-            }
-
-
-    logging.info("serial numbers : %s" % serials)
-
-    for serial in serials:
-        logging.info("Serialnumber: %s " % serial)
-        # find serial in SER-sn2info-covered
-        # print(serial)
-        if os.path.isfile(path_found + serial):
-            logging.info("Serial number covered: %s" % serial)
-            # print(f'found: {serial}')
-            serialscontract.append(_create_sn2info_record(path_found + serial, optionalcolumns))
-            logging.info("Serial number Record: %s" % serialscontract)
-        # find serial in SER-sn2info-notcovered
-        elif os.path.isfile(path_not_found + serial):
-            logging.info("Serial number not covered: %s" % serial)
-            modifytime = os.path.getmtime(path_not_found + serial)
-            serialscontract.append({"serial_number": serial,
-                                    "is_covered": "NO",
-                                    "Last_checked": time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                                    })
-        else:
-            # create new request in SER-sn2info-request
-            logging.info("SER request: %s" % serial)
-            if not os.path.isfile(path_request + serial):
-                with open(path_request + serial, 'w+') as f:
-                    pass
-            modifytime = os.path.getmtime(path_request + serial)
-            serialscontract.append({"serial_number": serial,
-                                    "is_covered": "requested",
-                                    "Last_checked": time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                                    })
-
-    logging.info("Serials contract : %s" % serialscontract)
-    # print(serialscontract)
-    # update default entrys with data
-    for physerialnum in nodes.keys():
-        #print(f'serial from nodes: {physerialnum}')
-        for serial in serialscontract:
-            #print(f'serial from serialscontract {serial}')
-            if physerialnum == serial['serial_number']:
-                key_columns = {'serial_number': physerialnum}
-                # print(f'serial_number from serial {serial["serial_number"]}, serial from nodes: {physerialnum}')
-                temp_ser = serial.copy()
-                temp_ser.pop('serial_number')
-                # print(f'inventory_columns {temp_ser}, key_columns: {key_columns}, path: {path}')
-                yield TableRow(
-                    path=path,
-                    key_columns=key_columns,
-                    inventory_columns=temp_ser
-                )
-
-
-    # # create and write back api status, will be used for active check cisco_api_status
-    # apistatus = {}
-    # lastrun = {}
-    # apiname = 'sn2info'
-    # use_keys = ['serial_number',
-    #             'covered_product_line_end_date',
-    #             'service_contract_number',
-    #             'Last_checked',
-    #             'pid',
-    #             'ProductIDDescription',
-    #             'Last_checked',
-    #             'is_covered',
-    #             ]
-    # thisrun = []
-    #
-    # # add only used keys to status
-    # for entry in node:
-    #     this_entry = {}
-    #     for key in entry.keys():
-    #         if key in use_keys:
-    #             this_entry.update({key: entry.get(key)})
-    #     thisrun.append(this_entry)
-    #
-    # # in CMK v1.5.x global variable g_hostname was replaced by API call host_name()
-    # try:
-    #     _hostname = host_name()
-    # except NameError:
-    #     _hostname = g_hostname
-    #
-    # if os.path.isfile(status_path + _hostname):
-    #     with open(status_path + _hostname) as f:
-    #         try:
-    #             apistatus = json.load(f)
-    #         except ValueError as e:
-    #             logging.warning(f'snmp_cisco_contract:status:JSON load error: {e}')
-    #
-    # if apistatus.get(apiname, None) is not None:
-    #     lastrun = apistatus.get(apiname).get('lastrun', {})
-    #
-    # if lastrun != thisrun:
-    #     apistatus.update({apiname: {}})
-    #     apistatus.get(apiname).update({'lastrun': thisrun})
-    #     with open(status_path + _hostname, 'w') as f:
-    #         json.dump(apistatus, f)
 
 
 register.snmp_section(
@@ -284,7 +187,6 @@ register.snmp_section(
             '13',  # entPhysicalModelName
         ]
     ),
-
     detect=any_of(
         contains('.1.3.6.1.2.1.1.1.0', 'cisco'),
         startswith('.1.3.6.1.2.1.1.2.0', '.1.3.6.1.4.1.9.1')
@@ -294,9 +196,6 @@ register.snmp_section(
 register.inventory_plugin(
     name='inv_cisco_contract',
     inventory_function=inventory_cisco_contract,
-    inventory_default_parameters={
-    },
+    inventory_default_parameters={},
     inventory_ruleset_name='inv_cisco_contract',
 )
-
-
diff --git a/agent_based/inv_cisco_eox.py b/agent_based/inv_cisco_eox.py
index ee287720ffdcbce1362350837b7bd1616d87aa4a..6827dc1b52c4c6338a802cf89191d32e059457a0 100644
--- a/agent_based/inv_cisco_eox.py
+++ b/agent_based/inv_cisco_eox.py
@@ -18,12 +18,15 @@
 # 2020-08-04: code cleanup (_create_eox_record function)
 #             moved node tree from hardware.system.support to hardware.system
 # 2021-07-23: rewrite for CMK 2.0
+# 2021-07-26: cleanup
 
 import os
 import json
-import logging
 import time
 
+from typing import Dict
+from dataclasses import dataclass
+
 from cmk.base.plugins.agent_based.agent_based_api.v1 import (
     register,
     SNMPTree,
@@ -39,19 +42,23 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
 
 # from cmk.base.plugins.agent_based.utils import ciscoapi as utils
 from cmk.base.plugins.agent_based.utils.ciscoapi import (
-    set_loglevel,
     expand_path,
-    compact_info_by_serial,
     check_bad_serial,
     pid_on_black_list,
     sn_on_black_list,
     pid_on_bad_list,
-    set_PID_black_list,
-    set_PID_bad_list,
-    set_SN_black_list,
+    set_pid_bad_list,
+    set_sn_black_list,
+    get_base_path,
 )
 
 
+@dataclass
+class SnmpContractEntry:
+    phydescr: str
+    pid: str
+
+
 def _create_eox_record(eoxfile, optional_columns):
     eox = {}
     if os.path.isfile(eoxfile):
@@ -64,7 +71,7 @@ def _create_eox_record(eoxfile, optional_columns):
         eox['pid'] = eoxrecord.get('EOLProductID')
 
         # Migration Info
-        EOXMigrationDetails = eoxrecord.get('EOXMigrationDetails')
+        eox_migration_details = eoxrecord.get('EOXMigrationDetails')
 
         for key, value in [
             ('ProductIDDescription', eoxrecord.get('ProductIDDescription', '')),
@@ -80,31 +87,49 @@ def _create_eox_record(eoxfile, optional_columns):
             ('EndOfRoutineFailureAnalysisDate', eoxrecord.get('EndOfRoutineFailureAnalysisDate').get('value', '')),
 
             # Migration Info
-            ('MigrationProductId', EOXMigrationDetails.get('MigrationProductId', '')),
-            ('MigrationInformation', EOXMigrationDetails.get('MigrationInformation', '')),
-            ('MigrationProductInfoURL', EOXMigrationDetails.get('MigrationProductInfoURL', '')),
-            ('MigrationProductName', EOXMigrationDetails.get('MigrationProductName', '')),
+            ('MigrationProductId', eox_migration_details.get('MigrationProductId', '')),
+            ('MigrationInformation', eox_migration_details.get('MigrationInformation', '')),
+            ('MigrationProductInfoURL', eox_migration_details.get('MigrationProductInfoURL', '')),
+            ('MigrationProductName', eox_migration_details.get('MigrationProductName', '')),
         ]:
             value = value.strip()
             if value != '' and key not in optional_columns:
                 eox[key] = value
 
         # add serial if EoX from get_eox_by_serial
-        EOXInputType = eoxrecord.get('EOXInputType')
-        if EOXInputType == 'ShowEOXBySerialNumber':
+        eox_input_type = eoxrecord.get('EOXInputType')
+        if eox_input_type == 'ShowEOXBySerialNumber':
             eox['serial_number'] = eoxrecord.get('EOXInputValue')
 
     return eox
 
 
-def parse_inv_cisco_eox(string_table: StringTable):
-    section = string_table
-    return section
-
-
-def inventory_cisco_eox(params, section) -> InventoryResult:
-    set_loglevel()
-
+def parse_inv_cisco_eox(string_table: StringTable) -> Dict[str, SnmpContractEntry]:
+    serials: Dict[str, SnmpContractEntry] = {}
+    for phydescr, physerialnum, phymodelname in string_table:
+        if not check_bad_serial(physerialnum):
+            if not pid_on_black_list(phymodelname):
+                phymodelname = phymodelname.strip()
+                physerialnum = physerialnum.strip()
+                if phymodelname == '':
+                    phymodelname = 'UNSPECIFIED'
+                pid = phymodelname.split()[0].upper()
+                serial = physerialnum.split()[0].upper()
+                if serial not in serials.keys():
+                    serials.update({serial: SnmpContractEntry(
+                        phydescr=phydescr,
+                        pid=pid
+                    )})
+                else:
+                    temp = serials[serial]
+                    temp.phydescr = ', '.join(list(set(temp.phydescr.split(', ') + [phydescr])))
+                    temp.pid = ', '.join(list(set(temp.pid.split(', )') + [pid])))
+                    serials[serial] = temp
+
+    return serials
+
+
+def inventory_cisco_eox(params, section: Dict[str, SnmpContractEntry]) -> InventoryResult:
     always_use_serial = False
     optionalcolumns = ['EndOfSecurityVulSupportDate',
                        'EndOfSWMaintenanceReleases',
@@ -114,32 +139,15 @@ def inventory_cisco_eox(params, section) -> InventoryResult:
                        'MigrationProductName',
                        ]
 
-    base_path = '~/var/ciscoapi'
-    conf_file = '~/etc/ciscoapi/ciscoapi.json'
-    conf_file = os.path.expanduser(conf_file)
-
     # get parameters from wato
     if params:
         always_use_serial = params.get('always_use_serial', always_use_serial)
         optionalcolumns = params.get('removecolumns', optionalcolumns)
-        set_SN_black_list(params.get('SN_black_list'))
-        set_SN_black_list(params.get('PID_black_list'))
-        set_PID_bad_list(params.get('PID_bad_list'))
-
-
-
-
-    # check for conf_file and read parameters
-    if os.path.isfile(conf_file):
-        with open(conf_file) as f:
-            try:
-                config = json.load(f)
-                base_path = config['global']['base_dir']
-            except ValueError as e:
-                logging.warning(f'snmp_cisco_eox:status:JSON load error: {e}')
-                # exit()
-
+        set_sn_black_list(params.get('SN_black_list'))
+        set_sn_black_list(params.get('PID_black_list'))
+        set_pid_bad_list(params.get('PID_bad_list'))
 
+    base_path = get_base_path()
     eox_path = base_path + '/EoX'
     path_found = eox_path + '/found'
     path_not_found = eox_path + '/not_found'
@@ -153,209 +161,109 @@ def inventory_cisco_eox(params, section) -> InventoryResult:
     path_found_ser = expand_path(path_found + '/ser')
     path_not_found_ser = expand_path(path_not_found + '/ser')
 
-    status_path = expand_path(base_path + '/status')
-
     path = ['hardware', 'support', 'cisco_eox']
 
     pids = []
-    pidseox = []
+    pidseox = {}
     serials = []
-    serialseox = []
-    nodes ={}
-
-    logging.info('raw info: %s' % section)
-
-    # if no serial or no PID no service ;-)
-    # remove all empty serials
-    info = [i for i in section if not check_bad_serial(i[1])]
-    # remove all empty PIDs
-    # info = [i for i in info if i[2] != '']
-
-    logging.info('info no empty pid/serail: %s' % info)
-
-    # compact info will remove PIDs on black list and join entry's with identical serial number
-    info = compact_info_by_serial(info)
-
-    for phydescr, physerialnum, phymodelname in info:
-
-        logging.info('raw pid  : %s' % phymodelname)
-        logging.info('raw sn   : %s' % physerialnum)
-        logging.info('raw desc : %s' % phydescr)
-
-        # cut PID/SN at first space sign, change to all Uppercase
-        pid = phymodelname.split()[0].upper()
-        physerialnum = physerialnum.split()[0].upper()
+    serialseox = {}
+    nodes = {}
 
+    for serial in section.keys():
         # drop all PIDs/SNs on Black List
-        if not pid_on_black_list(pid) and not sn_on_black_list(physerialnum):
-            logging.info('PID not on blacklist: %s' % pid)
-
-            if phydescr == "":
-                phydescr = 'requested'
+        if not pid_on_black_list(section[serial].pid) and not sn_on_black_list(serial):
+            if section[serial].phydescr == '':
+                section[serial].phydescr = 'requested'
 
             # check if PID on bad list, if so try serial number
-            if pid_on_bad_list(pid) or always_use_serial:
-                logging.info('PID on bad list: %s' % pid)
-                logging.info('use SN: %s' % physerialnum)
-                serials.append(physerialnum)
+            if pid_on_bad_list(section[serial].pid) or always_use_serial:
+                serials.append(serial)
             else:
-                pids.append(pid)
-                logging.info('PID not on bad list: %s' % pid)
+                pids.append(section[serial].pid)
 
             # create default node entry for PID/SN
-            key_columns = {'serial_number': physerialnum}
+            key_columns = {'serial_number': serial}
             yield TableRow(
                 path=path,
                 key_columns=key_columns,
                 inventory_columns={
-                    'pid': pid,
-                    'ProductIDDescription': phydescr,
+                    'pid': section[serial].pid,
+                    'ProductIDDescription': section[serial].phydescr,
                 }
             )
-            nodes[physerialnum] = {
-                'pid': pid,
-                'ProductIDDescription': phydescr,
+            nodes[serial] = {
+                'pid': section[serial].pid,
+                'ProductIDDescription': section[serial].phydescr,
             }
 
-    logging.info("PIDs : %s" % pids)
-
     # remove duplicates
     pids = list(set(pids))
-    logging.info("PIDs : %s" % pids)
-
     for pid in pids:
-        logging.info("PID: %s " % pid)
         pidfile = pid.replace('/', '_')
         # find pid in PID-EOX-known
         if os.path.isfile(path_found_pid + pidfile):
-            logging.info("PID known: %s" % pid)
-            pidseox.append(_create_eox_record(path_found_pid + pidfile, optionalcolumns))
-            logging.info("EOX Record: %s" % pidseox)
+            pidseox.update({pid: _create_eox_record(path_found_pid + pidfile, optionalcolumns)})
         # find pid in PID-EOX-unknown
         elif os.path.isfile(path_not_found_pid + pidfile):
-            logging.info("PID unknown: %s" % pid)
             modifytime = os.path.getmtime(path_not_found_pid + pidfile)
-            logging.info("PID unknown modify time: %s" % time.strftime('%Y-%m-%d', time.localtime(modifytime)))
-            pidseox.append({"pid": pid,
-                            "EOXExternalAnnouncementDate": "None found",
-                            "Last_checked": time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                            })
+            pidseox.update({pid: {
+                'pid': pid,
+                'EOXExternalAnnouncementDate': 'None found',
+                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+            }})
         else:
             # create new request in PID-EOX-request
-            logging.info("PID request: %s" % pid)
             if not os.path.isfile(path_request_pid + pidfile):
-                with open(path_request_pid + pidfile, 'w+') as f:
+                with open(path_request_pid + pidfile, 'w+') as f:  # only create file
                     # only create file
                     pass
             modifytime = os.path.getmtime(path_request_pid + pidfile)
-            pidseox.append({"pid": pid,
-                            "EOXExternalAnnouncementDate": 'requested',
-                            "Last_checked": time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                            })
+            pidseox.update({pid: {
+                'pid': pid,
+                'EOXExternalAnnouncementDate': 'requested',
+                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+            }})
 
     for serial in serials:
-        logging.info("Serialnumber: %s " % serial)
         # find serial in SER-EOX-known
         if os.path.isfile(path_found_ser + serial):
-            logging.info("Serialnumber known: %s" % serial)
-            serialseox.append(_create_eox_record(path_found_ser + serial, optionalcolumns))
-            logging.info("Serialnumber Record: %s" % serialseox)
+            serialseox.update({serial: _create_eox_record(path_found_ser + serial, optionalcolumns)})
         # find serial in SER-EOX-unknown
         elif os.path.isfile(path_not_found_ser + serial):
-            logging.info("Serialnumber unknown: %s" % serial)
             modifytime = os.path.getmtime(path_not_found_ser + serial)
-            serialseox.append({"serial_number": serial,
-                               "EOXExternalAnnouncementDate": "None found",
-                               "Last_checked": time.strftime('%Y-%m-%d', time.localtime(modifytime))
-                               })
+            serialseox.update({serial: {
+                'serial_number': serial,
+                'EOXExternalAnnouncementDate': 'None found',
+                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime))
+            }})
         else:
             # create new request in SER-EOX-request
-            logging.info("SER request: %s" % serial)
             if not os.path.isfile(path_request_ser + serial):
-                with open(path_request_ser + serial, 'w+') as f:
-                    # only create file
+                with open(path_request_ser + serial, 'w+') as f:  # only create file
                     pass
             modifytime = os.path.getmtime(path_request_ser + serial)
-            serialseox.append({"serial_number": serial,
-                               "EOXExternalAnnouncementDate": 'requested',
-                               "Last_checked": time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-                               })
-
-    logging.info("PID EoX : %s" % pidseox)
-    logging.info("Serials EoX : %s" % serialseox)
+            serialseox.update({serial: {
+                'serial_number': serial,
+                'EOXExternalAnnouncementDate': 'requested',
+                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+            }})
 
     for physerialnum in nodes.keys():
         # create default node entry for PID/SN
         key_columns = {'serial_number': physerialnum}
-        for pid in pidseox:
-            if nodes[physerialnum]['pid'] == pid['pid']:
-                yield TableRow(
-                    path=path,
-                    key_columns=key_columns,
-                    inventory_columns=pid
-                )
-        for serial in serialseox:
-            if physerialnum == serial['serial_number']:
-                temp_ser = serial.copy()
-                temp_ser.pop('serial_number')
-                yield TableRow(
-                    path=path,
-                    key_columns=key_columns,
-                    inventory_columns=temp_ser
-                )
-
-
-    # create and write back api status, will be used for active check cisco_api_status
-    # apistatus = {}
-    # lastrun = {}
-    # apiname = 'eox'
-    # use_keys = ['EndOfSaleDate',
-    #             'ProductIDDescription',
-    #             'ProductBulletinNumber',
-    #             'pid',
-    #             'UpdatedTimeStamp',
-    #             'EOXExternalAnnouncementDate',
-    #             'Last_checked',
-    #             'EndOfSvcAttachDate',
-    #             'LastDateOfSupport',
-    #             ]
-    # thisrun = []
-    # unique_entrys = {}
-
-    # add only used keys to status
-    # for entry in node:
-    #     this_entry = {}
-    #     for key in entry.keys():
-    #         if key in use_keys:
-    #             this_entry.update({key: entry.get(key)})
-    #     # add only unique entries to status
-    #     unique_entrys.update({this_entry.get('pid'): this_entry})
-
-    # create list for unique entries by PID
-    # for key in unique_entrys.keys():
-    #     thisrun.append(unique_entrys.get(key))
-
-    # in CMK v1.5.x global variable g_hostname was replaced by API call host_name()
-    # try:
-    #     _hostname = host_name()
-    # except NameError:
-    #     _hostname = g_hostname
-
-    # if os.path.isfile(status_path + _hostname):
-    #     with open(status_path + _hostname) as f:
-    #         try:
-    #             apistatus = json.load(f)
-    #         except ValueError:
-    #             logging.warning(f'snmp_cisco_eox:status:JSON load error: {status_path + _hostname}')
-    #
-    # if apistatus.get(apiname, None) is not None:
-    #     lastrun = apistatus.get(apiname).get('lastrun', {})
-    #
-    # if lastrun != thisrun:
-    #     apistatus.update({apiname: {}})
-    #     apistatus.get(apiname).update({'lastrun': thisrun})
-    #     with open(status_path + _hostname, 'w') as f:
-    #         json.dump(apistatus, f)
+        if nodes[physerialnum]['pid'] in pidseox.keys():
+            yield TableRow(
+                path=path,
+                key_columns=key_columns,
+                inventory_columns=pidseox[nodes[physerialnum]['pid']]
+            )
+        if physerialnum in serialseox.keys():
+            serialseox[physerialnum].pop('serial_number')
+            yield TableRow(
+                path=path,
+                key_columns=key_columns,
+                inventory_columns=serialseox[physerialnum]
+            )
 
 
 register.snmp_section(
@@ -379,7 +287,6 @@ register.snmp_section(
 register.inventory_plugin(
     name='inv_cisco_eox',
     inventory_function=inventory_cisco_eox,
-    inventory_default_parameters={
-    },
+    inventory_default_parameters={},
     inventory_ruleset_name='inv_cisco_eox',
 )
diff --git a/agent_based/inv_cisco_psirt.py b/agent_based/inv_cisco_psirt.py
index f337914e8cd91fb58f95c91b286779143a88e57c..26955bd8d2b9453b74fc332a12db5237ba1ffff4 100644
--- a/agent_based/inv_cisco_psirt.py
+++ b/agent_based/inv_cisco_psirt.py
@@ -18,19 +18,22 @@
 #              node = inv_tree('software.support.cisco_psirt.')
 #             changed variable host_name to _hostname for cmk 1.5
 # 2021-07-24: rewritten for CMK 2.0
+# 2021-07-26: cleanup
+#
+# ToDo: add number of psirt records to status information
 #
 
 import os
 import json
-import logging
 import time
 from typing import List
+from dataclasses import dataclass
+
 from cmk.base.plugins.agent_based.agent_based_api.v1 import (
     register,
     SNMPTree,
     contains,
     any_of,
-    OIDEnd,
     startswith,
     TableRow,
     Attributes,
@@ -41,70 +44,90 @@ from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
     InventoryResult,
 )
 
-# from cmk.base.plugins.agent_based.utils import ciscoapi as utils
 from cmk.base.plugins.agent_based.utils.ciscoapi import (
-    set_loglevel,
     expand_path,
-    compact_info_by_serial,
-    check_bad_serial,
-    pid_on_black_list,
-    sn_on_black_list,
-    pid_on_bad_list,
-    set_PID_black_list,
-    set_PID_bad_list,
-    set_SN_black_list,
+    get_base_path,
 )
 
 
-def parse_inv_cisco_psirt(string_table: List[StringTable]):
-    section = string_table
-    return section
-
-
-def inventory_cisco_psirt(params, section) -> InventoryResult:
-    def create_psirt_record(filepath, filename, not_updated, dont_show_older_then, optionalcolumns):
-        psirtfile = filepath + filename
-        advisories = {}
-        if os.path.isfile(psirtfile):
-            with open(psirtfile) as f:
-                psirtrecord = json.load(f)
-            advisories = psirtrecord.get('advisories')
-            remove_advisories = []
-            for advisory in advisories:
-                lastUpdated = advisory.get('lastUpdated', 'T1980-01-01').split('T')[0]
-                if lastUpdated == '':
-                    lastUpdated = '1980-01-01'
-                if (not_updated > int(
-                        (time.time() - time.mktime(time.strptime(lastUpdated, '%Y-%m-%d'))) / 86400)) and (
-                        lastUpdated >= dont_show_older_then):
-                    advisory.update({'installed_version': filename})
-                    advisory.update({'lastUpdated': advisory.get('lastUpdated', 'T1980-01-01').split('T')[0]})
-                    advisory.update({'firstPublished': advisory.get('firstPublished', 'T1980-01-01').split('T')[0]})
-                    advisory.update({'bugIDs': ', '.join(advisory.get('bugIDs', ''))})
-                    advisory.update({'firstFixed': ', '.join(advisory.get('firstFixed', ''))})
-                    advisory.update({'cves': ', '.join(advisory.get('cves', ''))})
-                    advisory.update({'cwe': ', '.join(advisory.get('cwe', ''))})
-
-                    keys = advisory.keys()
-                    for column in optionalcolumns:
-                        if column in keys:
-                            advisory.pop(column)
-                else:
-                    # add old advisories to remove list
-                    remove_advisories.insert(0, advisories.index(advisory))
-
-
-
-            # remove advisories older then not_updated
-            # if len(remove_advisories) > 0:
-            #    node['removed_advisories'] = len(remove_advisories)
-            # for index in remove_advisories:
-            #    advisories.pop(index)
-
-        logging.info('snmp_cisco_psirt:create_psirt_recort:advisories: %s' % advisories)
-        return advisories
-
-    def get_OS_Version(cw_version, sysdescription, pids):
+@dataclass
+class SnmpPsirt:
+    version: str
+    product_family: str
+
+
+def _create_psirt_record(filepath, filename, not_updated, dont_show_older_then, optionalcolumns):
+    psirtfile = filepath + filename
+    advisories = {}
+    if os.path.isfile(psirtfile):
+        with open(psirtfile) as f:
+            psirtrecord = json.load(f)
+        advisories = psirtrecord.get('advisories')
+        remove_advisories = []
+        for advisory in advisories:
+            last_updated = advisory.get('lastUpdated', 'T1980-01-01').split('T')[0]
+            if last_updated == '':
+                last_updated = '1980-01-01'
+            if (not_updated > int(
+                    (time.time() - time.mktime(time.strptime(last_updated, '%Y-%m-%d'))) / 86400)) and (
+                    last_updated >= dont_show_older_then):
+                advisory.update({'installed_version': filename})
+                advisory.update({'lastUpdated': advisory.get('lastUpdated', 'T1980-01-01').split('T')[0]})
+                advisory.update({'firstPublished': advisory.get('firstPublished', 'T1980-01-01').split('T')[0]})
+                advisory.update({'bugIDs': ', '.join(advisory.get('bugIDs', ''))})
+                advisory.update({'firstFixed': ', '.join(advisory.get('firstFixed', ''))})
+                advisory.update({'cves': ', '.join(advisory.get('cves', ''))})
+                advisory.update({'cwe': ', '.join(advisory.get('cwe', ''))})
+
+                keys = advisory.keys()
+                for column in optionalcolumns:
+                    if column in keys:
+                        advisory.pop(column)
+            else:
+                # add old advisories to remove list
+                remove_advisories.insert(0, advisories.index(advisory))
+
+        # remove advisories older then not_updated
+        # if len(remove_advisories) > 0:
+        #    node['removed_advisories'] = len(remove_advisories)
+        # for index in remove_advisories:
+        #    advisories.pop(index)
+    return advisories
+
+
+def _get_profuct_family(sysdescription: str) -> str:
+    if ' IOS ' in sysdescription:
+        if ' IOS-XE ' in sysdescription or 'IOSXE' in sysdescription or 'IOS Software [' in sysdescription:
+            return 'IOS-XE'
+        else:
+            return 'IOS'
+    elif 'NX-OS' in sysdescription:
+        if ' aci' in sysdescription or 'APIC ' in sysdescription:
+            return 'APIC'
+        else:
+            return 'NEXUS'
+    elif 'APIC ' in sysdescription:
+        return 'APIC'
+    elif 'Cisco Controller' in sysdescription:
+        return 'WLC'
+    elif 'Cisco Adaptive Security Appliance Version' in sysdescription:
+        return 'ASA'
+    elif 'Cisco PIX Security Appliance Version' in sysdescription:
+        return 'PIX'
+    elif ' AsyncOS ' in sysdescription:
+        if 'Model C' in sysdescription:
+            return 'ESA'
+        elif 'Model M' in sysdescription:
+            return 'SMA'
+        elif 'Model S' in sysdescription:
+            return 'WSA'
+    elif 'Integrated Management Controller' in sysdescription:
+        return 'IMC'
+    else:
+        return 'no_family_found_or_set'
+
+
+def _get_os_version(cw_version, sysdescription, pids):
         version = None
 
         if str(cw_version).find('CW_VERSION$') == 0:
@@ -131,110 +154,63 @@ def inventory_cisco_psirt(params, section) -> InventoryResult:
 
         return version
 
-    set_loglevel()
-
-    # disable_psirt = False
-    optionalcolumns = ['publicationUrl',
-                       'summary',
-                       'cwe',
-                       'cves',
-                       'productNames',
-                       'iosRelease',
-                       'ipsSignatures',
-                       'cvrfUrl',
-                       'ovalUrl',
-                       ]
+
+def parse_inv_cisco_psirt(string_table: List[StringTable]) -> SnmpPsirt:
+
+    cw_version, sysdescription = string_table[0][0]
+
+    pids = string_table[1]
+
+    return SnmpPsirt(
+        version=_get_os_version(cw_version, sysdescription, pids),
+        product_family=_get_profuct_family(sysdescription)
+    )
+
+
+def inventory_cisco_psirt(params, section: SnmpPsirt) -> InventoryResult:
+    optionalcolumns = [
+        'publicationUrl',
+        'summary',
+        'cwe',
+        'cves',
+        'productNames',
+        'iosRelease',
+        'ipsSignatures',
+        'cvrfUrl',
+        'ovalUrl',
+    ]
     product_family = None
     psirt_type = None
-    # version = ''
     not_updated = 10000000
-    dont_show_older_then = '0000-00-00'
-    conf_file = '~/etc/ciscoapi/ciscoapi.json'
-    conf_file = os.path.expanduser(conf_file)
-    psirt_status = {}
-    # prepare path variables (expand homedir and add '/')
-    base_path = '~/var/ciscoapi'
-    cw_version, sysdescription = section[0][0]
-    pids = section[1]
+    dont_show_older_then = '2000-01-01'
 
     # get parameters from wato
     if params:
         optionalcolumns = params.get('removecolumns', optionalcolumns)
         psirt_type = params.get('psirt_type', psirt_type)
         if type(psirt_type) == dict:
-            product_family = psirt_type.get('product_family', product_family)  # .upper()
+            product_family = psirt_type.get('product_family')  # .upper()
             not_updated = psirt_type.get('not_updated', not_updated)
-            dont_show_older_then = psirt_type.get('dont_show_older_then', dont_show_older_then)
-
-    # get OS version
-    version = get_OS_Version(cw_version, sysdescription, pids)
-    logging.info('snmp_cisco_psirt:version: %s' % version)
+            dont_show_older_then = psirt_type.get('dont_show_older_then')
 
     if not product_family:
         # try to get product family form system description
-        if ' IOS ' in sysdescription:
-            if ' IOS-XE ' in sysdescription or 'IOSXE' in sysdescription or 'IOS Software [' in sysdescription:
-                product_family = 'IOS-XE'
-            else:
-                product_family = 'IOS'
-        elif 'NX-OS' in sysdescription:
-            if ' aci' in sysdescription or 'APIC ' in sysdescription:
-                product_family = 'APIC'
-            else:
-                product_family = 'NEXUS'
-        elif 'APIC ' in sysdescription:
-            product_family = 'APIC'
-        elif 'Cisco Controller' in sysdescription:
-            product_family = 'WLC'
-        elif 'Cisco Adaptive Security Appliance Version' in sysdescription:
-            product_family = 'ASA'
-        elif 'Cisco PIX Security Appliance Version' in sysdescription:
-            product_family = 'PIX'
-        elif ' AsyncOS ' in sysdescription:
-            if 'Model C' in sysdescription:
-                product_family = 'ESA'
-            elif 'Model M' in sysdescription:
-                product_family = 'SMA'
-            elif 'Model S' in sysdescription:
-                product_family = 'WSA'
-        elif 'Integrated Management Controller' in sysdescription:
-            product_family = 'IMC'
-        else:
-            product_family = 'no_family_found_or_set'
-            logging.info('snmp_cisco_psirt:no_family_found_or_set: %s' % sysdescription)
+        product_family = section.product_family
     elif psirt_type in ['IOS', 'IOS-XE']:
         product_family = psirt_type
 
-    logging.info('snmp_cisco_psirt:product_family: %s' % product_family)
-
-    # check for conf_file and read parameters
-    if os.path.isfile(conf_file):
-        with open(conf_file) as f:
-            try:
-                config = json.load(f)
-                base_path = config['global']['base_dir']
-            except ValueError as e:
-                logging.warning(f'snmp_cisco_eox:status:JSON load error: {e}')
-
+    # set paths
+    base_path = get_base_path()
     psirt_dir = base_path + '/psirt'
-    status_path = expand_path(base_path + '/status')
-
-    path = ['software', 'support', 'cisco_psirt']
-    yield Attributes(
-        path=path,
-        inventory_attributes={
-            'os_version': version,
-            'Product_family': product_family,
-        })
-
     path_found = psirt_dir + '/found/'
     path_not_found = psirt_dir + '/not_found/'
     path_request = psirt_dir + '/request/'
+
     if product_family in ['IOS', 'IOS-XE']:
         path_found = path_found + product_family
         path_not_found = path_not_found + product_family
         path_request = path_request + product_family
-        psirt = version
+        psirt = section.version
     else:
         path_found = path_found + 'family'
         path_not_found = path_not_found + 'family'
@@ -245,36 +221,31 @@ def inventory_cisco_psirt(params, section) -> InventoryResult:
     path_not_found = expand_path(path_not_found)
     path_request = expand_path(path_request)
 
+    # set defaults for inventory entries
+    path = ['software', 'support', 'cisco_psirt']
+    attribute = {
+        'os_version': section.version,
+        'Product_family': product_family,
+    }
+
     # psirt record found
     if os.path.isfile(path_found + psirt):
-        logging.info('PSIRT:found: %s' % psirt)
         modifytime = os.path.getmtime(path_found + psirt)
-        yield Attributes(
-            path=path,
-            inventory_attributes={
-                'status': 'found',
-                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-            })
-
-        # psirt_status.update(node)
+        attribute.update({
+            'status': 'found',
+            'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+        })
 
         # create psirt advisory list
-        if product_family in ['IOS', 'IOS-XE']:  # do not remove entrys from IOS(XE)
-            advisories = create_psirt_record(path_found, psirt, 10000000, '2000-01-01', optionalcolumns)
+        if product_family in ['IOS', 'IOS-XE']:  # do not remove entry's from IOS(XE)
+            advisories = _create_psirt_record(path_found, psirt, 10000000, '2000-01-01', optionalcolumns)
         else:
-            advisories = create_psirt_record(path_found, psirt, not_updated, dont_show_older_then, optionalcolumns)
+            advisories = _create_psirt_record(path_found, psirt, not_updated, dont_show_older_then, optionalcolumns)
             if dont_show_older_then != '0000-00-00':
-                yield Attributes(
-                    path=path,
-                    inventory_attributes={
-                        'dont_show_older_than': dont_show_older_then,
-                    })
+                attribute['dont_show_older_than'] = dont_show_older_then
             if not_updated != 10000000:
-                yield Attributes(
-                    path=path,
-                    inventory_attributes={
-                        'dont_show_not_updated_since': not_updated,
-                    })
+                attribute['dont_show_not_updated_since'] = not_updated
+
         for advisory in advisories:
             key_columns = {'advisoryId': advisory.get('advisoryId')}
             advisory.pop('advisoryId')
@@ -283,92 +254,29 @@ def inventory_cisco_psirt(params, section) -> InventoryResult:
                 key_columns=key_columns,
                 inventory_columns=advisory,
             )
-        # if optionalcolumns is not None:
-        #     for advisory in node:
-        #         # remove optional columns
-        #         logging.info('remove columns: %s' % optionalcolumns)
-        #         for column in optionalcolumns:
-        #             logging.info('remove column: %s' % column)
-        #             advisory.pop(column, None)
+
     # psirt record not found exists
     elif os.path.isfile(path_not_found + psirt):
-        logging.info('PSIRT:notfound: %s' % psirt)
         modifytime = os.path.getmtime(path_not_found + psirt)
-        yield Attributes(
-            path=path,
-            inventory_attributes={
-                'status': 'not found',
-                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-            })
-        # psirt_status.update(node)
+        attribute.update({
+            'status': 'not found',
+            'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+        })
     elif psirt != 'no_family_found_or_set':
         # create new request
-        logging.info('PSIRT:request: %s' % psirt)
         if not os.path.isfile(path_request + psirt):
-            with open(path_request + psirt, 'w+') as f:
+            with open(path_request + psirt, 'w+') as f:  # only create file (touch)
                 pass
         modifytime = os.path.getmtime(path_request + psirt)
-        yield Attributes(
-            path=path,
-            inventory_attributes={
-                'status': 'requested',
-                'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
-            })
-        # psirt_status.update(node)
-
-    # # create and write back api status, will be used for active check cisco_api_status
-    # apistatus = {}
-    # lastrun = {}
-    # laststatus = {}
-    # thisrun = []
-    # apiname = 'psirt'
-    # rewrite = False
-    # use_keys = ['sir', 'advisoryTitle', 'lastUpdated']
-    #
-    # if psirt_status != {}:
-    #
-    #     # in CMK v1.5.x global variable g_hostname was replaced by API call host_name()
-    #     try:
-    #         _hostname = host_name()
-    #     except NameError:
-    #         _hostname = g_hostname
-    #
-    #     if os.path.isfile(status_path + _hostname):
-    #         with open(status_path + _hostname) as f:
-    #             try:
-    #                 apistatus = json.load(f)
-    #             except ValueError as e:
-    #                 logging.warning(f'{_hostname}:snmp_cisco_psirt:status:JSON load error: {e}')
-    #
-    #     if apistatus.get(apiname, None) is not None:
-    #         lastrun = apistatus.get(apiname).get('lastrun', {})
-    #         laststatus.update({'status': apistatus.get(apiname).get('status', '')})
-    #         laststatus.update({'product_family': apistatus.get(apiname).get('product_family', '')})
-    #         laststatus.update({'Last_checked': apistatus.get(apiname).get('Last_checked', '')})
-    #     else:
-    #         apistatus.update({apiname: {}})
-    #
-    #     if psirt_status != laststatus:
-    #         apistatus.get(apiname).update(psirt_status)
-    #         rewrite = True
-    #
-    #     if psirt_status != node:
-    #         # cleanup psirt infos for status
-    #         advisories = node.get('advisories', {})
-    #         if advisories != {}:
-    #             for entry in advisories:
-    #                 this_entry = {}
-    #                 for key in entry.keys():
-    #                     if key in use_keys:
-    #                         this_entry.update({key: entry.get(key)})
-    #                 thisrun.append(this_entry)
-    #
-    #         if lastrun != thisrun:
-    #             apistatus.get(apiname).update({'lastrun': thisrun})
-    #             rewrite = True
-    #     if rewrite:
-    #         with open(status_path + _hostname, 'w') as f:
-    #             json.dump(apistatus, f)
+        attribute.update({
+            'status': 'requested',
+            'Last_checked': time.strftime('%Y-%m-%d', time.localtime(modifytime)),
+        })
+
+    yield Attributes(
+        path=path,
+        inventory_attributes=attribute,
+    )
 
 
 register.snmp_section(
@@ -384,7 +292,6 @@ register.snmp_section(
         SNMPTree(
             base='.1.3.6.1.2.1.47.1.1.1.1',  # ENTITY-MIB::entPhysicalEntry
             oids=[
-                # OIDEnd(),  # index of entry
                 '10',  # entPhysicalSoftwareRev
                 '13',  # entPhysicalModelName
                 '5',  # entPhysicalClass
@@ -400,7 +307,6 @@ register.snmp_section(
 register.inventory_plugin(
     name='inv_cisco_psirt',
     inventory_function=inventory_cisco_psirt,
-    inventory_default_parameters={
-    },
+    inventory_default_parameters={},
     inventory_ruleset_name='inv_cisco_psirt',
 )
diff --git a/agent_based/utils/ciscoapi.py b/agent_based/utils/ciscoapi.py
index 4ae674c10c2445bd11fbfa8f060fa10a1a829bea..8d879e489004237ddc19f75dc13b6f788ea43beb 100644
--- a/agent_based/utils/ciscoapi.py
+++ b/agent_based/utils/ciscoapi.py
@@ -17,35 +17,52 @@
 import os
 import logging
 import re
+import json
 from typing import List
+
 #
 # global variables
 #
 # list of PIDs to drop
 g_PID_black_list = ['BUILT-IN', 'MICRON', 'C400-MTFDD']
 # list of PIDs to try by serial number
-g_PID_bad_list = ['UNSPECIFIED', 'FABRIC', 'ASA', 'C2611XM-2FE', 'FTLX8570D3BCL', 'FTLF8519P2BCL', 'FTLX8571D3BCL', 'FTRJ-8519-7D', 'PLRXPL-SC-S43']  #
+g_PID_bad_list = ['UNSPECIFIED', 'FABRIC', 'ASA', 'C2611XM-2FE', 'FTLX8570D3BCL', 'FTLF8519P2BCL', 'FTLX8571D3BCL',
+                  'FTRJ-8519-7D', 'PLRXPL-SC-S43']  #
 # list of S/Ns to drop
 g_SN_black_list = []
 
 
-def set_PID_black_list(PID_black_list: List[str]):
+def get_base_path() -> str:
+    conf_file = os.path.expanduser('~/etc/ciscoapi/ciscoapi.json')
+    base_path = '~/var/ciscoapi'
+    # check for conf_file and read parameters
+    if os.path.isfile(conf_file):
+        with open(conf_file) as f:
+            try:
+                config = json.load(f)
+                base_path = config['global'].get('base_dir', base_path)
+            except ValueError as e:
+                logging.warning(f'inv_cisco_contract:status:JSON load error: {e}')
+    return base_path
+
+
+def set_pid_black_list(pid_black_list: List[str]):
     global g_PID_black_list
-    if PID_black_list:
-        g_PID_black_list = list(set(g_PID_black_list + PID_black_list))
-    logging.info(f'PID_black_list: {g_PID_black_list}')
+    if pid_black_list:
+        g_PID_black_list = list(set(g_PID_black_list + pid_black_list))
+
+
+def set_pid_bad_list(pid_bad_list: List[str]):
+    global g_PID_bad_list
+    if pid_bad_list:
+        g_PID_bad_list = list(set(g_PID_bad_list + pid_bad_list))
 
-def set_PID_bad_list(PID_bad_list: List[str]):
-    global  g_PID_bad_list
-    if PID_bad_list:
-        g_PID_bad_list = list(set(g_PID_bad_list + PID_bad_list))
-    logging.info(f'PID_bad_list: {g_PID_bad_list}')
 
-def set_SN_black_list(SN_black_list: List[str]):
+def set_sn_black_list(sn_black_list: List[str]):
     global g_SN_black_list
-    if SN_black_list:
-        SN_black_list = list(set(g_SN_black_list + SN_black_list))
-    logging.info((f'SN_black_list: {SN_black_list}'))
+    if sn_black_list:
+        g_SN_black_list = list(set(g_SN_black_list + sn_black_list))
+
 
 # check if dir exists, if not try to create it.
 # return True if dir exists or creation was ok.
@@ -73,9 +90,9 @@ def expand_path(path):
 
 
 # returns True if SN on black list
-def sn_on_black_list(SN):
+def sn_on_black_list(serial):
     global g_SN_black_list
-    if SN.upper() in g_SN_black_list:
+    if serial.upper() in g_SN_black_list:
         return True
     return False
 
@@ -117,7 +134,7 @@ def check_bad_serial(serial):
 
     # remove all chars from string, except allowedchars
     allowedchars = re.compile('[^a-zA-Z0-9_=\-\+\.\\\]')
-    cleanserial = allowedchars.sub('',serial).strip()
+    cleanserial = allowedchars.sub('', serial).strip()
 
     logging.info('Check_bad_serial:cleanserial: %s' % cleanserial)
 
@@ -136,39 +153,3 @@ def set_loglevel():
     logging.basicConfig(level=logging.WARNING)
     # has no effect as long previous command is active (is by design)
     logging.basicConfig(level=logging.INFO)
-
-
-
-# compact info will remove PIDs on black list and join entry's with identical serial number
-# PIDs will by all uppercase and cut at the first space sign
-def compact_info_by_serial(info):
-    # compress module list by serial
-    # (base found on http://stackoverflow.com/questions/19811418/finding-duplicates-in-a-list-of-lists,
-    # answer from Mark Reed (thanks) http://stackoverflow.com/users/797049/mark-reed, a 'little' bit extended for my needs ;-) )
-    # objective: join module info for modules with identical serial number, if PIDs different then join them as comma separated
-    # string
-    compact_info = {}                                                                                 # empty dictionary
-    for description, serial, pid in info:                                                    # for every module in list
-        if len(pid.split()) != 0:
-            logging.info('compact_info_by_serial:pid_not_empty: pid %s' % pid)
-            pid = pid.split()[0].upper()                                                     # cut PID at first space sign, change to all Uppercase
-        else:
-            pid = 'UNSPECIFIED'
-        if not pid_on_black_list(pid):                                                                # drop blacklisted PIDs
-            if compact_info.get(serial) != None:                                                      # if module already in compact list
-                compact_info[serial] = pid + ',' + compact_info.get(serial, '') + ', ' + description  # update module info, append new PID + already covered module info + new description (pid/description are comma separated)
-            else:                                                                                     # for not already covered modules
-                compact_info[serial] = pid + '|' + description                                        # add key : value 'serial' : 'pid|description', pid and description are '|' separated
-    compact_info = map(list, compact_info.items())                                                    # change dictionary to list of lists , every entry ['serial', 'pid(s)|description(s)']
-    info = []                                                                                         # empty module list
-    for serial, summary in compact_info:                                                              # for every entry in compact_modules (summary is 'pid(s)|description(s)')
-        summary = summary.split('|')                                                                  # split pid and description (summary becomes list ['pid(s)', 'description(s)']
-        pid = ','.join(set(summary[0].split(',')))                                                    # split pid(s), remove duplicates, join as comma separated string
-        description = summary[1]                                                                      # description is comma separated string of description(s)
-        entry = []                                                                                    # create entry per module
-        entry.append(description)                                                                     # 1. description
-        entry.append(serial)                                                                          # 2. serial
-        entry.append(pid)                                                                             # 3. pid
-        info.append(entry)                                                                            # recreate the list of modules
-
-    return info
diff --git a/inv_cisco_support.mkp b/inv_cisco_support.mkp
index 98beed0b82237bae5dcfe94088455d38b073dd97..cdf4854337fad3c970f66c317fac6bc40f6cca77 100644
Binary files a/inv_cisco_support.mkp and b/inv_cisco_support.mkp differ