diff --git a/cmd/crowdsec-cli/decisions.go b/cmd/crowdsec-cli/decisions.go index db2cd1bc6..9c8dc0ec1 100644 --- a/cmd/crowdsec-cli/decisions.go +++ b/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 )") cmdDecisionsDelete.Flags().StringVarP(delFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value )") - 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") diff --git a/pkg/apiclient/alerts_service.go b/pkg/apiclient/alerts_service.go index e1088324d..b8d1b7fc1 100644 --- a/pkg/apiclient/alerts_service.go +++ b/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) diff --git a/pkg/apiclient/decisions_service.go b/pkg/apiclient/decisions_service.go index ba90a4354..e2b32e888 100644 --- a/pkg/apiclient/decisions_service.go +++ b/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) diff --git a/pkg/apiserver/alerts_test.go b/pkg/apiserver/alerts_test.go index 0281dd504..9b6fc04ad 100644 --- a/pkg/apiserver/alerts_test.go +++ b/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. diff --git a/pkg/apiserver/controllers/controller.go b/pkg/apiserver/controllers/controller.go index 6375b0121..ee8a86197 100644 --- a/pkg/apiserver/controllers/controller.go +++ b/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) diff --git a/pkg/apiserver/controllers/v1/alerts.go b/pkg/apiserver/controllers/v1/alerts.go index cf920c35a..1b227ff9c 100644 --- a/pkg/apiserver/controllers/v1/alerts.go +++ b/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() diff --git a/pkg/apiserver/decisions_test.go b/pkg/apiserver/decisions_test.go index 4d56cfc3a..5f92b1f08 100644 --- a/pkg/apiserver/decisions_test.go +++ b/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) diff --git a/pkg/database/alerts.go b/pkg/database/alerts.go index 8cdae4e06..8f2f6731d 100644 --- a/pkg/database/alerts.go +++ b/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 { diff --git a/pkg/database/decisions.go b/pkg/database/decisions.go index 565457e4b..60569966f 100644 --- a/pkg/database/decisions.go +++ b/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 { diff --git a/pkg/models/localapi_swagger.yaml b/pkg/models/localapi_swagger.yaml index 1849454dc..9d3bacbae 100644 --- a/pkg/models/localapi_swagger.yaml +++ b/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