From 0b40e5de82324a4bd5ffb36470699f745878ac8a Mon Sep 17 00:00:00 2001 From: Hector Date: Sat, 6 Feb 2021 11:45:46 +0000 Subject: [PATCH 1/3] feat: connect to fail2ban db and extract total bad ips Add dependencies on `sqlite` to allow connecting to the fail2ban database. Add a new `db` module to handle all the database connections and data queries used to generate metrics. Export a new metric for the total number of bad IPs stored in the fail2ban database. --- db/db.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ exporter.go | 32 ++++++++++++++++++++++++++------ go.mod | 5 ++++- go.sum | 2 ++ 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 db/db.go diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..86a29fe --- /dev/null +++ b/db/db.go @@ -0,0 +1,50 @@ +package db + +import ( + "database/sql" + "log" + "strconv" +) + +const queryCountTotalBadIps = "SELECT COUNT(1) FROM bips" + +type Fail2BanDB struct { + DatabasePath string + sqliteDB *sql.DB +} + +func MustConnectToDb(databasePath string) *Fail2BanDB { + db, err := sql.Open("sqlite3", databasePath) + if err != nil { + log.Fatal(err) + } + return &Fail2BanDB{ + DatabasePath: databasePath, + sqliteDB: db, + } +} + +func (db *Fail2BanDB) CountTotalBadIps() (int, error) { + stmt, err := db.sqliteDB.Prepare(queryCountTotalBadIps) + defer db.mustCloseStatement(stmt) + + if err != nil { + return -1, err + } + + result := "" + err = stmt.QueryRow().Scan(&result) + + if err != nil { + return -1, err + } + + return strconv.Atoi(result) +} + +func (db *Fail2BanDB) mustCloseStatement(stmt *sql.Stmt) { + err := stmt.Close() + if err != nil { + log.Fatal(err) + } +} diff --git a/exporter.go b/exporter.go index 19e3d2e..6cf2221 100644 --- a/exporter.go +++ b/exporter.go @@ -1,6 +1,8 @@ package main import ( + fail2banDb "fail2ban-prometheus-exporter/db" + _ "github.com/mattn/go-sqlite3" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "log" @@ -9,23 +11,41 @@ import ( const namespace = "fail2ban" -var up = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "up"), - "Was the last fail2ban query successful.", - nil, nil, +var ( + db = fail2banDb.MustConnectToDb("fail2ban.sqlite3") + metricUp = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "up"), + "Was the last fail2ban query successful.", + nil, nil, + ) + metricBadIpTotal = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "badip_total"), + "Total number of bad IPs stored in the database.", + nil, nil, + ) ) type Exporter struct { } func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - ch <- up + ch <- metricUp + ch <- metricBadIpTotal } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric( - up, prometheus.GaugeValue, 1, + metricUp, prometheus.GaugeValue, 1, ) + ch <- *collectTotalBadIpMetric() +} + +func collectTotalBadIpMetric() *prometheus.Metric { + count, _ := db.CountTotalBadIps() + metric := prometheus.MustNewConstMetric( + metricBadIpTotal, prometheus.GaugeValue, float64(count), + ) + return &metric } func main() { diff --git a/go.mod b/go.mod index 63dc5e1..2df78ea 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module fail2ban-prometheus-exporter go 1.15 -require github.com/prometheus/client_golang v1.9.0 +require ( + github.com/mattn/go-sqlite3 v1.14.6 + github.com/prometheus/client_golang v1.9.0 +) diff --git a/go.sum b/go.sum index 94b6a13..7de5c32 100644 --- a/go.sum +++ b/go.sum @@ -150,6 +150,8 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= From 4b965017d2aa485363415654b0b0f35b704ad7a0 Mon Sep 17 00:00:00 2001 From: Hector Date: Sat, 6 Feb 2021 12:12:01 +0000 Subject: [PATCH 2/3] feat: export bad ip count per jail Update exported metrics to spit the number of bad IPs per jail using metric value labels. This includes a change to the database code to use a different query that groups the count by the `jail` column. --- db/db.go | 34 ++++++++++++++++++++++++---------- exporter.go | 29 +++++++++++++++++------------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/db/db.go b/db/db.go index 86a29fe..f7146df 100644 --- a/db/db.go +++ b/db/db.go @@ -3,10 +3,9 @@ package db import ( "database/sql" "log" - "strconv" ) -const queryCountTotalBadIps = "SELECT COUNT(1) FROM bips" +const queryBadIpsPerJail = "SELECT jail, COUNT(1) FROM bips GROUP BY jail" type Fail2BanDB struct { DatabasePath string @@ -24,22 +23,37 @@ func MustConnectToDb(databasePath string) *Fail2BanDB { } } -func (db *Fail2BanDB) CountTotalBadIps() (int, error) { - stmt, err := db.sqliteDB.Prepare(queryCountTotalBadIps) +func (db *Fail2BanDB) CountBadIpsPerJail() (map[string]int, error) { + stmt, err := db.sqliteDB.Prepare(queryBadIpsPerJail) defer db.mustCloseStatement(stmt) if err != nil { - return -1, err + return nil, err } - result := "" - err = stmt.QueryRow().Scan(&result) - + jailNameToCountMap := map[string]int{} + rows, err := stmt.Query() if err != nil { - return -1, err + return nil, err + } + if rows == nil { + return jailNameToCountMap, nil } - return strconv.Atoi(result) + for rows.Next() { + if rows.Err() != nil { + return nil, err + } + jailName := "" + count := 0 + err = rows.Scan(&jailName, &count) + if err != nil { + return nil, err + } + + jailNameToCountMap[jailName] = count + } + return jailNameToCountMap, nil } func (db *Fail2BanDB) mustCloseStatement(stmt *sql.Stmt) { diff --git a/exporter.go b/exporter.go index 6cf2221..0fa3803 100644 --- a/exporter.go +++ b/exporter.go @@ -18,10 +18,10 @@ var ( "Was the last fail2ban query successful.", nil, nil, ) - metricBadIpTotal = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "badip_total"), - "Total number of bad IPs stored in the database.", - nil, nil, + metricBadIpsPerJail = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "bad_ips"), + "Number of bad IPs stored in the database (per jail).", + []string{"jail"}, nil, ) ) @@ -30,22 +30,27 @@ type Exporter struct { func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- metricUp - ch <- metricBadIpTotal + ch <- metricBadIpsPerJail } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric( metricUp, prometheus.GaugeValue, 1, ) - ch <- *collectTotalBadIpMetric() + collectBadIpsPerJailMetrics(ch) } -func collectTotalBadIpMetric() *prometheus.Metric { - count, _ := db.CountTotalBadIps() - metric := prometheus.MustNewConstMetric( - metricBadIpTotal, prometheus.GaugeValue, float64(count), - ) - return &metric +func collectBadIpsPerJailMetrics(ch chan<- prometheus.Metric) { + jailNameToCountMap, err := db.CountBadIpsPerJail() + if err != nil { + log.Print(err) + } + + for jailName, count := range jailNameToCountMap { + ch <- prometheus.MustNewConstMetric( + metricBadIpsPerJail, prometheus.GaugeValue, float64(count), jailName, + ) + } } func main() { From 91cba8080cf936d6da8d6fd506951cae38aa0134 Mon Sep 17 00:00:00 2001 From: Hector Date: Sat, 6 Feb 2021 12:24:31 +0000 Subject: [PATCH 3/3] feat: export number of banned ips Export the number of banned IPs stored in the fail2ban database as well as the number of bad IPs. Update the queries used to collect data to better handle cases where the database table for bad/banned IPs is empty. The new query always lists all jails, even when the count is zero. --- db/db.go | 13 +++++++++++-- exporter.go | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/db/db.go b/db/db.go index f7146df..cd425c4 100644 --- a/db/db.go +++ b/db/db.go @@ -5,7 +5,8 @@ import ( "log" ) -const queryBadIpsPerJail = "SELECT jail, COUNT(1) FROM bips GROUP BY jail" +const queryBadIpsPerJail = "SELECT j.name, (SELECT COUNT(1) FROM bips b WHERE j.name = b.jail) FROM jails j" +const queryBannedIpsPerJail = "SELECT j.name, (SELECT COUNT(1) FROM bans b WHERE j.name = b.jail) FROM jails j" type Fail2BanDB struct { DatabasePath string @@ -23,8 +24,16 @@ func MustConnectToDb(databasePath string) *Fail2BanDB { } } +func (db *Fail2BanDB) CountBannedIpsPerJail() (map[string]int, error) { + return db.RunJailNameToCountQuery(queryBannedIpsPerJail) +} + func (db *Fail2BanDB) CountBadIpsPerJail() (map[string]int, error) { - stmt, err := db.sqliteDB.Prepare(queryBadIpsPerJail) + return db.RunJailNameToCountQuery(queryBadIpsPerJail) +} + +func (db *Fail2BanDB) RunJailNameToCountQuery(query string) (map[string]int, error) { + stmt, err := db.sqliteDB.Prepare(query) defer db.mustCloseStatement(stmt) if err != nil { diff --git a/exporter.go b/exporter.go index 0fa3803..19b2d7d 100644 --- a/exporter.go +++ b/exporter.go @@ -18,6 +18,11 @@ var ( "Was the last fail2ban query successful.", nil, nil, ) + metricBannedIpsPerJail = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "banned_ips"), + "Number of banned IPs stored in the database (per jail).", + []string{"jail"}, nil, + ) metricBadIpsPerJail = prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "bad_ips"), "Number of bad IPs stored in the database (per jail).", @@ -31,6 +36,7 @@ type Exporter struct { func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { ch <- metricUp ch <- metricBadIpsPerJail + ch <- metricBannedIpsPerJail } func (e *Exporter) Collect(ch chan<- prometheus.Metric) { @@ -38,6 +44,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { metricUp, prometheus.GaugeValue, 1, ) collectBadIpsPerJailMetrics(ch) + collectBannedIpsPerJailMetrics(ch) } func collectBadIpsPerJailMetrics(ch chan<- prometheus.Metric) { @@ -53,6 +60,19 @@ func collectBadIpsPerJailMetrics(ch chan<- prometheus.Metric) { } } +func collectBannedIpsPerJailMetrics(ch chan<- prometheus.Metric) { + jailNameToCountMap, err := db.CountBannedIpsPerJail() + if err != nil { + log.Print(err) + } + + for jailName, count := range jailNameToCountMap { + ch <- prometheus.MustNewConstMetric( + metricBannedIpsPerJail, prometheus.GaugeValue, float64(count), jailName, + ) + } +} + func main() { log.Print("starting fail2ban exporter")