CLI: shorter, plainer output
Tables name a journal source by its first two units or identifiers; guard test shows 20 matching lines and addresses (--all for all), no /32; Housecarl connection errors without Go's request URL, said once by guard check; status tells "not running" from "no permission"; bans says when there are none; "1 ban lifted". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
96f551be80
commit
b87cc6fb62
5 files changed
+169
-37
No files matched your search
+100
-29
@@ -14,6 +14,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -161,7 +162,7 @@ func checkCmd() *cobra.Command {
|
||||
}
|
||||
lists[x.Ban.List] = true
|
||||
drops = drops || x.Kind() == config.SourceHousecarlDrops
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d in %s\t%s\t%s\n", x.Name, daemon.Describe(x), len(x.Match), x.Hits, x.Within, x.Ban.List, ports(x.Ban.Protocol, x.Ban.Port))
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d in %s\t%s\t%s\n", x.Name, daemon.Short(x), len(x.Match), x.Hits, x.Within, x.Ban.List, ports(x.Ban.Protocol, x.Ban.Port))
|
||||
}
|
||||
})
|
||||
hc, err := client(cfg)
|
||||
@@ -169,9 +170,17 @@ func checkCmd() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
bad := false
|
||||
names := make([]string, 0, len(lists))
|
||||
for l := range lists {
|
||||
names = append(names, l)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, l := range names {
|
||||
bl, err := hc.BanList(ctx(), l)
|
||||
var down *housecarl.UnreachableError
|
||||
switch {
|
||||
case errors.As(err, &down):
|
||||
return err
|
||||
case err != nil:
|
||||
fmt.Printf("ban list %s: %v\n", l, err)
|
||||
bad = true
|
||||
@@ -183,7 +192,11 @@ func checkCmd() *cobra.Command {
|
||||
}
|
||||
if drops {
|
||||
if _, err := hc.Log(ctx(), time.Now()); err != nil {
|
||||
fmt.Printf("Housecarl's log (housecarl-drops): %v (the token needs log:read)\n", err)
|
||||
hint := ""
|
||||
if housecarl.Refused(err) {
|
||||
hint = " (the token needs log:read)"
|
||||
}
|
||||
fmt.Printf("Housecarl's log (housecarl-drops): %v%s\n", err, hint)
|
||||
bad = true
|
||||
} else {
|
||||
fmt.Println("Housecarl's log (housecarl-drops): ok")
|
||||
@@ -266,7 +279,7 @@ func presetsCmd() *cobra.Command {
|
||||
fmt.Fprintln(w, "PRESET\tSOURCE\tWHAT")
|
||||
for _, n := range names {
|
||||
p := ps[n]
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", n, daemon.Describe(&config.Watch{File: p.File, Journal: p.Journal, Source: p.Source, Protocol: "tcp"}), p.Description)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", n, daemon.Short(&config.Watch{File: p.File, Journal: p.Journal, Source: p.Source, Protocol: "tcp"}), p.Description)
|
||||
}
|
||||
})
|
||||
return nil
|
||||
@@ -331,10 +344,14 @@ the watch's window (within) is used.`,
|
||||
count int
|
||||
ban bool
|
||||
}
|
||||
type found struct {
|
||||
addr, text, why string
|
||||
}
|
||||
hits := map[string]*hit{}
|
||||
order := []string{}
|
||||
tr := map[string][]time.Time{}
|
||||
matched := 0
|
||||
var shown []found
|
||||
matched, spared := 0, 0
|
||||
for _, l := range in {
|
||||
a, ok := m.Match(l.Text)
|
||||
if !ok {
|
||||
@@ -342,17 +359,14 @@ the watch's window (within) is used.`,
|
||||
}
|
||||
matched++
|
||||
why := never.Why(a)
|
||||
if all || matched <= 200 {
|
||||
note := ""
|
||||
if why != "" {
|
||||
note = " (never banned: " + why + ")"
|
||||
}
|
||||
fmt.Printf("%-39s %s%s\n", a, clip(l.Text, 160), note)
|
||||
if all || len(shown) < testShown {
|
||||
shown = append(shown, found{a.String(), l.Text, why})
|
||||
}
|
||||
if why != "" {
|
||||
spared++
|
||||
continue
|
||||
}
|
||||
g := match.Group(a, 64).String()
|
||||
g := address(match.Group(a, 64))
|
||||
h := hits[g]
|
||||
if h == nil {
|
||||
h = &hit{}
|
||||
@@ -371,39 +385,72 @@ the watch's window (within) is used.`,
|
||||
h.ban = h.ban || h.count >= w.Hits
|
||||
}
|
||||
}
|
||||
if matched > 200 && !all {
|
||||
fmt.Printf("… %d more matching lines (--all shows them)\n", matched-200)
|
||||
width := 0
|
||||
for _, f := range shown {
|
||||
width = max(width, len(f.addr))
|
||||
}
|
||||
fmt.Printf("\n%d lines, %d matched, %d addresses (IPv6 by /64)\n", len(in), matched, len(order))
|
||||
for _, f := range shown {
|
||||
note := ""
|
||||
if f.why != "" {
|
||||
note = " (never banned: " + f.why + ")"
|
||||
}
|
||||
fmt.Printf("%-*s %s%s\n", width, f.addr, clip(f.text, 120), note)
|
||||
}
|
||||
if matched > len(shown) {
|
||||
fmt.Printf("… and %d more matching lines (--all shows them)\n", matched-len(shown))
|
||||
}
|
||||
fmt.Printf("\n%d lines, %d matched, %s (IPv6 by /64)", len(in), matched, plural(len(order), "address"))
|
||||
if spared > 0 {
|
||||
fmt.Printf("; %d matches from never_ban addresses", spared)
|
||||
}
|
||||
fmt.Println()
|
||||
sort.SliceStable(order, func(i, j int) bool { return hits[order[i]].count > hits[order[j]].count })
|
||||
banned := 0
|
||||
var banned []string
|
||||
for _, g := range order {
|
||||
if hits[g].ban {
|
||||
banned++
|
||||
banned = append(banned, g)
|
||||
}
|
||||
}
|
||||
if banned == 0 {
|
||||
fmt.Printf("nobody would be banned (%d hits within %s)\n", w.Hits, w.Within)
|
||||
rule := fmt.Sprintf("%d hits within %s", w.Hits, w.Within)
|
||||
if !timed {
|
||||
rule = fmt.Sprintf("%d hits; a file's lines have no times, so all of them count", w.Hits)
|
||||
}
|
||||
if len(banned) == 0 {
|
||||
fmt.Printf("Nobody would be banned (%s)\n", rule)
|
||||
return nil
|
||||
}
|
||||
window := "within " + w.Within.String()
|
||||
if !timed {
|
||||
window = "in all (a file's lines have no time)"
|
||||
fmt.Printf("Would ban %d (%s):\n", len(banned), rule)
|
||||
width = 0
|
||||
for _, g := range banned {
|
||||
width = max(width, len(g))
|
||||
}
|
||||
fmt.Printf("would ban %d (%d hits %s):\n", banned, w.Hits, window)
|
||||
for _, g := range order {
|
||||
if hits[g].ban {
|
||||
fmt.Printf(" %-39s %d matches\n", g, hits[g].count)
|
||||
for i, g := range banned {
|
||||
if i == testShown && !all {
|
||||
fmt.Printf(" … and %d more (--all shows them)\n", len(banned)-i)
|
||||
break
|
||||
}
|
||||
fmt.Printf(" %-*s %s\n", width, g, plural(hits[g].count, "match"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVarP(&lines, "lines", "n", 5000, "a journal watch without FILE: the last this many entries")
|
||||
cmd.Flags().BoolVar(&all, "all", false, "print every matching line, not the first 200")
|
||||
cmd.Flags().BoolVar(&all, "all", false, "print every matching line and address, not the first 20")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// testShown is how many matching lines, and addresses to ban, guard
|
||||
// test prints without --all.
|
||||
const testShown = 20
|
||||
|
||||
// address is a ban's address: a single address without its /32 or /128.
|
||||
func address(p netip.Prefix) string {
|
||||
if p.IsSingleIP() {
|
||||
return p.Addr().String()
|
||||
}
|
||||
return p.String()
|
||||
}
|
||||
|
||||
func readLines(path string) ([]source.Line, error) {
|
||||
var r io.Reader = os.Stdin
|
||||
if path != "-" {
|
||||
@@ -473,8 +520,17 @@ func statusCmd() *cobra.Command {
|
||||
return d.DialContext(c, "unix", cfg.Socket)
|
||||
}}}
|
||||
resp, err := hc.Get("http://guard/status")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Guard isn't running, or %s can't be reached (as root, or the guard user): %w", cfg.Socket, err)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist), errors.Is(err, syscall.ECONNREFUSED):
|
||||
return fmt.Errorf("Guard isn't running (nothing answers on %s)", cfg.Socket)
|
||||
case errors.Is(err, os.ErrPermission):
|
||||
return fmt.Errorf("no permission for %s: run this as root or the guard user", cfg.Socket)
|
||||
case err != nil:
|
||||
var ue *url.Error
|
||||
if errors.As(err, &ue) {
|
||||
err = ue.Err
|
||||
}
|
||||
return fmt.Errorf("Guard's socket %s: %w", cfg.Socket, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var s daemon.Status
|
||||
@@ -519,6 +575,17 @@ func statusCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// plural is "1 ban", "2 bans", "2 matches".
|
||||
func plural(n int, what string) string {
|
||||
if n == 1 {
|
||||
return "1 " + what
|
||||
}
|
||||
if strings.HasSuffix(what, "s") || strings.HasSuffix(what, "ch") {
|
||||
return strconv.Itoa(n) + " " + what + "es"
|
||||
}
|
||||
return strconv.Itoa(n) + " " + what + "s"
|
||||
}
|
||||
|
||||
func splitComma(s string) []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
@@ -562,6 +629,10 @@ func bansCmd() *cobra.Command {
|
||||
if flagJSON {
|
||||
return printJSON(bans)
|
||||
}
|
||||
if len(bans) == 0 {
|
||||
fmt.Printf("No active bans in %s\n", l)
|
||||
return nil
|
||||
}
|
||||
table(func(w io.Writer) {
|
||||
fmt.Fprintln(w, "ID\tADDRESS\tPORT\tENDS IN\tBY\tSOURCE")
|
||||
for _, b := range bans {
|
||||
@@ -668,7 +739,7 @@ func unbanCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%d bans lifted\n", n)
|
||||
fmt.Printf("%s lifted\n", plural(n, "ban"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user