diff --git a/cmd/crowdsec-cli/clidecision/import.go b/cmd/crowdsec-cli/clidecision/import.go index 317fa5d62..ee6ac7c1a 100644 --- a/cmd/crowdsec-cli/clidecision/import.go +++ b/cmd/crowdsec-cli/clidecision/import.go @@ -8,7 +8,9 @@ import ( "errors" "fmt" "io" + "iter" "os" + "slices" "strings" "time" @@ -17,7 +19,6 @@ import ( "github.com/spf13/cobra" "github.com/crowdsecurity/go-cs-lib/ptr" - "github.com/crowdsecurity/go-cs-lib/slicetools" "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args" "github.com/crowdsecurity/crowdsec/pkg/models" @@ -38,7 +39,7 @@ func parseDecisionList(content []byte, format string) ([]decisionRaw, error) { switch format { case "values": - log.Infof("Parsing values") + fmt.Fprintln(os.Stdout, "Parsing values") scanner := bufio.NewScanner(bytes.NewReader(content)) for scanner.Scan() { @@ -50,13 +51,13 @@ func parseDecisionList(content []byte, format string) ([]decisionRaw, error) { return nil, fmt.Errorf("unable to parse values: '%w'", err) } case "json": - log.Infof("Parsing json") + fmt.Fprintln(os.Stdout, "Parsing json") if err := json.Unmarshal(content, &ret); err != nil { return nil, err } case "csv": - log.Infof("Parsing csv") + fmt.Fprintln(os.Stdout, "Parsing csv") if err := csvutil.Unmarshal(content, &ret); err != nil { return nil, fmt.Errorf("unable to parse csv: '%w'", err) @@ -68,6 +69,20 @@ func parseDecisionList(content []byte, format string) ([]decisionRaw, error) { return ret, nil } +func excludeAllowlistedDecisions(decisions []*models.Decision, allowlistedValues []string) iter.Seq[*models.Decision] { + return func(yield func(*models.Decision) bool) { + for _, d := range decisions { + if slices.Contains(allowlistedValues, *d.Value) { + continue + } + + if !yield(d) { + return + } + } + } +} + func (cli *cliDecisions) import_(ctx context.Context, input string, duration string, scope string, reason string, type_ string, batch int, format string) error { var ( content []byte @@ -124,6 +139,10 @@ func (cli *cliDecisions) import_(ctx context.Context, input string, duration str return err } + if len(decisionsListRaw) == 0 { + return errors.New("no decisions found") + } + decisions := make([]*models.Decision, len(decisionsListRaw)) for i, d := range decisionsListRaw { @@ -163,11 +182,50 @@ func (cli *cliDecisions) import_(ctx context.Context, input string, duration str } if len(decisions) > 1000 { - log.Infof("You are about to add %d decisions, this may take a while", len(decisions)) + fmt.Fprintf(os.Stdout, "You are about to add %d decisions, this may take a while\n", len(decisions)) } - for _, chunk := range slicetools.Chunks(decisions, batch) { + if batch == 0 { + batch = len(decisions) + } else { + fmt.Fprintf(os.Stdout, "batch size: %d\n", batch) + } + + allowlistedValues := make([]string, 0) + + for chunk := range slices.Chunk(decisions, batch) { log.Debugf("Processing chunk of %d decisions", len(chunk)) + + decisionsStr := make([]string, 0, len(chunk)) + + for _, d := range chunk { + if *d.Scope != types.Ip && *d.Scope != types.Range { + continue + } + + decisionsStr = append(decisionsStr, *d.Value) + } + + // Skip if no IPs or ranges + if len(decisionsStr) == 0 { + continue + } + + allowlistResp, _, err := cli.client.Allowlists.CheckIfAllowlistedBulk(ctx, decisionsStr) + + if err != nil { + return err + } + + for _, r := range allowlistResp.Results { + fmt.Fprintf(os.Stdout, "Value %s is allowlisted by %s\n", *r.Target, r.Allowlists) + allowlistedValues = append(allowlistedValues, *r.Target) + } + } + + actualDecisions := slices.Collect(excludeAllowlistedDecisions(decisions, allowlistedValues)) + + for chunk := range slices.Chunk(actualDecisions, batch) { importAlert := models.Alert{ CreatedAt: time.Now().UTC().Format(time.RFC3339), Scenario: ptr.Of(fmt.Sprintf("import %s: %d IPs", input, len(chunk))), @@ -195,7 +253,7 @@ func (cli *cliDecisions) import_(ctx context.Context, input string, duration str } } - log.Infof("Imported %d decisions", len(decisions)) + fmt.Fprintf(os.Stdout, "Imported %d decisions", len(actualDecisions)) return nil } diff --git a/pkg/apiclient/allowlists_service.go b/pkg/apiclient/allowlists_service.go index 049892157..427a198b0 100644 --- a/pkg/apiclient/allowlists_service.go +++ b/pkg/apiclient/allowlists_service.go @@ -83,7 +83,7 @@ func (s *AllowlistsService) CheckIfAllowlisted(ctx context.Context, value string return false, nil, err } - var discardBody interface{} + var discardBody any resp, err := s.client.Do(ctx, req, discardBody) if err != nil { @@ -111,3 +111,25 @@ func (s *AllowlistsService) CheckIfAllowlistedWithReason(ctx context.Context, va return body, resp, nil } + +func (s *AllowlistsService) CheckIfAllowlistedBulk(ctx context.Context, values []string) (*models.BulkCheckAllowlistResponse, *Response, error) { + u := s.client.URLPrefix + "/allowlists/check" + + body := &models.BulkCheckAllowlistRequest{ + Targets: values, + } + + req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, body) + if err != nil { + return nil, nil, err + } + + responseBody := &models.BulkCheckAllowlistResponse{} + + resp, err := s.client.Do(ctx, req, responseBody) + if err != nil { + return nil, resp, err + } + + return responseBody, resp, nil +} diff --git a/pkg/apiserver/allowlists_test.go b/pkg/apiserver/allowlists_test.go index 6e319da96..d2fa3d9bd 100644 --- a/pkg/apiserver/allowlists_test.go +++ b/pkg/apiserver/allowlists_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "net/http" + "strings" "testing" "time" @@ -147,3 +148,57 @@ func TestCheckInAllowlist(t *testing.T) { require.Equal(t, http.StatusNoContent, w.Code) } + +func TestBulkCheckAllowlist(t *testing.T) { + ctx := context.Background() + lapi := SetupLAPITest(t, ctx) + + // create an allowlist and add one live entry + l, err := lapi.DBClient.CreateAllowList(ctx, "test", "test", "", false) + require.NoError(t, err) + + added, err := lapi.DBClient.AddToAllowlist(ctx, l, []*models.AllowlistItem{ + {Value: "1.2.3.4"}, + }) + require.NoError(t, err) + assert.Equal(t, 1, added) + + // craft a bulk check payload with one matching and one non-matching target + reqBody := `{"targets":["1.2.3.4","2.3.4.5"]}` + w := lapi.RecordResponse(t, ctx, http.MethodPost, "/v1/allowlists/check", strings.NewReader(reqBody), passwordAuthType) + require.Equal(t, http.StatusOK, w.Code) + + // unmarshal and verify + resp := models.BulkCheckAllowlistResponse{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) + require.Len(t, resp.Results, 1) + + // expect only "1.2.3.4" in the "test" allowlist, while "2.3.4.5" should not be in the response + var match bool + + for _, r := range resp.Results { + switch *r.Target { + case "1.2.3.4": + match = true + + assert.Equal(t, []string{"1.2.3.4 from test"}, r.Allowlists) + default: + t.Errorf("unexpected target %v", r.Target) + } + } + + require.True(t, match, "did not see result for 1.2.3.4") +} + +func TestBulkCheckAllowlist_BadRequest(t *testing.T) { + ctx := context.Background() + lapi := SetupLAPITest(t, ctx) + + // missing or empty body should yield 400 + w := lapi.RecordResponse(t, ctx, http.MethodPost, "/v1/allowlists/check", emptyBody, passwordAuthType) + require.Equal(t, http.StatusBadRequest, w.Code) + + // malformed JSON should also yield 400 + w = lapi.RecordResponse(t, ctx, http.MethodPost, "/v1/allowlists/check", strings.NewReader("{invalid-json"), passwordAuthType) + require.Equal(t, http.StatusBadRequest, w.Code) +} diff --git a/pkg/apiserver/controllers/controller.go b/pkg/apiserver/controllers/controller.go index 84aa2c06a..c7503c9ef 100644 --- a/pkg/apiserver/controllers/controller.go +++ b/pkg/apiserver/controllers/controller.go @@ -129,6 +129,7 @@ func (c *Controller) NewV1() error { jwtAuth.GET("/allowlists/:allowlist_name", c.HandlerV1.GetAllowlist) jwtAuth.GET("/allowlists/check/:ip_or_range", c.HandlerV1.CheckInAllowlist) jwtAuth.HEAD("/allowlists/check/:ip_or_range", c.HandlerV1.CheckInAllowlist) + jwtAuth.POST("/allowlists/check", c.HandlerV1.CheckInAllowlistBulk) } apiKeyAuth := groupV1.Group("") diff --git a/pkg/apiserver/controllers/v1/allowlist.go b/pkg/apiserver/controllers/v1/allowlist.go index e77344eb2..e35354ff3 100644 --- a/pkg/apiserver/controllers/v1/allowlist.go +++ b/pkg/apiserver/controllers/v1/allowlist.go @@ -10,6 +10,43 @@ import ( "github.com/crowdsecurity/crowdsec/pkg/models" ) +func (c *Controller) CheckInAllowlistBulk(gctx *gin.Context) { + var req models.BulkCheckAllowlistRequest + + if err := gctx.ShouldBindJSON(&req); err != nil { + gctx.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) + return + } + + if len(req.Targets) == 0 { + gctx.JSON(http.StatusBadRequest, gin.H{"message": "targets list cannot be empty"}) + return + } + + resp := models.BulkCheckAllowlistResponse{ + Results: make([]*models.BulkCheckAllowlistResult, 0), + } + + for _, target := range req.Targets { + lists, err := c.DBClient.IsAllowlistedBy(gctx.Request.Context(), target) + if err != nil { + c.HandleDBErrors(gctx, err) + return + } + + if len(lists) == 0 { + continue + } + + resp.Results = append(resp.Results, &models.BulkCheckAllowlistResult{ + Target: &target, + Allowlists: lists, + }) + } + + gctx.JSON(http.StatusOK, resp) +} + func (c *Controller) CheckInAllowlist(gctx *gin.Context) { value := gctx.Param("ip_or_range") diff --git a/pkg/database/allowlists.go b/pkg/database/allowlists.go index c9b1c76ad..d14958f61 100644 --- a/pkg/database/allowlists.go +++ b/pkg/database/allowlists.go @@ -156,7 +156,7 @@ func (c *Client) AddToAllowlist(ctx context.Context, list *ent.AllowList, items SetComment(item.Description) if !time.Time(item.Expiration).IsZero() { - query = query.SetExpiresAt(time.Time(item.Expiration)) + query = query.SetExpiresAt(time.Time(item.Expiration).UTC()) } content, err := query.Save(ctx) @@ -236,7 +236,7 @@ func (c *Client) ReplaceAllowlist(ctx context.Context, list *ent.AllowList, item return added, nil } -func (c *Client) IsAllowlisted(ctx context.Context, value string) (bool, string, error) { +func (c *Client) IsAllowlistedBy(ctx context.Context, value string) ([]string, error) { /* Few cases: - value is an IP/range directly is in allowlist @@ -245,7 +245,7 @@ func (c *Client) IsAllowlisted(ctx context.Context, value string) (bool, string, */ sz, start_ip, start_sfx, end_ip, end_sfx, err := types.Addr2Ints(value) if err != nil { - return false, "", err + return nil, err } c.Log.Debugf("checking if %s is allowlisted", value) @@ -314,22 +314,41 @@ func (c *Client) IsAllowlisted(ctx context.Context, value string) (bool, string, ) } - allowed, err := query.WithAllowlist().First(ctx) + items, err := query.WithAllowlist().All(ctx) if err != nil { - if ent.IsNotFound(err) { - return false, "", nil + return nil, fmt.Errorf("unable to check if value is allowlisted: %w", err) + } + + reasons := make([]string, 0) + + for _, item := range items { + if len(item.Edges.Allowlist) == 0 { + continue } - return false, "", fmt.Errorf("unable to check if value is allowlisted: %w", err) + reason := item.Value + " from " + item.Edges.Allowlist[0].Name + if item.Comment != "" { + reason += " (" + item.Comment + ")" + } + + reasons = append(reasons, reason) } - allowlistName := allowed.Edges.Allowlist[0].Name - reason := allowed.Value + " from " + allowlistName + return reasons, nil +} - if allowed.Comment != "" { - reason += " (" + allowed.Comment + ")" +func (c *Client) IsAllowlisted(ctx context.Context, value string) (bool, string, error) { + reasons, err := c.IsAllowlistedBy(ctx, value) + if err != nil { + return false, "", err } + if len(reasons) == 0 { + return false, "", nil + } + + reason := strings.Join(reasons, ", ") + return true, reason, nil } diff --git a/pkg/database/allowlists_test.go b/pkg/database/allowlists_test.go index 9a4eb8e1f..5cd799acb 100644 --- a/pkg/database/allowlists_test.go +++ b/pkg/database/allowlists_test.go @@ -104,3 +104,63 @@ func TestCheckAllowlist(t *testing.T) { require.True(t, allowlisted) require.Equal(t, "8a95:c186:9f96:4c75:0dad:49c6:ff62:94b8 from test", reason) } + +func TestIsAllowListedBy_SingleAndMultiple(t *testing.T) { + ctx := context.Background() + dbClient := getDBClient(t, ctx) + + list1, err := dbClient.CreateAllowList(ctx, "list1", "first list", "", false) + require.NoError(t, err) + list2, err := dbClient.CreateAllowList(ctx, "list2", "second list", "", false) + require.NoError(t, err) + + // Add overlapping and distinct entries + _, err = dbClient.AddToAllowlist(ctx, list1, []*models.AllowlistItem{ + {Value: "1.1.1.1"}, + {Value: "10.0.0.0/8"}, + }) + require.NoError(t, err) + _, err = dbClient.AddToAllowlist(ctx, list2, []*models.AllowlistItem{ + {Value: "1.1.1.1"}, // overlaps with list1 + {Value: "192.168.0.0/16"}, // only in list2 + {Value: "2.2.2.2", Expiration: strfmt.DateTime(time.Now().Add(-time.Hour))}, // expired + }) + require.NoError(t, err) + + // Exact IP that lives in both + names, err := dbClient.IsAllowlistedBy(ctx, "1.1.1.1") + require.NoError(t, err) + assert.ElementsMatch(t, []string{"1.1.1.1 from list1", "1.1.1.1 from list2"}, names) + + // IP matching only list1's CIDR + names, err = dbClient.IsAllowlistedBy(ctx, "10.5.6.7") + require.NoError(t, err) + assert.Equal(t, []string{"10.0.0.0/8 from list1"}, names) + + // IP matching only list2's CIDR + names, err = dbClient.IsAllowlistedBy(ctx, "192.168.1.42") + require.NoError(t, err) + assert.Equal(t, []string{"192.168.0.0/16 from list2"}, names) + + // Expired entry in list2 should not appear + names, err = dbClient.IsAllowlistedBy(ctx, "2.2.2.2") + require.NoError(t, err) + assert.Empty(t, names) +} + +func TestIsAllowListedBy_NoMatch(t *testing.T) { + ctx := context.Background() + dbClient := getDBClient(t, ctx) + + list, err := dbClient.CreateAllowList(ctx, "solo", "single", "", false) + require.NoError(t, err) + _, err = dbClient.AddToAllowlist(ctx, list, []*models.AllowlistItem{ + {Value: "5.5.5.5"}, + }) + require.NoError(t, err) + + // completely unrelated IP + names, err := dbClient.IsAllowlistedBy(ctx, "8.8.4.4") + require.NoError(t, err) + assert.Empty(t, names) +} diff --git a/pkg/models/bulk_check_allowlist_request.go b/pkg/models/bulk_check_allowlist_request.go new file mode 100644 index 000000000..ab69c53cd --- /dev/null +++ b/pkg/models/bulk_check_allowlist_request.go @@ -0,0 +1,71 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BulkCheckAllowlistRequest bulk check allowlist request +// +// swagger:model BulkCheckAllowlistRequest +type BulkCheckAllowlistRequest struct { + + // Array of IP addresses or CIDR ranges to check + // Required: true + Targets []string `json:"targets"` +} + +// Validate validates this bulk check allowlist request +func (m *BulkCheckAllowlistRequest) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateTargets(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkCheckAllowlistRequest) validateTargets(formats strfmt.Registry) error { + + if err := validate.Required("targets", "body", m.Targets); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this bulk check allowlist request based on context it is used +func (m *BulkCheckAllowlistRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *BulkCheckAllowlistRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkCheckAllowlistRequest) UnmarshalBinary(b []byte) error { + var res BulkCheckAllowlistRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/models/bulk_check_allowlist_response.go b/pkg/models/bulk_check_allowlist_response.go new file mode 100644 index 000000000..0e6a467de --- /dev/null +++ b/pkg/models/bulk_check_allowlist_response.go @@ -0,0 +1,124 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BulkCheckAllowlistResponse bulk check allowlist response +// +// swagger:model BulkCheckAllowlistResponse +type BulkCheckAllowlistResponse struct { + + // Per-target allowlist membership results + // Required: true + Results []*BulkCheckAllowlistResult `json:"results"` +} + +// Validate validates this bulk check allowlist response +func (m *BulkCheckAllowlistResponse) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateResults(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkCheckAllowlistResponse) validateResults(formats strfmt.Registry) error { + + if err := validate.Required("results", "body", m.Results); err != nil { + return err + } + + for i := 0; i < len(m.Results); i++ { + if swag.IsZero(m.Results[i]) { // not required + continue + } + + if m.Results[i] != nil { + if err := m.Results[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("results" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("results" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this bulk check allowlist response based on the context it is used +func (m *BulkCheckAllowlistResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateResults(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkCheckAllowlistResponse) contextValidateResults(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Results); i++ { + + if m.Results[i] != nil { + + if swag.IsZero(m.Results[i]) { // not required + return nil + } + + if err := m.Results[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("results" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("results" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *BulkCheckAllowlistResponse) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkCheckAllowlistResponse) UnmarshalBinary(b []byte) error { + var res BulkCheckAllowlistResponse + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/models/bulk_check_allowlist_result.go b/pkg/models/bulk_check_allowlist_result.go new file mode 100644 index 000000000..31be0078f --- /dev/null +++ b/pkg/models/bulk_check_allowlist_result.go @@ -0,0 +1,88 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BulkCheckAllowlistResult bulk check allowlist result +// +// swagger:model BulkCheckAllowlistResult +type BulkCheckAllowlistResult struct { + + // Matching ip or range, name of the allowlist and comment related to the target + // Required: true + Allowlists []string `json:"allowlists"` + + // The IP or range that is allowlisted + // Required: true + Target *string `json:"target"` +} + +// Validate validates this bulk check allowlist result +func (m *BulkCheckAllowlistResult) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAllowlists(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTarget(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkCheckAllowlistResult) validateAllowlists(formats strfmt.Registry) error { + + if err := validate.Required("allowlists", "body", m.Allowlists); err != nil { + return err + } + + return nil +} + +func (m *BulkCheckAllowlistResult) validateTarget(formats strfmt.Registry) error { + + if err := validate.Required("target", "body", m.Target); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this bulk check allowlist result based on context it is used +func (m *BulkCheckAllowlistResult) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *BulkCheckAllowlistResult) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkCheckAllowlistResult) UnmarshalBinary(b []byte) error { + var res BulkCheckAllowlistResult + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/models/localapi_swagger.yaml b/pkg/models/localapi_swagger.yaml index 3de9b7351..adbb2ef82 100644 --- a/pkg/models/localapi_swagger.yaml +++ b/pkg/models/localapi_swagger.yaml @@ -833,6 +833,33 @@ paths: description: "missing ip_or_range" schema: $ref: "#/definitions/ErrorResponse" + /allowlists/check: + post: + description: Check multiple IPs or ranges against allowlists + summary: postCheckAllowlist + tags: + - watchers + operationId: postCheckAllowlist + consumes: + - application/json + produces: + - application/json + parameters: + - name: body + in: body + required: true + description: IP addresses or CIDR ranges to check + schema: + $ref: '#/definitions/BulkCheckAllowlistRequest' + responses: + '200': + description: Allowlists check results for each target + schema: + $ref: '#/definitions/BulkCheckAllowlistResponse' + '400': + description: "400 response" + schema: + $ref: "#/definitions/ErrorResponse" definitions: WatcherRegistrationRequest: title: WatcherRegistrationRequest @@ -1396,6 +1423,40 @@ definitions: reason: type: string description: 'item that matched the provided value' + BulkCheckAllowlistRequest: + type: object + properties: + targets: + type: array + items: + type: string + description: Array of IP addresses or CIDR ranges to check + required: + - targets + BulkCheckAllowlistResult: + type: object + properties: + target: + type: string + description: The IP or range that is allowlisted + allowlists: + type: array + items: + type: string + description: Matching ip or range, name of the allowlist and comment related to the target + required: + - target + - allowlists + BulkCheckAllowlistResponse: + type: object + properties: + results: + type: array + items: + $ref: '#/definitions/BulkCheckAllowlistResult' + description: Per-target allowlist membership results + required: + - results ErrorResponse: type: "object" required: diff --git a/test/bats/90_decisions.bats b/test/bats/90_decisions.bats index 64edea8f9..549048965 100644 --- a/test/bats/90_decisions.bats +++ b/test/bats/90_decisions.bats @@ -87,24 +87,24 @@ teardown() { assert_stderr --partial "unable to guess format from file extension, please provide a format with --format flag" rune -0 cscli decisions import -i "${TESTDATA}/decisions.json" - assert_stderr --partial "Parsing json" - assert_stderr --partial "Imported 5 decisions" + assert_output --partial "Parsing json" + assert_output --partial "Imported 5 decisions" # import from stdin rune -1 cscli decisions import -i /dev/stdin < <(cat "${TESTDATA}/decisions.json") assert_stderr --partial "unable to guess format from file extension, please provide a format with --format flag" rune -0 cscli decisions import -i /dev/stdin < <(cat "${TESTDATA}/decisions.json") --format json - assert_stderr --partial "Parsing json" - assert_stderr --partial "Imported 5 decisions" + assert_output --partial "Parsing json" + assert_output --partial "Imported 5 decisions" # invalid json rune -1 cscli decisions import -i - <<<'{"blah":"blah"}' --format json - assert_stderr --partial 'Parsing json' + assert_output --partial 'Parsing json' assert_stderr --partial 'json: cannot unmarshal object into Go value of type []clidecision.decisionRaw' # json with extra data rune -1 cscli decisions import -i - <<<'{"values":"1.2.3.4","blah":"blah"}' --format json - assert_stderr --partial 'Parsing json' + assert_output --partial 'Parsing json' assert_stderr --partial 'json: cannot unmarshal object into Go value of type []clidecision.decisionRaw' #---------- @@ -116,21 +116,21 @@ teardown() { assert_stderr --partial "unable to guess format from file extension, please provide a format with --format flag" rune -0 cscli decisions import -i "${TESTDATA}/decisions.csv" - assert_stderr --partial 'Parsing csv' - assert_stderr --partial 'Imported 5 decisions' + assert_output --partial 'Parsing csv' + assert_output --partial 'Imported 5 decisions' # import from stdin rune -1 cscli decisions import -i /dev/stdin < <(cat "${TESTDATA}/decisions.csv") assert_stderr --partial "unable to guess format from file extension, please provide a format with --format flag" rune -0 cscli decisions import -i /dev/stdin < <(cat "${TESTDATA}/decisions.csv") --format csv - assert_stderr --partial "Parsing csv" - assert_stderr --partial "Imported 5 decisions" + assert_output --partial "Parsing csv" + assert_output --partial "Imported 5 decisions" # invalid csv # XXX: improve validation - rune -0 cscli decisions import -i - <<<'value\n1.2.3.4,5.6.7.8' --format csv - assert_stderr --partial 'Parsing csv' - assert_stderr --partial "Imported 0 decisions" + rune -1 cscli decisions import -i - <<<'value\n1.2.3.4,5.6.7.8' --format csv + assert_output "Parsing csv" + assert_stderr "Error: no decisions found" #---------- # VALUES @@ -142,8 +142,8 @@ teardown() { 1.2.3.5 1.2.3.6 EOT - assert_stderr --partial 'Parsing values' - assert_stderr --partial 'Imported 3 decisions' + assert_output --partial 'Parsing values' + assert_output --partial 'Imported 3 decisions' # leading or trailing spaces are ignored rune -0 cscli decisions import -i - --format values <<-EOT @@ -151,20 +151,18 @@ teardown() { 10.2.3.5 10.2.3.6 EOT - assert_stderr --partial 'Parsing values' - assert_stderr --partial 'Imported 3 decisions' + assert_output --partial 'Parsing values' + assert_output --partial 'Imported 3 decisions' # silently discarding (but logging) invalid decisions rune -0 cscli alerts delete --all truncate -s 0 "$LOGFILE" - rune -0 cscli decisions import -i - --format values <<-EOT + rune -1 cscli decisions import -i - --format values <<-EOT whatever EOT - assert_stderr --partial 'Parsing values' - assert_stderr --partial 'Imported 1 decisions' - assert_file_contains "$LOGFILE" "invalid addr/range 'whatever': invalid ip address 'whatever'" + assert_stderr --partial "invalid ip address 'whatever'" rune -0 cscli decisions list -a -o json assert_json '[]' @@ -174,18 +172,17 @@ teardown() { rune -0 cscli alerts delete --all truncate -s 0 "$LOGFILE" - rune -0 cscli decisions import -i - --format values <<-EOT + rune -1 cscli decisions import -i - --format values <<-EOT 1.2.3.4 bad-apple 1.2.3.5 EOT - assert_stderr --partial 'Parsing values' - assert_stderr --partial 'Imported 3 decisions' - assert_file_contains "$LOGFILE" "invalid addr/range 'bad-apple': invalid ip address 'bad-apple'" + assert_output "Parsing values" + assert_stderr "Error: API error: invalid ip address 'bad-apple'" rune -0 cscli decisions list -a -o json rune -0 jq -r '.[0].decisions | length' <(output) - assert_output 2 + assert_output 0 #---------- # Batch @@ -198,5 +195,5 @@ teardown() { EOT assert_stderr --partial 'Processing chunk of 2 decisions' assert_stderr --partial 'Processing chunk of 1 decisions' - assert_stderr --partial 'Imported 3 decisions' + assert_output --partial 'Imported 3 decisions' } diff --git a/test/bats/cscli-allowlists.bats b/test/bats/cscli-allowlists.bats index 6a91518d9..24810110d 100644 --- a/test/bats/cscli-allowlists.bats +++ b/test/bats/cscli-allowlists.bats @@ -135,15 +135,36 @@ teardown() { refute_stderr } -@test "cscli allolists: range check" { +@test "cscli allowlists: check during decisions add" { + rune -0 cscli allowlist create foo -d 'a foo' + rune -0 cscli allowlist add foo 192.168.0.0/16 + rune -1 cscli decisions add -i 192.168.1.1 + assert_stderr 'Error: 192.168.1.1 is allowlisted by item 192.168.0.0/16 from foo, use --bypass-allowlist to add the decision anyway' + refute_output + rune -0 cscli decisions add -i 192.168.1.1 --bypass-allowlist + assert_stderr --partial 'Decision successfully added' + refute_output +} + +@test "cscli allowlists: check during decisions import" { + rune -0 cscli allowlist create foo -d 'a foo' + rune -0 cscli allowlist add foo 192.168.0.0/16 + rune -0 cscli decisions import -i - <<<'192.168.1.1' --format values + assert_output - <<-EOT + Parsing values + Value 192.168.1.1 is allowlisted by [192.168.0.0/16 from foo] + Imported 0 decisions + EOT + refute_stderr +} + +@test "cscli allowlists: range check" { rune -0 cscli allowlist create foo -d 'a foo' rune -0 cscli allowlist add foo 192.168.0.0/16 rune -1 cscli decisions add -r 192.168.10.20/24 - assert_stderr 'Error: 192.168.10.20/24 is allowlisted by item 192.168.0.0/16 from foo, use --bypass-allowlist to add the decision anyway' - refute_output + assert_stderr --partial '192.168.10.20/24 is allowlisted by item 192.168.0.0/16 from foo, use --bypass-allowlist to add the decision anyway' rune -0 cscli decisions add -r 192.168.10.20/24 --bypass-allowlist assert_stderr --partial 'Decision successfully added' - refute_output } @test "cscli allowlists delete" {