firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/fail2ban/collect.go

Summary

Maintainability
A
2 hrs
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package fail2ban

import (
    "bufio"
    "bytes"
    "errors"
    "fmt"
    "strconv"
    "strings"
    "time"
)

func (f *Fail2Ban) collect() (map[string]int64, error) {
    now := time.Now()

    if now.Sub(f.lastDiscoverTime) > f.discoverEvery || f.forceDiscover {
        jails, err := f.discoverJails()
        if err != nil {
            return nil, err
        }
        f.jails = jails
        f.lastDiscoverTime = now
        f.forceDiscover = false
    }

    mx := make(map[string]int64)

    if err := f.collectJails(mx); err != nil {
        return nil, err
    }

    return mx, nil
}

func (f *Fail2Ban) discoverJails() ([]string, error) {
    bs, err := f.exec.status()
    if err != nil {
        return nil, err
    }

    jails, err := parseFail2banStatus(bs)
    if err != nil {
        return nil, err
    }

    if len(jails) == 0 {
        return nil, errors.New("no jails found")
    }

    f.Debugf("discovered %d jails: %v", len(jails), jails)

    return jails, nil
}

func (f *Fail2Ban) collectJails(mx map[string]int64) error {
    seen := make(map[string]bool)

    for _, jail := range f.jails {
        f.Debugf("querying status for jail '%s'", jail)
        bs, err := f.exec.jailStatus(jail)
        if err != nil {
            if errors.Is(err, errJailNotExist) {
                f.forceDiscover = true
                continue
            }
            return err
        }

        failed, banned, err := parseFail2banJailStatus(bs)
        if err != nil {
            return err
        }

        if !f.seenJails[jail] {
            f.seenJails[jail] = true
            f.addJailCharts(jail)
        }
        seen[jail] = true

        px := fmt.Sprintf("jail_%s_", jail)

        mx[px+"currently_failed"] = failed
        mx[px+"currently_banned"] = banned
    }

    for jail := range f.seenJails {
        if !seen[jail] {
            delete(f.seenJails, jail)
            f.removeJailCharts(jail)
        }
    }

    return nil
}

func parseFail2banJailStatus(jailStatus []byte) (failed, banned int64, err error) {
    const (
        failedSub = "Currently failed:"
        bannedSub = "Currently banned:"
    )

    var failedFound, bannedFound bool

    sc := bufio.NewScanner(bytes.NewReader(jailStatus))

    for sc.Scan() && !(failedFound && bannedFound) {
        text := strings.TrimSpace(sc.Text())
        if text == "" {
            continue
        }

        if !failedFound {
            if i := strings.Index(text, failedSub); i != -1 {
                failedFound = true
                s := strings.TrimSpace(text[i+len(failedSub):])
                if failed, err = strconv.ParseInt(s, 10, 64); err != nil {
                    return 0, 0, fmt.Errorf("failed to parse currently failed value (%s): %v", s, err)
                }
            }
        }
        if !bannedFound {
            if i := strings.Index(text, bannedSub); i != -1 {
                bannedFound = true
                s := strings.TrimSpace(text[i+len(bannedSub):])
                if banned, err = strconv.ParseInt(s, 10, 64); err != nil {
                    return 0, 0, fmt.Errorf("failed to parse currently banned value (%s): %v", s, err)
                }
            }
        }
    }

    if !failedFound || !bannedFound {
        return 0, 0, errors.New("failed to find failed and banned values")
    }

    return failed, banned, nil
}

func parseFail2banStatus(status []byte) ([]string, error) {
    const sub = "Jail list:"

    var jails []string

    sc := bufio.NewScanner(bytes.NewReader(status))

    for sc.Scan() {
        text := strings.TrimSpace(sc.Text())

        if i := strings.Index(text, sub); i != -1 {
            s := strings.ReplaceAll(text[i+len(sub):], ",", "")
            jails = strings.Fields(s)
            break
        }
    }

    if len(jails) == 0 {
        return nil, errors.New("failed to find jails")
    }

    return jails, nil
}