Guard: watch logs, ban misbehaving addresses in Housecarl
Files and the journal, patterns with one <IP>, bounties within a window, bans in a Housecarl ban list with repeat offenders' bans growing; presets for sshd, jarl, housecarl, steward, bind-xfr, postfix-sasl, dovecot, nginx-auth and housecarl-drops; guard run/check/test/status/bans/ban/unban; a Debian package with a sandboxed unit; an end-to-end test with Housecarl. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d06bf8d531
47 files changed
+4614
No files matched your search
@@ -0,0 +1,120 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"guard/internal/match"
|
||||
)
|
||||
|
||||
// TestPresets runs each preset over its sample lines in testdata/presets:
|
||||
// what it must find, and what it must leave alone.
|
||||
func TestPresets(t *testing.T) {
|
||||
for name, p := range Presets() {
|
||||
if p.Source != "" {
|
||||
continue
|
||||
}
|
||||
m, err := match.Compile(p.Match, p.Ignore)
|
||||
if err != nil {
|
||||
t.Fatalf("preset %s: %v", name, err)
|
||||
}
|
||||
f, err := os.Open(filepath.Join("..", "..", "testdata", "presets", name+".txt"))
|
||||
if err != nil {
|
||||
t.Errorf("preset %s has no sample lines: %v", name, err)
|
||||
continue
|
||||
}
|
||||
sc := bufio.NewScanner(f)
|
||||
n := 0
|
||||
for sc.Scan() {
|
||||
want, line, ok := strings.Cut(sc.Text(), "\t")
|
||||
if !ok || strings.HasPrefix(want, "#") {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
a, matched := m.Match(line)
|
||||
switch {
|
||||
case want == "-" && matched:
|
||||
t.Errorf("%s: matched %s in %q", name, a, line)
|
||||
case want != "-" && !matched:
|
||||
t.Errorf("%s: no match in %q", name, line)
|
||||
case want != "-" && a.String() != want:
|
||||
t.Errorf("%s: %s, want %s, in %q", name, a, want, line)
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
if n == 0 {
|
||||
t.Errorf("preset %s: no sample lines", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExampleParses(t *testing.T) {
|
||||
c, err := Parse([]byte(Example()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := c.Watch[0]
|
||||
if w.Kind() != "journal" || w.Ban.List != "web" || w.Hits != 5 || w.Within.D() != 10*time.Minute || len(w.Match) == 0 {
|
||||
t.Errorf("the ssh watch: %+v", w)
|
||||
}
|
||||
if c.Defaults.Repeat.Max.D() != 7*24*time.Hour {
|
||||
t.Errorf("repeat %+v", c.Defaults.Repeat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
base := "housecarl: {url: https://fw:8070, list: web}\n"
|
||||
cases := []struct{ yaml, want string }{
|
||||
{"", "give url"},
|
||||
{base + "watch: [{name: a, preset: nope}]", `no preset "nope"`},
|
||||
{base + "watch: [{name: a, file: /x, match: ['no address']}]", "exactly one"},
|
||||
{base + "watch: [{name: a, file: /x, journal: {unit: x}, match: ['<IP>']}]", "one source"},
|
||||
{base + "watch: [{name: a, file: /x}]", "no match patterns"},
|
||||
{base + "watch: [{name: a, source: housecarl-drops, match: ['<IP>']}]", "no patterns"},
|
||||
{base + "watch: [{name: a, preset: sshd, ban: {protocol: tcp}}]", "without ban.port"},
|
||||
{base + "watch: [{name: a, preset: sshd, within: 10 minutes}]", "isn't a length of time"},
|
||||
{base + "watch: [{name: a, preset: sshd}, {name: A, preset: sshd}]", "used twice"},
|
||||
{base + "never_ban: ['not an address!']\nwatch: [{name: a, preset: sshd}]", "never_ban"},
|
||||
{base + "bogus: 1\nwatch: [{name: a, preset: sshd}]", "bogus"},
|
||||
{"housecarl: {url: https://fw:8070}\nwatch: [{name: a, preset: sshd}]", "which ban list"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := Parse([]byte(c.yaml))
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%q: %v, want %q", c.yaml, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresetSourceAndOwnPatterns(t *testing.T) {
|
||||
c, err := Parse([]byte(`housecarl: {socket: /run/housecarl/housecarl.sock, list: router}
|
||||
watch:
|
||||
- {name: ssh, preset: sshd, file: /var/log/auth.log, match: ['custom <IP> line']}
|
||||
- {name: scans, preset: housecarl-drops}
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ssh := c.Watch[0]
|
||||
if ssh.Kind() != "file" || ssh.File != "/var/log/auth.log" || ssh.Match[len(ssh.Match)-1] != "custom <IP> line" || len(ssh.Match) < 2 {
|
||||
t.Errorf("own file and pattern beside the preset's: %+v", ssh)
|
||||
}
|
||||
if s := c.Watch[1]; s.Kind() != SourceHousecarlDrops || s.Protocol != "tcp" {
|
||||
t.Errorf("housecarl-drops: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuration(t *testing.T) {
|
||||
for in, want := range map[string]time.Duration{"30s": 30 * time.Second, "1d12h": 36 * time.Hour, "1w": 7 * 24 * time.Hour} {
|
||||
if d, err := ParseDuration(in); err != nil || d != want {
|
||||
t.Errorf("%s: %v %v", in, d, err)
|
||||
}
|
||||
if got := FormatDuration(want); got != in {
|
||||
t.Errorf("format %v: %s, want %s", want, got, in)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user