From e7ab5f99f0cd4645dfa82b0d0c6891ff955c7a39 Mon Sep 17 00:00:00 2001 From: grugrut Date: Thu, 22 Jun 2017 04:42:13 +0900 Subject: [PATCH] Add input plugin for Fail2ban (#2875) --- plugins/inputs/all/all.go | 1 + plugins/inputs/fail2ban/README.md | 60 +++++++++ plugins/inputs/fail2ban/fail2ban.go | 135 ++++++++++++++++++++ plugins/inputs/fail2ban/fail2ban_nolinux.go | 3 + plugins/inputs/fail2ban/fail2ban_test.go | 125 ++++++++++++++++++ 5 files changed, 324 insertions(+) create mode 100644 plugins/inputs/fail2ban/README.md create mode 100644 plugins/inputs/fail2ban/fail2ban.go create mode 100644 plugins/inputs/fail2ban/fail2ban_nolinux.go create mode 100644 plugins/inputs/fail2ban/fail2ban_test.go diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 456852ff2c7f0..3bf98c9d972eb 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -21,6 +21,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/dovecot" _ "github.com/influxdata/telegraf/plugins/inputs/elasticsearch" _ "github.com/influxdata/telegraf/plugins/inputs/exec" + _ "github.com/influxdata/telegraf/plugins/inputs/fail2ban" _ "github.com/influxdata/telegraf/plugins/inputs/filestat" _ "github.com/influxdata/telegraf/plugins/inputs/graylog" _ "github.com/influxdata/telegraf/plugins/inputs/haproxy" diff --git a/plugins/inputs/fail2ban/README.md b/plugins/inputs/fail2ban/README.md new file mode 100644 index 0000000000000..e785a0dd37f4e --- /dev/null +++ b/plugins/inputs/fail2ban/README.md @@ -0,0 +1,60 @@ +# Fail2ban Plugin + +The fail2ban plugin gathers counts of failed and banned ip addresses from fail2ban. + +This plugin run fail2ban-client command, and fail2ban-client require root access. +You have to grant telegraf to run fail2ban-client: + +- Run telegraf as root. (deprecate) +- Configure sudo to grant telegraf to fail2ban-client. + +### Using sudo + +You may edit your sudo configuration with the following: + +``` sudo +telegraf ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status * +``` + +### Configuration: + +``` toml +# Read metrics from fail2ban. +[[inputs.fail2ban]] + ## fail2ban-client require root access. + ## Setting 'use_sudo' to true will make use of sudo to run fail2ban-client. + ## Users must configure sudo to allow telegraf user to run fail2ban-client with no password. + ## This plugin run only "fail2ban-client status". + use_sudo = false +``` + +### Measurements & Fields: + +- fail2ban + - failed (integer, count) + - banned (integer, count) + +### Tags: + +- All measurements have the following tags: + - jail + +### Example Output: + +``` +# fail2ban-client status sshd +Status for the jail: sshd +|- Filter +| |- Currently failed: 5 +| |- Total failed: 20 +| `- File list: /var/log/secure +`- Actions + |- Currently banned: 2 + |- Total banned: 10 + `- Banned IP list: 192.168.0.1 192.168.0.2 +``` + +``` +$ ./telegraf --config telegraf.conf --input-filter fail2ban --test +fail2ban,jail=sshd failed=5i,banned=2i 1495868667000000000 +``` diff --git a/plugins/inputs/fail2ban/fail2ban.go b/plugins/inputs/fail2ban/fail2ban.go new file mode 100644 index 0000000000000..8ecad3b5215dd --- /dev/null +++ b/plugins/inputs/fail2ban/fail2ban.go @@ -0,0 +1,135 @@ +// +build linux + +package fail2ban + +import ( + "errors" + "fmt" + "os/exec" + "strings" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" + "strconv" +) + +var ( + execCommand = exec.Command // execCommand is used to mock commands in tests. +) + +type Fail2ban struct { + path string + UseSudo bool +} + +var sampleConfig = ` + ## fail2ban-client require root access. + ## Setting 'use_sudo' to true will make use of sudo to run fail2ban-client. + ## Users must configure sudo to allow telegraf user to run fail2ban-client with no password. + ## This plugin run only "fail2ban-client status". + use_sudo = false +` + +var metricsTargets = []struct { + target string + field string +}{ + { + target: "Currently failed:", + field: "failed", + }, + { + target: "Currently banned:", + field: "banned", + }, +} + +func (f *Fail2ban) Description() string { + return "Read metrics from fail2ban." +} + +func (f *Fail2ban) SampleConfig() string { + return sampleConfig +} + +func (f *Fail2ban) Gather(acc telegraf.Accumulator) error { + if len(f.path) == 0 { + return errors.New("fail2ban-client not found: verify that fail2ban is installed and that fail2ban-client is in your PATH") + } + + name := f.path + var arg []string + + if f.UseSudo { + name = "sudo" + arg = append(arg, f.path) + } + + args := append(arg, "status") + + cmd := execCommand(name, args...) + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to run command %s: %s - %s", strings.Join(cmd.Args, " "), err, string(out)) + } + lines := strings.Split(string(out), "\n") + const targetString = "Jail list:" + var jails []string + for _, line := range lines { + idx := strings.LastIndex(line, targetString) + if idx < 0 { + // not target line, skip. + continue + } + jails = strings.Split(strings.TrimSpace(line[idx+len(targetString):]), ", ") + break + } + + for _, jail := range jails { + fields := make(map[string]interface{}) + args := append(arg, "status", jail) + cmd := execCommand(name, args...) + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to run command %s: %s - %s", strings.Join(cmd.Args, " "), err, string(out)) + } + + lines := strings.Split(string(out), "\n") + for _, line := range lines { + key, value := extractCount(line) + if key != "" { + fields[key] = value + } + } + acc.AddFields("fail2ban", fields, map[string]string{"jail": jail}) + } + return nil +} + +func extractCount(line string) (string, int) { + for _, metricsTarget := range metricsTargets { + idx := strings.LastIndex(line, metricsTarget.target) + if idx < 0 { + continue + } + ban := strings.TrimSpace(line[idx+len(metricsTarget.target):]) + banCount, err := strconv.Atoi(ban) + if err != nil { + return "", -1 + } + return metricsTarget.field, banCount + } + return "", -1 +} + +func init() { + f := Fail2ban{} + path, _ := exec.LookPath("fail2ban-client") + if len(path) > 0 { + f.path = path + } + inputs.Add("fail2ban", func() telegraf.Input { + f := f + return &f + }) +} diff --git a/plugins/inputs/fail2ban/fail2ban_nolinux.go b/plugins/inputs/fail2ban/fail2ban_nolinux.go new file mode 100644 index 0000000000000..e62efa5b1b0e7 --- /dev/null +++ b/plugins/inputs/fail2ban/fail2ban_nolinux.go @@ -0,0 +1,3 @@ +// +build !linux + +package fail2ban diff --git a/plugins/inputs/fail2ban/fail2ban_test.go b/plugins/inputs/fail2ban/fail2ban_test.go new file mode 100644 index 0000000000000..b28d824ee3aed --- /dev/null +++ b/plugins/inputs/fail2ban/fail2ban_test.go @@ -0,0 +1,125 @@ +package fail2ban + +import ( + "fmt" + "os" + "os/exec" + "strings" + "testing" + + "github.com/influxdata/telegraf/testutil" +) + +// By all rights, we should use `string literal`, but the string contains "`". +var execStatusOutput = "Status\n" + + "|- Number of jail:\t3\n" + + "`- Jail list:\tdovecot, postfix, sshd" +var execStatusDovecotOutput = "Status for the jail: dovecot\n" + + "|- Filter\n" + + "| |- Currently failed:\t11\n" + + "| |- Total failed:\t22\n" + + "| `- File list:\t/var/log/maillog\n" + + "`- Actions\n" + + " |- Currently banned:\t0\n" + + " |- Total banned:\t100\n" + + " `- Banned IP list:" +var execStatusPostfixOutput = "Status for the jail: postfix\n" + + "|- Filter\n" + + "| |- Currently failed:\t4\n" + + "| |- Total failed:\t10\n" + + "| `- File list:\t/var/log/maillog\n" + + "`- Actions\n" + + " |- Currently banned:\t3\n" + + " |- Total banned:\t60\n" + + " `- Banned IP list:\t192.168.10.1 192.168.10.3" +var execStatusSshdOutput = "Status for the jail: sshd\n" + + "|- Filter\n" + + "| |- Currently failed:\t0\n" + + "| |- Total failed:\t5\n" + + "| `- File list:\t/var/log/secure\n" + + "`- Actions\n" + + " |- Currently banned:\t2\n" + + " |- Total banned:\t50\n" + + " `- Banned IP list:\t192.168.0.1 192.168.1.1" + +func TestGather(t *testing.T) { + f := Fail2ban{ + path: "/usr/bin/fail2ban-client", + } + + execCommand = fakeExecCommand + defer func() { execCommand = exec.Command }() + var acc testutil.Accumulator + err := f.Gather(&acc) + if err != nil { + t.Fatal(err) + } + + fields1 := map[string]interface{}{ + "banned": 2, + "failed": 0, + } + tags1 := map[string]string{ + "jail": "sshd", + } + + fields2 := map[string]interface{}{ + "banned": 3, + "failed": 4, + } + tags2 := map[string]string{ + "jail": "postfix", + } + + fields3 := map[string]interface{}{ + "banned": 0, + "failed": 11, + } + tags3 := map[string]string{ + "jail": "dovecot", + } + + acc.AssertContainsTaggedFields(t, "fail2ban", fields1, tags1) + acc.AssertContainsTaggedFields(t, "fail2ban", fields2, tags2) + acc.AssertContainsTaggedFields(t, "fail2ban", fields3, tags3) +} + +func fakeExecCommand(command string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestHelperProcess", "--", command} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + return cmd +} + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + args := os.Args + cmd, args := args[3], args[4:] + + if !strings.HasSuffix(cmd, "fail2ban-client") { + fmt.Fprint(os.Stdout, "command not found") + os.Exit(1) + } + + if len(args) == 1 && args[0] == "status" { + fmt.Fprint(os.Stdout, execStatusOutput) + os.Exit(0) + } else if len(args) == 2 && args[0] == "status" { + if args[1] == "sshd" { + fmt.Fprint(os.Stdout, execStatusSshdOutput) + os.Exit(0) + } else if args[1] == "postfix" { + fmt.Fprint(os.Stdout, execStatusPostfixOutput) + os.Exit(0) + } else if args[1] == "dovecot" { + fmt.Fprint(os.Stdout, execStatusDovecotOutput) + os.Exit(0) + } + } + fmt.Fprint(os.Stdout, "invalid argument") + os.Exit(1) +}