Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
This file documents the revision history for the SNClient agent.

1.00 Fri Oct 16 16:47:03 CEST 2026
- add backslash line continuation for configuration values, including external commands and check_multi
- add check_multi: execute multiple subchecks and evaluate the overall result
- check_dns: resend lost packets and use the timeout and attempts settings of the resolv.conf
- fix remaining zombie processes after automatic updates
Expand Down
26 changes: 23 additions & 3 deletions docs/checks/external_commands/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ You can add your custom scripts to SNClient using either a concise or verbose fo
**Concise Format**:

```ini
[/settings/external scripts]
[/settings/external scripts/scripts]
my_check1 = check_custom.bat
my_check2 = myscripts\check_custom.bat
```
Expand All @@ -34,16 +34,36 @@ my_check2 = myscripts\check_custom.bat

```ini
[/settings/external scripts/scripts/my_check1]
my_check1 = check_custom.bat
command = check_custom.bat

[/settings/external scripts/scripts/my_check2]
my_check2 = myscripts\check_custom.bat
command = myscripts\check_custom.bat
```

Both formats achieve the same outcome by adding two new commands, `my_check1` and `my_check2`, which execute
the `check_custom.bat` script. Usually you use the short format, but if you want to provide individual options
to a command, the long format is the way to go.

In the concise format, the key is the command name. In the verbose format, the last
part of the section path is the command name and the `command` key specifies what
to execute. Settings such as `timeout` and `allow arguments` can be set in that
section to override the global external script settings for that command.

### Multiline Commands

Long commands can be split across lines using a space or tab followed by `\` at
the end of each continued line. For example:

```ini
[/settings/external scripts/scripts]
check_myscript = ${scripts}/myscript.sh \
--arg1 foo \
--arg2 bar
```

The lines are joined with a single space before the command is parsed.
See [multiline values](../../configuration/#multiline-values) for the syntax rules.

### Handling Script Arguments

You can manage script arguments in two ways: embedding them directly into the command or allowing for argument pass-through.
Expand Down
24 changes: 24 additions & 0 deletions docs/configuration/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,30 @@ check_also_keep = "part 1" something else "part 2"
check_wrong = "unclosed quotes
```

### Multiline Values

End a line with a space or tab followed by `\` to continue its value on the next
line. The backslash and surrounding whitespace, including indentation on the next
line, are replaced with a single space. This works for configuration values in
all sections, including external commands and `check_multi` commands.

For example, check the size of a backup folder:

```ini
[/settings/check/multi/backups]
command[size] = check_files \
"path=/srv/backups" \
"warn=total_size > 10GiB"
```

A continuation must be followed immediately by a nonempty line. A blank or
whitespace-only line, or the end of the file, causes an `unfinished line
continuation` error. Windows paths such as `C:\scripts\` remain unchanged because
the final backslash is not preceded by a space or tab.

See [external commands](../checks/external_commands/#multiline-commands) for an
external command example.

### Appending Values

You may use the `+=` operator to append to existing values and write
Expand Down
20 changes: 20 additions & 0 deletions pkg/snclient/check_multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ import (
"github.com/stretchr/testify/require"
)

func TestCheckMultiLineContinuation(t *testing.T) {
config := `
[/settings/check/multi/multiline]
command[ok] = check_dummy \
0 \
'multiline ok'
command[warning] = check_dummy \
1 \
'multiline warning'
`
snc := StartTestAgent(t, config)
defer StopTestAgent(t, snc)

res := snc.RunCheck("check_multi", []string{"config=multiline"})
assert.Equal(t, CheckExitWarning, res.State)
assert.Contains(t, res.Output, "2 plugins checked: 1 ok, 1 warning, 0 critical, 0 unknown")
assert.Contains(t, res.Details, "[ok] multiline ok")
assert.Contains(t, res.Details, "[warning] multiline warning")
}

func TestCheckMultiInline(t *testing.T) {
config := `
[/modules]
Expand Down
17 changes: 14 additions & 3 deletions pkg/snclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,9 @@ func (config *Config) ParseINI(configData, iniPath string, snc *Agent) error {
lines = lines[:len(lines)-1]
}

for x, line := range lines {
lineNr := x + 1
line = strings.TrimSpace(line)
for lineIndex := 0; lineIndex < len(lines); lineIndex++ {
lineNr := lineIndex + 1
line := strings.TrimSpace(lines[lineIndex])
if line == "" || line[0] == ';' || line[0] == '#' {
currentComments = append(currentComments, line)

Expand Down Expand Up @@ -337,6 +337,17 @@ func (config *Config) ParseINI(configData, iniPath string, snc *Agent) error {
continue
}
val[0] = strings.TrimSpace(val[0])

// Require whitespace before continuation to preserve trailing Windows path separators.
for strings.HasSuffix(val[1], " \\") || strings.HasSuffix(val[1], "\t\\") {
if lineIndex+1 == len(lines) || strings.TrimSpace(lines[lineIndex+1]) == "" {
parseErrors = append(parseErrors, fmt.Errorf("config error in %s:%d: unfinished line continuation", iniPath, lineNr))

break
}
lineIndex++
val[1] = strings.TrimSpace(strings.TrimSuffix(val[1], "\\")) + " " + strings.TrimSpace(lines[lineIndex])
}
val[1] = strings.TrimSpace(val[1])

// silently skip UNKNOWN values which were placeholder in nsclient
Expand Down
51 changes: 51 additions & 0 deletions pkg/snclient/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,57 @@ test4 += c
assert.Equalf(t, expData, cfg.Section("/test").data, "config parsed")
}

func TestConfigLineContinuation(t *testing.T) {
configText := `[/settings/external scripts/scripts]
check_ext = snclient run check_dummy \
0 \
$ARGS$
[/test]
quoted = "hello \
world"
path = C:\scripts\
next = unchanged
append = first
append += \
second
`
for _, newline := range []string{"\n", "\r\n"} {
t.Run(fmt.Sprintf("newline=%q", newline), func(t *testing.T) {
cfg := NewConfig(false)
err := cfg.ParseINI(strings.ReplaceAll(configText, "\n", newline), "testfile.ini", nil)
require.NoError(t, err)
command, _ := cfg.Section("/settings/external scripts/scripts").GetString("check_ext")
assert.Equal(t, "snclient run check_dummy 0 $ARGS$", command)
assert.Equal(t, ConfigData{
"quoted": "hello world",
"path": `C:\scripts\`,
"next": "unchanged",
"append": "firstsecond",
}, cfg.Section("/test").data)
})
}
}

func TestConfigLineContinuationErrors(t *testing.T) {
for _, test := range []struct {
name string
config string
error string
}{
{"unfinished", "[/test]\nkey = value \\", "testfile.ini:2: unfinished line continuation"},
{"blank line", "[/test]\nkey = value \\\n\nnext = value\n", "testfile.ini:2: unfinished line continuation"},
{"whitespace line", "[/test]\r\nkey = value \\\r\n \t\r\nnext = value\r\n", "testfile.ini:2: unfinished line continuation"},
{"trailing blank line", "[/test]\nkey = value \\\n\n", "testfile.ini:2: unfinished line continuation"},
{"line number", "[/test]\nkey = value \\\n more\nbad\n", "testfile.ini:4: found key without '='"},
} {
t.Run(test.name, func(t *testing.T) {
cfg := NewConfig(false)
err := cfg.ParseINI(test.config, "testfile.ini", nil)
require.ErrorContains(t, err, test.error)
})
}
}

func TestConfigErrorI(t *testing.T) {
configText := `
[/test]
Expand Down
Loading