Browse Source

support decisions deletion via scenario + alerts delete via ID (#1798)

Thibault "bui" Koechlin 2 năm trước cách đây
mục cha
commit
ae6bf39495

+ 12 - 7
cmd/crowdsec-cli/decisions.go

@@ -372,11 +372,12 @@ cscli decisions add --scope username --value foobar
 	cmdDecisions.AddCommand(cmdDecisionsAdd)
 
 	var delFilter = apiclient.DecisionsDeleteOpts{
-		ScopeEquals: new(string),
-		ValueEquals: new(string),
-		TypeEquals:  new(string),
-		IPEquals:    new(string),
-		RangeEquals: new(string),
+		ScopeEquals:    new(string),
+		ValueEquals:    new(string),
+		TypeEquals:     new(string),
+		IPEquals:       new(string),
+		RangeEquals:    new(string),
+		ScenarioEquals: new(string),
 	}
 	var delDecisionId string
 	var delDecisionAll bool
@@ -397,7 +398,7 @@ cscli decisions delete --type captcha
 			}
 			if *delFilter.ScopeEquals == "" && *delFilter.ValueEquals == "" &&
 				*delFilter.TypeEquals == "" && *delFilter.IPEquals == "" &&
-				*delFilter.RangeEquals == "" && delDecisionId == "" {
+				*delFilter.RangeEquals == "" && *delFilter.ScenarioEquals == "" && delDecisionId == "" {
 				cmd.Usage()
 				log.Fatalln("At least one filter or --all must be specified")
 			}
@@ -416,6 +417,9 @@ cscli decisions delete --type captcha
 			if *delFilter.ValueEquals == "" {
 				delFilter.ValueEquals = nil
 			}
+			if *delFilter.ScenarioEquals == "" {
+				delFilter.ScenarioEquals = nil
+			}
 
 			if *delFilter.TypeEquals == "" {
 				delFilter.TypeEquals = nil
@@ -453,9 +457,10 @@ cscli decisions delete --type captcha
 	cmdDecisionsDelete.Flags().SortFlags = false
 	cmdDecisionsDelete.Flags().StringVarP(delFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
 	cmdDecisionsDelete.Flags().StringVarP(delFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
-	cmdDecisionsDelete.Flags().StringVar(&delDecisionId, "id", "", "decision id")
 	cmdDecisionsDelete.Flags().StringVarP(delFilter.TypeEquals, "type", "t", "", "the decision type (ie. ban,captcha)")
 	cmdDecisionsDelete.Flags().StringVarP(delFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
+	cmdDecisionsDelete.Flags().StringVarP(delFilter.ScenarioEquals, "scenario", "s", "", "the scenario name (ie. crowdsecurity/ssh-bf)")
+	cmdDecisionsDelete.Flags().StringVar(&delDecisionId, "id", "", "decision id")
 	cmdDecisionsDelete.Flags().BoolVar(&delDecisionAll, "all", false, "delete all decisions")
 	cmdDecisionsDelete.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
 

+ 18 - 2
pkg/apiclient/alerts_service.go

@@ -65,7 +65,7 @@ func (s *AlertsService) Add(ctx context.Context, alerts models.AddAlertsRequest)
 	return &added_ids, resp, nil
 }
 
-//to demo query arguments
+// to demo query arguments
 func (s *AlertsService) List(ctx context.Context, opts AlertsListOpts) (*models.GetAlertsResponse, *Response, error) {
 	var alerts models.GetAlertsResponse
 	var URI string
@@ -92,7 +92,7 @@ func (s *AlertsService) List(ctx context.Context, opts AlertsListOpts) (*models.
 	return &alerts, resp, nil
 }
 
-//to demo query arguments
+// to demo query arguments
 func (s *AlertsService) Delete(ctx context.Context, opts AlertsDeleteOpts) (*models.DeleteAlertsResponse, *Response, error) {
 	var alerts models.DeleteAlertsResponse
 	params, err := qs.Values(opts)
@@ -113,6 +113,22 @@ func (s *AlertsService) Delete(ctx context.Context, opts AlertsDeleteOpts) (*mod
 	return &alerts, resp, nil
 }
 
+func (s *AlertsService) DeleteOne(ctx context.Context, alert_id string) (*models.DeleteAlertsResponse, *Response, error) {
+	var alerts models.DeleteAlertsResponse
+	u := fmt.Sprintf("%s/alerts/%s", s.client.URLPrefix, alert_id)
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	resp, err := s.client.Do(ctx, req, &alerts)
+	if err != nil {
+		return nil, resp, err
+	}
+	return &alerts, resp, nil
+}
+
 func (s *AlertsService) GetByID(ctx context.Context, alertID int) (*models.Alert, *Response, error) {
 	var alert models.Alert
 	u := fmt.Sprintf("%s/alerts/%d", s.client.URLPrefix, alertID)

+ 3 - 1
pkg/apiclient/decisions_service.go

@@ -44,10 +44,12 @@ type DecisionsDeleteOpts struct {
 	IPEquals    *string `url:"ip,omitempty"`
 	RangeEquals *string `url:"range,omitempty"`
 	Contains    *bool   `url:"contains,omitempty"`
+	//
+	ScenarioEquals *string `url:"scenario,omitempty"`
 	ListOpts
 }
 
-//to demo query arguments
+// to demo query arguments
 func (s *DecisionsService) List(ctx context.Context, opts DecisionsListOpts) (*models.GetDecisionsResponse, *Response, error) {
 	var decisions models.GetDecisionsResponse
 	params, err := qs.Values(opts)

+ 23 - 0
pkg/apiserver/alerts_test.go

@@ -419,6 +419,29 @@ func TestDeleteAlert(t *testing.T) {
 	assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
 }
 
+func TestDeleteAlertByID(t *testing.T) {
+	lapi := SetupLAPITest(t)
+	lapi.InsertAlertFromFile("./tests/alert_sample.json")
+
+	// Fail Delete Alert
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest(http.MethodDelete, "/v1/alerts/1", strings.NewReader(""))
+	AddAuthHeaders(req, lapi.loginResp)
+	req.RemoteAddr = "127.0.0.2:4242"
+	lapi.router.ServeHTTP(w, req)
+	assert.Equal(t, 403, w.Code)
+	assert.Equal(t, `{"message":"access forbidden from this IP (127.0.0.2)"}`, w.Body.String())
+
+	// Delete Alert
+	w = httptest.NewRecorder()
+	req, _ = http.NewRequest(http.MethodDelete, "/v1/alerts/1", strings.NewReader(""))
+	AddAuthHeaders(req, lapi.loginResp)
+	req.RemoteAddr = "127.0.0.1:4242"
+	lapi.router.ServeHTTP(w, req)
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
+}
+
 func TestDeleteAlertTrustedIPS(t *testing.T) {
 	cfg := LoadTestConfig()
 	// IPv6 mocking doesn't seem to work.

+ 1 - 0
pkg/apiserver/controllers/controller.go

@@ -95,6 +95,7 @@ func (c *Controller) NewV1() error {
 		jwtAuth.HEAD("/alerts", c.HandlerV1.FindAlerts)
 		jwtAuth.GET("/alerts/:alert_id", c.HandlerV1.FindAlertByID)
 		jwtAuth.HEAD("/alerts/:alert_id", c.HandlerV1.FindAlertByID)
+		jwtAuth.DELETE("/alerts/:alert_id", c.HandlerV1.DeleteAlertByID)
 		jwtAuth.DELETE("/alerts", c.HandlerV1.DeleteAlerts)
 		jwtAuth.DELETE("/decisions", c.HandlerV1.DeleteDecisions)
 		jwtAuth.DELETE("/decisions/:decision_id", c.HandlerV1.DeleteDecisionById)

+ 30 - 0
pkg/apiserver/controllers/v1/alerts.go

@@ -239,6 +239,36 @@ func (c *Controller) FindAlertByID(gctx *gin.Context) {
 	gctx.JSON(http.StatusOK, data)
 }
 
+// DeleteAlertByID delete the alert associated to the ID
+func (c *Controller) DeleteAlertByID(gctx *gin.Context) {
+	var err error
+
+	incomingIP := gctx.ClientIP()
+	if incomingIP != "127.0.0.1" && incomingIP != "::1" && !networksContainIP(c.TrustedIPs, incomingIP) {
+		gctx.JSON(http.StatusForbidden, gin.H{"message": fmt.Sprintf("access forbidden from this IP (%s)", incomingIP)})
+		return
+	}
+
+	decisionIDStr := gctx.Param("alert_id")
+	decisionID, err := strconv.Atoi(decisionIDStr)
+	if err != nil {
+		gctx.JSON(http.StatusBadRequest, gin.H{"message": "alert_id must be valid integer"})
+		return
+	}
+	err = c.DBClient.DeleteAlertByID(decisionID)
+	if err != nil {
+		c.HandleDBErrors(gctx, err)
+		return
+	}
+
+	deleteAlertResp := models.DeleteAlertsResponse{
+		NbDeleted: "1",
+	}
+
+	gctx.JSON(http.StatusOK, deleteAlertResp)
+}
+
+
 // DeleteAlerts deletes alerts from the database based on the specified filter
 func (c *Controller) DeleteAlerts(gctx *gin.Context) {
 	incomingIP := gctx.ClientIP()

+ 19 - 0
pkg/apiserver/decisions_test.go

@@ -61,6 +61,25 @@ func TestDeleteDecisionFilter(t *testing.T) {
 	assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
 }
 
+func TestDeleteDecisionFilterByScenario(t *testing.T) {
+	lapi := SetupLAPITest(t)
+
+	// Create Valid Alert
+	lapi.InsertAlertFromFile("./tests/alert_minibulk.json")
+
+	// delete by wrong scenario
+
+	w := lapi.RecordResponse("DELETE", "/v1/decisions?scenario=crowdsecurity/ssh-bff", emptyBody, PASSWORD)
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String())
+
+	// delete by scenario good
+
+	w = lapi.RecordResponse("DELETE", "/v1/decisions?scenario=crowdsecurity/ssh-bf", emptyBody, PASSWORD)
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, `{"nbDeleted":"2"}`, w.Body.String())
+}
+
 func TestGetDecisionFilters(t *testing.T) {
 	lapi := SetupLAPITest(t)
 

+ 9 - 0
pkg/database/alerts.go

@@ -909,6 +909,15 @@ func (c *Client) DeleteAlertGraph(alertItem *ent.Alert) error {
 	return nil
 }
 
+func (c *Client) DeleteAlertByID(id int) error {
+	alertItem, err := c.Ent.Alert.Query().Where(alert.IDEQ(id)).Only(c.CTX)
+	if err != nil {
+		return err
+	}
+
+	return c.DeleteAlertGraph(alertItem)
+}
+
 func (c *Client) DeleteAlertWithFilter(filter map[string][]string) (int, error) {
 	preds, err := AlertPredicatesFromFilter(filter)
 	if err != nil {

+ 5 - 1
pkg/database/decisions.go

@@ -305,6 +305,8 @@ func (c *Client) DeleteDecisionsWithFilter(filter map[string][]string) (string,
 			if err != nil {
 				return "0", errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
 			}
+		case "scenario":
+			decisions = decisions.Where(decision.ScenarioEQ(value[0]))
 		default:
 			return "0", errors.Wrap(InvalidFilter, fmt.Sprintf("'%s' doesn't exist", param))
 		}
@@ -415,6 +417,8 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
 			if err != nil {
 				return "0", errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
 			}
+		case "scenario":
+			decisions = decisions.Where(decision.ScenarioEQ(value[0]))
 		default:
 			return "0", errors.Wrapf(InvalidFilter, "'%s' doesn't exist", param)
 		}
@@ -498,7 +502,7 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
 	return strconv.Itoa(nbDeleted), nil
 }
 
-//SoftDeleteDecisionByID set the expiration of a decision to now()
+// SoftDeleteDecisionByID set the expiration of a decision to now()
 func (c *Client) SoftDeleteDecisionByID(decisionID int) (int, error) {
 	nbUpdated, err := c.Ent.Decision.Update().Where(decision.IDEQ(decisionID)).SetUntil(time.Now().UTC()).Save(c.CTX)
 	if err != nil || nbUpdated == 0 {

+ 33 - 1
pkg/models/localapi_swagger.yaml

@@ -242,6 +242,11 @@ paths:
           required: false
           type: string
           description: range to search for (shorthand for scope=range&value=)
+        - name: scenario
+          in: query
+          required: false
+          type: string
+          description: scenario to search
       responses:
         '200':
           description: successful operation
@@ -256,7 +261,7 @@ paths:
       - JWTAuthorizer: []
   '/decisions/{decision_id}':
     delete:
-      description: Delete decision for given ban ID (only from cscli)
+      description: Delete decision for given decision ID (only from cscli)
       summary: DeleteDecision
       tags:
         - watchers
@@ -652,6 +657,33 @@ paths:
           description: "400 response"
       security:
       - JWTAuthorizer: []
+    delete:
+      description: Delete alert for given alert ID (only from cscli)
+      summary: DeleteAlert
+      tags:
+        - watchers
+      operationId: DeleteAlert
+      deprecated: false
+      produces:
+        - application/json
+      parameters:
+        - name: alert_id
+          in: path
+          required: true
+          type: string
+          description: ''
+      responses:
+        '200':
+          description: successful operation
+          schema:
+            $ref: '#/definitions/DeleteAlertsResponse'
+          headers: {}
+        '404':
+          description: "404 response"
+          schema:
+            $ref: "#/definitions/ErrorResponse"
+      security:
+      - JWTAuthorizer: []
 definitions:
   WatcherRegistrationRequest:
     title: WatcherRegistrationRequest