add go.d fail2ban (#17501)
* add go.d fail2ban * update contexts
Ilya Mashchenko committed
Apr 23, 2024 at 19:37 UTC
355753638683af0036d4626c3a3a7ce68dcc531e
17 files changed
+833
-2
src/go/collectors/go.d.plugin/README.md
+1
@@ -72,6 +72,7 @@ see the appropriate collector readme.
72
| [energid](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/energid) | Energi Core |
73
| [envoy](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/envoy) | Envoy |
74
| [example](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/example) | - |
75
+| [fail2ban](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fail2ban) | Fail2Ban Jails |
76
| [filecheck](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/filecheck) | Files and Directories |
77
| [fluentd](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fluentd) | Fluentd |
78
| [freeradius](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/freeradius) | FreeRADIUS |
src/go/collectors/go.d.plugin/config/go.d.conf
+1
@@ -35,6 +35,7 @@ modules:
35
# elasticsearch: yes
36
# envoy: yes
37
# example: no
38
+# fail2ban: yes
39
# filecheck: yes
40
# fluentd: yes
41
# freeradius: yes
src/go/collectors/go.d.plugin/config/go.d/fail2ban.conf
new
+5
@@ -0,0 +1,5 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/modules/fail2ban#readme
3
+
4
+jobs:
5
+ - name: fail2ban
src/go/collectors/go.d.plugin/modules/fail2ban/charts.go
new
+75
@@ -0,0 +1,75 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package fail2ban
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
10
+)
11
+
12
+const (
13
+ prioJailBannedIPs = module.Priority + iota
14
+ prioJailActiveFailures
15
+)
16
+
17
+var jailChartsTmpl = module.Charts{
18
+ jailCurrentBannedIPs.Copy(),
19
+ jailActiveFailures.Copy(),
20
+}
21
+
22
+var (
23
+ jailCurrentBannedIPs = module.Chart{
24
+ ID: "jail_%s_banned_ips",
25
+ Title: "Fail2Ban Jail banned IPs",
26
+ Units: "addresses",
27
+ Fam: "bans",
28
+ Ctx: "fail2ban.jail_banned_ips",
29
+ Type: module.Line,
30
+ Priority: prioJailBannedIPs,
31
+ Dims: module.Dims{
32
+ {ID: "jail_%s_currently_banned", Name: "banned"},
33
+ },
34
+ }
35
+ jailActiveFailures = module.Chart{
36
+ ID: "jail_%s_active_failures",
37
+ Title: "Fail2Ban Jail active failures",
38
+ Units: "failures",
39
+ Fam: "failures",
40
+ Ctx: "fail2ban.jail_active_failures",
41
+ Type: module.Line,
42
+ Priority: prioJailActiveFailures,
43
+ Dims: module.Dims{
44
+ {ID: "jail_%s_currently_failed", Name: "active_failures"},
45
+ },
46
+ }
47
+)
48
+
49
+func (f *Fail2Ban) addJailCharts(jail string) {
50
+ charts := jailChartsTmpl.Copy()
51
+
52
+ for _, chart := range *charts {
53
+ chart.ID = fmt.Sprintf(chart.ID, jail)
54
+ chart.Labels = []module.Label{
55
+ {Key: "jail", Value: jail},
56
+ }
57
+ for _, dim := range chart.Dims {
58
+ dim.ID = fmt.Sprintf(dim.ID, jail)
59
+ }
60
+ }
61
+
62
+ if err := f.Charts().Add(*charts...); err != nil {
63
+ f.Warning(err)
64
+ }
65
+}
66
+
67
+func (f *Fail2Ban) removeJailCharts(jail string) {
68
+ px := fmt.Sprintf("jail_%s_", jail)
69
+ for _, chart := range *f.Charts() {
70
+ if strings.HasPrefix(chart.ID, px) {
71
+ chart.MarkRemove()
72
+ chart.MarkNotCreated()
73
+ }
74
+ }
75
+}
src/go/collectors/go.d.plugin/modules/fail2ban/collect.go
new
+163
@@ -0,0 +1,163 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package fail2ban
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+ "time"
13
+)
14
+
15
+func (f *Fail2Ban) collect() (map[string]int64, error) {
16
+ now := time.Now()
17
+
18
+ if now.Sub(f.lastDiscoverTime) > f.discoverEvery || f.forceDiscover {
19
+ jails, err := f.discoverJails()
20
+ if err != nil {
21
+ return nil, err
22
+ }
23
+ f.jails = jails
24
+ f.lastDiscoverTime = now
25
+ f.forceDiscover = false
26
+ }
27
+
28
+ mx := make(map[string]int64)
29
+
30
+ if err := f.collectJails(mx); err != nil {
31
+ return nil, err
32
+ }
33
+
34
+ return mx, nil
35
+}
36
+
37
+func (f *Fail2Ban) discoverJails() ([]string, error) {
38
+ bs, err := f.exec.status()
39
+ if err != nil {
40
+ return nil, err
41
+ }
42
+
43
+ jails, err := parseFail2banStatus(bs)
44
+ if err != nil {
45
+ return nil, err
46
+ }
47
+
48
+ if len(jails) == 0 {
49
+ return nil, errors.New("no jails found")
50
+ }
51
+
52
+ f.Debugf("discovered %d jails: %v", len(jails), jails)
53
+
54
+ return jails, nil
55
+}
56
+
57
+func (f *Fail2Ban) collectJails(mx map[string]int64) error {
58
+ seen := make(map[string]bool)
59
+
60
+ for _, jail := range f.jails {
61
+ f.Debugf("querying status for jail '%s'", jail)
62
+ bs, err := f.exec.jailStatus(jail)
63
+ if err != nil {
64
+ if errors.Is(err, errJailNotExist) {
65
+ f.forceDiscover = true
66
+ continue
67
+ }
68
+ return err
69
+ }
70
+
71
+ failed, banned, err := parseFail2banJailStatus(bs)
72
+ if err != nil {
73
+ return err
74
+ }
75
+
76
+ if !f.seenJails[jail] {
77
+ f.seenJails[jail] = true
78
+ f.addJailCharts(jail)
79
+ }
80
+ seen[jail] = true
81
+
82
+ px := fmt.Sprintf("jail_%s_", jail)
83
+
84
+ mx[px+"currently_failed"] = failed
85
+ mx[px+"currently_banned"] = banned
86
+ }
87
+
88
+ for jail := range f.seenJails {
89
+ if !seen[jail] {
90
+ delete(f.seenJails, jail)
91
+ f.removeJailCharts(jail)
92
+ }
93
+ }
94
+
95
+ return nil
96
+}
97
+
98
+func parseFail2banJailStatus(jailStatus []byte) (failed, banned int64, err error) {
99
+ const (
100
+ failedSub = "Currently failed:"
101
+ bannedSub = "Currently banned:"
102
+ )
103
+
104
+ var failedFound, bannedFound bool
105
+
106
+ sc := bufio.NewScanner(bytes.NewReader(jailStatus))
107
+
108
+ for sc.Scan() && !(failedFound && bannedFound) {
109
+ text := strings.TrimSpace(sc.Text())
110
+ if text == "" {
111
+ continue
112
+ }
113
+
114
+ if !failedFound {
115
+ if i := strings.Index(text, failedSub); i != -1 {
116
+ failedFound = true
117
+ s := strings.TrimSpace(text[i+len(failedSub):])
118
+ if failed, err = strconv.ParseInt(s, 10, 64); err != nil {
119
+ return 0, 0, fmt.Errorf("failed to parse currently failed value (%s): %v", s, err)
120
+ }
121
+ }
122
+ }
123
+ if !bannedFound {
124
+ if i := strings.Index(text, bannedSub); i != -1 {
125
+ bannedFound = true
126
+ s := strings.TrimSpace(text[i+len(bannedSub):])
127
+ if banned, err = strconv.ParseInt(s, 10, 64); err != nil {
128
+ return 0, 0, fmt.Errorf("failed to parse currently banned value (%s): %v", s, err)
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ if !failedFound || !bannedFound {
135
+ return 0, 0, errors.New("failed to find failed and banned values")
136
+ }
137
+
138
+ return failed, banned, nil
139
+}
140
+
141
+func parseFail2banStatus(status []byte) ([]string, error) {
142
+ const sub = "Jail list:"
143
+
144
+ var jails []string
145
+
146
+ sc := bufio.NewScanner(bytes.NewReader(status))
147
+
148
+ for sc.Scan() {
149
+ text := strings.TrimSpace(sc.Text())
150
+
151
+ if i := strings.Index(text, sub); i != -1 {
152
+ s := strings.ReplaceAll(text[i+len(sub):], ",", "")
153
+ jails = strings.Fields(s)
154
+ break
155
+ }
156
+ }
157
+
158
+ if len(jails) == 0 {
159
+ return nil, errors.New("failed to find jails")
160
+ }
161
+
162
+ return jails, nil
163
+}
src/go/collectors/go.d.plugin/modules/fail2ban/config_schema.json
new
+35
@@ -0,0 +1,35 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Fail2Ban collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 10
13
+ },
14
+ "timeout": {
15
+ "title": "Timeout",
16
+ "description": "Timeout for executing the binary, specified in seconds.",
17
+ "type": "number",
18
+ "minimum": 0.5,
19
+ "default": 2
20
+ }
21
+ },
22
+ "additionalProperties": false,
23
+ "patternProperties": {
24
+ "^name$": {}
25
+ }
26
+ },
27
+ "uiSchema": {
28
+ "uiOptions": {
29
+ "fullPage": true
30
+ },
31
+ "timeout": {
32
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
33
+ }
34
+ }
35
+}
src/go/collectors/go.d.plugin/modules/fail2ban/exec.go
new
+57
@@ -0,0 +1,57 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package fail2ban
4
+
5
+import (
6
+ "context"
7
+ "errors"
8
+ "fmt"
9
+ "os/exec"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
14
+)
15
+
16
+var errJailNotExist = errors.New("jail not exist")
17
+
18
+func newFail2BanClientCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *fail2banClientCliExec {
19
+ return &fail2banClientCliExec{
20
+ Logger: log,
21
+ ndsudoPath: ndsudoPath,
22
+ timeout: timeout,
23
+ }
24
+}
25
+
26
+type fail2banClientCliExec struct {
27
+ *logger.Logger
28
+
29
+ ndsudoPath string
30
+ timeout time.Duration
31
+}
32
+
33
+func (e *fail2banClientCliExec) status() ([]byte, error) {
34
+ return e.execute("fail2ban-client-status")
35
+}
36
+
37
+func (e *fail2banClientCliExec) jailStatus(jail string) ([]byte, error) {
38
+ return e.execute("fail2ban-client-status-jail", "--jail", jail)
39
+}
40
+
41
+func (e *fail2banClientCliExec) execute(args ...string) ([]byte, error) {
42
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
43
+ defer cancel()
44
+
45
+ cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
46
+ e.Debugf("executing '%s'", cmd)
47
+
48
+ bs, err := cmd.Output()
49
+ if err != nil {
50
+ if strings.HasPrefix(strings.TrimSpace(string(bs)), "Sorry but the jail") {
51
+ return nil, errJailNotExist
52
+ }
53
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
54
+ }
55
+
56
+ return bs, nil
57
+}
src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban.go
new
+111
@@ -0,0 +1,111 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package fail2ban
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
11
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
12
+)
13
+
14
+//go:embed "config_schema.json"
15
+var configSchema string
16
+
17
+func init() {
18
+ module.Register("fail2ban", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Defaults: module.Defaults{
21
+ UpdateEvery: 10,
22
+ },
23
+ Create: func() module.Module { return New() },
24
+ })
25
+}
26
+
27
+func New() *Fail2Ban {
28
+ return &Fail2Ban{
29
+ Config: Config{
30
+ Timeout: web.Duration(time.Second * 2),
31
+ },
32
+ charts: &module.Charts{},
33
+ discoverEvery: time.Minute * 5,
34
+ seenJails: make(map[string]bool),
35
+ }
36
+}
37
+
38
+type Config struct {
39
+ UpdateEvery int `yaml:"update_every" json:"update_every"`
40
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
41
+}
42
+
43
+type (
44
+ Fail2Ban struct {
45
+ module.Base
46
+ Config `yaml:",inline" json:""`
47
+
48
+ charts *module.Charts
49
+
50
+ exec fail2banClientCli
51
+
52
+ discoverEvery time.Duration
53
+ lastDiscoverTime time.Time
54
+ forceDiscover bool
55
+ jails []string
56
+
57
+ seenJails map[string]bool
58
+ }
59
+ fail2banClientCli interface {
60
+ status() ([]byte, error)
61
+ jailStatus(s string) ([]byte, error)
62
+ }
63
+)
64
+
65
+func (f *Fail2Ban) Configuration() any {
66
+ return f.Config
67
+}
68
+
69
+func (f *Fail2Ban) Init() error {
70
+ f2bClientExec, err := f.initFail2banClientCliExec()
71
+ if err != nil {
72
+ f.Errorf("fail2ban-client exec initialization: %v", err)
73
+ return err
74
+ }
75
+ f.exec = f2bClientExec
76
+
77
+ return nil
78
+}
79
+
80
+func (f *Fail2Ban) Check() error {
81
+ mx, err := f.collect()
82
+ if err != nil {
83
+ f.Error(err)
84
+ return err
85
+ }
86
+
87
+ if len(mx) == 0 {
88
+ return errors.New("no metrics collected")
89
+ }
90
+
91
+ return nil
92
+}
93
+
94
+func (f *Fail2Ban) Charts() *module.Charts {
95
+ return f.charts
96
+}
97
+
98
+func (f *Fail2Ban) Collect() map[string]int64 {
99
+ mx, err := f.collect()
100
+ if err != nil {
101
+ f.Error(err)
102
+ }
103
+
104
+ if len(mx) == 0 {
105
+ return nil
106
+ }
107
+
108
+ return mx
109
+}
110
+
111
+func (f *Fail2Ban) Cleanup() {}
src/go/collectors/go.d.plugin/modules/fail2ban/fail2ban_test.go
new
+238
@@ -0,0 +1,238 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package fail2ban
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "testing"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
11
+
12
+ "github.com/stretchr/testify/assert"
13
+ "github.com/stretchr/testify/require"
14
+)
15
+
16
+var (
17
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19
+
20
+ dataStatus, _ = os.ReadFile("testdata/fail2ban-status.txt")
21
+ dataJailStatus, _ = os.ReadFile("testdata/fail2ban-jail-status.txt")
22
+)
23
+
24
+func Test_testDataIsValid(t *testing.T) {
25
+ for name, data := range map[string][]byte{
26
+ "dataConfigJSON": dataConfigJSON,
27
+ "dataConfigYAML": dataConfigYAML,
28
+
29
+ "dataStatus": dataStatus,
30
+ "dataJailStatus": dataJailStatus,
31
+ } {
32
+ require.NotNil(t, data, name)
33
+
34
+ }
35
+}
36
+
37
+func TestFail2Ban_Configuration(t *testing.T) {
38
+ module.TestConfigurationSerialize(t, &Fail2Ban{}, dataConfigJSON, dataConfigYAML)
39
+}
40
+
41
+func TestFail2Ban_Init(t *testing.T) {
42
+ tests := map[string]struct {
43
+ config Config
44
+ wantFail bool
45
+ }{
46
+ "fails if failed to locate ndsudo": {
47
+ wantFail: true,
48
+ config: New().Config,
49
+ },
50
+ }
51
+
52
+ for name, test := range tests {
53
+ t.Run(name, func(t *testing.T) {
54
+ f2b := New()
55
+ f2b.Config = test.config
56
+
57
+ if test.wantFail {
58
+ assert.Error(t, f2b.Init())
59
+ } else {
60
+ assert.NoError(t, f2b.Init())
61
+ }
62
+ })
63
+ }
64
+}
65
+
66
+func TestFail2Ban_Cleanup(t *testing.T) {
67
+ tests := map[string]struct {
68
+ prepare func() *Fail2Ban
69
+ }{
70
+ "not initialized exec": {
71
+ prepare: func() *Fail2Ban {
72
+ return New()
73
+ },
74
+ },
75
+ "after check": {
76
+ prepare: func() *Fail2Ban {
77
+ f2b := New()
78
+ f2b.exec = prepareMockOk()
79
+ _ = f2b.Check()
80
+ return f2b
81
+ },
82
+ },
83
+ "after collect": {
84
+ prepare: func() *Fail2Ban {
85
+ f2b := New()
86
+ f2b.exec = prepareMockOk()
87
+ _ = f2b.Collect()
88
+ return f2b
89
+ },
90
+ },
91
+ }
92
+
93
+ for name, test := range tests {
94
+ t.Run(name, func(t *testing.T) {
95
+ f2b := test.prepare()
96
+
97
+ assert.NotPanics(t, f2b.Cleanup)
98
+ })
99
+ }
100
+}
101
+
102
+func TestFail2Ban_Charts(t *testing.T) {
103
+ assert.NotNil(t, New().Charts())
104
+}
105
+
106
+func TestFail2Ban_Check(t *testing.T) {
107
+ tests := map[string]struct {
108
+ prepareMock func() *mockFail2BanClientCliExec
109
+ wantFail bool
110
+ }{
111
+ "success multiple jails": {
112
+ wantFail: false,
113
+ prepareMock: prepareMockOk,
114
+ },
115
+ "error on status": {
116
+ wantFail: true,
117
+ prepareMock: prepareMockErrOnStatus,
118
+ },
119
+ "empty response (no jails)": {
120
+ prepareMock: prepareMockEmptyResponse,
121
+ wantFail: true,
122
+ },
123
+ }
124
+
125
+ for name, test := range tests {
126
+ t.Run(name, func(t *testing.T) {
127
+ f2b := New()
128
+ mock := test.prepareMock()
129
+ f2b.exec = mock
130
+
131
+ if test.wantFail {
132
+ assert.Error(t, f2b.Check())
133
+ } else {
134
+ assert.NoError(t, f2b.Check())
135
+ }
136
+ })
137
+ }
138
+}
139
+
140
+func TestFail2Ban_Collect(t *testing.T) {
141
+ tests := map[string]struct {
142
+ prepareMock func() *mockFail2BanClientCliExec
143
+ wantMetrics map[string]int64
144
+ }{
145
+ "success multiple jails": {
146
+ prepareMock: prepareMockOk,
147
+ wantMetrics: map[string]int64{
148
+ "jail_dovecot_currently_banned": 30,
149
+ "jail_dovecot_currently_failed": 10,
150
+ "jail_sshd_currently_banned": 30,
151
+ "jail_sshd_currently_failed": 10,
152
+ },
153
+ },
154
+ "error on status": {
155
+ prepareMock: prepareMockErrOnStatus,
156
+ wantMetrics: nil,
157
+ },
158
+ "empty response (no jails)": {
159
+ prepareMock: prepareMockEmptyResponse,
160
+ wantMetrics: nil,
161
+ },
162
+ }
163
+
164
+ for name, test := range tests {
165
+ t.Run(name, func(t *testing.T) {
166
+ f2b := New()
167
+ mock := test.prepareMock()
168
+ f2b.exec = mock
169
+
170
+ mx := f2b.Collect()
171
+
172
+ assert.Equal(t, test.wantMetrics, mx)
173
+ if len(test.wantMetrics) > 0 {
174
+ assert.Len(t, *f2b.Charts(), len(jailChartsTmpl)*2)
175
+ testMetricsHasAllChartsDims(t, f2b, mx)
176
+ }
177
+ })
178
+ }
179
+}
180
+
181
+func testMetricsHasAllChartsDims(t *testing.T, f2b *Fail2Ban, mx map[string]int64) {
182
+ for _, chart := range *f2b.Charts() {
183
+ if chart.Obsolete {
184
+ continue
185
+ }
186
+ for _, dim := range chart.Dims {
187
+ _, ok := mx[dim.ID]
188
+ assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
189
+ }
190
+ for _, v := range chart.Vars {
191
+ _, ok := mx[v.ID]
192
+ assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID)
193
+ }
194
+ }
195
+}
196
+
197
+func prepareMockOk() *mockFail2BanClientCliExec {
198
+ return &mockFail2BanClientCliExec{
199
+ statusData: dataStatus,
200
+ jailStatusData: dataJailStatus,
201
+ }
202
+}
203
+
204
+func prepareMockErrOnStatus() *mockFail2BanClientCliExec {
205
+ return &mockFail2BanClientCliExec{
206
+ errOnStatus: true,
207
+ statusData: dataStatus,
208
+ jailStatusData: dataJailStatus,
209
+ }
210
+}
211
+
212
+func prepareMockEmptyResponse() *mockFail2BanClientCliExec {
213
+ return &mockFail2BanClientCliExec{}
214
+}
215
+
216
+type mockFail2BanClientCliExec struct {
217
+ errOnStatus bool
218
+ statusData []byte
219
+
220
+ errOnJailStatus bool
221
+ jailStatusData []byte
222
+}
223
+
224
+func (m *mockFail2BanClientCliExec) status() ([]byte, error) {
225
+ if m.errOnStatus {
226
+ return nil, errors.New("mock.status() error")
227
+ }
228
+
229
+ return m.statusData, nil
230
+}
231
+
232
+func (m *mockFail2BanClientCliExec) jailStatus(_ string) ([]byte, error) {
233
+ if m.errOnJailStatus {
234
+ return nil, errors.New("mock.jailStatus() error")
235
+ }
236
+
237
+ return m.jailStatusData, nil
238
+}
src/go/collectors/go.d.plugin/modules/fail2ban/init.go
new
+23
@@ -0,0 +1,23 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package fail2ban
4
+
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+
10
+ "github.com/netdata/netdata/go/go.d.plugin/agent/executable"
11
+)
12
+
13
+func (f *Fail2Ban) initFail2banClientCliExec() (fail2banClientCli, error) {
14
+ ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15
+ if _, err := os.Stat(ndsudoPath); err != nil {
16
+ return nil, fmt.Errorf("ndsudo executable not found: %v", err)
17
+
18
+ }
19
+
20
+ f2bClientExec := newFail2BanClientCliExec(ndsudoPath, f.Timeout.Duration(), f.Logger)
21
+
22
+ return f2bClientExec, nil
23
+}
src/go/collectors/go.d.plugin/modules/fail2ban/metadata.yaml
new
+105
@@ -0,0 +1,105 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-fail2ban
5
+ plugin_name: go.d.plugin
6
+ module_name: fail2ban
7
+ monitored_instance:
8
+ name: Fail2ban
9
+ link: "https://github.com/fail2ban/fail2ban#readme"
10
+ icon_filename: fail2ban.png
11
+ categories:
12
+ - data-collection.authentication-and-authorization
13
+ keywords:
14
+ - fail2ban
15
+ - security
16
+ - authentication
17
+ - authorization
18
+ related_resources:
19
+ integrations:
20
+ list: []
21
+ info_provided_to_referring_integrations:
22
+ description: ""
23
+ most_popular: false
24
+ overview:
25
+ data_collection:
26
+ metrics_description: >
27
+ This collector tracks two main metrics for each jail: currently banned IPs and active failure incidents.
28
+ It relies on the [`fail2ban-client`](https://linux.die.net/man/1/fail2ban-client) CLI tool but avoids directly executing the binary.
29
+ Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment.
30
+ This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management.
31
+ method_description: ""
32
+ supported_platforms:
33
+ include: []
34
+ exclude: []
35
+ multi_instance: false
36
+ additional_permissions:
37
+ description: ""
38
+ default_behavior:
39
+ auto_detection:
40
+ description: ""
41
+ limits:
42
+ description: ""
43
+ performance_impact:
44
+ description: ""
45
+ setup:
46
+ prerequisites:
47
+ list: []
48
+ configuration:
49
+ file:
50
+ name: go.d/fail2ban.conf
51
+ options:
52
+ description: |
53
+ The following options can be defined globally: update_every.
54
+ folding:
55
+ title: Config options
56
+ enabled: true
57
+ list:
58
+ - name: update_every
59
+ description: Data collection frequency.
60
+ default_value: 10
61
+ required: false
62
+ - name: timeout
63
+ description: fail2ban-client binary execution timeout.
64
+ default_value: 2
65
+ required: false
66
+ examples:
67
+ folding:
68
+ title: Config
69
+ enabled: true
70
+ list:
71
+ - name: Custom update_every
72
+ description: Allows you to override the default data collection interval.
73
+ config: |
74
+ jobs:
75
+ - name: fail2ban
76
+ update_every: 5 # Collect Fail2Ban jails statistics every 5 seconds
77
+ troubleshooting:
78
+ problems:
79
+ list: []
80
+ alerts: []
81
+ metrics:
82
+ folding:
83
+ title: Metrics
84
+ enabled: false
85
+ description: ""
86
+ availability: []
87
+ scopes:
88
+ - name: jail
89
+ description: These metrics refer to the Jail.
90
+ labels:
91
+ - name: jail
92
+ description: Jail's name
93
+ metrics:
94
+ - name: fail2ban.jail_banned_ips
95
+ description: Fail2Ban Jail banned IPs
96
+ unit: addresses
97
+ chart_type: line
98
+ dimensions:
99
+ - name: banned
100
+ - name: fail2ban.jail_active_failures
101
+ description: Fail2Ban Jail active failures
102
+ unit: failures
103
+ chart_type: line
104
+ dimensions:
105
+ - name: active_failures
src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.json
new
+4
@@ -0,0 +1,4 @@
1
+{
2
+ "update_every": 123,
3
+ "timeout": 123.123
4
+}
src/go/collectors/go.d.plugin/modules/fail2ban/testdata/config.yaml
new
+2
@@ -0,0 +1,2 @@
1
+update_every: 123
2
+timeout: 123.123
src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-jail-status.txt
new
+9
@@ -0,0 +1,9 @@
1
+Status for the jail: JAIL
2
+|- Filter
3
+| |- Currently failed: 10
4
+| |- Total failed: 20
5
+| `- File list: /var/log/auth.log
6
+`- Actions
7
+ |- Currently banned: 30
8
+ |- Total banned: 40
9
+ `- Banned IP list:
src/go/collectors/go.d.plugin/modules/fail2ban/testdata/fail2ban-status.txt
new
+3
@@ -0,0 +1,3 @@
1
+Status
2
+|- Number of jail: 1
3
+`- Jail list: sshd, dovecot
\ No newline at end of file
src/go/collectors/go.d.plugin/modules/init.go
+1
@@ -24,6 +24,7 @@ import (
24
_ "github.com/netdata/netdata/go/go.d.plugin/modules/elasticsearch"
25
_ "github.com/netdata/netdata/go/go.d.plugin/modules/envoy"
26
_ "github.com/netdata/netdata/go/go.d.plugin/modules/example"
27
+ _ "github.com/netdata/netdata/go/go.d.plugin/modules/fail2ban"
28
_ "github.com/netdata/netdata/go/go.d.plugin/modules/filecheck"
29
_ "github.com/netdata/netdata/go/go.d.plugin/modules/fluentd"
30
_ "github.com/netdata/netdata/go/go.d.plugin/modules/freeradius"
src/go/collectors/go.d.plugin/modules/lvm/lvm_test.go
-2
@@ -61,7 +61,6 @@ func TestLVM_Init(t *testing.T) {
61
}
62
})
63
}
64
-
64
}
65
66
func TestLVM_Cleanup(t *testing.T) {
@@ -190,7 +189,6 @@ func TestLVM_Collect(t *testing.T) {
189
}
190
})
191
}
193
-
192
}
193
194
func prepareMockOK() *mockLvmCliExec {