forked from influxdata/telegraf
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add input plugin for Fail2ban (influxdata#2875)
- Loading branch information
Showing
5 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
// +build !linux | ||
|
||
package fail2ban |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |