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

     1  package v1
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/gin-gonic/gin"
    13  	"github.com/go-openapi/strfmt"
    14  	"github.com/google/uuid"
    15  	log "github.com/sirupsen/logrus"
    16  
    17  	"github.com/crowdsecurity/crowdsec/pkg/csplugin"
    18  	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
    19  	"github.com/crowdsecurity/crowdsec/pkg/models"
    20  	"github.com/crowdsecurity/crowdsec/pkg/types"
    21  )
    22  
    23  func FormatOneAlert(alert *ent.Alert) *models.Alert {
    24  	startAt := alert.StartedAt.String()
    25  	StopAt := alert.StoppedAt.String()
    26  
    27  	machineID := "N/A"
    28  	if alert.Edges.Owner != nil {
    29  		machineID = alert.Edges.Owner.MachineId
    30  	}
    31  
    32  	outputAlert := models.Alert{
    33  		ID:              int64(alert.ID),
    34  		MachineID:       machineID,
    35  		CreatedAt:       alert.CreatedAt.Format(time.RFC3339),
    36  		Scenario:        &alert.Scenario,
    37  		ScenarioVersion: &alert.ScenarioVersion,
    38  		ScenarioHash:    &alert.ScenarioHash,
    39  		Message:         &alert.Message,
    40  		EventsCount:     &alert.EventsCount,
    41  		StartAt:         &startAt,
    42  		StopAt:          &StopAt,
    43  		Capacity:        &alert.Capacity,
    44  		Leakspeed:       &alert.LeakSpeed,
    45  		Simulated:       &alert.Simulated,
    46  		UUID:            alert.UUID,
    47  		Source: &models.Source{
    48  			Scope:     &alert.SourceScope,
    49  			Value:     &alert.SourceValue,
    50  			IP:        alert.SourceIp,
    51  			Range:     alert.SourceRange,
    52  			AsNumber:  alert.SourceAsNumber,
    53  			AsName:    alert.SourceAsName,
    54  			Cn:        alert.SourceCountry,
    55  			Latitude:  alert.SourceLatitude,
    56  			Longitude: alert.SourceLongitude,
    57  		},
    58  	}
    59  
    60  	for _, eventItem := range alert.Edges.Events {
    61  		timestamp := eventItem.Time.String()
    62  
    63  		var Metas models.Meta
    64  
    65  		if err := json.Unmarshal([]byte(eventItem.Serialized), &Metas); err != nil {
    66  			log.Errorf("unable to unmarshall events meta '%s' : %s", eventItem.Serialized, err)
    67  		}
    68  
    69  		outputAlert.Events = append(outputAlert.Events, &models.Event{
    70  			Timestamp: &timestamp,
    71  			Meta:      Metas,
    72  		})
    73  	}
    74  
    75  	for _, metaItem := range alert.Edges.Metas {
    76  		outputAlert.Meta = append(outputAlert.Meta, &models.MetaItems0{
    77  			Key:   metaItem.Key,
    78  			Value: metaItem.Value,
    79  		})
    80  	}
    81  
    82  	for _, decisionItem := range alert.Edges.Decisions {
    83  		duration := decisionItem.Until.Sub(time.Now().UTC()).String()
    84  		outputAlert.Decisions = append(outputAlert.Decisions, &models.Decision{
    85  			Duration:  &duration, // transform into time.Time ?
    86  			Scenario:  &decisionItem.Scenario,
    87  			Type:      &decisionItem.Type,
    88  			Scope:     &decisionItem.Scope,
    89  			Value:     &decisionItem.Value,
    90  			Origin:    &decisionItem.Origin,
    91  			Simulated: outputAlert.Simulated,
    92  			ID:        int64(decisionItem.ID),
    93  		})
    94  	}
    95  
    96  	return &outputAlert
    97  }
    98  
    99  // FormatAlerts : Format results from the database to be swagger model compliant
   100  func FormatAlerts(result []*ent.Alert) models.AddAlertsRequest {
   101  	var data models.AddAlertsRequest
   102  	for _, alertItem := range result {
   103  		data = append(data, FormatOneAlert(alertItem))
   104  	}
   105  
   106  	return data
   107  }
   108  
   109  func (c *Controller) sendAlertToPluginChannel(alert *models.Alert, profileID uint) {
   110  	if c.PluginChannel != nil {
   111  	RETRY:
   112  		for try := 0; try < 3; try++ {
   113  			select {
   114  			case c.PluginChannel <- csplugin.ProfileAlert{ProfileID: profileID, Alert: alert}:
   115  				log.Debugf("alert sent to Plugin channel")
   116  
   117  				break RETRY
   118  			default:
   119  				log.Warningf("Cannot send alert to Plugin channel (try: %d)", try)
   120  				time.Sleep(time.Millisecond * 50)
   121  			}
   122  		}
   123  	}
   124  }
   125  
   126  func normalizeScope(scope string) string {
   127  	switch strings.ToLower(scope) {
   128  	case "ip":
   129  		return types.Ip
   130  	case "range":
   131  		return types.Range
   132  	case "as":
   133  		return types.AS
   134  	case "country":
   135  		return types.Country
   136  	default:
   137  		return scope
   138  	}
   139  }
   140  
   141  // CreateAlert writes the alerts received in the body to the database
   142  func (c *Controller) CreateAlert(gctx *gin.Context) {
   143  	var input models.AddAlertsRequest
   144  
   145  	machineID, _ := getMachineIDFromContext(gctx)
   146  
   147  	if err := gctx.ShouldBindJSON(&input); err != nil {
   148  		gctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
   149  		return
   150  	}
   151  
   152  	if err := input.Validate(strfmt.Default); err != nil {
   153  		c.HandleDBErrors(gctx, err)
   154  		return
   155  	}
   156  
   157  	stopFlush := false
   158  
   159  	for _, alert := range input {
   160  		// normalize scope for alert.Source and decisions
   161  		if alert.Source.Scope != nil {
   162  			*alert.Source.Scope = normalizeScope(*alert.Source.Scope)
   163  		}
   164  
   165  		for _, decision := range alert.Decisions {
   166  			if decision.Scope != nil {
   167  				*decision.Scope = normalizeScope(*decision.Scope)
   168  			}
   169  		}
   170  
   171  		alert.MachineID = machineID
   172  		// generate uuid here for alert
   173  		alert.UUID = uuid.NewString()
   174  
   175  		// if coming from cscli, alert already has decisions
   176  		if len(alert.Decisions) != 0 {
   177  			// alert already has a decision (cscli decisions add etc.), generate uuid here
   178  			for _, decision := range alert.Decisions {
   179  				decision.UUID = uuid.NewString()
   180  			}
   181  
   182  			for pIdx, profile := range c.Profiles {
   183  				_, matched, err := profile.EvaluateProfile(alert)
   184  				if err != nil {
   185  					profile.Logger.Warningf("error while evaluating profile %s : %v", profile.Cfg.Name, err)
   186  
   187  					continue
   188  				}
   189  
   190  				if !matched {
   191  					continue
   192  				}
   193  
   194  				c.sendAlertToPluginChannel(alert, uint(pIdx))
   195  
   196  				if profile.Cfg.OnSuccess == "break" {
   197  					break
   198  				}
   199  			}
   200  
   201  			decision := alert.Decisions[0]
   202  			if decision.Origin != nil && *decision.Origin == types.CscliImportOrigin {
   203  				stopFlush = true
   204  			}
   205  
   206  			continue
   207  		}
   208  
   209  		for pIdx, profile := range c.Profiles {
   210  			profileDecisions, matched, err := profile.EvaluateProfile(alert)
   211  			forceBreak := false
   212  
   213  			if err != nil {
   214  				switch profile.Cfg.OnError {
   215  				case "apply":
   216  					profile.Logger.Warningf("applying profile %s despite error: %s", profile.Cfg.Name, err)
   217  
   218  					matched = true
   219  				case "continue":
   220  					profile.Logger.Warningf("skipping %s profile due to error: %s", profile.Cfg.Name, err)
   221  				case "break":
   222  					forceBreak = true
   223  				case "ignore":
   224  					profile.Logger.Warningf("ignoring error: %s", err)
   225  				default:
   226  					gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
   227  					return
   228  				}
   229  			}
   230  
   231  			if !matched {
   232  				continue
   233  			}
   234  
   235  			for _, decision := range profileDecisions {
   236  				decision.UUID = uuid.NewString()
   237  			}
   238  
   239  			// generate uuid here for alert
   240  			if len(alert.Decisions) == 0 { // non manual decision
   241  				alert.Decisions = append(alert.Decisions, profileDecisions...)
   242  			}
   243  
   244  			profileAlert := *alert
   245  			c.sendAlertToPluginChannel(&profileAlert, uint(pIdx))
   246  
   247  			if profile.Cfg.OnSuccess == "break" || forceBreak {
   248  				break
   249  			}
   250  		}
   251  	}
   252  
   253  	if stopFlush {
   254  		c.DBClient.CanFlush = false
   255  	}
   256  
   257  	alerts, err := c.DBClient.CreateAlert(machineID, input)
   258  	c.DBClient.CanFlush = true
   259  
   260  	if err != nil {
   261  		c.HandleDBErrors(gctx, err)
   262  		return
   263  	}
   264  
   265  	if c.AlertsAddChan != nil {
   266  		select {
   267  		case c.AlertsAddChan <- input:
   268  			log.Debug("alert sent to CAPI channel")
   269  		default:
   270  			log.Warning("Cannot send alert to Central API channel")
   271  		}
   272  	}
   273  
   274  	gctx.JSON(http.StatusCreated, alerts)
   275  }
   276  
   277  // FindAlerts: returns alerts from the database based on the specified filter
   278  func (c *Controller) FindAlerts(gctx *gin.Context) {
   279  	result, err := c.DBClient.QueryAlertWithFilter(gctx.Request.URL.Query())
   280  	if err != nil {
   281  		c.HandleDBErrors(gctx, err)
   282  		return
   283  	}
   284  
   285  	data := FormatAlerts(result)
   286  
   287  	if gctx.Request.Method == http.MethodHead {
   288  		gctx.String(http.StatusOK, "")
   289  		return
   290  	}
   291  
   292  	gctx.JSON(http.StatusOK, data)
   293  }
   294  
   295  // FindAlertByID returns the alert associated with the ID
   296  func (c *Controller) FindAlertByID(gctx *gin.Context) {
   297  	alertIDStr := gctx.Param("alert_id")
   298  	alertID, err := strconv.Atoi(alertIDStr)
   299  
   300  	if err != nil {
   301  		gctx.JSON(http.StatusBadRequest, gin.H{"message": "alert_id must be valid integer"})
   302  		return
   303  	}
   304  
   305  	result, err := c.DBClient.GetAlertByID(alertID)
   306  	if err != nil {
   307  		c.HandleDBErrors(gctx, err)
   308  		return
   309  	}
   310  
   311  	data := FormatOneAlert(result)
   312  
   313  	if gctx.Request.Method == http.MethodHead {
   314  		gctx.String(http.StatusOK, "")
   315  		return
   316  	}
   317  
   318  	gctx.JSON(http.StatusOK, data)
   319  }
   320  
   321  // DeleteAlertByID delete the alert associated to the ID
   322  func (c *Controller) DeleteAlertByID(gctx *gin.Context) {
   323  	var err error
   324  
   325  	incomingIP := gctx.ClientIP()
   326  	if incomingIP != "127.0.0.1" && incomingIP != "::1" && !networksContainIP(c.TrustedIPs, incomingIP) && !isUnixSocket(gctx) {
   327  		gctx.JSON(http.StatusForbidden, gin.H{"message": fmt.Sprintf("access forbidden from this IP (%s)", incomingIP)})
   328  		return
   329  	}
   330  
   331  	decisionIDStr := gctx.Param("alert_id")
   332  
   333  	decisionID, err := strconv.Atoi(decisionIDStr)
   334  	if err != nil {
   335  		gctx.JSON(http.StatusBadRequest, gin.H{"message": "alert_id must be valid integer"})
   336  		return
   337  	}
   338  
   339  	err = c.DBClient.DeleteAlertByID(decisionID)
   340  	if err != nil {
   341  		c.HandleDBErrors(gctx, err)
   342  		return
   343  	}
   344  
   345  	deleteAlertResp := models.DeleteAlertsResponse{NbDeleted: "1"}
   346  
   347  	gctx.JSON(http.StatusOK, deleteAlertResp)
   348  }
   349  
   350  // DeleteAlerts deletes alerts from the database based on the specified filter
   351  func (c *Controller) DeleteAlerts(gctx *gin.Context) {
   352  	incomingIP := gctx.ClientIP()
   353  	if incomingIP != "127.0.0.1" && incomingIP != "::1" && !networksContainIP(c.TrustedIPs, incomingIP) && !isUnixSocket(gctx) {
   354  		gctx.JSON(http.StatusForbidden, gin.H{"message": fmt.Sprintf("access forbidden from this IP (%s)", incomingIP)})
   355  		return
   356  	}
   357  
   358  	nbDeleted, err := c.DBClient.DeleteAlertWithFilter(gctx.Request.URL.Query())
   359  	if err != nil {
   360  		c.HandleDBErrors(gctx, err)
   361  		return
   362  	}
   363  
   364  	deleteAlertsResp := models.DeleteAlertsResponse{
   365  		NbDeleted: strconv.Itoa(nbDeleted),
   366  	}
   367  
   368  	gctx.JSON(http.StatusOK, deleteAlertsResp)
   369  }
   370  
   371  func networksContainIP(networks []net.IPNet, ip string) bool {
   372  	parsedIP := net.ParseIP(ip)
   373  	for _, network := range networks {
   374  		if network.Contains(parsedIP) {
   375  			return true
   376  		}
   377  	}
   378  
   379  	return false
   380  }