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

     1  package database
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"sort"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/mattn/go-sqlite3"
    13  
    14  	"github.com/davecgh/go-spew/spew"
    15  	"github.com/pkg/errors"
    16  	log "github.com/sirupsen/logrus"
    17  
    18  	"github.com/crowdsecurity/go-cs-lib/slicetools"
    19  
    20  	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
    21  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/alert"
    22  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
    23  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/event"
    24  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/meta"
    25  	"github.com/crowdsecurity/crowdsec/pkg/database/ent/predicate"
    26  	"github.com/crowdsecurity/crowdsec/pkg/models"
    27  	"github.com/crowdsecurity/crowdsec/pkg/types"
    28  )
    29  
    30  const (
    31  	paginationSize = 100 // used to queryAlert to avoid 'too many SQL variable'
    32  	defaultLimit   = 100 // default limit of element to returns when query alerts
    33  	bulkSize       = 50  // bulk size when create alerts
    34  	maxLockRetries = 10  // how many times to retry a bulk operation when sqlite3.ErrBusy is encountered
    35  )
    36  
    37  func formatAlertCN(source models.Source) string {
    38  	cn := source.Cn
    39  
    40  	if source.AsNumber != "" {
    41  		cn += "/" + source.AsNumber
    42  	}
    43  
    44  	return cn
    45  }
    46  
    47  func formatAlertSource(alert *models.Alert) string {
    48  	if alert.Source == nil || alert.Source.Scope == nil || *alert.Source.Scope == "" {
    49  		return "empty source"
    50  	}
    51  
    52  	if *alert.Source.Scope == types.Ip {
    53  		ret := "ip " + *alert.Source.Value
    54  
    55  		cn := formatAlertCN(*alert.Source)
    56  		if cn != "" {
    57  			ret += " (" + cn + ")"
    58  		}
    59  
    60  		return ret
    61  	}
    62  
    63  	if *alert.Source.Scope == types.Range {
    64  		ret := "range " + *alert.Source.Value
    65  
    66  		cn := formatAlertCN(*alert.Source)
    67  		if cn != "" {
    68  			ret += " (" + cn + ")"
    69  		}
    70  
    71  		return ret
    72  	}
    73  
    74  	return *alert.Source.Scope + " " + *alert.Source.Value
    75  }
    76  
    77  func formatAlertAsString(machineID string, alert *models.Alert) []string {
    78  	src := formatAlertSource(alert)
    79  
    80  	msg := "empty scenario"
    81  	if alert.Scenario != nil && *alert.Scenario != "" {
    82  		msg = *alert.Scenario
    83  	} else if alert.Message != nil && *alert.Message != "" {
    84  		msg = *alert.Message
    85  	}
    86  
    87  	reason := fmt.Sprintf("%s by %s", msg, src)
    88  
    89  	if len(alert.Decisions) == 0 {
    90  		return []string{fmt.Sprintf("(%s) alert : %s", machineID, reason)}
    91  	}
    92  
    93  	var retStr []string
    94  
    95  	if alert.Decisions[0].Origin != nil && *alert.Decisions[0].Origin == types.CscliImportOrigin {
    96  		return []string{fmt.Sprintf("(%s) alert : %s", machineID, reason)}
    97  	}
    98  
    99  	for i, decisionItem := range alert.Decisions {
   100  		decision := ""
   101  		if alert.Simulated != nil && *alert.Simulated {
   102  			decision = "(simulated alert)"
   103  		} else if decisionItem.Simulated != nil && *decisionItem.Simulated {
   104  			decision = "(simulated decision)"
   105  		}
   106  
   107  		if log.GetLevel() >= log.DebugLevel {
   108  			/*spew is expensive*/
   109  			log.Debugf("%s", spew.Sdump(decisionItem))
   110  		}
   111  
   112  		if len(alert.Decisions) > 1 {
   113  			reason = fmt.Sprintf("%s for %d/%d decisions", msg, i+1, len(alert.Decisions))
   114  		}
   115  
   116  		var machineIDOrigin string
   117  		if machineID == "" {
   118  			machineIDOrigin = *decisionItem.Origin
   119  		} else {
   120  			machineIDOrigin = fmt.Sprintf("%s/%s", machineID, *decisionItem.Origin)
   121  		}
   122  
   123  		decision += fmt.Sprintf("%s %s on %s %s", *decisionItem.Duration,
   124  			*decisionItem.Type, *decisionItem.Scope, *decisionItem.Value)
   125  		retStr = append(retStr,
   126  			fmt.Sprintf("(%s) %s : %s", machineIDOrigin, reason, decision))
   127  	}
   128  
   129  	return retStr
   130  }
   131  
   132  // CreateOrUpdateAlert is specific to PAPI : It checks if alert already exists, otherwise inserts it
   133  // if alert already exists, it checks it associated decisions already exists
   134  // if some associated decisions are missing (ie. previous insert ended up in error) it inserts them
   135  func (c *Client) CreateOrUpdateAlert(machineID string, alertItem *models.Alert) (string, error) {
   136  	if alertItem.UUID == "" {
   137  		return "", fmt.Errorf("alert UUID is empty")
   138  	}
   139  
   140  	alerts, err := c.Ent.Alert.Query().Where(alert.UUID(alertItem.UUID)).WithDecisions().All(c.CTX)
   141  
   142  	if err != nil && !ent.IsNotFound(err) {
   143  		return "", fmt.Errorf("unable to query alerts for uuid %s: %w", alertItem.UUID, err)
   144  	}
   145  
   146  	//alert wasn't found, insert it (expected hotpath)
   147  	if ent.IsNotFound(err) || len(alerts) == 0 {
   148  		alertIDs, err := c.CreateAlert(machineID, []*models.Alert{alertItem})
   149  		if err != nil {
   150  			return "", fmt.Errorf("unable to create alert: %w", err)
   151  		}
   152  
   153  		return alertIDs[0], nil
   154  	}
   155  
   156  	//this should never happen
   157  	if len(alerts) > 1 {
   158  		return "", fmt.Errorf("multiple alerts found for uuid %s", alertItem.UUID)
   159  	}
   160  
   161  	log.Infof("Alert %s already exists, checking associated decisions", alertItem.UUID)
   162  
   163  	//alert is found, check for any missing decisions
   164  
   165  	newUuids := make([]string, len(alertItem.Decisions))
   166  	for i, decItem := range alertItem.Decisions {
   167  		newUuids[i] = decItem.UUID
   168  	}
   169  
   170  	foundAlert := alerts[0]
   171  	foundUuids := make([]string, len(foundAlert.Edges.Decisions))
   172  
   173  	for i, decItem := range foundAlert.Edges.Decisions {
   174  		foundUuids[i] = decItem.UUID
   175  	}
   176  
   177  	sort.Strings(foundUuids)
   178  	sort.Strings(newUuids)
   179  
   180  	missingUuids := []string{}
   181  
   182  	for idx, uuid := range newUuids {
   183  		if len(foundUuids) < idx+1 || uuid != foundUuids[idx] {
   184  			log.Warningf("Decision with uuid %s not found in alert %s", uuid, foundAlert.UUID)
   185  			missingUuids = append(missingUuids, uuid)
   186  		}
   187  	}
   188  
   189  	if len(missingUuids) == 0 {
   190  		log.Warningf("alert %s was already complete with decisions %+v", alertItem.UUID, foundUuids)
   191  		return "", nil
   192  	}
   193  
   194  	// add any and all missing decisions based on their uuids
   195  	// prepare missing decisions
   196  	missingDecisions := []*models.Decision{}
   197  
   198  	for _, uuid := range missingUuids {
   199  		for _, newDecision := range alertItem.Decisions {
   200  			if newDecision.UUID == uuid {
   201  				missingDecisions = append(missingDecisions, newDecision)
   202  			}
   203  		}
   204  	}
   205  
   206  	//add missing decisions
   207  	log.Debugf("Adding %d missing decisions to alert %s", len(missingDecisions), foundAlert.UUID)
   208  
   209  	decisionBuilders := []*ent.DecisionCreate{}
   210  
   211  	for _, decisionItem := range missingDecisions {
   212  		var start_ip, start_sfx, end_ip, end_sfx int64
   213  		var sz int
   214  
   215  		/*if the scope is IP or Range, convert the value to integers */
   216  		if strings.ToLower(*decisionItem.Scope) == "ip" || strings.ToLower(*decisionItem.Scope) == "range" {
   217  			sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(*decisionItem.Value)
   218  			if err != nil {
   219  				log.Errorf("invalid addr/range '%s': %s", *decisionItem.Value, err)
   220  				continue
   221  			}
   222  		}
   223  
   224  		decisionDuration, err := time.ParseDuration(*decisionItem.Duration)
   225  		if err != nil {
   226  			log.Warningf("invalid duration %s for decision %s", *decisionItem.Duration, decisionItem.UUID)
   227  			continue
   228  		}
   229  
   230  		//use the created_at from the alert instead
   231  		alertTime, err := time.Parse(time.RFC3339, alertItem.CreatedAt)
   232  		if err != nil {
   233  			log.Errorf("unable to parse alert time %s : %s", alertItem.CreatedAt, err)
   234  
   235  			alertTime = time.Now()
   236  		}
   237  
   238  		decisionUntil := alertTime.UTC().Add(decisionDuration)
   239  
   240  		decisionBuilder := c.Ent.Decision.Create().
   241  			SetUntil(decisionUntil).
   242  			SetScenario(*decisionItem.Scenario).
   243  			SetType(*decisionItem.Type).
   244  			SetStartIP(start_ip).
   245  			SetStartSuffix(start_sfx).
   246  			SetEndIP(end_ip).
   247  			SetEndSuffix(end_sfx).
   248  			SetIPSize(int64(sz)).
   249  			SetValue(*decisionItem.Value).
   250  			SetScope(*decisionItem.Scope).
   251  			SetOrigin(*decisionItem.Origin).
   252  			SetSimulated(*alertItem.Simulated).
   253  			SetUUID(decisionItem.UUID)
   254  
   255  		decisionBuilders = append(decisionBuilders, decisionBuilder)
   256  	}
   257  
   258  	decisions := []*ent.Decision{}
   259  
   260  	builderChunks := slicetools.Chunks(decisionBuilders, c.decisionBulkSize)
   261  
   262  	for _, builderChunk := range builderChunks {
   263  		decisionsCreateRet, err := c.Ent.Decision.CreateBulk(builderChunk...).Save(c.CTX)
   264  		if err != nil {
   265  			return "", fmt.Errorf("creating alert decisions: %w", err)
   266  		}
   267  
   268  		decisions = append(decisions, decisionsCreateRet...)
   269  	}
   270  
   271  	//now that we bulk created missing decisions, let's update the alert
   272  
   273  	decisionChunks := slicetools.Chunks(decisions, c.decisionBulkSize)
   274  
   275  	for _, decisionChunk := range decisionChunks {
   276  		err = c.Ent.Alert.Update().Where(alert.UUID(alertItem.UUID)).AddDecisions(decisionChunk...).Exec(c.CTX)
   277  		if err != nil {
   278  			return "", fmt.Errorf("updating alert %s: %w", alertItem.UUID, err)
   279  		}
   280  	}
   281  
   282  	return "", nil
   283  }
   284  
   285  // UpdateCommunityBlocklist is called to update either the community blocklist (or other lists the user subscribed to)
   286  // it takes care of creating the new alert with the associated decisions, and it will as well deleted the "older" overlapping decisions:
   287  // 1st pull, you get decisions [1,2,3]. it inserts [1,2,3]
   288  // 2nd pull, you get decisions [1,2,3,4]. it inserts [1,2,3,4] and will try to delete [1,2,3,4] with a different alert ID and same origin
   289  func (c *Client) UpdateCommunityBlocklist(alertItem *models.Alert) (int, int, int, error) {
   290  	if alertItem == nil {
   291  		return 0, 0, 0, fmt.Errorf("nil alert")
   292  	}
   293  
   294  	if alertItem.StartAt == nil {
   295  		return 0, 0, 0, fmt.Errorf("nil start_at")
   296  	}
   297  
   298  	startAtTime, err := time.Parse(time.RFC3339, *alertItem.StartAt)
   299  	if err != nil {
   300  		return 0, 0, 0, errors.Wrapf(ParseTimeFail, "start_at field time '%s': %s", *alertItem.StartAt, err)
   301  	}
   302  
   303  	if alertItem.StopAt == nil {
   304  		return 0, 0, 0, fmt.Errorf("nil stop_at")
   305  	}
   306  
   307  	stopAtTime, err := time.Parse(time.RFC3339, *alertItem.StopAt)
   308  	if err != nil {
   309  		return 0, 0, 0, errors.Wrapf(ParseTimeFail, "stop_at field time '%s': %s", *alertItem.StopAt, err)
   310  	}
   311  
   312  	ts, err := time.Parse(time.RFC3339, *alertItem.StopAt)
   313  	if err != nil {
   314  		c.Log.Errorf("While parsing StartAt of item %s : %s", *alertItem.StopAt, err)
   315  
   316  		ts = time.Now().UTC()
   317  	}
   318  
   319  	alertB := c.Ent.Alert.
   320  		Create().
   321  		SetScenario(*alertItem.Scenario).
   322  		SetMessage(*alertItem.Message).
   323  		SetEventsCount(*alertItem.EventsCount).
   324  		SetStartedAt(startAtTime).
   325  		SetStoppedAt(stopAtTime).
   326  		SetSourceScope(*alertItem.Source.Scope).
   327  		SetSourceValue(*alertItem.Source.Value).
   328  		SetSourceIp(alertItem.Source.IP).
   329  		SetSourceRange(alertItem.Source.Range).
   330  		SetSourceAsNumber(alertItem.Source.AsNumber).
   331  		SetSourceAsName(alertItem.Source.AsName).
   332  		SetSourceCountry(alertItem.Source.Cn).
   333  		SetSourceLatitude(alertItem.Source.Latitude).
   334  		SetSourceLongitude(alertItem.Source.Longitude).
   335  		SetCapacity(*alertItem.Capacity).
   336  		SetLeakSpeed(*alertItem.Leakspeed).
   337  		SetSimulated(*alertItem.Simulated).
   338  		SetScenarioVersion(*alertItem.ScenarioVersion).
   339  		SetScenarioHash(*alertItem.ScenarioHash)
   340  
   341  	alertRef, err := alertB.Save(c.CTX)
   342  	if err != nil {
   343  		return 0, 0, 0, errors.Wrapf(BulkError, "error creating alert : %s", err)
   344  	}
   345  
   346  	if len(alertItem.Decisions) == 0 {
   347  		return alertRef.ID, 0, 0, nil
   348  	}
   349  
   350  	txClient, err := c.Ent.Tx(c.CTX)
   351  	if err != nil {
   352  		return 0, 0, 0, errors.Wrapf(BulkError, "error creating transaction : %s", err)
   353  	}
   354  
   355  	DecOrigin := CapiMachineID
   356  
   357  	if *alertItem.Decisions[0].Origin == CapiMachineID || *alertItem.Decisions[0].Origin == CapiListsMachineID {
   358  		DecOrigin = *alertItem.Decisions[0].Origin
   359  	} else {
   360  		log.Warningf("unexpected origin %s", *alertItem.Decisions[0].Origin)
   361  	}
   362  
   363  	deleted := 0
   364  	inserted := 0
   365  
   366  	decisionBuilders := make([]*ent.DecisionCreate, 0, len(alertItem.Decisions))
   367  	valueList := make([]string, 0, len(alertItem.Decisions))
   368  
   369  	for _, decisionItem := range alertItem.Decisions {
   370  		var start_ip, start_sfx, end_ip, end_sfx int64
   371  		var sz int
   372  
   373  		if decisionItem.Duration == nil {
   374  			log.Warning("nil duration in community decision")
   375  			continue
   376  		}
   377  
   378  		duration, err := time.ParseDuration(*decisionItem.Duration)
   379  		if err != nil {
   380  			rollbackErr := txClient.Rollback()
   381  			if rollbackErr != nil {
   382  				log.Errorf("rollback error: %s", rollbackErr)
   383  			}
   384  
   385  			return 0, 0, 0, errors.Wrapf(ParseDurationFail, "decision duration '%+v' : %s", *decisionItem.Duration, err)
   386  		}
   387  
   388  		if decisionItem.Scope == nil {
   389  			log.Warning("nil scope in community decision")
   390  			continue
   391  		}
   392  
   393  		/*if the scope is IP or Range, convert the value to integers */
   394  		if strings.ToLower(*decisionItem.Scope) == "ip" || strings.ToLower(*decisionItem.Scope) == "range" {
   395  			sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(*decisionItem.Value)
   396  			if err != nil {
   397  				rollbackErr := txClient.Rollback()
   398  				if rollbackErr != nil {
   399  					log.Errorf("rollback error: %s", rollbackErr)
   400  				}
   401  
   402  				return 0, 0, 0, errors.Wrapf(InvalidIPOrRange, "invalid addr/range %s : %s", *decisionItem.Value, err)
   403  			}
   404  		}
   405  
   406  		/*bulk insert some new decisions*/
   407  		decisionBuilder := c.Ent.Decision.Create().
   408  			SetUntil(ts.Add(duration)).
   409  			SetScenario(*decisionItem.Scenario).
   410  			SetType(*decisionItem.Type).
   411  			SetStartIP(start_ip).
   412  			SetStartSuffix(start_sfx).
   413  			SetEndIP(end_ip).
   414  			SetEndSuffix(end_sfx).
   415  			SetIPSize(int64(sz)).
   416  			SetValue(*decisionItem.Value).
   417  			SetScope(*decisionItem.Scope).
   418  			SetOrigin(*decisionItem.Origin).
   419  			SetSimulated(*alertItem.Simulated).
   420  			SetOwner(alertRef)
   421  
   422  		decisionBuilders = append(decisionBuilders, decisionBuilder)
   423  
   424  		/*for bulk delete of duplicate decisions*/
   425  		if decisionItem.Value == nil {
   426  			log.Warning("nil value in community decision")
   427  			continue
   428  		}
   429  
   430  		valueList = append(valueList, *decisionItem.Value)
   431  	}
   432  
   433  	deleteChunks := slicetools.Chunks(valueList, c.decisionBulkSize)
   434  
   435  	for _, deleteChunk := range deleteChunks {
   436  		// Deleting older decisions from capi
   437  		deletedDecisions, err := txClient.Decision.Delete().
   438  			Where(decision.And(
   439  				decision.OriginEQ(DecOrigin),
   440  				decision.Not(decision.HasOwnerWith(alert.IDEQ(alertRef.ID))),
   441  				decision.ValueIn(deleteChunk...),
   442  			)).Exec(c.CTX)
   443  		if err != nil {
   444  			rollbackErr := txClient.Rollback()
   445  			if rollbackErr != nil {
   446  				log.Errorf("rollback error: %s", rollbackErr)
   447  			}
   448  
   449  			return 0, 0, 0, fmt.Errorf("while deleting older community blocklist decisions: %w", err)
   450  		}
   451  
   452  		deleted += deletedDecisions
   453  	}
   454  
   455  	builderChunks := slicetools.Chunks(decisionBuilders, c.decisionBulkSize)
   456  
   457  	for _, builderChunk := range builderChunks {
   458  		insertedDecisions, err := txClient.Decision.CreateBulk(builderChunk...).Save(c.CTX)
   459  		if err != nil {
   460  			rollbackErr := txClient.Rollback()
   461  			if rollbackErr != nil {
   462  				log.Errorf("rollback error: %s", rollbackErr)
   463  			}
   464  
   465  			return 0, 0, 0, fmt.Errorf("while bulk creating decisions: %w", err)
   466  		}
   467  
   468  		inserted += len(insertedDecisions)
   469  	}
   470  
   471  	log.Debugf("deleted %d decisions for %s vs %s", deleted, DecOrigin, *alertItem.Decisions[0].Origin)
   472  
   473  	err = txClient.Commit()
   474  	if err != nil {
   475  		rollbackErr := txClient.Rollback()
   476  		if rollbackErr != nil {
   477  			log.Errorf("rollback error: %s", rollbackErr)
   478  		}
   479  
   480  		return 0, 0, 0, fmt.Errorf("error committing transaction: %w", err)
   481  	}
   482  
   483  	return alertRef.ID, inserted, deleted, nil
   484  }
   485  
   486  func (c *Client) createDecisionChunk(simulated bool, stopAtTime time.Time, decisions []*models.Decision) ([]*ent.Decision, error) {
   487  	decisionCreate := []*ent.DecisionCreate{}
   488  
   489  	for _, decisionItem := range decisions {
   490  		var start_ip, start_sfx, end_ip, end_sfx int64
   491  		var sz int
   492  
   493  		duration, err := time.ParseDuration(*decisionItem.Duration)
   494  		if err != nil {
   495  			return nil, errors.Wrapf(ParseDurationFail, "decision duration '%+v' : %s", *decisionItem.Duration, err)
   496  		}
   497  
   498  		/*if the scope is IP or Range, convert the value to integers */
   499  		if strings.ToLower(*decisionItem.Scope) == "ip" || strings.ToLower(*decisionItem.Scope) == "range" {
   500  			sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(*decisionItem.Value)
   501  			if err != nil {
   502  				log.Errorf("invalid addr/range '%s': %s", *decisionItem.Value, err)
   503  				continue
   504  			}
   505  		}
   506  
   507  		newDecision := c.Ent.Decision.Create().
   508  			SetUntil(stopAtTime.Add(duration)).
   509  			SetScenario(*decisionItem.Scenario).
   510  			SetType(*decisionItem.Type).
   511  			SetStartIP(start_ip).
   512  			SetStartSuffix(start_sfx).
   513  			SetEndIP(end_ip).
   514  			SetEndSuffix(end_sfx).
   515  			SetIPSize(int64(sz)).
   516  			SetValue(*decisionItem.Value).
   517  			SetScope(*decisionItem.Scope).
   518  			SetOrigin(*decisionItem.Origin).
   519  			SetSimulated(simulated).
   520  			SetUUID(decisionItem.UUID)
   521  
   522  		decisionCreate = append(decisionCreate, newDecision)
   523  	}
   524  
   525  	if len(decisionCreate) == 0 {
   526  		return nil, nil
   527  	}
   528  
   529  	ret, err := c.Ent.Decision.CreateBulk(decisionCreate...).Save(c.CTX)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  
   534  	return ret, nil
   535  }
   536  
   537  func (c *Client) createAlertChunk(machineID string, owner *ent.Machine, alerts []*models.Alert) ([]string, error) {
   538  	alertBuilders := []*ent.AlertCreate{}
   539  	alertDecisions := [][]*ent.Decision{}
   540  
   541  	for _, alertItem := range alerts {
   542  		var metas []*ent.Meta
   543  		var events []*ent.Event
   544  
   545  		startAtTime, err := time.Parse(time.RFC3339, *alertItem.StartAt)
   546  		if err != nil {
   547  			c.Log.Errorf("CreateAlertBulk: Failed to parse startAtTime '%s', defaulting to now: %s", *alertItem.StartAt, err)
   548  
   549  			startAtTime = time.Now().UTC()
   550  		}
   551  
   552  		stopAtTime, err := time.Parse(time.RFC3339, *alertItem.StopAt)
   553  		if err != nil {
   554  			c.Log.Errorf("CreateAlertBulk: Failed to parse stopAtTime '%s', defaulting to now: %s", *alertItem.StopAt, err)
   555  
   556  			stopAtTime = time.Now().UTC()
   557  		}
   558  		/*display proper alert in logs*/
   559  		for _, disp := range formatAlertAsString(machineID, alertItem) {
   560  			c.Log.Info(disp)
   561  		}
   562  
   563  		//let's track when we strip or drop data, notify outside of loop to avoid spam
   564  		stripped := false
   565  		dropped := false
   566  
   567  		if len(alertItem.Events) > 0 {
   568  			eventBulk := make([]*ent.EventCreate, len(alertItem.Events))
   569  
   570  			for i, eventItem := range alertItem.Events {
   571  				ts, err := time.Parse(time.RFC3339, *eventItem.Timestamp)
   572  				if err != nil {
   573  					c.Log.Errorf("CreateAlertBulk: Failed to parse event timestamp '%s', defaulting to now: %s", *eventItem.Timestamp, err)
   574  
   575  					ts = time.Now().UTC()
   576  				}
   577  
   578  				marshallMetas, err := json.Marshal(eventItem.Meta)
   579  				if err != nil {
   580  					return nil, errors.Wrapf(MarshalFail, "event meta '%v' : %s", eventItem.Meta, err)
   581  				}
   582  
   583  				//the serialized field is too big, let's try to progressively strip it
   584  				if event.SerializedValidator(string(marshallMetas)) != nil {
   585  					stripped = true
   586  
   587  					valid := false
   588  					stripSize := 2048
   589  
   590  					for !valid && stripSize > 0 {
   591  						for _, serializedItem := range eventItem.Meta {
   592  							if len(serializedItem.Value) > stripSize*2 {
   593  								serializedItem.Value = serializedItem.Value[:stripSize] + "<stripped>"
   594  							}
   595  						}
   596  
   597  						marshallMetas, err = json.Marshal(eventItem.Meta)
   598  						if err != nil {
   599  							return nil, errors.Wrapf(MarshalFail, "event meta '%v' : %s", eventItem.Meta, err)
   600  						}
   601  
   602  						if event.SerializedValidator(string(marshallMetas)) == nil {
   603  							valid = true
   604  						}
   605  
   606  						stripSize /= 2
   607  					}
   608  
   609  					//nothing worked, drop it
   610  					if !valid {
   611  						dropped = true
   612  						stripped = false
   613  						marshallMetas = []byte("")
   614  					}
   615  				}
   616  
   617  				eventBulk[i] = c.Ent.Event.Create().
   618  					SetTime(ts).
   619  					SetSerialized(string(marshallMetas))
   620  			}
   621  
   622  			if stripped {
   623  				c.Log.Warningf("stripped 'serialized' field (machine %s / scenario %s)", machineID, *alertItem.Scenario)
   624  			}
   625  
   626  			if dropped {
   627  				c.Log.Warningf("dropped 'serialized' field (machine %s / scenario %s)", machineID, *alertItem.Scenario)
   628  			}
   629  
   630  			events, err = c.Ent.Event.CreateBulk(eventBulk...).Save(c.CTX)
   631  			if err != nil {
   632  				return nil, errors.Wrapf(BulkError, "creating alert events: %s", err)
   633  			}
   634  		}
   635  
   636  		if len(alertItem.Meta) > 0 {
   637  			metaBulk := make([]*ent.MetaCreate, len(alertItem.Meta))
   638  			for i, metaItem := range alertItem.Meta {
   639  				metaBulk[i] = c.Ent.Meta.Create().
   640  					SetKey(metaItem.Key).
   641  					SetValue(metaItem.Value)
   642  			}
   643  
   644  			metas, err = c.Ent.Meta.CreateBulk(metaBulk...).Save(c.CTX)
   645  			if err != nil {
   646  				return nil, errors.Wrapf(BulkError, "creating alert meta: %s", err)
   647  			}
   648  		}
   649  
   650  		decisions := []*ent.Decision{}
   651  
   652  		decisionChunks := slicetools.Chunks(alertItem.Decisions, c.decisionBulkSize)
   653  		for _, decisionChunk := range decisionChunks {
   654  			decisionRet, err := c.createDecisionChunk(*alertItem.Simulated, stopAtTime, decisionChunk)
   655  			if err != nil {
   656  				return nil, fmt.Errorf("creating alert decisions: %w", err)
   657  			}
   658  
   659  			decisions = append(decisions, decisionRet...)
   660  		}
   661  
   662  		discarded := len(alertItem.Decisions) - len(decisions)
   663  		if discarded > 0 {
   664  			c.Log.Warningf("discarded %d decisions for %s", discarded, alertItem.UUID)
   665  		}
   666  
   667  		// if all decisions were discarded, discard the alert too
   668  		if discarded > 0 && len(decisions) == 0 {
   669  			c.Log.Warningf("dropping alert %s with invalid decisions", alertItem.UUID)
   670  			continue
   671  		}
   672  
   673  		alertBuilder := c.Ent.Alert.
   674  			Create().
   675  			SetScenario(*alertItem.Scenario).
   676  			SetMessage(*alertItem.Message).
   677  			SetEventsCount(*alertItem.EventsCount).
   678  			SetStartedAt(startAtTime).
   679  			SetStoppedAt(stopAtTime).
   680  			SetSourceScope(*alertItem.Source.Scope).
   681  			SetSourceValue(*alertItem.Source.Value).
   682  			SetSourceIp(alertItem.Source.IP).
   683  			SetSourceRange(alertItem.Source.Range).
   684  			SetSourceAsNumber(alertItem.Source.AsNumber).
   685  			SetSourceAsName(alertItem.Source.AsName).
   686  			SetSourceCountry(alertItem.Source.Cn).
   687  			SetSourceLatitude(alertItem.Source.Latitude).
   688  			SetSourceLongitude(alertItem.Source.Longitude).
   689  			SetCapacity(*alertItem.Capacity).
   690  			SetLeakSpeed(*alertItem.Leakspeed).
   691  			SetSimulated(*alertItem.Simulated).
   692  			SetScenarioVersion(*alertItem.ScenarioVersion).
   693  			SetScenarioHash(*alertItem.ScenarioHash).
   694  			SetUUID(alertItem.UUID).
   695  			AddEvents(events...).
   696  			AddMetas(metas...)
   697  
   698  		if owner != nil {
   699  			alertBuilder.SetOwner(owner)
   700  		}
   701  
   702  		alertBuilders = append(alertBuilders, alertBuilder)
   703  		alertDecisions = append(alertDecisions, decisions)
   704  	}
   705  
   706  	if len(alertBuilders) == 0 {
   707  		log.Warningf("no alerts to create, discarded?")
   708  		return nil, nil
   709  	}
   710  
   711  	alertsCreateBulk, err := c.Ent.Alert.CreateBulk(alertBuilders...).Save(c.CTX)
   712  	if err != nil {
   713  		return nil, errors.Wrapf(BulkError, "bulk creating alert : %s", err)
   714  	}
   715  
   716  	ret := make([]string, len(alertsCreateBulk))
   717  	for i, a := range alertsCreateBulk {
   718  		ret[i] = strconv.Itoa(a.ID)
   719  
   720  		d := alertDecisions[i]
   721  		decisionsChunk := slicetools.Chunks(d, c.decisionBulkSize)
   722  
   723  		for _, d2 := range decisionsChunk {
   724  			retry := 0
   725  
   726  			for retry < maxLockRetries {
   727  				// so much for the happy path... but sqlite3 errors work differently
   728  				_, err := c.Ent.Alert.Update().Where(alert.IDEQ(a.ID)).AddDecisions(d2...).Save(c.CTX)
   729  				if err == nil {
   730  					break
   731  				}
   732  
   733  				if sqliteErr, ok := err.(sqlite3.Error); ok {
   734  					if sqliteErr.Code == sqlite3.ErrBusy {
   735  						// sqlite3.Error{
   736  						//   Code:         5,
   737  						//   ExtendedCode: 5,
   738  						//   SystemErrno:  0,
   739  						//   err:          "database is locked",
   740  						// }
   741  						retry++
   742  						log.Warningf("while updating decisions, sqlite3.ErrBusy: %s, retry %d of %d", err, retry, maxLockRetries)
   743  						time.Sleep(1 * time.Second)
   744  
   745  						continue
   746  					}
   747  				}
   748  
   749  				return nil, fmt.Errorf("error while updating decisions: %w", err)
   750  			}
   751  		}
   752  	}
   753  
   754  	return ret, nil
   755  }
   756  
   757  func (c *Client) CreateAlert(machineID string, alertList []*models.Alert) ([]string, error) {
   758  	var owner *ent.Machine
   759  	var err error
   760  
   761  	if machineID != "" {
   762  		owner, err = c.QueryMachineByID(machineID)
   763  		if err != nil {
   764  			if !errors.Is(err, UserNotExists) {
   765  				return nil, fmt.Errorf("machine '%s': %w", machineID, err)
   766  			}
   767  
   768  			c.Log.Debugf("CreateAlertBulk: Machine Id %s doesn't exist", machineID)
   769  
   770  			owner = nil
   771  		}
   772  	}
   773  
   774  	c.Log.Debugf("writing %d items", len(alertList))
   775  
   776  	alertChunks := slicetools.Chunks(alertList, bulkSize)
   777  	alertIDs := []string{}
   778  
   779  	for _, alertChunk := range alertChunks {
   780  		ids, err := c.createAlertChunk(machineID, owner, alertChunk)
   781  		if err != nil {
   782  			return nil, fmt.Errorf("machine '%s': %w", machineID, err)
   783  		}
   784  
   785  		alertIDs = append(alertIDs, ids...)
   786  	}
   787  
   788  	return alertIDs, nil
   789  }
   790  
   791  func AlertPredicatesFromFilter(filter map[string][]string) ([]predicate.Alert, error) {
   792  	predicates := make([]predicate.Alert, 0)
   793  
   794  	var err error
   795  	var start_ip, start_sfx, end_ip, end_sfx int64
   796  	var hasActiveDecision bool
   797  	var ip_sz int
   798  	var contains = true
   799  
   800  	/*if contains is true, return bans that *contains* the given value (value is the inner)
   801  	  else, return bans that are *contained* by the given value (value is the outer)*/
   802  
   803  	/*the simulated filter is a bit different : if it's not present *or* set to false, specifically exclude records with simulated to true */
   804  	if v, ok := filter["simulated"]; ok {
   805  		if v[0] == "false" {
   806  			predicates = append(predicates, alert.SimulatedEQ(false))
   807  		}
   808  	}
   809  
   810  	if _, ok := filter["origin"]; ok {
   811  		filter["include_capi"] = []string{"true"}
   812  	}
   813  
   814  	for param, value := range filter {
   815  		switch param {
   816  		case "contains":
   817  			contains, err = strconv.ParseBool(value[0])
   818  			if err != nil {
   819  				return nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
   820  			}
   821  		case "scope":
   822  			var scope = value[0]
   823  			if strings.ToLower(scope) == "ip" {
   824  				scope = types.Ip
   825  			} else if strings.ToLower(scope) == "range" {
   826  				scope = types.Range
   827  			}
   828  
   829  			predicates = append(predicates, alert.SourceScopeEQ(scope))
   830  		case "value":
   831  			predicates = append(predicates, alert.SourceValueEQ(value[0]))
   832  		case "scenario":
   833  			predicates = append(predicates, alert.HasDecisionsWith(decision.ScenarioEQ(value[0])))
   834  		case "ip", "range":
   835  			ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(value[0])
   836  			if err != nil {
   837  				return nil, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
   838  			}
   839  		case "since":
   840  			duration, err := ParseDuration(value[0])
   841  			if err != nil {
   842  				return nil, fmt.Errorf("while parsing duration: %w", err)
   843  			}
   844  
   845  			since := time.Now().UTC().Add(-duration)
   846  			if since.IsZero() {
   847  				return nil, fmt.Errorf("empty time now() - %s", since.String())
   848  			}
   849  
   850  			predicates = append(predicates, alert.StartedAtGTE(since))
   851  		case "created_before":
   852  			duration, err := ParseDuration(value[0])
   853  			if err != nil {
   854  				return nil, fmt.Errorf("while parsing duration: %w", err)
   855  			}
   856  
   857  			since := time.Now().UTC().Add(-duration)
   858  			if since.IsZero() {
   859  				return nil, fmt.Errorf("empty time now() - %s", since.String())
   860  			}
   861  
   862  			predicates = append(predicates, alert.CreatedAtLTE(since))
   863  		case "until":
   864  			duration, err := ParseDuration(value[0])
   865  			if err != nil {
   866  				return nil, fmt.Errorf("while parsing duration: %w", err)
   867  			}
   868  
   869  			until := time.Now().UTC().Add(-duration)
   870  			if until.IsZero() {
   871  				return nil, fmt.Errorf("empty time now() - %s", until.String())
   872  			}
   873  
   874  			predicates = append(predicates, alert.StartedAtLTE(until))
   875  		case "decision_type":
   876  			predicates = append(predicates, alert.HasDecisionsWith(decision.TypeEQ(value[0])))
   877  		case "origin":
   878  			predicates = append(predicates, alert.HasDecisionsWith(decision.OriginEQ(value[0])))
   879  		case "include_capi": //allows to exclude one or more specific origins
   880  			if value[0] == "false" {
   881  				predicates = append(predicates, alert.And(
   882  					//do not show alerts with active decisions having origin CAPI or lists
   883  					alert.And(
   884  						alert.Not(alert.HasDecisionsWith(decision.OriginEQ(types.CAPIOrigin))),
   885  						alert.Not(alert.HasDecisionsWith(decision.OriginEQ(types.ListOrigin))),
   886  					),
   887  					alert.Not(
   888  						alert.And(
   889  							//do not show neither alerts with no decisions if the Source Scope is lists: or CAPI
   890  							alert.Not(alert.HasDecisions()),
   891  							alert.Or(
   892  								alert.SourceScopeHasPrefix(types.ListOrigin+":"),
   893  								alert.SourceScopeEQ(types.CommunityBlocklistPullSourceScope),
   894  							),
   895  						),
   896  					),
   897  				),
   898  				)
   899  
   900  			} else if value[0] != "true" {
   901  				log.Errorf("Invalid bool '%s' for include_capi", value[0])
   902  			}
   903  		case "has_active_decision":
   904  			if hasActiveDecision, err = strconv.ParseBool(value[0]); err != nil {
   905  				return nil, errors.Wrapf(ParseType, "'%s' is not a boolean: %s", value[0], err)
   906  			}
   907  
   908  			if hasActiveDecision {
   909  				predicates = append(predicates, alert.HasDecisionsWith(decision.UntilGTE(time.Now().UTC())))
   910  			} else {
   911  				predicates = append(predicates, alert.Not(alert.HasDecisions()))
   912  			}
   913  		case "limit":
   914  			continue
   915  		case "sort":
   916  			continue
   917  		case "simulated":
   918  			continue
   919  		case "with_decisions":
   920  			continue
   921  		default:
   922  			return nil, errors.Wrapf(InvalidFilter, "Filter parameter '%s' is unknown (=%s)", param, value[0])
   923  		}
   924  	}
   925  
   926  	if ip_sz == 4 {
   927  		if contains { /*decision contains {start_ip,end_ip}*/
   928  			predicates = append(predicates, alert.And(
   929  				alert.HasDecisionsWith(decision.StartIPLTE(start_ip)),
   930  				alert.HasDecisionsWith(decision.EndIPGTE(end_ip)),
   931  				alert.HasDecisionsWith(decision.IPSizeEQ(int64(ip_sz))),
   932  			))
   933  		} else { /*decision is contained within {start_ip,end_ip}*/
   934  			predicates = append(predicates, alert.And(
   935  				alert.HasDecisionsWith(decision.StartIPGTE(start_ip)),
   936  				alert.HasDecisionsWith(decision.EndIPLTE(end_ip)),
   937  				alert.HasDecisionsWith(decision.IPSizeEQ(int64(ip_sz))),
   938  			))
   939  		}
   940  	} else if ip_sz == 16 {
   941  		if contains { /*decision contains {start_ip,end_ip}*/
   942  			predicates = append(predicates, alert.And(
   943  				//matching addr size
   944  				alert.HasDecisionsWith(decision.IPSizeEQ(int64(ip_sz))),
   945  				alert.Or(
   946  					//decision.start_ip < query.start_ip
   947  					alert.HasDecisionsWith(decision.StartIPLT(start_ip)),
   948  					alert.And(
   949  						//decision.start_ip == query.start_ip
   950  						alert.HasDecisionsWith(decision.StartIPEQ(start_ip)),
   951  						//decision.start_suffix <= query.start_suffix
   952  						alert.HasDecisionsWith(decision.StartSuffixLTE(start_sfx)),
   953  					)),
   954  				alert.Or(
   955  					//decision.end_ip > query.end_ip
   956  					alert.HasDecisionsWith(decision.EndIPGT(end_ip)),
   957  					alert.And(
   958  						//decision.end_ip == query.end_ip
   959  						alert.HasDecisionsWith(decision.EndIPEQ(end_ip)),
   960  						//decision.end_suffix >= query.end_suffix
   961  						alert.HasDecisionsWith(decision.EndSuffixGTE(end_sfx)),
   962  					),
   963  				),
   964  			))
   965  		} else { /*decision is contained within {start_ip,end_ip}*/
   966  			predicates = append(predicates, alert.And(
   967  				//matching addr size
   968  				alert.HasDecisionsWith(decision.IPSizeEQ(int64(ip_sz))),
   969  				alert.Or(
   970  					//decision.start_ip > query.start_ip
   971  					alert.HasDecisionsWith(decision.StartIPGT(start_ip)),
   972  					alert.And(
   973  						//decision.start_ip == query.start_ip
   974  						alert.HasDecisionsWith(decision.StartIPEQ(start_ip)),
   975  						//decision.start_suffix >= query.start_suffix
   976  						alert.HasDecisionsWith(decision.StartSuffixGTE(start_sfx)),
   977  					)),
   978  				alert.Or(
   979  					//decision.end_ip < query.end_ip
   980  					alert.HasDecisionsWith(decision.EndIPLT(end_ip)),
   981  					alert.And(
   982  						//decision.end_ip == query.end_ip
   983  						alert.HasDecisionsWith(decision.EndIPEQ(end_ip)),
   984  						//decision.end_suffix <= query.end_suffix
   985  						alert.HasDecisionsWith(decision.EndSuffixLTE(end_sfx)),
   986  					),
   987  				),
   988  			))
   989  		}
   990  	} else if ip_sz != 0 {
   991  		return nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
   992  	}
   993  
   994  	return predicates, nil
   995  }
   996  
   997  func BuildAlertRequestFromFilter(alerts *ent.AlertQuery, filter map[string][]string) (*ent.AlertQuery, error) {
   998  	preds, err := AlertPredicatesFromFilter(filter)
   999  	if err != nil {
  1000  		return nil, err
  1001  	}
  1002  
  1003  	return alerts.Where(preds...), nil
  1004  }
  1005  
  1006  func (c *Client) AlertsCountPerScenario(filters map[string][]string) (map[string]int, error) {
  1007  	var res []struct {
  1008  		Scenario string
  1009  		Count    int
  1010  	}
  1011  
  1012  	ctx := context.Background()
  1013  
  1014  	query := c.Ent.Alert.Query()
  1015  
  1016  	query, err := BuildAlertRequestFromFilter(query, filters)
  1017  
  1018  	if err != nil {
  1019  		return nil, fmt.Errorf("failed to build alert request: %w", err)
  1020  	}
  1021  
  1022  	err = query.GroupBy(alert.FieldScenario).Aggregate(ent.Count()).Scan(ctx, &res)
  1023  
  1024  	if err != nil {
  1025  		return nil, fmt.Errorf("failed to count alerts per scenario: %w", err)
  1026  	}
  1027  
  1028  	counts := make(map[string]int)
  1029  
  1030  	for _, r := range res {
  1031  		counts[r.Scenario] = r.Count
  1032  	}
  1033  
  1034  	return counts, nil
  1035  }
  1036  
  1037  func (c *Client) TotalAlerts() (int, error) {
  1038  	return c.Ent.Alert.Query().Count(c.CTX)
  1039  }
  1040  
  1041  func (c *Client) QueryAlertWithFilter(filter map[string][]string) ([]*ent.Alert, error) {
  1042  	sort := "DESC" // we sort by desc by default
  1043  
  1044  	if val, ok := filter["sort"]; ok {
  1045  		if val[0] != "ASC" && val[0] != "DESC" {
  1046  			c.Log.Errorf("invalid 'sort' parameter: %s", val)
  1047  		} else {
  1048  			sort = val[0]
  1049  		}
  1050  	}
  1051  
  1052  	limit := defaultLimit
  1053  
  1054  	if val, ok := filter["limit"]; ok {
  1055  		limitConv, err := strconv.Atoi(val[0])
  1056  		if err != nil {
  1057  			return nil, errors.Wrapf(QueryFail, "bad limit in parameters: %s", val)
  1058  		}
  1059  
  1060  		limit = limitConv
  1061  	}
  1062  
  1063  	offset := 0
  1064  	ret := make([]*ent.Alert, 0)
  1065  
  1066  	for {
  1067  		alerts := c.Ent.Alert.Query()
  1068  
  1069  		alerts, err := BuildAlertRequestFromFilter(alerts, filter)
  1070  		if err != nil {
  1071  			return nil, err
  1072  		}
  1073  
  1074  		//only if with_decisions is present and set to false, we exclude this
  1075  		if val, ok := filter["with_decisions"]; ok && val[0] == "false" {
  1076  			c.Log.Debugf("skipping decisions")
  1077  		} else {
  1078  			alerts = alerts.
  1079  				WithDecisions()
  1080  		}
  1081  
  1082  		alerts = alerts.
  1083  			WithEvents().
  1084  			WithMetas().
  1085  			WithOwner()
  1086  
  1087  		if limit == 0 {
  1088  			limit, err = alerts.Count(c.CTX)
  1089  			if err != nil {
  1090  				return nil, fmt.Errorf("unable to count nb alerts: %s", err)
  1091  			}
  1092  		}
  1093  
  1094  		if sort == "ASC" {
  1095  			alerts = alerts.Order(ent.Asc(alert.FieldCreatedAt), ent.Asc(alert.FieldID))
  1096  		} else {
  1097  			alerts = alerts.Order(ent.Desc(alert.FieldCreatedAt), ent.Desc(alert.FieldID))
  1098  		}
  1099  
  1100  		result, err := alerts.Limit(paginationSize).Offset(offset).All(c.CTX)
  1101  		if err != nil {
  1102  			return nil, errors.Wrapf(QueryFail, "pagination size: %d, offset: %d: %s", paginationSize, offset, err)
  1103  		}
  1104  
  1105  		if diff := limit - len(ret); diff < paginationSize {
  1106  			if len(result) < diff {
  1107  				ret = append(ret, result...)
  1108  				c.Log.Debugf("Pagination done, %d < %d", len(result), diff)
  1109  
  1110  				break
  1111  			}
  1112  
  1113  			ret = append(ret, result[0:diff]...)
  1114  		} else {
  1115  			ret = append(ret, result...)
  1116  		}
  1117  
  1118  		if len(ret) == limit || len(ret) == 0 || len(ret) < paginationSize {
  1119  			c.Log.Debugf("Pagination done len(ret) = %d", len(ret))
  1120  			break
  1121  		}
  1122  
  1123  		offset += paginationSize
  1124  	}
  1125  
  1126  	return ret, nil
  1127  }
  1128  
  1129  func (c *Client) DeleteAlertGraphBatch(alertItems []*ent.Alert) (int, error) {
  1130  	idList := make([]int, 0)
  1131  	for _, alert := range alertItems {
  1132  		idList = append(idList, alert.ID)
  1133  	}
  1134  
  1135  	_, err := c.Ent.Event.Delete().
  1136  		Where(event.HasOwnerWith(alert.IDIn(idList...))).Exec(c.CTX)
  1137  	if err != nil {
  1138  		c.Log.Warningf("DeleteAlertGraphBatch : %s", err)
  1139  		return 0, errors.Wrapf(DeleteFail, "alert graph delete batch events")
  1140  	}
  1141  
  1142  	_, err = c.Ent.Meta.Delete().
  1143  		Where(meta.HasOwnerWith(alert.IDIn(idList...))).Exec(c.CTX)
  1144  	if err != nil {
  1145  		c.Log.Warningf("DeleteAlertGraphBatch : %s", err)
  1146  		return 0, errors.Wrapf(DeleteFail, "alert graph delete batch meta")
  1147  	}
  1148  
  1149  	_, err = c.Ent.Decision.Delete().
  1150  		Where(decision.HasOwnerWith(alert.IDIn(idList...))).Exec(c.CTX)
  1151  	if err != nil {
  1152  		c.Log.Warningf("DeleteAlertGraphBatch : %s", err)
  1153  		return 0, errors.Wrapf(DeleteFail, "alert graph delete batch decisions")
  1154  	}
  1155  
  1156  	deleted, err := c.Ent.Alert.Delete().
  1157  		Where(alert.IDIn(idList...)).Exec(c.CTX)
  1158  	if err != nil {
  1159  		c.Log.Warningf("DeleteAlertGraphBatch : %s", err)
  1160  		return deleted, errors.Wrapf(DeleteFail, "alert graph delete batch")
  1161  	}
  1162  
  1163  	c.Log.Debug("Done batch delete alerts")
  1164  
  1165  	return deleted, nil
  1166  }
  1167  
  1168  func (c *Client) DeleteAlertGraph(alertItem *ent.Alert) error {
  1169  	// delete the associated events
  1170  	_, err := c.Ent.Event.Delete().
  1171  		Where(event.HasOwnerWith(alert.IDEQ(alertItem.ID))).Exec(c.CTX)
  1172  	if err != nil {
  1173  		c.Log.Warningf("DeleteAlertGraph : %s", err)
  1174  		return errors.Wrapf(DeleteFail, "event with alert ID '%d'", alertItem.ID)
  1175  	}
  1176  
  1177  	// delete the associated meta
  1178  	_, err = c.Ent.Meta.Delete().
  1179  		Where(meta.HasOwnerWith(alert.IDEQ(alertItem.ID))).Exec(c.CTX)
  1180  	if err != nil {
  1181  		c.Log.Warningf("DeleteAlertGraph : %s", err)
  1182  		return errors.Wrapf(DeleteFail, "meta with alert ID '%d'", alertItem.ID)
  1183  	}
  1184  
  1185  	// delete the associated decisions
  1186  	_, err = c.Ent.Decision.Delete().
  1187  		Where(decision.HasOwnerWith(alert.IDEQ(alertItem.ID))).Exec(c.CTX)
  1188  	if err != nil {
  1189  		c.Log.Warningf("DeleteAlertGraph : %s", err)
  1190  		return errors.Wrapf(DeleteFail, "decision with alert ID '%d'", alertItem.ID)
  1191  	}
  1192  
  1193  	// delete the alert
  1194  	err = c.Ent.Alert.DeleteOne(alertItem).Exec(c.CTX)
  1195  	if err != nil {
  1196  		c.Log.Warningf("DeleteAlertGraph : %s", err)
  1197  		return errors.Wrapf(DeleteFail, "alert with ID '%d'", alertItem.ID)
  1198  	}
  1199  
  1200  	return nil
  1201  }
  1202  
  1203  func (c *Client) DeleteAlertByID(id int) error {
  1204  	alertItem, err := c.Ent.Alert.Query().Where(alert.IDEQ(id)).Only(c.CTX)
  1205  	if err != nil {
  1206  		return err
  1207  	}
  1208  
  1209  	return c.DeleteAlertGraph(alertItem)
  1210  }
  1211  
  1212  func (c *Client) DeleteAlertWithFilter(filter map[string][]string) (int, error) {
  1213  	preds, err := AlertPredicatesFromFilter(filter)
  1214  	if err != nil {
  1215  		return 0, err
  1216  	}
  1217  
  1218  	return c.Ent.Alert.Delete().Where(preds...).Exec(c.CTX)
  1219  }
  1220  
  1221  func (c *Client) GetAlertByID(alertID int) (*ent.Alert, error) {
  1222  	alert, err := c.Ent.Alert.Query().Where(alert.IDEQ(alertID)).WithDecisions().WithEvents().WithMetas().WithOwner().First(c.CTX)
  1223  	if err != nil {
  1224  		/*record not found, 404*/
  1225  		if ent.IsNotFound(err) {
  1226  			log.Warningf("GetAlertByID (not found): %s", err)
  1227  			return &ent.Alert{}, ItemNotFound
  1228  		}
  1229  
  1230  		c.Log.Warningf("GetAlertByID : %s", err)
  1231  
  1232  		return &ent.Alert{}, QueryFail
  1233  	}
  1234  
  1235  	return alert, nil
  1236  }