@cryptotaxi247 / netdata-1 / commits / 9b837d836

feat(scripts.d/nagios): auto-invoke Windows script files (.ps1, .bat, .cmd) through their interpreters (#22162)

Ilya Mashchenko committed Apr 7, 2026 at 21:25 UTC 9b837d836791c73fd46791c7b380bf410c3d5312
5 files changed +102 -11
src/go/plugin/scripts.d/collector/nagios/config_schema.json
+3 -3
@@ -258,15 +258,15 @@
258 },
259 "check_name": {
260 "ui:placeholder": "check_ping",
261 - "ui:help": "**Controls chart grouping and metric naming for performance data.**\n\nCharts appear under `Synthetic > Nagios > Perfdata > <check_name>` in the dashboard.\n\nIf omitted, Netdata derives it from the `plugin` basename (e.g. `/usr/lib/nagios/plugins/check_ping` → `check_ping`).\n\n### When to set it\n\n- When multiple jobs run the same plugin (e.g. `check_nrpe`) against different remote checks — set `check_name` to distinguish them (`check_disk`, `check_load`, etc.)\n- **Windows:** Always set it when using `powershell.exe` as `plugin` — otherwise all PowerShell jobs share `Perfdata/powershell`\n\n### Example metric names\n\nWith `check_name: check_memory` and a script that outputs `caches=2380912KB`:\n\n- `nagios.perfdata.check_memory.job.execution_state`\n- `nagios.perfdata.check_memory.bytes_caches`\n- `nagios.perfdata.check_memory.bytes_caches_threshold_state`"
261 + "ui:help": "**Controls chart grouping and metric naming for performance data.**\n\nCharts appear under `Synthetic > Nagios > Perfdata > <check_name>` in the dashboard.\n\nIf omitted, Netdata derives it from the `plugin` basename (e.g. `/usr/lib/nagios/plugins/check_ping` → `check_ping`, `C:\\Netdata\\checks\\check_service.ps1` → `check_service`).\n\n### When to set it\n\n- When multiple jobs run the same plugin (e.g. `check_nrpe`) against different remote checks — set `check_name` to distinguish them (`check_disk`, `check_load`, etc.)\n\n### Example metric names\n\nWith `check_name: check_memory` and a script that outputs `caches=2380912KB`:\n\n- `nagios.perfdata.check_memory.job.execution_state`\n- `nagios.perfdata.check_memory.bytes_caches`\n- `nagios.perfdata.check_memory.bytes_caches_threshold_state`"
262 },
263 "plugin": {
264 "ui:placeholder": "/usr/lib/nagios/plugins/check_ping",
265 - "ui:help": "**Absolute path to the check command.**\n\n### Linux / macOS\n\nThe executable must meet these security requirements:\n- Owned by **root**\n- Not writable by group or others\n- All ancestor directories owned by root and not group/other-writable\n- Must be a regular, executable file\n- Symlinks are resolved before validation\n\nExample:\n```\n/usr/lib/nagios/plugins/check_ping\n```\n\n### Windows\n\nSince `.ps1` scripts cannot be executed directly, point `plugin` to `powershell.exe` and pass the script in `args`:\n```\nC:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\n```\nPath validation is not enforced on Windows — ensure scripts are stored in directories with appropriate ACLs.\n\n### Output format\n\nThe command should return exit code `0` (OK), `1` (WARNING), `2` (CRITICAL), or `3` (UNKNOWN) and may print performance data after `|`:\n```\nSTATUS TEXT | label=value;warn;crit;min;max\n```"
265 + "ui:help": "**Absolute path to the check command.**\n\n### Linux / macOS\n\nThe executable must meet these security requirements:\n- Owned by **root**\n- Not writable by group or others\n- All ancestor directories owned by root and not group/other-writable\n- Must be a regular, executable file\n- Symlinks are resolved before validation\n\nExample:\n```\n/usr/lib/nagios/plugins/check_ping\n```\n\n### Windows\n\nPoint `plugin` directly to a `.ps1`, `.bat`, or `.cmd` script. Netdata automatically invokes `.ps1` scripts through `powershell.exe` and `.bat`/`.cmd` scripts through `cmd.exe`.\n\nExample:\n```\nC:\\Netdata\\checks\\check_service.ps1\n```\nPath validation is not enforced on Windows — ensure scripts are stored in directories with appropriate ACLs.\n\n### Output format\n\nThe command should return exit code `0` (OK), `1` (WARNING), `2` (CRITICAL), or `3` (UNKNOWN) and may print performance data after `|`:\n```\nSTATUS TEXT | label=value;warn;crit;min;max\n```"
266 },
267 "args": {
268 "ui:listFlavour": "list",
269 - "ui:help": "**Arguments passed to the check command after macro expansion.**\n\nYou can use Nagios-style macros like `$ARG1$`, `$HOSTADDRESS$`, `$_SERVICEDBHOST$` — they are expanded before execution.\n\n### Windows (PowerShell)\n\nWhen `plugin` is `powershell.exe`, pass the script path here:\n```\n-NoProfile\n-ExecutionPolicy\nBypass\n-File\nC:\\Netdata\\checks\\check_service.ps1\n```"
269 + "ui:help": "**Arguments passed to the check command after macro expansion.**\n\nYou can use Nagios-style macros like `$ARG1$`, `$HOSTADDRESS$`, `$_SERVICEDBHOST$` — they are expanded before execution.\n\nOn Windows, when `plugin` points to a `.ps1`/`.bat`/`.cmd` script, these arguments are appended after the script path."
270 },
271 "arg_values": {
272 "ui:listFlavour": "list",
src/go/plugin/scripts.d/collector/nagios/init.go
+7
@@ -31,6 +31,13 @@ func (c *Collector) compileConfiguredJob() (compiledJob, error) {
31 return compiledJob{}, err
32 }
33
34 + pluginPath, args, err := rewriteScriptCommand(job.config.Plugin, job.config.Args)
35 + if err != nil {
36 + return compiledJob{}, fmt.Errorf("job '%s': %w", job.config.Name, err)
37 + }
38 + job.config.Plugin = pluginPath
39 + job.config.Args = args
40 +
41 validatedPath, err := c.validatePlugin(job.config.Plugin)
42 if err != nil {
43 return compiledJob{}, fmt.Errorf("job '%s': %w", job.config.Name, err)
src/go/plugin/scripts.d/collector/nagios/metadata.yaml
+7 -8
@@ -185,8 +185,9 @@ modules:
185
186 :::caution
187
188 - - **Linux/macOS:** Using an interpreter (e.g. `/bin/bash`) as `plugin` with a script path in `args` is **discouraged**. Netdata validates the interpreter binary but **cannot verify scripts passed via `args`**. A writable script in `args` is a privilege escalation vector. Instead, make scripts directly executable and point `plugin` to the script itself.
189 - - **Windows:** Since `.ps1` scripts cannot be executed directly, use `powershell.exe` as `plugin` and pass the script in `args`. Path validation is not enforced on Windows — ensure scripts are stored in directories with appropriate ACLs.
188 + **Linux/macOS:** Using an interpreter (e.g. `/bin/bash`) as `plugin` with a script path in `args` is **discouraged**. Netdata validates the interpreter binary but **cannot verify scripts passed via `args`**. A writable script in `args` is a privilege escalation vector. Instead, make scripts directly executable and point `plugin` to the script itself.
189 +
190 + **Windows:** Point `plugin` directly to a `.ps1`, `.bat`, or `.cmd` script — Netdata invokes the correct interpreter automatically. Path validation is not enforced on Windows — ensure scripts are stored in directories with appropriate ACLs.
191
192 :::
193 - title: Install check commands
@@ -416,7 +417,7 @@ modules:
417 check_interval: 1m
418 - name: Windows PowerShell check
419 description: |
419 - On Windows, `.ps1` scripts cannot be executed directly. Point `plugin` to `powershell.exe` and pass the script in `args`. Set `check_name` so that charts are grouped under the script identity instead of `powershell`.
420 + On Windows, point `plugin` directly to a `.ps1` script. Netdata automatically invokes it through `powershell.exe` with `-NoProfile -ExecutionPolicy Bypass -File`. The `.bat` and `.cmd` scripts are also supported (invoked via `cmd.exe /c`).
421
422 **1. Create the script** (e.g., `C:\Netdata\checks\check_service.ps1`):
423
@@ -451,9 +452,7 @@ modules:
452 config: |
453 jobs:
454 - name: service_health_win
454 - check_name: check_service
455 - plugin: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
456 - args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\Netdata\\checks\\check_service.ps1"]
455 + plugin: C:\Netdata\checks\check_service.ps1
456 timeout: 10s
457 check_interval: 1m
458 - name: Remote check via NRPE
@@ -530,9 +529,9 @@ modules:
529 - name: Check works as root but fails under Netdata
530 description: |
531 The Netdata Agent runs as the `netdata` user. If a check needs to read protected files, access SNMP, or connect to local sockets, it must be accessible to the `netdata` user. Test as that user first: `sudo -u netdata /path/to/check`. Common fixes include adding the `netdata` user to the required system group or using `sudo` with a specific NOPASSWD rule for the check command.
533 - - name: Windows checks need an executable entry point
532 + - name: Windows script support
533 description: |
535 - The collector runs the command named in `plugin` directly. On Windows, `.ps1` scripts cannot be executed directly — point `plugin` to the absolute path of `powershell.exe` and pass the script in `args`. Always set `check_name` when using an interpreter so charts are grouped under the script identity instead of `powershell`. Ensure scripts are stored in directories with appropriate ACLs.
534 + On Windows, point `plugin` directly to a `.ps1`, `.bat`, or `.cmd` script. Netdata automatically invokes `.ps1` scripts through `powershell.exe` and `.bat`/`.cmd` scripts through `cmd.exe`. Ensure scripts are stored in directories with appropriate ACLs.
535 alerts:
536 - name: nagios_job_execution_state_warn
537 metric: nagios.job.execution_state
src/go/plugin/scripts.d/collector/nagios/script_rewrite_stub.go new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +//go:build !windows
4 +
5 +package nagios
6 +
7 +// rewriteScriptCommand is a no-op on non-Windows platforms.
8 +// On Linux/macOS, scripts should be directly executable (chmod +x).
9 +func rewriteScriptCommand(pluginPath string, args []string) (string, []string, error) {
10 + return pluginPath, args, nil
11 +}
src/go/plugin/scripts.d/collector/nagios/script_rewrite_windows.go new
+74
@@ -0,0 +1,74 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +//go:build windows
4 +
5 +package nagios
6 +
7 +import (
8 + "fmt"
9 + "os"
10 + "path/filepath"
11 + "strings"
12 +)
13 +
14 +// rewriteScriptCommand detects Windows script files (.ps1, .bat, .cmd)
15 +// and rewrites the command to invoke them through the appropriate interpreter.
16 +// This allows users to set plugin directly to a script path without manually
17 +// configuring the interpreter.
18 +func rewriteScriptCommand(pluginPath string, args []string) (string, []string, error) {
19 + lower := strings.ToLower(pluginPath)
20 +
21 + switch {
22 + case strings.HasSuffix(lower, ".ps1"):
23 + psPath, err := findPowerShell()
24 + if err != nil {
25 + return "", nil, fmt.Errorf("plugin '%s' is a PowerShell script but powershell.exe was not found: %w", pluginPath, err)
26 + }
27 + newArgs := make([]string, 0, 5+len(args))
28 + newArgs = append(newArgs, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", pluginPath)
29 + newArgs = append(newArgs, args...)
30 + return psPath, newArgs, nil
31 +
32 + case strings.HasSuffix(lower, ".bat"), strings.HasSuffix(lower, ".cmd"):
33 + cmdPath, err := findCmd()
34 + if err != nil {
35 + return "", nil, fmt.Errorf("plugin '%s' is a batch script but cmd.exe was not found: %w", pluginPath, err)
36 + }
37 + newArgs := make([]string, 0, 2+len(args))
38 + newArgs = append(newArgs, "/c", pluginPath)
39 + newArgs = append(newArgs, args...)
40 + return cmdPath, newArgs, nil
41 + }
42 +
43 + return pluginPath, args, nil
44 +}
45 +
46 +func findPowerShell() (string, error) {
47 + sysRoot := os.Getenv("SystemRoot")
48 + if sysRoot == "" {
49 + sysRoot = `C:\Windows`
50 + }
51 + path := filepath.Join(sysRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
52 + if _, err := os.Stat(path); err != nil {
53 + return "", fmt.Errorf("powershell.exe not found at %s: %w", path, err)
54 + }
55 + return path, nil
56 +}
57 +
58 +func findCmd() (string, error) {
59 + // ComSpec is the standard Windows env var pointing to cmd.exe.
60 + if comSpec := os.Getenv("ComSpec"); comSpec != "" {
61 + if _, err := os.Stat(comSpec); err == nil {
62 + return comSpec, nil
63 + }
64 + }
65 + sysRoot := os.Getenv("SystemRoot")
66 + if sysRoot == "" {
67 + sysRoot = `C:\Windows`
68 + }
69 + path := filepath.Join(sysRoot, "System32", "cmd.exe")
70 + if _, err := os.Stat(path); err != nil {
71 + return "", fmt.Errorf("cmd.exe not found at %s: %w", path, err)
72 + }
73 + return path, nil
74 +}