Skip to content

Commit

Permalink
Add input plugin for Fail2ban (influxdata#2875)
Browse files Browse the repository at this point in the history
  • Loading branch information
grugrut authored and jeichorn committed Jul 24, 2017
1 parent 2aff7da commit e7ab5f9
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Expand Up @@ -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"
Expand Down
60 changes: 60 additions & 0 deletions 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
```
135 changes: 135 additions & 0 deletions 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
})
}
3 changes: 3 additions & 0 deletions plugins/inputs/fail2ban/fail2ban_nolinux.go
@@ -0,0 +1,3 @@
// +build !linux

package fail2ban
125 changes: 125 additions & 0 deletions 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)
}

0 comments on commit e7ab5f9

Please sign in to comment.