pkg/database/decisiosn: remove filter parameter, which is always passed empty (#2954)

This commit is contained in:
mmetc 2024-04-23 11:15:27 +02:00 committed by GitHub
parent b48b728317
commit 718d1c54b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 76 additions and 59 deletions

View file

@ -58,7 +58,7 @@ linters-settings:
min-complexity: 28 min-complexity: 28
nlreturn: nlreturn:
block-size: 4 block-size: 5
nolintlint: nolintlint:
allow-unused: false # report any unused nolint directives allow-unused: false # report any unused nolint directives

View file

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@ -22,7 +21,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/parser" "github.com/crowdsecurity/crowdsec/pkg/parser"
) )
/*prometheus*/ // Prometheus
var globalParserHits = prometheus.NewCounterVec( var globalParserHits = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "cs_parser_hits_total", Name: "cs_parser_hits_total",
@ -30,6 +30,7 @@ var globalParserHits = prometheus.NewCounterVec(
}, },
[]string{"source", "type"}, []string{"source", "type"},
) )
var globalParserHitsOk = prometheus.NewCounterVec( var globalParserHitsOk = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "cs_parser_hits_ok_total", Name: "cs_parser_hits_ok_total",
@ -37,6 +38,7 @@ var globalParserHitsOk = prometheus.NewCounterVec(
}, },
[]string{"source", "type"}, []string{"source", "type"},
) )
var globalParserHitsKo = prometheus.NewCounterVec( var globalParserHitsKo = prometheus.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "cs_parser_hits_ko_total", Name: "cs_parser_hits_ko_total",
@ -116,9 +118,7 @@ func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.Ha
return return
} }
decisionsFilters := make(map[string][]string, 0) decisions, err := dbClient.QueryDecisionCountByScenario()
decisions, err := dbClient.QueryDecisionCountByScenario(decisionsFilters)
if err != nil { if err != nil {
log.Errorf("Error querying decisions for metrics: %v", err) log.Errorf("Error querying decisions for metrics: %v", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -139,7 +139,6 @@ func computeDynamicMetrics(next http.Handler, dbClient *database.Client) http.Ha
} }
alerts, err := dbClient.AlertsCountPerScenario(alertsFilter) alerts, err := dbClient.AlertsCountPerScenario(alertsFilter)
if err != nil { if err != nil {
log.Errorf("Error querying alerts for metrics: %v", err) log.Errorf("Error querying alerts for metrics: %v", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@ -194,7 +193,6 @@ func servePrometheus(config *csconfig.PrometheusCfg, dbClient *database.Client,
defer trace.CatchPanic("crowdsec/servePrometheus") defer trace.CatchPanic("crowdsec/servePrometheus")
http.Handle("/metrics", computeDynamicMetrics(promhttp.Handler(), dbClient)) http.Handle("/metrics", computeDynamicMetrics(promhttp.Handler(), dbClient))
log.Debugf("serving metrics after %s ms", time.Since(crowdsecT0))
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", config.ListenAddr, config.ListenPort), nil); err != nil { if err := http.ListenAndServe(fmt.Sprintf("%s:%d", config.ListenAddr, config.ListenPort), nil); err != nil {
// in time machine, we most likely have the LAPI using the port // in time machine, we most likely have the LAPI using the port

View file

@ -37,6 +37,7 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
if v[0] == "false" { if v[0] == "false" {
query = query.Where(decision.SimulatedEQ(false)) query = query.Where(decision.SimulatedEQ(false))
} }
delete(filter, "simulated") delete(filter, "simulated")
} else { } else {
query = query.Where(decision.SimulatedEQ(false)) query = query.Where(decision.SimulatedEQ(false))
@ -49,7 +50,7 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
if err != nil { if err != nil {
return nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err) return nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
} }
case "scopes", "scope": //Swagger mentions both of them, let's just support both to make sure we don't break anything case "scopes", "scope": // Swagger mentions both of them, let's just support both to make sure we don't break anything
scopes := strings.Split(value[0], ",") scopes := strings.Split(value[0], ",")
for i, scope := range scopes { for i, scope := range scopes {
switch strings.ToLower(scope) { switch strings.ToLower(scope) {
@ -63,6 +64,7 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
scopes[i] = types.AS scopes[i] = types.AS
} }
} }
query = query.Where(decision.ScopeIn(scopes...)) query = query.Where(decision.ScopeIn(scopes...))
case "value": case "value":
query = query.Where(decision.ValueEQ(value[0])) query = query.Where(decision.ValueEQ(value[0]))
@ -164,11 +166,11 @@ func (c *Client) QueryExpiredDecisionsWithFilters(filters map[string][]string) (
return data, nil return data, nil
} }
func (c *Client) QueryDecisionCountByScenario(filters map[string][]string) ([]*DecisionsByScenario, error) { func (c *Client) QueryDecisionCountByScenario() ([]*DecisionsByScenario, error) {
query := c.Ent.Decision.Query().Where( query := c.Ent.Decision.Query().Where(
decision.UntilGT(time.Now().UTC()), decision.UntilGT(time.Now().UTC()),
) )
query, err := BuildDecisionRequestWithFilter(query, filters) query, err := BuildDecisionRequestWithFilter(query, make(map[string][]string))
if err != nil { if err != nil {
c.Log.Warningf("QueryDecisionCountByScenario : %s", err) c.Log.Warningf("QueryDecisionCountByScenario : %s", err)
@ -277,10 +279,12 @@ func (c *Client) QueryNewDecisionsSinceWithFilters(since time.Time, filters map[
decision.CreatedAtGT(since), decision.CreatedAtGT(since),
decision.UntilGT(time.Now().UTC()), decision.UntilGT(time.Now().UTC()),
) )
//Allow a bouncer to ask for non-deduplicated results
// Allow a bouncer to ask for non-deduplicated results
if v, ok := filters["dedup"]; !ok || v[0] != "false" { if v, ok := filters["dedup"]; !ok || v[0] != "false" {
query = query.Where(longestDecisionForScopeTypeValue) query = query.Where(longestDecisionForScopeTypeValue)
} }
query, err := BuildDecisionRequestWithFilter(query, filters) query, err := BuildDecisionRequestWithFilter(query, filters)
if err != nil { if err != nil {
c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err) c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)
@ -294,17 +298,20 @@ func (c *Client) QueryNewDecisionsSinceWithFilters(since time.Time, filters map[
c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err) c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)
return []*ent.Decision{}, errors.Wrapf(QueryFail, "new decisions since '%s'", since.String()) return []*ent.Decision{}, errors.Wrapf(QueryFail, "new decisions since '%s'", since.String())
} }
return data, nil return data, nil
} }
func (c *Client) DeleteDecisionById(decisionId int) ([]*ent.Decision, error) { func (c *Client) DeleteDecisionById(decisionID int) ([]*ent.Decision, error) {
toDelete, err := c.Ent.Decision.Query().Where(decision.IDEQ(decisionId)).All(c.CTX) toDelete, err := c.Ent.Decision.Query().Where(decision.IDEQ(decisionID)).All(c.CTX)
if err != nil { if err != nil {
c.Log.Warningf("DeleteDecisionById : %s", err) c.Log.Warningf("DeleteDecisionById : %s", err)
return nil, errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionId) return nil, errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionID)
} }
count, err := c.BulkDeleteDecisions(toDelete, false) count, err := c.BulkDeleteDecisions(toDelete, false)
c.Log.Debugf("deleted %d decisions", count) c.Log.Debugf("deleted %d decisions", count)
return toDelete, err return toDelete, err
} }
@ -317,6 +324,7 @@ func (c *Client) DeleteDecisionsWithFilter(filter map[string][]string) (string,
else, return bans that are *contained* by the given value (value is the outer) */ else, return bans that are *contained* by the given value (value is the outer) */
decisions := c.Ent.Decision.Query() decisions := c.Ent.Decision.Query()
for param, value := range filter { for param, value := range filter {
switch param { switch param {
case "contains": case "contains":
@ -359,48 +367,48 @@ func (c *Client) DeleteDecisionsWithFilter(filter map[string][]string) (string,
} else if ip_sz == 16 { } else if ip_sz == 16 {
if contains { /*decision contains {start_ip,end_ip}*/ if contains { /*decision contains {start_ip,end_ip}*/
decisions = decisions.Where(decision.And( decisions = decisions.Where(decision.And(
//matching addr size // matching addr size
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
decision.Or( decision.Or(
//decision.start_ip < query.start_ip // decision.start_ip < query.start_ip
decision.StartIPLT(start_ip), decision.StartIPLT(start_ip),
decision.And( decision.And(
//decision.start_ip == query.start_ip // decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip), decision.StartIPEQ(start_ip),
//decision.start_suffix <= query.start_suffix // decision.start_suffix <= query.start_suffix
decision.StartSuffixLTE(start_sfx), decision.StartSuffixLTE(start_sfx),
)), )),
decision.Or( decision.Or(
//decision.end_ip > query.end_ip // decision.end_ip > query.end_ip
decision.EndIPGT(end_ip), decision.EndIPGT(end_ip),
decision.And( decision.And(
//decision.end_ip == query.end_ip // decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip), decision.EndIPEQ(end_ip),
//decision.end_suffix >= query.end_suffix // decision.end_suffix >= query.end_suffix
decision.EndSuffixGTE(end_sfx), decision.EndSuffixGTE(end_sfx),
), ),
), ),
)) ))
} else { } else {
decisions = decisions.Where(decision.And( decisions = decisions.Where(decision.And(
//matching addr size // matching addr size
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
decision.Or( decision.Or(
//decision.start_ip > query.start_ip // decision.start_ip > query.start_ip
decision.StartIPGT(start_ip), decision.StartIPGT(start_ip),
decision.And( decision.And(
//decision.start_ip == query.start_ip // decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip), decision.StartIPEQ(start_ip),
//decision.start_suffix >= query.start_suffix // decision.start_suffix >= query.start_suffix
decision.StartSuffixGTE(start_sfx), decision.StartSuffixGTE(start_sfx),
)), )),
decision.Or( decision.Or(
//decision.end_ip < query.end_ip // decision.end_ip < query.end_ip
decision.EndIPLT(end_ip), decision.EndIPLT(end_ip),
decision.And( decision.And(
//decision.end_ip == query.end_ip // decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip), decision.EndIPEQ(end_ip),
//decision.end_suffix <= query.end_suffix // decision.end_suffix <= query.end_suffix
decision.EndSuffixLTE(end_sfx), decision.EndSuffixLTE(end_sfx),
), ),
), ),
@ -415,11 +423,13 @@ func (c *Client) DeleteDecisionsWithFilter(filter map[string][]string) (string,
c.Log.Warningf("DeleteDecisionsWithFilter : %s", err) c.Log.Warningf("DeleteDecisionsWithFilter : %s", err)
return "0", nil, errors.Wrap(DeleteFail, "decisions with provided filter") return "0", nil, errors.Wrap(DeleteFail, "decisions with provided filter")
} }
count, err := c.BulkDeleteDecisions(toDelete, false) count, err := c.BulkDeleteDecisions(toDelete, false)
if err != nil { if err != nil {
c.Log.Warningf("While deleting decisions : %s", err) c.Log.Warningf("While deleting decisions : %s", err)
return "0", nil, errors.Wrap(DeleteFail, "decisions with provided filter") return "0", nil, errors.Wrap(DeleteFail, "decisions with provided filter")
} }
return strconv.Itoa(count), toDelete, nil return strconv.Itoa(count), toDelete, nil
} }
@ -432,6 +442,7 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
/*if contains is true, return bans that *contains* the given value (value is the inner) /*if contains is true, return bans that *contains* the given value (value is the inner)
else, return bans that are *contained* by the given value (value is the outer)*/ else, return bans that are *contained* by the given value (value is the outer)*/
decisions := c.Ent.Decision.Query().Where(decision.UntilGT(time.Now().UTC())) decisions := c.Ent.Decision.Query().Where(decision.UntilGT(time.Now().UTC()))
for param, value := range filter { for param, value := range filter {
switch param { switch param {
case "contains": case "contains":
@ -480,24 +491,24 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
/*decision contains {start_ip,end_ip}*/ /*decision contains {start_ip,end_ip}*/
if contains { if contains {
decisions = decisions.Where(decision.And( decisions = decisions.Where(decision.And(
//matching addr size // matching addr size
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
decision.Or( decision.Or(
//decision.start_ip < query.start_ip // decision.start_ip < query.start_ip
decision.StartIPLT(start_ip), decision.StartIPLT(start_ip),
decision.And( decision.And(
//decision.start_ip == query.start_ip // decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip), decision.StartIPEQ(start_ip),
//decision.start_suffix <= query.start_suffix // decision.start_suffix <= query.start_suffix
decision.StartSuffixLTE(start_sfx), decision.StartSuffixLTE(start_sfx),
)), )),
decision.Or( decision.Or(
//decision.end_ip > query.end_ip // decision.end_ip > query.end_ip
decision.EndIPGT(end_ip), decision.EndIPGT(end_ip),
decision.And( decision.And(
//decision.end_ip == query.end_ip // decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip), decision.EndIPEQ(end_ip),
//decision.end_suffix >= query.end_suffix // decision.end_suffix >= query.end_suffix
decision.EndSuffixGTE(end_sfx), decision.EndSuffixGTE(end_sfx),
), ),
), ),
@ -505,24 +516,24 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
} else { } else {
/*decision is contained within {start_ip,end_ip}*/ /*decision is contained within {start_ip,end_ip}*/
decisions = decisions.Where(decision.And( decisions = decisions.Where(decision.And(
//matching addr size // matching addr size
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
decision.Or( decision.Or(
//decision.start_ip > query.start_ip // decision.start_ip > query.start_ip
decision.StartIPGT(start_ip), decision.StartIPGT(start_ip),
decision.And( decision.And(
//decision.start_ip == query.start_ip // decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip), decision.StartIPEQ(start_ip),
//decision.start_suffix >= query.start_suffix // decision.start_suffix >= query.start_suffix
decision.StartSuffixGTE(start_sfx), decision.StartSuffixGTE(start_sfx),
)), )),
decision.Or( decision.Or(
//decision.end_ip < query.end_ip // decision.end_ip < query.end_ip
decision.EndIPLT(end_ip), decision.EndIPLT(end_ip),
decision.And( decision.And(
//decision.end_ip == query.end_ip // decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip), decision.EndIPEQ(end_ip),
//decision.end_suffix <= query.end_suffix // decision.end_suffix <= query.end_suffix
decision.EndSuffixLTE(end_sfx), decision.EndSuffixLTE(end_sfx),
), ),
), ),
@ -531,6 +542,7 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
} else if ip_sz != 0 { } else if ip_sz != 0 {
return "0", nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz) return "0", nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
} }
DecisionsToDelete, err := decisions.All(c.CTX) DecisionsToDelete, err := decisions.All(c.CTX)
if err != nil { if err != nil {
c.Log.Warningf("SoftDeleteDecisionsWithFilter : %s", err) c.Log.Warningf("SoftDeleteDecisionsWithFilter : %s", err)
@ -541,13 +553,14 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
if err != nil { if err != nil {
return "0", nil, errors.Wrapf(DeleteFail, "soft delete decisions with provided filter : %s", err) return "0", nil, errors.Wrapf(DeleteFail, "soft delete decisions with provided filter : %s", err)
} }
return strconv.Itoa(count), DecisionsToDelete, err return strconv.Itoa(count), DecisionsToDelete, err
} }
// BulkDeleteDecisions set the expiration of a bulk of decisions to now() or hard deletes them. // BulkDeleteDecisions sets the expiration of a bulk of decisions to now() or hard deletes them.
// We are doing it this way so we can return impacted decisions for sync with CAPI/PAPI // We are doing it this way so we can return impacted decisions for sync with CAPI/PAPI
func (c *Client) BulkDeleteDecisions(decisionsToDelete []*ent.Decision, softDelete bool) (int, error) { func (c *Client) BulkDeleteDecisions(decisionsToDelete []*ent.Decision, softDelete bool) (int, error) {
const bulkSize = 256 //scientifically proven to be the best value for bulk delete const bulkSize = 256 // scientifically proven to be the best value for bulk delete
var ( var (
nbUpdates int nbUpdates int
@ -576,6 +589,7 @@ func (c *Client) BulkDeleteDecisions(decisionsToDelete []*ent.Decision, softDele
return totalUpdates, fmt.Errorf("hard delete decisions with provided filter: %w", err) return totalUpdates, fmt.Errorf("hard delete decisions with provided filter: %w", err)
} }
} }
totalUpdates += nbUpdates totalUpdates += nbUpdates
} }
@ -612,6 +626,7 @@ func (c *Client) CountDecisionsByValue(decisionValue string) (int, error) {
contains := true contains := true
decisions := c.Ent.Decision.Query() decisions := c.Ent.Decision.Query()
decisions, err = applyStartIpEndIpFilter(decisions, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx) decisions, err = applyStartIpEndIpFilter(decisions, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
if err != nil { if err != nil {
return 0, errors.Wrapf(err, "fail to apply StartIpEndIpFilter") return 0, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
@ -667,6 +682,7 @@ func applyStartIpEndIpFilter(decisions *ent.DecisionQuery, contains bool, ip_sz
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
)) ))
} }
return decisions, nil return decisions, nil
} }
@ -674,24 +690,24 @@ func applyStartIpEndIpFilter(decisions *ent.DecisionQuery, contains bool, ip_sz
/*decision contains {start_ip,end_ip}*/ /*decision contains {start_ip,end_ip}*/
if contains { if contains {
decisions = decisions.Where(decision.And( decisions = decisions.Where(decision.And(
//matching addr size // matching addr size
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
decision.Or( decision.Or(
//decision.start_ip < query.start_ip // decision.start_ip < query.start_ip
decision.StartIPLT(start_ip), decision.StartIPLT(start_ip),
decision.And( decision.And(
//decision.start_ip == query.start_ip // decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip), decision.StartIPEQ(start_ip),
//decision.start_suffix <= query.start_suffix // decision.start_suffix <= query.start_suffix
decision.StartSuffixLTE(start_sfx), decision.StartSuffixLTE(start_sfx),
)), )),
decision.Or( decision.Or(
//decision.end_ip > query.end_ip // decision.end_ip > query.end_ip
decision.EndIPGT(end_ip), decision.EndIPGT(end_ip),
decision.And( decision.And(
//decision.end_ip == query.end_ip // decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip), decision.EndIPEQ(end_ip),
//decision.end_suffix >= query.end_suffix // decision.end_suffix >= query.end_suffix
decision.EndSuffixGTE(end_sfx), decision.EndSuffixGTE(end_sfx),
), ),
), ),
@ -699,29 +715,30 @@ func applyStartIpEndIpFilter(decisions *ent.DecisionQuery, contains bool, ip_sz
} else { } else {
/*decision is contained within {start_ip,end_ip}*/ /*decision is contained within {start_ip,end_ip}*/
decisions = decisions.Where(decision.And( decisions = decisions.Where(decision.And(
//matching addr size // matching addr size
decision.IPSizeEQ(int64(ip_sz)), decision.IPSizeEQ(int64(ip_sz)),
decision.Or( decision.Or(
//decision.start_ip > query.start_ip // decision.start_ip > query.start_ip
decision.StartIPGT(start_ip), decision.StartIPGT(start_ip),
decision.And( decision.And(
//decision.start_ip == query.start_ip // decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip), decision.StartIPEQ(start_ip),
//decision.start_suffix >= query.start_suffix // decision.start_suffix >= query.start_suffix
decision.StartSuffixGTE(start_sfx), decision.StartSuffixGTE(start_sfx),
)), )),
decision.Or( decision.Or(
//decision.end_ip < query.end_ip // decision.end_ip < query.end_ip
decision.EndIPLT(end_ip), decision.EndIPLT(end_ip),
decision.And( decision.And(
//decision.end_ip == query.end_ip // decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip), decision.EndIPEQ(end_ip),
//decision.end_suffix <= query.end_suffix // decision.end_suffix <= query.end_suffix
decision.EndSuffixLTE(end_sfx), decision.EndSuffixLTE(end_sfx),
), ),
), ),
)) ))
} }
return decisions, nil return decisions, nil
} }
@ -735,8 +752,10 @@ func applyStartIpEndIpFilter(decisions *ent.DecisionQuery, contains bool, ip_sz
func decisionPredicatesFromStr(s string, predicateFunc func(string) predicate.Decision) []predicate.Decision { func decisionPredicatesFromStr(s string, predicateFunc func(string) predicate.Decision) []predicate.Decision {
words := strings.Split(s, ",") words := strings.Split(s, ",")
predicates := make([]predicate.Decision, len(words)) predicates := make([]predicate.Decision, len(words))
for i, word := range words { for i, word := range words {
predicates[i] = predicateFunc(word) predicates[i] = predicateFunc(word)
} }
return predicates return predicates
} }