allowlists: check during bulk decision import (#3588)

This commit is contained in:
mmetc 2025-04-28 17:11:17 +02:00 committed by GitHub
parent 8689783ade
commit 5bc2b49387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 664 additions and 50 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
}

View 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
}

View 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
}

View file

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

View file

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

View file

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