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

This commit is contained in:
Thibault "bui" Koechlin 2022-10-19 14:37:27 +02:00 committed by GitHub
parent 24b540ecde
commit ae6bf39495
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 153 additions and 12 deletions

View file

@ -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")

View file

@ -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)

View file

@ -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)

View file

@ -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.

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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