@cryptotaxi247 / netdata-1 / commits / f6f0b6f13

feat(scripts.d/nagios): validate plugin path security (#22160)

Ilya Mashchenko committed Apr 7, 2026 at 20:23 UTC f6f0b6f137905cdec4d7b79300dc767bc5ece84a
10 files changed +148 -152
src/go/plugin/go.d/collector/azure_monitor/azureprofiles/default_catalog_test.go
+1 -1
@@ -59,7 +59,7 @@ func TestLoadFromDefaultDirs_StockProfilesUseSelectorShorthand(t *testing.T) {
59 data, err := os.ReadFile(filepath.Join(dir, name))
60 require.NoError(t, err)
61
62 - for _, line := range strings.Split(string(data), "\n") {
62 + for line := range strings.SplitSeq(string(data), "\n") {
63 line = strings.TrimSpace(line)
64 if !strings.HasPrefix(line, "- selector:") {
65 continue
src/go/plugin/go.d/pkg/pathvalidate/validate_unix.go
+21 -16
@@ -56,24 +56,29 @@ func ValidateBinaryPath(path string) (string, error) {
56 return "", fmt.Errorf("binary at %s must be executable", absPath)
57 }
58
59 - // Step 7: Check parent directory
60 - dir := filepath.Dir(absPath)
61 - dirInfo, err := os.Stat(dir)
62 - if err != nil {
63 - return "", fmt.Errorf("directory stat error for %s: %w", dir, err)
64 - }
59 + // Step 7: Check all ancestor directories up to and including root
60 + for dir := filepath.Dir(absPath); ; dir = filepath.Dir(dir) {
61 + dirInfo, err := os.Stat(dir)
62 + if err != nil {
63 + return "", fmt.Errorf("directory stat error for %s: %w", dir, err)
64 + }
65
66 - dirStat, ok := dirInfo.Sys().(*syscall.Stat_t)
67 - if !ok {
68 - return "", fmt.Errorf("unable to get directory stat information for %s", dir)
69 - }
70 - if dirStat.Uid != 0 {
71 - return "", fmt.Errorf("directory %s must be owned by root (current uid: %d)", dir, dirStat.Uid)
72 - }
66 + dirStat, ok := dirInfo.Sys().(*syscall.Stat_t)
67 + if !ok {
68 + return "", fmt.Errorf("unable to get directory stat information for %s", dir)
69 + }
70 + if dirStat.Uid != 0 {
71 + return "", fmt.Errorf("directory %s must be owned by root (current uid: %d)", dir, dirStat.Uid)
72 + }
73 +
74 + if perm := dirInfo.Mode().Perm(); perm&0022 != 0 {
75 + return "", fmt.Errorf("directory %s must not be writable by group/others (current permissions: %s / %04o)",
76 + dir, dirInfo.Mode().String(), perm)
77 + }
78
74 - if perm := dirInfo.Mode().Perm(); perm&0022 != 0 {
75 - return "", fmt.Errorf("directory %s must not be writable by group/others (current permissions: %s / %04o)",
76 - dir, dirInfo.Mode().String(), perm)
79 + if dir == filepath.Dir(dir) {
80 + break
81 + }
82 }
83
84 return absPath, nil
src/go/plugin/scripts.d/collector/nagios/collector.go
+12 -9
@@ -10,6 +10,7 @@ import (
10 "github.com/netdata/netdata/go/plugins/pkg/metrix"
11 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
12 "github.com/netdata/netdata/go/plugins/plugin/framework/vnodes"
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pathvalidate"
14 "github.com/netdata/netdata/go/plugins/plugin/scripts.d/pkg/timeperiod"
15 )
16
@@ -45,11 +46,12 @@ type Collector struct {
46 collectorapi.Base
47 Config `yaml:",inline" json:",inline"`
48
48 - store metrix.CollectorStore
49 - router *perfdataRouter
50 - runner checkRunner
51 - now func() time.Time
52 - vnode vnodes.VirtualNode
49 + store metrix.CollectorStore
50 + router *perfdataRouter
51 + runner checkRunner
52 + validatePlugin func(string) (string, error)
53 + now func() time.Time
54 + vnode vnodes.VirtualNode
55
56 job compiledJob
57 state collectState
@@ -63,10 +65,11 @@ func New() *Collector {
65 UpdateEvery: defaultCollectorUpdateEvery,
66 JobConfig: defaultedJobConfig(JobConfig{}),
67 },
66 - store: metrix.NewCollectorStore(),
67 - router: newPerfdataRouter(defaultPerfdataMetricKeyBudget),
68 - runner: systemCheckRunner{},
69 - now: time.Now,
68 + store: metrix.NewCollectorStore(),
69 + router: newPerfdataRouter(defaultPerfdataMetricKeyBudget),
70 + runner: systemCheckRunner{},
71 + validatePlugin: pathvalidate.ValidateBinaryPath,
72 + now: time.Now,
73 }
74 }
75
src/go/plugin/scripts.d/collector/nagios/collector_test.go
+33 -73
@@ -168,26 +168,6 @@ func TestCollector_Check(t *testing.T) {
168 },
169 wantErr: false,
170 },
171 - "plugin path does not exist": {
172 - config: Config{
173 - JobConfig: JobConfig{
174 - Name: "missing-plugin",
175 - Plugin: filepath.Join(t.TempDir(), "missing-check"),
176 - },
177 - },
178 - wantErr: true,
179 - errMatch: "stat error",
180 - },
181 - "plugin path must be a regular file": {
182 - config: Config{
183 - JobConfig: JobConfig{
184 - Name: "plugin-dir",
185 - Plugin: t.TempDir(),
186 - },
187 - },
188 - wantErr: true,
189 - errMatch: "must be a regular file",
190 - },
171 "valid config": {
172 config: Config{
173 UpdateEvery: 1,
@@ -203,7 +183,7 @@ func TestCollector_Check(t *testing.T) {
183
184 for name, tc := range tests {
185 t.Run(name, func(t *testing.T) {
206 - coll := New()
186 + coll := newTestCollector()
187 coll.runner = &fakeRunner{}
188 coll.Config = tc.config
189
@@ -220,59 +200,33 @@ func TestCollector_Check(t *testing.T) {
200 }
201 }
202
223 -func TestCollector_CheckRevalidatesConfiguredPlugin(t *testing.T) {
224 - dir := t.TempDir()
225 - pluginPath := filepath.Join(dir, "check_mock.sh")
226 - mode := os.FileMode(0o644)
227 - if runtime.GOOS != "windows" {
228 - mode = 0o755
229 - }
230 - require.NoError(t, os.WriteFile(pluginPath, []byte("#!/bin/sh\nexit 0\n"), mode))
231 -
232 - coll := New()
233 - coll.runner = &fakeRunner{}
234 - coll.Config = Config{
235 - UpdateEvery: 1,
236 - JobConfig: JobConfig{
237 - Name: "revalidate",
238 - Plugin: pluginPath,
239 - CheckInterval: confDuration(5 * time.Second),
240 - RetryInterval: confDuration(5 * time.Second),
241 - },
242 - }
243 -
244 - require.NoError(t, coll.Init(context.Background()))
245 - require.NoError(t, os.Remove(pluginPath))
246 -
247 - err := coll.Check(context.Background())
248 - require.Error(t, err)
249 - assert.Contains(t, err.Error(), "stat error")
250 -}
251 -
252 -func TestCollector_CheckRejectsNonExecutablePlugin(t *testing.T) {
253 - if runtime.GOOS == "windows" {
254 - t.Skip("windows does not use unix executable bits")
203 +func TestIsKnownInterpreter(t *testing.T) {
204 + tests := map[string]struct {
205 + path string
206 + want bool
207 + }{
208 + "bash": {path: "/bin/bash", want: true},
209 + "sh": {path: "/bin/sh", want: true},
210 + "python3": {path: "/usr/bin/python3", want: true},
211 + "python3 versioned": {path: "/usr/bin/python3.11", want: true},
212 + "powershell": {path: "/usr/bin/powershell", want: true},
213 + "powershell.exe": {path: "/powershell.exe", want: true},
214 + "pwsh": {path: "/usr/bin/pwsh", want: true},
215 + "env": {path: "/usr/bin/env", want: true},
216 + "node": {path: "/usr/bin/node", want: true},
217 + "cmd.exe": {path: "/cmd.exe", want: true},
218 + "php": {path: "/usr/bin/php", want: true},
219 + "check_ping": {path: "/usr/lib/nagios/plugins/check_ping", want: false},
220 + "check_http": {path: "/usr/lib/nagios/plugins/check_http", want: false},
221 + "custom script": {path: "/opt/netdata/checks/check_api.sh", want: false},
222 + "custom exe": {path: "/opt/checks/check_service.exe", want: false},
223 }
224
257 - dir := t.TempDir()
258 - pluginPath := filepath.Join(dir, "check_mock.sh")
259 - require.NoError(t, os.WriteFile(pluginPath, []byte("#!/bin/sh\nexit 0\n"), 0o644))
260 -
261 - coll := New()
262 - coll.runner = &fakeRunner{}
263 - coll.Config = Config{
264 - UpdateEvery: 1,
265 - JobConfig: JobConfig{
266 - Name: "not-executable",
267 - Plugin: pluginPath,
268 - CheckInterval: confDuration(5 * time.Second),
269 - RetryInterval: confDuration(5 * time.Second),
270 - },
225 + for name, tc := range tests {
226 + t.Run(name, func(t *testing.T) {
227 + assert.Equal(t, tc.want, isKnownInterpreter(tc.path))
228 + })
229 }
272 -
273 - err := coll.Check(context.Background())
274 - require.Error(t, err)
275 - assert.Contains(t, err.Error(), "must be executable")
230 }
231
232 func TestCompileCollectorConfig_CadenceWarning(t *testing.T) {
@@ -351,7 +305,7 @@ func TestCollector_Init(t *testing.T) {
305
306 for name, tc := range tests {
307 t.Run(name, func(t *testing.T) {
354 - coll := New()
308 + coll := newTestCollector()
309 coll.runner = &fakeRunner{}
310 coll.Config = tc.config
311 require.NoError(t, coll.Init(context.Background()))
@@ -930,7 +884,7 @@ func TestCollector_Collect(t *testing.T) {
884 t.Run(name, func(t *testing.T) {
885 now := time.Date(2026, 3, 21, 12, 0, 0, 0, time.UTC)
886 runner := &fakeRunner{results: tc.results}
933 - coll := New()
887 + coll := newTestCollector()
888 coll.runner = runner
889 coll.now = func() time.Time { return now }
890 coll.Config = tc.config
@@ -1270,6 +1224,12 @@ func writeTestPluginFile(t *testing.T, name string) string {
1224 return path
1225 }
1226
1227 +func newTestCollector() *Collector {
1228 + coll := New()
1229 + coll.validatePlugin = func(path string) (string, error) { return path, nil }
1230 + return coll
1231 +}
1232 +
1233 func confDuration(d time.Duration) confopt.Duration { return confopt.Duration(d) }
1234
1235 func findChartDimensionByContext(specYAML *charttpl.Spec, context string) (charttpl.Dimension, bool) {
src/go/plugin/scripts.d/collector/nagios/config_schema.json
+13 -5
@@ -20,12 +20,12 @@
20 },
21 "check_name": {
22 "title": "Check name",
23 - "description": "Name that identifies this check type for chart grouping and metric naming. Charts appear under `Perfdata/<check_name>` in the dashboard. When `plugin` points to an interpreter, set this to the actual check identity — otherwise all interpreter-based jobs share a single chart section. If omitted, derived from the `plugin` basename.",
23 + "description": "Name that identifies this check type for chart grouping and metric naming. Charts appear under `Perfdata/<check_name>` in the dashboard. If omitted, derived from the `plugin` basename.",
24 "type": "string"
25 },
26 "plugin": {
27 "title": "Plugin path",
28 - "description": "Absolute path to the Nagios-compatible command entry point. This can be a packaged plugin, your own executable, or an interpreter. When `plugin` points to an interpreter, pass the script path in `args` and set `check_name` to the actual check identity. The command should return exit code 0, 1, 2, or 3 and may print performance data after `|`.",
28 + "description": "Absolute path to the Nagios-compatible check command. On Linux/macOS, must be root-owned and not writable by group or others. The command should return exit code 0, 1, 2, or 3 and may print performance data after `|`.",
29 "type": "string"
30 },
31 "args": {
@@ -258,13 +258,15 @@
258 },
259 "check_name": {
260 "ui:placeholder": "check_ping",
261 - "ui:help": "Controls the chart section name and metric context for performance data. Required when plugin is an interpreter."
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`"
262 },
263 "plugin": {
264 - "ui:placeholder": "/usr/lib/nagios/plugins/check_ping"
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```"
266 },
267 "args": {
267 - "ui:listFlavour": "list"
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```"
270 },
271 "arg_values": {
272 "ui:listFlavour": "list",
@@ -285,6 +287,12 @@
287 "time_periods": {
288 "ui:listFlavour": "list"
289 },
290 + "environment": {
291 + "ui:help": "**Extra environment variables for the check process.**\n\nChecks run with a limited baseline environment (only `PATH`, `HOME`, `USER`, `TZ`, `TMPDIR`, `SHELL`, `LC_ALL` on Linux/macOS). If your script depends on additional variables, set them here.\n\n### Example\n```yaml\nenvironment:\n ORACLE_HOME: /opt/oracle/product/19c\n LD_LIBRARY_PATH: /opt/oracle/product/19c/lib\n```"
292 + },
293 + "custom_vars": {
294 + "ui:help": "**Custom service variables exposed as Nagios-style macros.**\n\nEach entry is available as:\n- Environment variable: `NAGIOS__SERVICE<UPPERCASE_KEY>`\n- Macro in `args`: `$_SERVICE<KEY>$`\n\n### Example\n```yaml\ncustom_vars:\n DBHOST: db.example.com\n DBNAME: production\n```\nUse in args: `[\"-H\", \"$_SERVICEDBHOST$\", \"-d\", \"$_SERVICEDBNAME$\"]`"
295 + },
296 "vnode": {
297 "ui:placeholder": "To use this option, first create a Virtual Node and then reference its name here."
298 },
src/go/plugin/scripts.d/collector/nagios/init.go
+32 -15
@@ -4,8 +4,10 @@ package nagios
4
5 import (
6 "fmt"
7 - "os"
7 + "path/filepath"
8 "runtime"
9 + "slices"
10 + "strings"
11 )
12
13 func (c *Collector) initCollector() error {
@@ -28,9 +30,20 @@ func (c *Collector) compileConfiguredJob() (compiledJob, error) {
30 if err != nil {
31 return compiledJob{}, err
32 }
31 - if err := validateConfiguredPlugin(job.config); err != nil {
32 - return compiledJob{}, err
33 +
34 + validatedPath, err := c.validatePlugin(job.config.Plugin)
35 + if err != nil {
36 + return compiledJob{}, fmt.Errorf("job '%s': %w", job.config.Name, err)
37 }
38 + job.config.Plugin = validatedPath
39 +
40 + if runtime.GOOS != "windows" && isKnownInterpreter(job.config.Plugin) {
41 + c.Warningf("job '%s': plugin '%s' appears to be an interpreter; "+
42 + "Netdata validates the interpreter binary but cannot verify scripts passed in args — "+
43 + "ensure scripts are root-owned and not writable by group/others",
44 + job.config.Name, job.config.Plugin)
45 + }
46 +
47 c.warnCadenceResolution(job)
48 return job, nil
49 }
@@ -43,16 +56,20 @@ func (c *Collector) warnCadenceResolution(job compiledJob) {
56 c.cadenceWarning = job.cadenceWarning
57 }
58
46 -func validateConfiguredPlugin(job JobConfig) error {
47 - info, err := os.Stat(job.Plugin)
48 - if err != nil {
49 - return fmt.Errorf("job '%s': plugin path '%s' stat error: %w", job.Name, job.Plugin, err)
50 - }
51 - if !info.Mode().IsRegular() {
52 - return fmt.Errorf("job '%s': plugin path '%s' must be a regular file", job.Name, job.Plugin)
53 - }
54 - if runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 {
55 - return fmt.Errorf("job '%s': plugin path '%s' must be executable", job.Name, job.Plugin)
56 - }
57 - return nil
59 +var knownInterpreters = []string{
60 + "bash", "sh", "dash", "zsh", "ksh", "csh", "tcsh", "fish",
61 + "python", "python2", "python3",
62 + "perl", "ruby", "lua",
63 + "powershell", "pwsh",
64 + "cmd",
65 + "node",
66 + "env",
67 + "php",
68 + "tclsh", "wish", "expect",
69 +}
70 +
71 +func isKnownInterpreter(pluginPath string) bool {
72 + base := filepath.Base(pluginPath)
73 + name := strings.TrimSuffix(base, filepath.Ext(base))
74 + return slices.Contains(knownInterpreters, strings.ToLower(name))
75 }
src/go/plugin/scripts.d/collector/nagios/job_v2_integration_test.go
+2 -2
@@ -49,7 +49,7 @@ func TestNagiosCollectorJobV2(t *testing.T) {
49 },
50 },
51 }
52 - coll := New()
52 + coll := newTestCollector()
53 coll.runner = runner
54 coll.now = func() time.Time { return now }
55 coll.Config.JobConfig = JobConfig{
@@ -89,7 +89,7 @@ func TestNagiosCollectorJobV2(t *testing.T) {
89 scriptPath := filepath.Join(dir, "check_slow.sh")
90 writeExecutable(t, scriptPath, "#!/bin/sh\nset -eu\nstarted_file=\"$1\"\necho started > \"$started_file\"\ntrap 'exit 0' TERM INT\nsleep 30\n")
91
92 - coll := New()
92 + coll := newTestCollector()
93 coll.Config.JobConfig = JobConfig{
94 Name: "cancel_job",
95 Plugin: scriptPath,
src/go/plugin/scripts.d/collector/nagios/metadata.yaml
+33 -21
@@ -167,6 +167,28 @@ modules:
167 setup:
168 prerequisites:
169 list:
170 + - title: Security requirements for plugin executables
171 + description: |
172 + Netdata validates the `plugin` path before execution. On Linux/macOS, the executable must meet these requirements:
173 +
174 + - Must be a regular file (not a directory or device node)
175 + - Must be executable (at least one execute bit set)
176 + - Owned by **root**
177 + - Not writable by group or others (no `g+w` or `o+w`)
178 + - All ancestor directories (up to and including `/`) owned by **root**
179 + - All ancestor directories not writable by group or others
180 + - Symlinks are resolved — the target must meet these rules
181 +
182 + On Windows, path validation is not enforced. Ensure executables are stored in directories with appropriate ACLs.
183 +
184 + This prevents local privilege escalation through a modified check script. If validation fails, the job will not start and an error is logged.
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.
190 +
191 + :::
192 - title: Install check commands
193 description: |
194 Install the Nagios plugins or other Nagios-compatible scripts that you want Netdata to run.
@@ -181,13 +203,13 @@ modules:
203 dnf install nagios-plugins-all
204 ```
205
184 - Make sure the configured command path exists and is executable by the `netdata` user.
206 + Packaged Nagios plugins are typically installed as root-owned executables, which satisfies the security requirements above.
207 - title: Prepare custom check scripts
208 description: |
209 If you are writing your own check scripts instead of using packaged Nagios plugins:
210
189 - - Place scripts anywhere accessible to the `netdata` user (e.g., `/usr/local/lib/netdata/checks/`)
190 - - Make scripts executable: `chmod +x /path/to/script.sh`
211 + - Place scripts in a root-owned directory (e.g., `/usr/local/lib/netdata/checks/`)
212 + - Set ownership and permissions: `sudo chown root:root /path/to/script.sh && sudo chmod 755 /path/to/script.sh`
213 - Test as the `netdata` user to verify permissions and environment: `sudo -u netdata /path/to/script.sh`
214 - Verify the exit code: `echo $?` (must be 0, 1, 2, or 3)
215 - Verify the output matches the Nagios plugin output format described in the Overview above
@@ -209,7 +231,7 @@ modules:
231
232 - name: check_name
233 description: |
212 - Name that identifies this check type for chart grouping and metric naming. If omitted, Netdata derives it from the basename of `plugin` (removing any file extension). Set this when `plugin` points to an interpreter (e.g. `powershell.exe`, `/bin/bash`) — otherwise all jobs using the same interpreter share the same chart section.
234 + Name that identifies this check type for chart grouping and metric naming. If omitted, Netdata derives it from the basename of `plugin` (removing any file extension). Use this when the plugin filename is generic and you want a more descriptive chart section — for example, when multiple jobs run `check_nrpe` against different remote checks, set `check_name` to distinguish them (`check_disk`, `check_load`, etc.).
235
236 In the dashboard, charts appear under `Synthetic > Nagios > Perfdata > <check_name>`. For example, with `check_name: check_memory` and a script that outputs `caches=2380912KB`, Netdata creates:
237
@@ -220,7 +242,7 @@ modules:
242 required: false
243 group: Target
244 - name: plugin
223 - description: Absolute path to the Nagios-compatible command entry point to run. This can be a packaged Nagios plugin, your own executable, or an interpreter executable. When `plugin` points to an interpreter, pass the script path in `args` and set `check_name` to the actual check identity — otherwise all jobs using the same interpreter share the same chart section. The command should return exit code `0`, `1`, `2`, or `3` and may print performance data after <code>&#124;</code>.
245 + description: Absolute path to the Nagios-compatible check command to run. This can be a packaged Nagios plugin or your own executable script. The executable must be root-owned and not writable by group or others (see prerequisites). The command should return exit code `0`, `1`, `2`, or `3` and may print performance data after <code>&#124;</code>.
246 default_value: ""
247 required: true
248 group: Target
@@ -366,10 +388,11 @@ modules:
388 exit 0
389 ```
390
369 - **2. Make it executable and test it:**
391 + **2. Set ownership, permissions, and test it:**
392
393 ```bash
372 - chmod +x /usr/local/lib/netdata/checks/check_api.sh
394 + sudo chown root:root /usr/local/lib/netdata/checks/check_api.sh
395 + sudo chmod 755 /usr/local/lib/netdata/checks/check_api.sh
396 sudo -u netdata /usr/local/lib/netdata/checks/check_api.sh
397 echo "Exit code: $?"
398 ```
@@ -391,9 +414,9 @@ modules:
414 plugin: /opt/netdata/check_memory.sh
415 timeout: 5s
416 check_interval: 1m
394 - - name: End-to-end custom script (Windows PowerShell)
417 + - name: Windows PowerShell check
418 description: |
396 - Write a PowerShell check script, then configure Netdata to run it. Because `plugin` points to the PowerShell interpreter, `check_name` is required to keep charts grouped under the script identity instead of `powershell`.
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
421 **1. Create the script** (e.g., `C:\Netdata\checks\check_service.ps1`):
422
@@ -433,17 +456,6 @@ modules:
456 args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\Netdata\\checks\\check_service.ps1"]
457 timeout: 10s
458 check_interval: 1m
436 - - name: Interpreter-launched script (Bash)
437 - description: |
438 - Run a Bash script through an interpreter. Without `check_name`, charts would be grouped under `bash`.
439 - config: |
440 - jobs:
441 - - name: db_health
442 - check_name: check_postgres
443 - plugin: /bin/bash
444 - args: ["/opt/netdata/checks/check_postgres.sh", "--host", "db.example.com"]
445 - timeout: 10s
446 - check_interval: 5m
459 - name: Remote check via NRPE
460 description: |
461 Run a check on a remote host using `check_nrpe`. This works exactly like a Nagios NRPE configuration — install `nagios-nrpe-plugin` and point to the remote NRPE agent. Increase `timeout` if the remote host is slow to respond.
@@ -520,7 +532,7 @@ modules:
532 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
534 description: |
523 - The collector runs the command named in `plugin` directly. On Windows, point `plugin` to the absolute path of a compiled executable or an interpreter (e.g. `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`). When using an interpreter, pass the script path in `args` and always set `check_name` — without it, all jobs using the same interpreter share the same chart section (e.g. `Perfdata/powershell`).
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.
536 alerts:
537 - name: nagios_job_execution_state_warn
538 metric: nagios.job.execution_state
src/go/plugin/scripts.d/collector/nagios/v2_gate_test.go
+1 -1
@@ -433,7 +433,7 @@ func assertPlanHasNoRemoveForTarget(t *testing.T, plan chartengine.Plan, removeM
433 }
434
435 func TestV2Gate_SmokeCollect(t *testing.T) {
436 - coll := New()
436 + coll := newTestCollector()
437 coll.runner = &fakeRunner{}
438 coll.Config.JobConfig.Plugin = writeTestPluginFile(t, "true")
439 coll.Config.JobConfig.Name = "smoke"
src/go/plugin/scripts.d/config/scripts.d/nagios.conf
-9
@@ -17,12 +17,3 @@
17 # - type: weekly
18 # days: [sunday, monday, tuesday, wednesday, thursday, friday, saturday]
19 # ranges: ["00:00-24:00"]
20 -#
21 -# # When plugin is an interpreter, set check_name to identify the actual check.
22 -# # Without it, charts are grouped under the interpreter name (e.g. "powershell").
23 -# - name: service_health_win
24 -# check_name: check_service
25 -# plugin: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
26 -# args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\Netdata\\checks\\check_service.ps1"]
27 -# timeout: 10s
28 -# check_interval: 1m