summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIlya Mashchenko <ilya@netdata.cloud>2024-04-23 19:37:20 +0300
committerGitHub <noreply@github.com>2024-04-23 19:37:20 +0300
commit355753638683af0036d4626c3a3a7ce68dcc531e (patch)
treec3156f4ce21502276b00aa60682117d1eec339a0
parentae783041d93b67ca7c4c8d9425aff45dc04515a7 (diff)
add go.d fail2ban (#17501)
* add go.d fail2ban * update contexts
-rw-r--r--src/go/collectors/go.d.plugin/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/config/go.d.conf1
-rw-r--r--src/go/collectors/go.d.plugin/config/go.d/fail2ban.conf5
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/charts.go75
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/collect.go163
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/config_schema.json35
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/exec.go57
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban.go111
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban_test.go238
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/init.go23
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/metadata.yaml105
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.json4
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.yaml2
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-jail-status.txt9
-rw-r--r--src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-status.txt3
-rw-r--r--src/go/collectors/go.d.plugin/modules/init.go1
-rw-r--r--src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go2
17 files changed, 833 insertions, 2 deletions
diff --git a/src/go/collectors/go.d.plugin/README.md b/src/go/collectors/go.d.plugin/README.md
index b7f35a1e54..de64966164 100644
--- a/src/go/collectors/go.d.plugin/README.md
+++ b/src/go/collectors/go.d.plugin/README.md
@@ -72,6 +72,7 @@ see the appropriate collector readme.
| [energid](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/energid) | Energi Core |
| [envoy](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/envoy) | Envoy |
| [example](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/example) | - |
+| [fail2ban](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fail2ban) | Fail2Ban Jails |
| [filecheck](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/filecheck) | Files and Directories |
| [fluentd](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fluentd) | Fluentd |
| [freeradius](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/freeradius) | FreeRADIUS |
diff --git a/src/go/collectors/go.d.plugin/config/go.d.conf b/src/go/collectors/go.d.plugin/config/go.d.conf
index 928cb15a06..4d143b1cf9 100644
--- a/src/go/collectors/go.d.plugin/config/go.d.conf
+++ b/src/go/collectors/go.d.plugin/config/go.d.conf
@@ -35,6 +35,7 @@ modules:
# elasticsearch: yes
# envoy: yes
# example: no
+# fail2ban: yes
# filecheck: yes
# fluentd: yes
# freeradius: yes
diff --git a/src/go/collectors/go.d.plugin/config/go.d/fail2ban.conf b/src/go/collectors/go.d.plugin/config/go.d/fail2ban.conf
new file mode 100644
index 0000000000..56f4a59e51
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/config/go.d/fail2ban.conf
@@ -0,0 +1,5 @@
+## All available configuration options, their descriptions and default values:
+## https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fail2ban#readme
+
+jobs:
+ - name: fail2ban
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/charts.go b/src/go/collectors/go.d.plugin/modules/fail2ban/charts.go
new file mode 100644
index 0000000000..d203e7864d
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/charts.go
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package fail2ban
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioJailBannedIPs = module.Priority + iota
+ prioJailActiveFailures
+)
+
+var jailChartsTmpl = module.Charts{
+ jailCurrentBannedIPs.Copy(),
+ jailActiveFailures.Copy(),
+}
+
+var (
+ jailCurrentBannedIPs = module.Chart{
+ ID: "jail_%s_banned_ips",
+ Title: "Fail2Ban Jail banned IPs",
+ Units: "addresses",
+ Fam: "bans",
+ Ctx: "fail2ban.jail_banned_ips",
+ Type: module.Line,
+ Priority: prioJailBannedIPs,
+ Dims: module.Dims{
+ {ID: "jail_%s_currently_banned", Name: "banned"},
+ },
+ }
+ jailActiveFailures = module.Chart{
+ ID: "jail_%s_active_failures",
+ Title: "Fail2Ban Jail active failures",
+ Units: "failures",
+ Fam: "failures",
+ Ctx: "fail2ban.jail_active_failures",
+ Type: module.Line,
+ Priority: prioJailActiveFailures,
+ Dims: module.Dims{
+ {ID: "jail_%s_currently_failed", Name: "active_failures"},
+ },
+ }
+)
+
+func (f *Fail2Ban) addJailCharts(jail string) {
+ charts := jailChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, jail)
+ chart.Labels = []module.Label{
+ {Key: "jail", Value: jail},
+ }
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, jail)
+ }
+ }
+
+ if err := f.Charts().Add(*charts...); err != nil {
+ f.Warning(err)
+ }
+}
+
+func (f *Fail2Ban) removeJailCharts(jail string) {
+ px := fmt.Sprintf("jail_%s_", jail)
+ for _, chart := range *f.Charts() {
+ if strings.HasPrefix(chart.ID, px) {
+ chart.MarkRemove()
+ chart.MarkNotCreated()
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/collect.go b/src/go/collectors/go.d.plugin/modules/fail2ban/collect.go
new file mode 100644
index 0000000000..8ca413c3b8
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/collect.go
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package fail2ban
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func (f *Fail2Ban) collect() (map[string]int64, error) {
+ now := time.Now()
+
+ if now.Sub(f.lastDiscoverTime) > f.discoverEvery || f.forceDiscover {
+ jails, err := f.discoverJails()
+ if err != nil {
+ return nil, err
+ }
+ f.jails = jails
+ f.lastDiscoverTime = now
+ f.forceDiscover = false
+ }
+
+ mx := make(map[string]int64)
+
+ if err := f.collectJails(mx); err != nil {
+ return nil, err
+ }
+
+ return mx, nil
+}
+
+func (f *Fail2Ban) discoverJails() ([]string, error) {
+ bs, err := f.exec.status()
+ if err != nil {
+ return nil, err
+ }
+
+ jails, err := parseFail2banStatus(bs)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(jails) == 0 {
+ return nil, errors.New("no jails found")
+ }
+
+ f.Debugf("discovered %d jails: %v", len(jails), jails)
+
+ return jails, nil
+}
+
+func (f *Fail2Ban) collectJails(mx map[string]int64) error {
+ seen := make(map[string]bool)
+
+ for _, jail := range f.jails {
+ f.Debugf("querying status for jail '%s'", jail)
+ bs, err := f.exec.jailStatus(jail)
+ if err != nil {
+ if errors.Is(err, errJailNotExist) {
+ f.forceDiscover = true
+ continue
+ }
+ return err
+ }
+
+ failed, banned, err := parseFail2banJailStatus(bs)
+ if err != nil {
+ return err
+ }
+
+ if !f.seenJails[jail] {
+ f.seenJails[jail] = true
+ f.addJailCharts(jail)
+ }
+ seen[jail] = true
+
+ px := fmt.Sprintf("jail_%s_", jail)
+
+ mx[px+"currently_failed"] = failed
+ mx[px+"currently_banned"] = banned
+ }
+
+ for jail := range f.seenJails {
+ if !seen[jail] {
+ delete(f.seenJails, jail)
+ f.removeJailCharts(jail)
+ }
+ }
+
+ return nil
+}
+
+func parseFail2banJailStatus(jailStatus []byte) (failed, banned int64, err error) {
+ const (
+ failedSub = "Currently failed:"
+ bannedSub = "Currently banned:"
+ )
+
+ var failedFound, bannedFound bool
+
+ sc := bufio.NewScanner(bytes.NewReader(jailStatus))
+
+ for sc.Scan() && !(failedFound && bannedFound) {
+ text := strings.TrimSpace(sc.Text())
+ if text == "" {
+ continue
+ }
+
+ if !failedFound {
+ if i := strings.Index(text, failedSub); i != -1 {
+ failedFound = true
+ s := strings.TrimSpace(text[i+len(failedSub):])
+ if failed, err = strconv.ParseInt(s, 10, 64); err != nil {
+ return 0, 0, fmt.Errorf("failed to parse currently failed value (%s): %v", s, err)
+ }
+ }
+ }
+ if !bannedFound {
+ if i := strings.Index(text, bannedSub); i != -1 {
+ bannedFound = true
+ s := strings.TrimSpace(text[i+len(bannedSub):])
+ if banned, err = strconv.ParseInt(s, 10, 64); err != nil {
+ return 0, 0, fmt.Errorf("failed to parse currently banned value (%s): %v", s, err)
+ }
+ }
+ }
+ }
+
+ if !failedFound || !bannedFound {
+ return 0, 0, errors.New("failed to find failed and banned values")
+ }
+
+ return failed, banned, nil
+}
+
+func parseFail2banStatus(status []byte) ([]string, error) {
+ const sub = "Jail list:"
+
+ var jails []string
+
+ sc := bufio.NewScanner(bytes.NewReader(status))
+
+ for sc.Scan() {
+ text := strings.TrimSpace(sc.Text())
+
+ if i := strings.Index(text, sub); i != -1 {
+ s := strings.ReplaceAll(text[i+len(sub):], ",", "")
+ jails = strings.Fields(s)
+ break
+ }
+ }
+
+ if len(jails) == 0 {
+ return nil, errors.New("failed to find jails")
+ }
+
+ return jails, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/config_schema.json b/src/go/collectors/go.d.plugin/modules/fail2ban/config_schema.json
new file mode 100644
index 0000000000..7fd0d91aff
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/config_schema.json
@@ -0,0 +1,35 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Fail2Ban collector configuration.",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 10
+ },
+ "timeout": {
+ "title": "Timeout",
+ "description": "Timeout for executing the binary, specified in seconds.",
+ "type": "number",
+ "minimum": 0.5,
+ "default": 2
+ }
+ },
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ },
+ "timeout": {
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/exec.go b/src/go/collectors/go.d.plugin/modules/fail2ban/exec.go
new file mode 100644
index 0000000000..06b5841e73
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/exec.go
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package fail2ban
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
+)
+
+var errJailNotExist = errors.New("jail not exist")
+
+func newFail2BanClientCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *fail2banClientCliExec {
+ return &fail2banClientCliExec{
+ Logger: log,
+ ndsudoPath: ndsudoPath,
+ timeout: timeout,
+ }
+}
+
+type fail2banClientCliExec struct {
+ *logger.Logger
+
+ ndsudoPath string
+ timeout time.Duration
+}
+
+func (e *fail2banClientCliExec) status() ([]byte, error) {
+ return e.execute("fail2ban-client-status")
+}
+
+func (e *fail2banClientCliExec) jailStatus(jail string) ([]byte, error) {
+ return e.execute("fail2ban-client-status-jail", "--jail", jail)
+}
+
+func (e *fail2banClientCliExec) execute(args ...string) ([]byte, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
+ e.Debugf("executing '%s'", cmd)
+
+ bs, err := cmd.Output()
+ if err != nil {
+ if strings.HasPrefix(strings.TrimSpace(string(bs)), "Sorry but the jail") {
+ return nil, errJailNotExist
+ }
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
+ }
+
+ return bs, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban.go b/src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban.go
new file mode 100644
index 0000000000..b746f88960
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package fail2ban
+
+import (
+ _ "embed"
+ "errors"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("fail2ban", module.Creator{
+ JobConfigSchema: configSchema,
+ Defaults: module.Defaults{
+ UpdateEvery: 10,
+ },
+ Create: func() module.Module { return New() },
+ })
+}
+
+func New() *Fail2Ban {
+ return &Fail2Ban{
+ Config: Config{
+ Timeout: web.Duration(time.Second * 2),
+ },
+ charts: &module.Charts{},
+ discoverEvery: time.Minute * 5,
+ seenJails: make(map[string]bool),
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
+}
+
+type (
+ Fail2Ban struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ exec fail2banClientCli
+
+ discoverEvery time.Duration
+ lastDiscoverTime time.Time
+ forceDiscover bool
+ jails []string
+
+ seenJails map[string]bool
+ }
+ fail2banClientCli interface {
+ status() ([]byte, error)
+ jailStatus(s string) ([]byte, error)
+ }
+)
+
+func (f *Fail2Ban) Configuration() any {
+ return f.Config
+}
+
+func (f *Fail2Ban) Init() error {
+ f2bClientExec, err := f.initFail2banClientCliExec()
+ if err != nil {
+ f.Errorf("fail2ban-client exec initialization: %v", err)
+ return err
+ }
+ f.exec = f2bClientExec
+
+ return nil
+}
+
+func (f *Fail2Ban) Check() error {
+ mx, err := f.collect()
+ if err != nil {
+ f.Error(err)
+ return err
+ }
+
+ if len(mx) == 0 {
+ return errors.New("no metrics collected")
+ }
+
+ return nil
+}
+
+func (f *Fail2Ban) Charts() *module.Charts {
+ return f.charts
+}
+
+func (f *Fail2Ban) Collect() map[string]int64 {
+ mx, err := f.collect()
+ if err != nil {
+ f.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+
+ return mx
+}
+
+func (f *Fail2Ban) Cleanup() {}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban_test.go b/src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban_test.go
new file mode 100644
index 0000000000..7d1988bd21
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban_test.go
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package fail2ban
+
+import (
+ "errors"
+ "os"
+ "testing"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+
+ dataStatus, _ = os.ReadFile("testdata/fail2ban-status.txt")
+ dataJailStatus, _ = os.ReadFile("testdata/fail2ban-jail-status.txt")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+
+ "dataStatus": dataStatus,
+ "dataJailStatus": dataJailStatus,
+ } {
+ require.NotNil(t, data, name)
+
+ }
+}
+
+func TestFail2Ban_Configuration(t *testing.T) {
+ module.TestConfigurationSerialize(t, &Fail2Ban{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestFail2Ban_Init(t *testing.T) {
+ tests := map[string]struct {
+ config Config
+ wantFail bool
+ }{
+ "fails if failed to locate ndsudo": {
+ wantFail: true,
+ config: New().Config,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ f2b := New()
+ f2b.Config = test.config
+
+ if test.wantFail {
+ assert.Error(t, f2b.Init())
+ } else {
+ assert.NoError(t, f2b.Init())
+ }
+ })
+ }
+}
+
+func TestFail2Ban_Cleanup(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() *Fail2Ban
+ }{
+ "not initialized exec": {
+ prepare: func() *Fail2Ban {
+ return New()
+ },
+ },
+ "after check": {
+ prepare: func() *Fail2Ban {
+ f2b := New()
+ f2b.exec = prepareMockOk()
+ _ = f2b.Check()
+ return f2b
+ },
+ },
+ "after collect": {
+ prepare: func() *Fail2Ban {
+ f2b := New()
+ f2b.exec = prepareMockOk()
+ _ = f2b.Collect()
+ return f2b
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ f2b := test.prepare()
+
+ assert.NotPanics(t, f2b.Cleanup)
+ })
+ }
+}
+
+func TestFail2Ban_Charts(t *testing.T) {
+ assert.NotNil(t, New().Charts())
+}
+
+func TestFail2Ban_Check(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockFail2BanClientCliExec
+ wantFail bool
+ }{
+ "success multiple jails": {
+ wantFail: false,
+ prepareMock: prepareMockOk,
+ },
+ "error on status": {
+ wantFail: true,
+ prepareMock: prepareMockErrOnStatus,
+ },
+ "empty response (no jails)": {
+ prepareMock: prepareMockEmptyResponse,
+ wantFail: true,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ f2b := New()
+ mock := test.prepareMock()
+ f2b.exec = mock
+
+ if test.wantFail {
+ assert.Error(t, f2b.Check())
+ } else {
+ assert.NoError(t, f2b.Check())
+ }
+ })
+ }
+}
+
+func TestFail2Ban_Collect(t *testing.T) {
+ tests := map[string]struct {
+ prepareMock func() *mockFail2BanClientCliExec
+ wantMetrics map[string]int64
+ }{
+ "success multiple jails": {
+ prepareMock: prepareMockOk,
+ wantMetrics: map[string]int64{
+ "jail_dovecot_currently_banned": 30,
+ "jail_dovecot_currently_failed": 10,
+ "jail_sshd_currently_banned": 30,
+ "jail_sshd_currently_failed": 10,
+ },
+ },
+ "error on status": {
+ prepareMock: prepareMockErrOnStatus,
+ wantMetrics: nil,
+ },
+ "empty response (no jails)": {
+ prepareMock: prepareMockEmptyResponse,
+ wantMetrics: nil,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ f2b := New()
+ mock := test.prepareMock()
+ f2b.exec = mock
+
+ mx := f2b.Collect()
+
+ assert.Equal(t, test.wantMetrics, mx)
+ if len(test.wantMetrics) > 0 {
+ assert.Len(t, *f2b.Charts(), len(jailChartsTmpl)*2)
+ testMetricsHasAllChartsDims(t, f2b, mx)
+ }
+ })
+ }
+}
+
+func testMetricsHasAllChartsDims(t *testing.T, f2b *Fail2Ban, mx map[string]int64) {
+ for _, chart := range *f2b.Charts() {
+ if chart.Obsolete {
+ continue
+ }
+ for _, dim := range chart.Dims {
+ _, ok := mx[dim.ID]
+ assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
+ }
+ for _, v := range chart.Vars {
+ _, ok := mx[v.ID]
+ assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID)
+ }
+ }
+}
+
+func prepareMockOk() *mockFail2BanClientCliExec {
+ return &mockFail2BanClientCliExec{
+ statusData: dataStatus,
+ jailStatusData: dataJailStatus,
+ }
+}
+
+func prepareMockErrOnStatus() *mockFail2BanClientCliExec {
+ return &mockFail2BanClientCliExec{
+ errOnStatus: true,
+ statusData: dataStatus,
+ jailStatusData: dataJailStatus,
+ }
+}
+
+func prepareMockEmptyResponse() *mockFail2BanClientCliExec {
+ return &mockFail2BanClientCliExec{}
+}
+
+type mockFail2BanClientCliExec struct {
+ errOnStatus bool
+ statusData []byte
+
+ errOnJailStatus bool
+ jailStatusData []byte
+}
+
+func (m *mockFail2BanClientCliExec) status() ([]byte, error) {
+ if m.errOnStatus {
+ return nil, errors.New("mock.status() error")
+ }
+
+ return m.statusData, nil
+}
+
+func (m *mockFail2BanClientCliExec) jailStatus(_ string) ([]byte, error) {
+ if m.errOnJailStatus {
+ return nil, errors.New("mock.jailStatus() error")
+ }
+
+ return m.jailStatusData, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/init.go b/src/go/collectors/go.d.plugin/modules/fail2ban/init.go
new file mode 100644
index 0000000000..938c9697ab
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/init.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package fail2ban
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/executable"
+)
+
+func (f *Fail2Ban) initFail2banClientCliExec() (fail2banClientCli, error) {
+ ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
+ if _, err := os.Stat(ndsudoPath); err != nil {
+ return nil, fmt.Errorf("ndsudo executable not found: %v", err)
+
+ }
+
+ f2bClientExec := newFail2BanClientCliExec(ndsudoPath, f.Timeout.Duration(), f.Logger)
+
+ return f2bClientExec, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/metadata.yaml b/src/go/collectors/go.d.plugin/modules/fail2ban/metadata.yaml
new file mode 100644
index 0000000000..87a5732a12
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/metadata.yaml
@@ -0,0 +1,105 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-fail2ban
+ plugin_name: go.d.plugin
+ module_name: fail2ban
+ monitored_instance:
+ name: Fail2ban
+ link: "https://github.com/fail2ban/fail2ban#readme"
+ icon_filename: fail2ban.png
+ categories:
+ - data-collection.authentication-and-authorization
+ keywords:
+ - fail2ban
+ - security
+ - authentication
+ - authorization
+ related_resources:
+ integrations:
+ list: []
+ info_provided_to_referring_integrations:
+ description: ""
+ most_popular: false
+ overview:
+ data_collection:
+ metrics_description: >
+ This collector tracks two main metrics for each jail: currently banned IPs and active failure incidents.
+ It relies on the [`fail2ban-client`](https://linux.die.net/man/1/fail2ban-client) CLI tool but avoids directly executing the binary.
+ Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment.
+ This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management.
+ method_description: ""
+ supported_platforms:
+ include: []
+ exclude: []
+ multi_instance: false
+ additional_permissions:
+ description: ""
+ default_behavior:
+ auto_detection:
+ description: ""
+ limits:
+ description: ""
+ performance_impact:
+ description: ""
+ setup:
+ prerequisites:
+ list: []
+ configuration:
+ file:
+ name: go.d/fail2ban.conf
+ options:
+ description: |
+ The following options can be defined globally: update_every.
+ folding:
+ title: Config options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 10
+ required: false
+ - name: timeout
+ description: fail2ban-client binary execution timeout.
+ default_value: 2
+ required: false
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list:
+ - name: Custom update_every
+ description: Allows you to override the default data collection interval.
+ config: |
+ jobs:
+ - name: fail2ban
+ update_every: 5 # Collect Fail2Ban jails statistics every 5 seconds
+ troubleshooting:
+ problems:
+ list: []
+ alerts: []
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: jail
+ description: These metrics refer to the Jail.
+ labels:
+ - name: jail
+ description: Jail's name
+ metrics:
+ - name: fail2ban.jail_banned_ips
+ description: Fail2Ban Jail banned IPs
+ unit: addresses
+ chart_type: line
+ dimensions:
+ - name: banned
+ - name: fail2ban.jail_active_failures
+ description: Fail2Ban Jail active failures
+ unit: failures
+ chart_type: line
+ dimensions:
+ - name: active_failures
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.json b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.json
new file mode 100644
index 0000000000..291ecee3d6
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.json
@@ -0,0 +1,4 @@
+{
+ "update_every": 123,
+ "timeout": 123.123
+}
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.yaml
new file mode 100644
index 0000000000..25b0b4c780
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.yaml
@@ -0,0 +1,2 @@
+update_every: 123
+timeout: 123.123
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-jail-status.txt b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-jail-status.txt
new file mode 100644
index 0000000000..17a3f53c12
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-jail-status.txt
@@ -0,0 +1,9 @@
+Status for the jail: JAIL
+|- Filter
+| |- Currently failed: 10
+| |- Total failed: 20
+| `- File list: /var/log/auth.log
+`- Actions
+ |- Currently banned: 30
+ |- Total banned: 40
+ `- Banned IP list:
diff --git a/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-status.txt b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-status.txt
new file mode 100644
index 0000000000..1e65a78cf2
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-status.txt
@@ -0,0 +1,3 @@
+Status
+|- Number of jail: 1
+`- Jail list: sshd, dovecot \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/init.go b/src/go/collectors/go.d.plugin/modules/init.go
index 8691ca024d..b0f1f9e838 100644
--- a/src/go/collectors/go.d.plugin/modules/init.go
+++ b/src/go/collectors/go.d.plugin/modules/init.go
@@ -24,6 +24,7 @@ import (
_ "github.com/netdata/netdata/go/go.d.plugin/modules/elasticsearch"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/envoy"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/example"
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/fail2ban"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/filecheck"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/fluentd"
_ "github.com/netdata/netdata/go/go.d.plugin/modules/freeradius"
diff --git a/src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go b/src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go
index 46bc8a8d1e..db85fc7dfd 100644
--- a/src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go
+++ b/src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go
@@ -61,7 +61,6 @@ func TestLVM_