From 67c952a048a4eebc4a8d45995d26c5acc63263f0 Mon Sep 17 00:00:00 2001 From: Jim Strang <47609385+jimstrang@users.noreply.github.com> Date: Sun, 17 May 2026 13:40:51 -0400 Subject: [PATCH] Avoid correlated alert decision filters --- pkg/database/alertfilter.go | 6 +++--- pkg/database/ent/alert/decision_predicates.go | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 pkg/database/ent/alert/decision_predicates.go diff --git a/pkg/database/alertfilter.go b/pkg/database/alertfilter.go index 6d5987f848a..c2e460c17e4 100644 --- a/pkg/database/alertfilter.go +++ b/pkg/database/alertfilter.go @@ -220,9 +220,9 @@ func alertPredicatesFromFilter(filter map[string][]string) ([]predicate.Alert, e return nil, err } case "decision_type": - predicates = append(predicates, alert.HasDecisionsWith(decision.TypeEQ(value[0]))) + predicates = append(predicates, alert.HasDecisionsMatching(decision.TypeEQ(value[0]))) case "origin": - predicates = append(predicates, alert.HasDecisionsWith(decision.OriginEQ(value[0]))) + predicates = append(predicates, alert.HasDecisionsMatching(decision.OriginEQ(value[0]))) case "include_capi": // allows to exclude one or more specific origins if err = handleIncludeCapiFilter(value[0], &predicates); err != nil { return nil, err @@ -233,7 +233,7 @@ func alertPredicatesFromFilter(filter map[string][]string) ([]predicate.Alert, e } if hasActiveDecision { - predicates = append(predicates, alert.HasDecisionsWith(decision.UntilGTE(time.Now().UTC()))) + predicates = append(predicates, alert.HasDecisionsMatching(decision.UntilGTE(time.Now().UTC()))) } else { predicates = append(predicates, alert.Not(alert.HasDecisions())) } diff --git a/pkg/database/ent/alert/decision_predicates.go b/pkg/database/ent/alert/decision_predicates.go new file mode 100644 index 00000000000..a963e4c8891 --- /dev/null +++ b/pkg/database/ent/alert/decision_predicates.go @@ -0,0 +1,19 @@ +package alert + +import ( + "entgo.io/ent/dialect/sql" + + "github.com/crowdsecurity/crowdsec/pkg/database/ent/predicate" +) + +// HasDecisionsMatching keeps independent decision filters as independent subqueries, +// but avoids correlated EXISTS plans that are expensive for SQLite on large decision sets. +func HasDecisionsMatching(preds ...predicate.Decision) predicate.Alert { + return predicate.Alert(func(selector *sql.Selector) { + decisions := sql.Select(DecisionsColumn).From(sql.Table(DecisionsTable)) + for _, pred := range preds { + pred(decisions) + } + selector.Where(sql.In(selector.C(FieldID), decisions)) + }) +}