mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-10 20:05:55 +02:00
allowlists: check during bulk decision import (#3588)
This commit is contained in:
parent
8689783ade
commit
5bc2b49387
13 changed files with 664 additions and 50 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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("")
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
71
pkg/models/bulk_check_allowlist_request.go
Normal file
71
pkg/models/bulk_check_allowlist_request.go
Normal file
|
@ -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
|
||||
}
|
124
pkg/models/bulk_check_allowlist_response.go
Normal file
124
pkg/models/bulk_check_allowlist_response.go
Normal file
|
@ -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
|
||||
}
|
88
pkg/models/bulk_check_allowlist_result.go
Normal file
88
pkg/models/bulk_check_allowlist_result.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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" {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue