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: ×tamp, 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 }