Rule files, and bans for a clear sign of attack (closes #24)
check / check (push) Waiting to run
check / check (push) Waiting to run
Every *.rules file in SWWAF_RULES_DIR not named with a leading dot is read at start, and again 2 seconds after the directory's last change. Each request is checked against the rules after the rate limits: log notes a match, block refuses with 403, ban refuses and bans the netblock for SWWAF_ATTACK_BAN_DURATION, made permanent by its next request or attack. path, query and uri are matched as the request line sent them; header:Host and header:Transfer-Encoding are refused. Bans gain a cause. The image ships 00-default.rules. Judgement call: a header sent twice is matched with its values joined by ", ". Judgement call: SWWAF_MAX_BAN_DURATION does not cap a ban for an attack. Not in this unit: offences for rule matches, with the error burst. Model: opus-5-5
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||
"sneak.berlin/go/smallwebwaf/internal/ratelimit"
|
||||
"sneak.berlin/go/smallwebwaf/internal/requestlog"
|
||||
"sneak.berlin/go/smallwebwaf/internal/rules"
|
||||
)
|
||||
|
||||
// Metrics are smallwebwaf's metrics. They are safe for concurrent use.
|
||||
@@ -30,7 +31,9 @@ type Metrics struct {
|
||||
rateLimitHits *prometheus.CounterVec
|
||||
sizeAndTimeLimitHits *prometheus.CounterVec
|
||||
offences *prometheus.CounterVec
|
||||
countries *countries
|
||||
// ruleMatches are made by AddRules.
|
||||
ruleMatches *prometheus.CounterVec
|
||||
countries *countries
|
||||
|
||||
// GeoJSRequests are the requests to GeoJS, and GeoJSFailures those
|
||||
// that failed. GeoJSUnanswered are the requests whose client counted
|
||||
@@ -135,19 +138,22 @@ func New(topN int) *Metrics {
|
||||
|
||||
// AddBansAndClients adds the metrics read from the ledger and the table
|
||||
// of clients as the metrics are asked for: the bans made since the start,
|
||||
// the bans active and permanent at now, and the clients in the table.
|
||||
// by cause, the bans active and permanent at now, and the clients in the
|
||||
// table.
|
||||
func (m *Metrics) AddBansAndClients(
|
||||
ledger *bans.Ledger, limiter *ratelimit.Limiter, now func() time.Time,
|
||||
) {
|
||||
m.registry.MustRegister(
|
||||
// Every ban smallwebwaf makes so far is for a broken limit.
|
||||
prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||
for _, cause := range []string{bans.CauseLimit, bans.CauseAttack} {
|
||||
m.registry.MustRegister(prometheus.NewCounterFunc(prometheus.CounterOpts{
|
||||
Name: "smallwebwaf_bans_made_total",
|
||||
Help: "Bans made, by cause.",
|
||||
ConstLabels: prometheus.Labels{"cause": "limit"},
|
||||
ConstLabels: prometheus.Labels{"cause": cause},
|
||||
}, func() float64 {
|
||||
return float64(ledger.Made())
|
||||
}),
|
||||
return float64(ledger.Made(cause))
|
||||
}))
|
||||
}
|
||||
|
||||
m.registry.MustRegister(
|
||||
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "smallwebwaf_active_bans",
|
||||
Help: "Bans active now, the permanent ones included.",
|
||||
@@ -173,6 +179,24 @@ func (m *Metrics) AddBansAndClients(
|
||||
)
|
||||
}
|
||||
|
||||
// AddRules adds the metrics of the rule files: the requests that matched
|
||||
// each rule, which RuleMatched counts, and the rules loaded from
|
||||
// ruleFiles, read as the metrics are asked for. It is called once, before
|
||||
// RuleMatched.
|
||||
func (m *Metrics) AddRules(ruleFiles *rules.Files) {
|
||||
m.ruleMatches = counterVec("smallwebwaf_rule_matches_total",
|
||||
"Requests that matched a rule of the rule files, by its id and action.",
|
||||
[]string{"rule_id", "action"})
|
||||
|
||||
m.registry.MustRegister(m.ruleMatches,
|
||||
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "smallwebwaf_rules_loaded",
|
||||
Help: "Rules loaded from the rule files.",
|
||||
}, func() float64 {
|
||||
return float64(ruleFiles.Len())
|
||||
}))
|
||||
}
|
||||
|
||||
// ServeHTTP answers with the metrics in the Prometheus text format.
|
||||
func (m *Metrics) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
m.handler.ServeHTTP(w, r)
|
||||
@@ -219,6 +243,12 @@ func (m *Metrics) RequestEnded(
|
||||
}
|
||||
}
|
||||
|
||||
// RuleMatched counts a request that matched the rule id, whose action is
|
||||
// action.
|
||||
func (m *Metrics) RuleMatched(id, action string) {
|
||||
m.ruleMatches.WithLabelValues(id, action).Inc()
|
||||
}
|
||||
|
||||
// StateFileWritten counts a write of the state file name, of size bytes,
|
||||
// that ended with err.
|
||||
func (m *Metrics) StateFileWritten(name string, size int, err error) {
|
||||
|
||||
Reference in New Issue
Block a user