Create an Inventory Plugin

Goal: Get an inventory about all installed Powershell Modules on Windows Systems

The plugin ps_modules.ps1
This will get installed on the monitored host at C:\ProgramData\checkmk\agent\plugins\

# To get an object from the command running in another shell
# we need to return json and convert it back to an object
# before returning it
function Get-ModulesFromPwsh {
    $pwsh = Get-Command 'pwsh.exe' -ErrorAction SilentlyContinue
    if (-not $pwsh) { return @() }
    $code = @'
Get-Module -ListAvailable -PSEdition Core |
  Select-Object Name,
                @{n='Version';e={$_.Version.ToString()}},
                ModuleBase,
                Path,
                CompatiblePSEditions |
  ConvertTo-Json -Depth 6 -Compress -AsArray
'@
    try {
        $json = & $pwsh.Path -NoProfile -Command $code
	$objs = $json | ConvertFrom-Json
        return $objs
    } catch { @() }
}


$engine     = if ($PSVersionTable.PSEdition -eq 'Core') { 'Core' } else { 'PowerShell5' }
$myPsEdition  = if ($engine -eq 'Core') { 'Core' } else { 'Desktop' }
$res5 = @()
Get-Module -ListAvailable -PSEdition $myPsEdition |    # <-- Filtert nach Manifest-Eignung
	ForEach-Object {
		$res5 += [PSCustomObject]@{
			Name       = $_.Name
			Version    = ($_.Version | ForEach-Object { $_.ToString() }) -join ''
			ModuleBase = $_.ModuleBase
			Engine     = $engine
		}
	}

$ps7 = Get-ModulesFromPwsh
$res7 = @()
$ps7 | ForEach-Object {
	$res7 += [pscustomobject]@{
		Name       = $_.Name
		Version    = ($_.Version | ForEach-Object { $_.ToString() }) -join ''
		ModuleBase = $_.ModuleBase
		Engine     = 'Core'
	}
}

$all = $res5
# pwsh has all powershell5 pathes also for compatibility, we strip these...
$all += $res7 | where-object { $_.ModuleBase -notin $all.ModuleBase }
$dedup = $all 

# Agent-Section ausgeben (eine JSON-Zeile pro Modul)
'<<<psmodules>>>'
foreach ($m in $dedup) {
    $m | ConvertTo-Json -Compress
}

The server side
You need to extend the Check_MK server to interpret the new section the plugin will produce.
Login as the OMD site user and create path and file ~/local/lib/python3/cmk_addons/plugins/powershell/agent_based/psmodules_inventory.py
Additionally copy your ps1 plugin from above here: ~/local/share/check_mk/agents/windows/plugins/ on server.
Now create the python addon.

# ~/local/lib/python3/cmk_addons/plugins/powershell/agent_based/psmodules_inventory.py
# Checkmk 2.4 (Check API v2)
from __future__ import annotations

import json
from typing import Any, Dict, List

from cmk.agent_based.v2 import (
    AgentSection,
    InventoryPlugin,
    TableRow,
)

Section = List[Dict[str, Any]]

def parse_psmodules(string_table: List[List[str]]) -> Section:
    """Rekonstruiert JSON je Zeile und gibt eine Liste von Dicts zurück."""
    out: Section = []
    for row in string_table:
        # Row ist tokenisiert; wir fügen die Zeile wieder zusammen
        js = " ".join(row).strip()
        if not js:
            continue
        try:
            obj = json.loads(js)
            if isinstance(obj, dict):
                out.append(obj)
        except Exception:
            # Ignoriere fehlerhafte Zeilen, um die Section robust zu halten
            continue
    return out

agent_section_psmodules = AgentSection(
    name="psmodules",
    parse_function=parse_psmodules,
)

def inventory_psmodules(section: Section):
    """Erzeugt Inventory-Einträge unter software/applications/powershell/modules."""
    path = ["software", "applications", "powershell", "modules"]
    for item in section:
        name = str(item.get("Name", "")).strip()
        if not name:
            continue
        engine = str(item.get("Engine", "")).strip()
        version = str(item.get("Version", "")).strip()
        base = str(item.get("ModuleBase", "")).strip()

        # Key-Spalten: Name + Engine (Edition). Version als Inventar-Spalte, damit Änderungen sichtbar werden.
        yield TableRow(
            path=path,
            key_columns={"name": name, "engine": engine},
            inventory_columns={"version": version, "module_base": base},
            # status_columns könnten hier optional ergänzt werden, wenn dynamische Zustände benötigt werden.
        )

inventory_plugin_psmodules = InventoryPlugin(
    name="psmodules",
    inventory_function=inventory_psmodules,
    sections=["psmodules"],
)

Packaging the plugin (server)

# make sure folder exists
mkdir -p ~/tmp/check_mk

# create template
mkp template psmodules_inventory

Edit the template/manifest file to your needs

{
  "name": "psmodules_inventory",
  "title": "PowerShell Modules Inventory (PS5.1 & PS7)",
  "version": "1.0.0",
  "author": "Thor himself",
  "description": "Create inventory of PowerShell modules and show in inventory unter software/applications/powershell/modules in the GUI.",
  "download_url": "",
  "version.min_required": "2.4.0",
  "version.packaged": "2.4.0",
  "version.usable_until": null,

  "files": {
    "agent_based": [
      "cmk_addons/plugins/powershell/agent_based/psmodules_inventory.py"
    ],
    "agents": [
      "plugins/ps_modules.ps1"
    ]
  }
}

The pathes in the above file are relative to ~/local/lib/python3/ for agent_based and ~/local/share/check_mk/agents/ for the agents pathes.

Create the package & install

mkp package ~/tmp/check_mk/psmodules_inventory.manifest.temp

# install package to site
mkp add ~/var/check_mk/packages_local/psmodules_inventory-1.0.1.mkp

# enable package in site
mkp enable psmodules_inventory

# check
mkp list
mkp show psmodules_inventory

Optionally you can give that plugin a longer timeout to be processed and activate caching.
On monitored host edit: C:\ProgramData\checkmk\agent\check_mk.user.yml
Make sure to find the ‘plugins’ section and the ‘execution’ section and add your plugin there.

plugins:
    [...]
    execution:
        - pattern     : '$CUSTOM_PLUGINS_PATH$\ps_modules.ps1'
          run         : yes
          async       : yes
          timeout     : 120
          cache_age   : 7200
        [...]
        - pattern     : '$CUSTOM_PLUGINS_PATH$\*.*'
          timeout     : 30
          run         : yes

To upgrade an installed package, create the new version like 1.0.2 and create the package like above: psmodules_inventory-1.0.2.mkp

# update addon
mkp add ~/var/check_mk/packages_local/psmodules_inventory-1.0.2.mkp
mkp enable psmodules_inventory

# rollback from 1.0.2 to 1.0.1
mkp add ~/var/check_mk/packages_local/psmodules_inventory-1.0.1.mkp
mkp enable psmodules_inventory

# disable/remove
mkp disable psmodules_inventory
mkp remove  psmodules_inventory

Leave a Reply