github.com/crowdsecurity/crowdsec@v1.6.1/pkg/database/decisions.go (about)

     1  package database
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"time"
     8  
     9  	"entgo.io/ent/dialect/sql"
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/crowdsecurity/go-cs-lib/slicetools"
    13  
    14  	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
    15  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
    16  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/predicate"
    17  	"github.com/crowdsecurity/crowdsec/pkg/types"
    18  )
    19  
    20  type DecisionsByScenario struct {
    21  	Scenario string
    22  	Count    int
    23  	Origin   string
    24  	Type     string
    25  }
    26  
    27  func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string][]string) (*ent.DecisionQuery, error) {
    28  	var err error
    29  	var start_ip, start_sfx, end_ip, end_sfx int64
    30  	var ip_sz int
    31  	var contains = true
    32  	/*if contains is true, return bans that *contains* the given value (value is the inner)
    33  	  else, return bans that are *contained* by the given value (value is the outer)*/
    34  
    35  	/*the simulated filter is a bit different : if it's not present *or* set to false, specifically exclude records with simulated to true */
    36  	if v, ok := filter["simulated"]; ok {
    37  		if v[0] == "false" {
    38  			query = query.Where(decision.SimulatedEQ(false))
    39  		}
    40  		delete(filter, "simulated")
    41  	} else {
    42  		query = query.Where(decision.SimulatedEQ(false))
    43  	}
    44  
    45  	for param, value := range filter {
    46  		switch param {
    47  		case "contains":
    48  			contains, err = strconv.ParseBool(value[0])
    49  			if err != nil {
    50  				return nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
    51  			}
    52  		case "scopes", "scope": //Swagger mentions both of them, let's just support both to make sure we don't break anything
    53  			scopes := strings.Split(value[0], ",")
    54  			for i, scope := range scopes {
    55  				switch strings.ToLower(scope) {
    56  				case "ip":
    57  					scopes[i] = types.Ip
    58  				case "range":
    59  					scopes[i] = types.Range
    60  				case "country":
    61  					scopes[i] = types.Country
    62  				case "as":
    63  					scopes[i] = types.AS
    64  				}
    65  			}
    66  			query = query.Where(decision.ScopeIn(scopes...))
    67  		case "value":
    68  			query = query.Where(decision.ValueEQ(value[0]))
    69  		case "type":
    70  			query = query.Where(decision.TypeEQ(value[0]))
    71  		case "origins":
    72  			query = query.Where(
    73  				decision.OriginIn(strings.Split(value[0], ",")...),
    74  			)
    75  		case "scenarios_containing":
    76  			predicates := decisionPredicatesFromStr(value[0], decision.ScenarioContainsFold)
    77  			query = query.Where(decision.Or(predicates...))
    78  		case "scenarios_not_containing":
    79  			predicates := decisionPredicatesFromStr(value[0], decision.ScenarioContainsFold)
    80  			query = query.Where(decision.Not(
    81  				decision.Or(
    82  					predicates...,
    83  				),
    84  			))
    85  		case "ip", "range":
    86  			ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(value[0])
    87  			if err != nil {
    88  				return nil, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
    89  			}
    90  		case "limit":
    91  			limit, err := strconv.Atoi(value[0])
    92  			if err != nil {
    93  				return nil, errors.Wrapf(InvalidFilter, "invalid limit value : %s", err)
    94  			}
    95  			query = query.Limit(limit)
    96  		case "offset":
    97  			offset, err := strconv.Atoi(value[0])
    98  			if err != nil {
    99  				return nil, errors.Wrapf(InvalidFilter, "invalid offset value : %s", err)
   100  			}
   101  			query = query.Offset(offset)
   102  		case "id_gt":
   103  			id, err := strconv.Atoi(value[0])
   104  			if err != nil {
   105  				return nil, errors.Wrapf(InvalidFilter, "invalid id_gt value : %s", err)
   106  			}
   107  			query = query.Where(decision.IDGT(id))
   108  		}
   109  	}
   110  	query, err = applyStartIpEndIpFilter(query, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
   111  	if err != nil {
   112  		return nil, fmt.Errorf("fail to apply StartIpEndIpFilter: %w", err)
   113  	}
   114  	return query, nil
   115  }
   116  func (c *Client) QueryAllDecisionsWithFilters(filters map[string][]string) ([]*ent.Decision, error) {
   117  	query := c.Ent.Decision.Query().Where(
   118  		decision.UntilGT(time.Now().UTC()),
   119  	)
   120  	//Allow a bouncer to ask for non-deduplicated results
   121  	if v, ok := filters["dedup"]; !ok || v[0] != "false" {
   122  		query = query.Where(longestDecisionForScopeTypeValue)
   123  	}
   124  
   125  	query, err := BuildDecisionRequestWithFilter(query, filters)
   126  
   127  	if err != nil {
   128  		c.Log.Warningf("QueryAllDecisionsWithFilters : %s", err)
   129  		return []*ent.Decision{}, errors.Wrap(QueryFail, "get all decisions with filters")
   130  	}
   131  
   132  	query = query.Order(ent.Asc(decision.FieldID))
   133  
   134  	data, err := query.All(c.CTX)
   135  	if err != nil {
   136  		c.Log.Warningf("QueryAllDecisionsWithFilters : %s", err)
   137  		return []*ent.Decision{}, errors.Wrap(QueryFail, "get all decisions with filters")
   138  	}
   139  	return data, nil
   140  }
   141  
   142  func (c *Client) QueryExpiredDecisionsWithFilters(filters map[string][]string) ([]*ent.Decision, error) {
   143  	query := c.Ent.Decision.Query().Where(
   144  		decision.UntilLT(time.Now().UTC()),
   145  	)
   146  	//Allow a bouncer to ask for non-deduplicated results
   147  	if v, ok := filters["dedup"]; !ok || v[0] != "false" {
   148  		query = query.Where(longestDecisionForScopeTypeValue)
   149  	}
   150  
   151  	query, err := BuildDecisionRequestWithFilter(query, filters)
   152  
   153  	query = query.Order(ent.Asc(decision.FieldID))
   154  
   155  	if err != nil {
   156  		c.Log.Warningf("QueryExpiredDecisionsWithFilters : %s", err)
   157  		return []*ent.Decision{}, errors.Wrap(QueryFail, "get expired decisions with filters")
   158  	}
   159  	data, err := query.All(c.CTX)
   160  	if err != nil {
   161  		c.Log.Warningf("QueryExpiredDecisionsWithFilters : %s", err)
   162  		return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions")
   163  	}
   164  	return data, nil
   165  }
   166  
   167  func (c *Client) QueryDecisionCountByScenario(filters map[string][]string) ([]*DecisionsByScenario, error) {
   168  	query := c.Ent.Decision.Query().Where(
   169  		decision.UntilGT(time.Now().UTC()),
   170  	)
   171  	query, err := BuildDecisionRequestWithFilter(query, filters)
   172  
   173  	if err != nil {
   174  		c.Log.Warningf("QueryDecisionCountByScenario : %s", err)
   175  		return nil, errors.Wrap(QueryFail, "count all decisions with filters")
   176  	}
   177  
   178  	var r []*DecisionsByScenario
   179  
   180  	err = query.GroupBy(decision.FieldScenario, decision.FieldOrigin, decision.FieldType).Aggregate(ent.Count()).Scan(c.CTX, &r)
   181  
   182  	if err != nil {
   183  		c.Log.Warningf("QueryDecisionCountByScenario : %s", err)
   184  		return nil, errors.Wrap(QueryFail, "count all decisions with filters")
   185  	}
   186  
   187  	return r, nil
   188  }
   189  
   190  func (c *Client) QueryDecisionWithFilter(filter map[string][]string) ([]*ent.Decision, error) {
   191  	var data []*ent.Decision
   192  	var err error
   193  
   194  	decisions := c.Ent.Decision.Query().
   195  		Where(decision.UntilGTE(time.Now().UTC()))
   196  
   197  	decisions, err = BuildDecisionRequestWithFilter(decisions, filter)
   198  	if err != nil {
   199  		return []*ent.Decision{}, err
   200  	}
   201  
   202  	err = decisions.Select(
   203  		decision.FieldID,
   204  		decision.FieldUntil,
   205  		decision.FieldScenario,
   206  		decision.FieldType,
   207  		decision.FieldStartIP,
   208  		decision.FieldEndIP,
   209  		decision.FieldValue,
   210  		decision.FieldScope,
   211  		decision.FieldOrigin,
   212  	).Scan(c.CTX, &data)
   213  	if err != nil {
   214  		c.Log.Warningf("QueryDecisionWithFilter : %s", err)
   215  		return []*ent.Decision{}, errors.Wrap(QueryFail, "query decision failed")
   216  	}
   217  
   218  	return data, nil
   219  }
   220  
   221  // ent translation of https://stackoverflow.com/a/28090544
   222  func longestDecisionForScopeTypeValue(s *sql.Selector) {
   223  	t := sql.Table(decision.Table)
   224  	s.LeftJoin(t).OnP(sql.And(
   225  		sql.ColumnsEQ(
   226  			t.C(decision.FieldValue),
   227  			s.C(decision.FieldValue),
   228  		),
   229  		sql.ColumnsEQ(
   230  			t.C(decision.FieldType),
   231  			s.C(decision.FieldType),
   232  		),
   233  		sql.ColumnsEQ(
   234  			t.C(decision.FieldScope),
   235  			s.C(decision.FieldScope),
   236  		),
   237  		sql.ColumnsGT(
   238  			t.C(decision.FieldUntil),
   239  			s.C(decision.FieldUntil),
   240  		),
   241  	))
   242  	s.Where(
   243  		sql.IsNull(
   244  			t.C(decision.FieldUntil),
   245  		),
   246  	)
   247  }
   248  
   249  func (c *Client) QueryExpiredDecisionsSinceWithFilters(since time.Time, filters map[string][]string) ([]*ent.Decision, error) {
   250  	query := c.Ent.Decision.Query().Where(
   251  		decision.UntilLT(time.Now().UTC()),
   252  		decision.UntilGT(since),
   253  	)
   254  	//Allow a bouncer to ask for non-deduplicated results
   255  	if v, ok := filters["dedup"]; !ok || v[0] != "false" {
   256  		query = query.Where(longestDecisionForScopeTypeValue)
   257  	}
   258  	query, err := BuildDecisionRequestWithFilter(query, filters)
   259  	if err != nil {
   260  		c.Log.Warningf("QueryExpiredDecisionsSinceWithFilters : %s", err)
   261  		return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions with filters")
   262  	}
   263  
   264  	query = query.Order(ent.Asc(decision.FieldID))
   265  
   266  	data, err := query.All(c.CTX)
   267  	if err != nil {
   268  		c.Log.Warningf("QueryExpiredDecisionsSinceWithFilters : %s", err)
   269  		return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions with filters")
   270  	}
   271  
   272  	return data, nil
   273  }
   274  
   275  func (c *Client) QueryNewDecisionsSinceWithFilters(since time.Time, filters map[string][]string) ([]*ent.Decision, error) {
   276  	query := c.Ent.Decision.Query().Where(
   277  		decision.CreatedAtGT(since),
   278  		decision.UntilGT(time.Now().UTC()),
   279  	)
   280  	//Allow a bouncer to ask for non-deduplicated results
   281  	if v, ok := filters["dedup"]; !ok || v[0] != "false" {
   282  		query = query.Where(longestDecisionForScopeTypeValue)
   283  	}
   284  	query, err := BuildDecisionRequestWithFilter(query, filters)
   285  	if err != nil {
   286  		c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)
   287  		return []*ent.Decision{}, errors.Wrapf(QueryFail, "new decisions since '%s'", since.String())
   288  	}
   289  
   290  	query = query.Order(ent.Asc(decision.FieldID))
   291  
   292  	data, err := query.All(c.CTX)
   293  	if err != nil {
   294  		c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)
   295  		return []*ent.Decision{}, errors.Wrapf(QueryFail, "new decisions since '%s'", since.String())
   296  	}
   297  	return data, nil
   298  }
   299  
   300  func (c *Client) DeleteDecisionById(decisionId int) ([]*ent.Decision, error) {
   301  	toDelete, err := c.Ent.Decision.Query().Where(decision.IDEQ(decisionId)).All(c.CTX)
   302  	if err != nil {
   303  		c.Log.Warningf("DeleteDecisionById : %s", err)
   304  		return nil, errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionId)
   305  	}
   306  	count, err := c.BulkDeleteDecisions(toDelete, false)
   307  	c.Log.Debugf("deleted %d decisions", count)
   308  	return toDelete, err
   309  }
   310  
   311  func (c *Client) DeleteDecisionsWithFilter(filter map[string][]string) (string, []*ent.Decision, error) {
   312  	var err error
   313  	var start_ip, start_sfx, end_ip, end_sfx int64
   314  	var ip_sz int
   315  	var contains = true
   316  	/*if contains is true, return bans that *contains* the given value (value is the inner)
   317  	  else, return bans that are *contained* by the given value (value is the outer) */
   318  
   319  	decisions := c.Ent.Decision.Query()
   320  	for param, value := range filter {
   321  		switch param {
   322  		case "contains":
   323  			contains, err = strconv.ParseBool(value[0])
   324  			if err != nil {
   325  				return "0", nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
   326  			}
   327  		case "scope":
   328  			decisions = decisions.Where(decision.ScopeEQ(value[0]))
   329  		case "value":
   330  			decisions = decisions.Where(decision.ValueEQ(value[0]))
   331  		case "type":
   332  			decisions = decisions.Where(decision.TypeEQ(value[0]))
   333  		case "ip", "range":
   334  			ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(value[0])
   335  			if err != nil {
   336  				return "0", nil, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
   337  			}
   338  		case "scenario":
   339  			decisions = decisions.Where(decision.ScenarioEQ(value[0]))
   340  		default:
   341  			return "0", nil, errors.Wrap(InvalidFilter, fmt.Sprintf("'%s' doesn't exist", param))
   342  		}
   343  	}
   344  
   345  	if ip_sz == 4 {
   346  		if contains { /*decision contains {start_ip,end_ip}*/
   347  			decisions = decisions.Where(decision.And(
   348  				decision.StartIPLTE(start_ip),
   349  				decision.EndIPGTE(end_ip),
   350  				decision.IPSizeEQ(int64(ip_sz)),
   351  			))
   352  		} else { /*decision is contained within {start_ip,end_ip}*/
   353  			decisions = decisions.Where(decision.And(
   354  				decision.StartIPGTE(start_ip),
   355  				decision.EndIPLTE(end_ip),
   356  				decision.IPSizeEQ(int64(ip_sz)),
   357  			))
   358  		}
   359  	} else if ip_sz == 16 {
   360  		if contains { /*decision contains {start_ip,end_ip}*/
   361  			decisions = decisions.Where(decision.And(
   362  				//matching addr size
   363  				decision.IPSizeEQ(int64(ip_sz)),
   364  				decision.Or(
   365  					//decision.start_ip < query.start_ip
   366  					decision.StartIPLT(start_ip),
   367  					decision.And(
   368  						//decision.start_ip == query.start_ip
   369  						decision.StartIPEQ(start_ip),
   370  						//decision.start_suffix <= query.start_suffix
   371  						decision.StartSuffixLTE(start_sfx),
   372  					)),
   373  				decision.Or(
   374  					//decision.end_ip > query.end_ip
   375  					decision.EndIPGT(end_ip),
   376  					decision.And(
   377  						//decision.end_ip == query.end_ip
   378  						decision.EndIPEQ(end_ip),
   379  						//decision.end_suffix >= query.end_suffix
   380  						decision.EndSuffixGTE(end_sfx),
   381  					),
   382  				),
   383  			))
   384  		} else {
   385  			decisions = decisions.Where(decision.And(
   386  				//matching addr size
   387  				decision.IPSizeEQ(int64(ip_sz)),
   388  				decision.Or(
   389  					//decision.start_ip > query.start_ip
   390  					decision.StartIPGT(start_ip),
   391  					decision.And(
   392  						//decision.start_ip == query.start_ip
   393  						decision.StartIPEQ(start_ip),
   394  						//decision.start_suffix >= query.start_suffix
   395  						decision.StartSuffixGTE(start_sfx),
   396  					)),
   397  				decision.Or(
   398  					//decision.end_ip < query.end_ip
   399  					decision.EndIPLT(end_ip),
   400  					decision.And(
   401  						//decision.end_ip == query.end_ip
   402  						decision.EndIPEQ(end_ip),
   403  						//decision.end_suffix <= query.end_suffix
   404  						decision.EndSuffixLTE(end_sfx),
   405  					),
   406  				),
   407  			))
   408  		}
   409  	} else if ip_sz != 0 {
   410  		return "0", nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
   411  	}
   412  
   413  	toDelete, err := decisions.All(c.CTX)
   414  	if err != nil {
   415  		c.Log.Warningf("DeleteDecisionsWithFilter : %s", err)
   416  		return "0", nil, errors.Wrap(DeleteFail, "decisions with provided filter")
   417  	}
   418  	count, err := c.BulkDeleteDecisions(toDelete, false)
   419  	if err != nil {
   420  		c.Log.Warningf("While deleting decisions : %s", err)
   421  		return "0", nil, errors.Wrap(DeleteFail, "decisions with provided filter")
   422  	}
   423  	return strconv.Itoa(count), toDelete, nil
   424  }
   425  
   426  // SoftDeleteDecisionsWithFilter updates the expiration time to now() for the decisions matching the filter, and returns the updated items
   427  func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (string, []*ent.Decision, error) {
   428  	var err error
   429  	var start_ip, start_sfx, end_ip, end_sfx int64
   430  	var ip_sz int
   431  	var contains = true
   432  	/*if contains is true, return bans that *contains* the given value (value is the inner)
   433  	  else, return bans that are *contained* by the given value (value is the outer)*/
   434  	decisions := c.Ent.Decision.Query().Where(decision.UntilGT(time.Now().UTC()))
   435  	for param, value := range filter {
   436  		switch param {
   437  		case "contains":
   438  			contains, err = strconv.ParseBool(value[0])
   439  			if err != nil {
   440  				return "0", nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
   441  			}
   442  		case "scopes":
   443  			decisions = decisions.Where(decision.ScopeEQ(value[0]))
   444  		case "uuid":
   445  			decisions = decisions.Where(decision.UUIDIn(value...))
   446  		case "origin":
   447  			decisions = decisions.Where(decision.OriginEQ(value[0]))
   448  		case "value":
   449  			decisions = decisions.Where(decision.ValueEQ(value[0]))
   450  		case "type":
   451  			decisions = decisions.Where(decision.TypeEQ(value[0]))
   452  		case "ip", "range":
   453  			ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(value[0])
   454  			if err != nil {
   455  				return "0", nil, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
   456  			}
   457  		case "scenario":
   458  			decisions = decisions.Where(decision.ScenarioEQ(value[0]))
   459  		default:
   460  			return "0", nil, errors.Wrapf(InvalidFilter, "'%s' doesn't exist", param)
   461  		}
   462  	}
   463  	if ip_sz == 4 {
   464  		if contains {
   465  			/*Decision contains {start_ip,end_ip}*/
   466  			decisions = decisions.Where(decision.And(
   467  				decision.StartIPLTE(start_ip),
   468  				decision.EndIPGTE(end_ip),
   469  				decision.IPSizeEQ(int64(ip_sz)),
   470  			))
   471  		} else {
   472  			/*Decision is contained within {start_ip,end_ip}*/
   473  			decisions = decisions.Where(decision.And(
   474  				decision.StartIPGTE(start_ip),
   475  				decision.EndIPLTE(end_ip),
   476  				decision.IPSizeEQ(int64(ip_sz)),
   477  			))
   478  		}
   479  	} else if ip_sz == 16 {
   480  		/*decision contains {start_ip,end_ip}*/
   481  		if contains {
   482  			decisions = decisions.Where(decision.And(
   483  				//matching addr size
   484  				decision.IPSizeEQ(int64(ip_sz)),
   485  				decision.Or(
   486  					//decision.start_ip < query.start_ip
   487  					decision.StartIPLT(start_ip),
   488  					decision.And(
   489  						//decision.start_ip == query.start_ip
   490  						decision.StartIPEQ(start_ip),
   491  						//decision.start_suffix <= query.start_suffix
   492  						decision.StartSuffixLTE(start_sfx),
   493  					)),
   494  				decision.Or(
   495  					//decision.end_ip > query.end_ip
   496  					decision.EndIPGT(end_ip),
   497  					decision.And(
   498  						//decision.end_ip == query.end_ip
   499  						decision.EndIPEQ(end_ip),
   500  						//decision.end_suffix >= query.end_suffix
   501  						decision.EndSuffixGTE(end_sfx),
   502  					),
   503  				),
   504  			))
   505  		} else {
   506  			/*decision is contained within {start_ip,end_ip}*/
   507  			decisions = decisions.Where(decision.And(
   508  				//matching addr size
   509  				decision.IPSizeEQ(int64(ip_sz)),
   510  				decision.Or(
   511  					//decision.start_ip > query.start_ip
   512  					decision.StartIPGT(start_ip),
   513  					decision.And(
   514  						//decision.start_ip == query.start_ip
   515  						decision.StartIPEQ(start_ip),
   516  						//decision.start_suffix >= query.start_suffix
   517  						decision.StartSuffixGTE(start_sfx),
   518  					)),
   519  				decision.Or(
   520  					//decision.end_ip < query.end_ip
   521  					decision.EndIPLT(end_ip),
   522  					decision.And(
   523  						//decision.end_ip == query.end_ip
   524  						decision.EndIPEQ(end_ip),
   525  						//decision.end_suffix <= query.end_suffix
   526  						decision.EndSuffixLTE(end_sfx),
   527  					),
   528  				),
   529  			))
   530  		}
   531  	} else if ip_sz != 0 {
   532  		return "0", nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
   533  	}
   534  	DecisionsToDelete, err := decisions.All(c.CTX)
   535  	if err != nil {
   536  		c.Log.Warningf("SoftDeleteDecisionsWithFilter : %s", err)
   537  		return "0", nil, errors.Wrap(DeleteFail, "soft delete decisions with provided filter")
   538  	}
   539  
   540  	count, err := c.BulkDeleteDecisions(DecisionsToDelete, true)
   541  	if err != nil {
   542  		return "0", nil, errors.Wrapf(DeleteFail, "soft delete decisions with provided filter : %s", err)
   543  	}
   544  	return strconv.Itoa(count), DecisionsToDelete, err
   545  }
   546  
   547  // BulkDeleteDecisions set the expiration of a bulk of decisions to now() or hard deletes them.
   548  // We are doing it this way so we can return impacted decisions for sync with CAPI/PAPI
   549  func (c *Client) BulkDeleteDecisions(decisionsToDelete []*ent.Decision, softDelete bool) (int, error) {
   550  	const bulkSize = 256 //scientifically proven to be the best value for bulk delete
   551  
   552  	var (
   553  		nbUpdates    int
   554  		err          error
   555  		totalUpdates = 0
   556  	)
   557  
   558  	idsToDelete := make([]int, len(decisionsToDelete))
   559  	for i, decision := range decisionsToDelete {
   560  		idsToDelete[i] = decision.ID
   561  	}
   562  
   563  	for _, chunk := range slicetools.Chunks(idsToDelete, bulkSize) {
   564  		if softDelete {
   565  			nbUpdates, err = c.Ent.Decision.Update().Where(
   566  				decision.IDIn(chunk...),
   567  			).SetUntil(time.Now().UTC()).Save(c.CTX)
   568  			if err != nil {
   569  				return totalUpdates, fmt.Errorf("soft delete decisions with provided filter: %w", err)
   570  			}
   571  		} else {
   572  			nbUpdates, err = c.Ent.Decision.Delete().Where(
   573  				decision.IDIn(chunk...),
   574  			).Exec(c.CTX)
   575  			if err != nil {
   576  				return totalUpdates, fmt.Errorf("hard delete decisions with provided filter: %w", err)
   577  			}
   578  		}
   579  		totalUpdates += nbUpdates
   580  	}
   581  
   582  	return totalUpdates, nil
   583  }
   584  
   585  // SoftDeleteDecisionByID set the expiration of a decision to now()
   586  func (c *Client) SoftDeleteDecisionByID(decisionID int) (int, []*ent.Decision, error) {
   587  	toUpdate, err := c.Ent.Decision.Query().Where(decision.IDEQ(decisionID)).All(c.CTX)
   588  
   589  	// XXX: do we want 500 or 404 here?
   590  	if err != nil || len(toUpdate) == 0 {
   591  		c.Log.Warningf("SoftDeleteDecisionByID : %v (nb soft deleted: %d)", err, len(toUpdate))
   592  		return 0, nil, errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionID)
   593  	}
   594  
   595  	if len(toUpdate) == 0 {
   596  		return 0, nil, ItemNotFound
   597  	}
   598  
   599  	count, err := c.BulkDeleteDecisions(toUpdate, true)
   600  	return count, toUpdate, err
   601  }
   602  
   603  func (c *Client) CountDecisionsByValue(decisionValue string) (int, error) {
   604  	var err error
   605  	var start_ip, start_sfx, end_ip, end_sfx int64
   606  	var ip_sz, count int
   607  	ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(decisionValue)
   608  
   609  	if err != nil {
   610  		return 0, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", decisionValue, err)
   611  	}
   612  
   613  	contains := true
   614  	decisions := c.Ent.Decision.Query()
   615  	decisions, err = applyStartIpEndIpFilter(decisions, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
   616  	if err != nil {
   617  		return 0, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
   618  	}
   619  
   620  	count, err = decisions.Count(c.CTX)
   621  	if err != nil {
   622  		return 0, errors.Wrapf(err, "fail to count decisions")
   623  	}
   624  
   625  	return count, nil
   626  }
   627  
   628  func (c *Client) CountDecisionsSinceByValue(decisionValue string, since time.Time) (int, error) {
   629  	ip_sz, start_ip, start_sfx, end_ip, end_sfx, err := types.Addr2Ints(decisionValue)
   630  
   631  	if err != nil {
   632  		return 0, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", decisionValue, err)
   633  	}
   634  
   635  	contains := true
   636  	decisions := c.Ent.Decision.Query().Where(
   637  		decision.CreatedAtGT(since),
   638  	)
   639  
   640  	decisions, err = applyStartIpEndIpFilter(decisions, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
   641  	if err != nil {
   642  		return 0, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
   643  	}
   644  
   645  	count, err := decisions.Count(c.CTX)
   646  	if err != nil {
   647  		return 0, errors.Wrapf(err, "fail to count decisions")
   648  	}
   649  
   650  	return count, nil
   651  }
   652  
   653  func applyStartIpEndIpFilter(decisions *ent.DecisionQuery, contains bool, ip_sz int, start_ip int64, start_sfx int64, end_ip int64, end_sfx int64) (*ent.DecisionQuery, error) {
   654  	if ip_sz == 4 {
   655  		if contains {
   656  			/*Decision contains {start_ip,end_ip}*/
   657  			decisions = decisions.Where(decision.And(
   658  				decision.StartIPLTE(start_ip),
   659  				decision.EndIPGTE(end_ip),
   660  				decision.IPSizeEQ(int64(ip_sz)),
   661  			))
   662  		} else {
   663  			/*Decision is contained within {start_ip,end_ip}*/
   664  			decisions = decisions.Where(decision.And(
   665  				decision.StartIPGTE(start_ip),
   666  				decision.EndIPLTE(end_ip),
   667  				decision.IPSizeEQ(int64(ip_sz)),
   668  			))
   669  		}
   670  		return decisions, nil
   671  	}
   672  
   673  	if ip_sz == 16 {
   674  		/*decision contains {start_ip,end_ip}*/
   675  		if contains {
   676  			decisions = decisions.Where(decision.And(
   677  				//matching addr size
   678  				decision.IPSizeEQ(int64(ip_sz)),
   679  				decision.Or(
   680  					//decision.start_ip < query.start_ip
   681  					decision.StartIPLT(start_ip),
   682  					decision.And(
   683  						//decision.start_ip == query.start_ip
   684  						decision.StartIPEQ(start_ip),
   685  						//decision.start_suffix <= query.start_suffix
   686  						decision.StartSuffixLTE(start_sfx),
   687  					)),
   688  				decision.Or(
   689  					//decision.end_ip > query.end_ip
   690  					decision.EndIPGT(end_ip),
   691  					decision.And(
   692  						//decision.end_ip == query.end_ip
   693  						decision.EndIPEQ(end_ip),
   694  						//decision.end_suffix >= query.end_suffix
   695  						decision.EndSuffixGTE(end_sfx),
   696  					),
   697  				),
   698  			))
   699  		} else {
   700  			/*decision is contained within {start_ip,end_ip}*/
   701  			decisions = decisions.Where(decision.And(
   702  				//matching addr size
   703  				decision.IPSizeEQ(int64(ip_sz)),
   704  				decision.Or(
   705  					//decision.start_ip > query.start_ip
   706  					decision.StartIPGT(start_ip),
   707  					decision.And(
   708  						//decision.start_ip == query.start_ip
   709  						decision.StartIPEQ(start_ip),
   710  						//decision.start_suffix >= query.start_suffix
   711  						decision.StartSuffixGTE(start_sfx),
   712  					)),
   713  				decision.Or(
   714  					//decision.end_ip < query.end_ip
   715  					decision.EndIPLT(end_ip),
   716  					decision.And(
   717  						//decision.end_ip == query.end_ip
   718  						decision.EndIPEQ(end_ip),
   719  						//decision.end_suffix <= query.end_suffix
   720  						decision.EndSuffixLTE(end_sfx),
   721  					),
   722  				),
   723  			))
   724  		}
   725  		return decisions, nil
   726  	}
   727  
   728  	if ip_sz != 0 {
   729  		return nil, errors.Wrapf(InvalidFilter, "unknown ip size %d", ip_sz)
   730  	}
   731  
   732  	return decisions, nil
   733  }
   734  
   735  func decisionPredicatesFromStr(s string, predicateFunc func(string) predicate.Decision) []predicate.Decision {
   736  	words := strings.Split(s, ",")
   737  	predicates := make([]predicate.Decision, len(words))
   738  	for i, word := range words {
   739  		predicates[i] = predicateFunc(word)
   740  	}
   741  	return predicates
   742  }