parent
a1d5a02646
commit
0a39066f9d
9 changed files with 1680 additions and 240 deletions
|
@ -284,7 +284,7 @@ cscli decisions list -t ban
|
|||
cmdDecisionsList.Flags().StringVar(filter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
|
||||
cmdDecisionsList.Flags().StringVarP(filter.TypeEquals, "type", "t", "", "restrict to this decision type (ie. ban,captcha)")
|
||||
cmdDecisionsList.Flags().StringVar(filter.ScopeEquals, "scope", "", "restrict to this scope (ie. ip,range,session)")
|
||||
cmdDecisionsList.Flags().StringVar(filter.OriginEquals, "origin", "", "restrict to this origin (ie. lists,CAPI,cscli)")
|
||||
cmdDecisionsList.Flags().StringVar(filter.OriginEquals, "origin", "", "restrict to this origin (ie. lists,CAPI,cscli,cscli-import,crowdsec)")
|
||||
cmdDecisionsList.Flags().StringVarP(filter.ValueEquals, "value", "v", "", "restrict to this value (ie. 1.2.3.4,userName)")
|
||||
cmdDecisionsList.Flags().StringVarP(filter.ScenarioEquals, "scenario", "s", "", "restrict to this scenario (ie. crowdsecurity/ssh-bf)")
|
||||
cmdDecisionsList.Flags().StringVarP(filter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
|
||||
|
@ -419,7 +419,6 @@ cscli decisions add --scope username --value foobar
|
|||
Aliases: []string{"remove"},
|
||||
Example: `cscli decisions delete -r 1.2.3.0/24
|
||||
cscli decisions delete -i 1.2.3.4
|
||||
cscli decisions delete -s crowdsecurity/ssh-bf
|
||||
cscli decisions delete --id 42
|
||||
cscli decisions delete --type captcha
|
||||
`,
|
||||
|
|
|
@ -23,6 +23,7 @@ type LAPI struct {
|
|||
loginResp models.WatcherAuthResponse
|
||||
bouncerKey string
|
||||
t *testing.T
|
||||
DBConfig *csconfig.DatabaseCfg
|
||||
}
|
||||
|
||||
func SetupLAPITest(t *testing.T) LAPI {
|
||||
|
@ -36,10 +37,12 @@ func SetupLAPITest(t *testing.T) LAPI {
|
|||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
|
||||
return LAPI{
|
||||
router: router,
|
||||
loginResp: loginResp,
|
||||
bouncerKey: APIKey,
|
||||
DBConfig: config.API.Server.DbConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,20 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func FormatDecisions(decisions []*ent.Decision) ([]*models.Decision, error) {
|
||||
//Format decisions for the bouncers, and deduplicate them by keeping only the longest one
|
||||
func FormatDecisions(decisions []*ent.Decision, dedup bool) ([]*models.Decision, error) {
|
||||
var results []*models.Decision
|
||||
|
||||
seen := make(map[string]struct{}, 0)
|
||||
|
||||
for _, dbDecision := range decisions {
|
||||
if dedup {
|
||||
key := dbDecision.Value + dbDecision.Scope + dbDecision.Type
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
duration := dbDecision.Until.Sub(time.Now().UTC()).String()
|
||||
decision := models.Decision{
|
||||
ID: int64(dbDecision.ID),
|
||||
|
@ -46,7 +57,7 @@ func (c *Controller) GetDecision(gctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
results, err = FormatDecisions(data)
|
||||
results, err = FormatDecisions(data, false)
|
||||
if err != nil {
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
|
@ -82,14 +93,14 @@ func (c *Controller) DeleteDecisionById(gctx *gin.Context) {
|
|||
gctx.JSON(http.StatusBadRequest, gin.H{"message": "decision_id must be valid integer"})
|
||||
return
|
||||
}
|
||||
err = c.DBClient.SoftDeleteDecisionByID(decisionID)
|
||||
nbDeleted, err := c.DBClient.SoftDeleteDecisionByID(decisionID)
|
||||
if err != nil {
|
||||
c.HandleDBErrors(gctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
deleteDecisionResp := models.DeleteDecisionResponse{
|
||||
NbDeleted: "1",
|
||||
NbDeleted: strconv.Itoa(nbDeleted),
|
||||
}
|
||||
|
||||
gctx.JSON(http.StatusOK, deleteDecisionResp)
|
||||
|
@ -138,7 +149,8 @@ func (c *Controller) StreamDecision(gctx *gin.Context) {
|
|||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
ret["new"], err = FormatDecisions(data)
|
||||
//data = KeepLongestDecision(data)
|
||||
ret["new"], err = FormatDecisions(data, true)
|
||||
if err != nil {
|
||||
log.Errorf("unable to format expired decision for '%s' : %v", bouncerInfo.Name, err)
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
|
@ -152,7 +164,7 @@ func (c *Controller) StreamDecision(gctx *gin.Context) {
|
|||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
ret["deleted"], err = FormatDecisions(data)
|
||||
ret["deleted"], err = FormatDecisions(data, true)
|
||||
if err != nil {
|
||||
log.Errorf("unable to format expired decision for '%s' : %v", bouncerInfo.Name, err)
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
|
@ -180,7 +192,8 @@ func (c *Controller) StreamDecision(gctx *gin.Context) {
|
|||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
ret["new"], err = FormatDecisions(data)
|
||||
//data = KeepLongestDecision(data)
|
||||
ret["new"], err = FormatDecisions(data, true)
|
||||
if err != nil {
|
||||
log.Errorf("unable to format new decision for '%s' : %v", bouncerInfo.Name, err)
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
|
@ -194,7 +207,7 @@ func (c *Controller) StreamDecision(gctx *gin.Context) {
|
|||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
ret["deleted"], err = FormatDecisions(data)
|
||||
ret["deleted"], err = FormatDecisions(data, true)
|
||||
if err != nil {
|
||||
log.Errorf("unable to format expired decision for '%s' : %v", bouncerInfo.Name, err)
|
||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||
|
|
File diff suppressed because it is too large
Load diff
266
pkg/apiserver/tests/alert_duplicate.json
Normal file
266
pkg/apiserver/tests/alert_duplicate.json
Normal file
|
@ -0,0 +1,266 @@
|
|||
[
|
||||
{
|
||||
"id": 42,
|
||||
"machine_id": "test",
|
||||
"capacity": 1,
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "1h",
|
||||
"origin": "test",
|
||||
"scenario": "crowdsecurity/test",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.1",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.1",
|
||||
"range": "127.0.0.1/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.1"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"scenario": "crowdsecurity/test",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "3h",
|
||||
"origin": "another_origin",
|
||||
"scenario": "crowdsecurity/ssh_bf",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.1",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.1",
|
||||
"range": "127.0.0.1/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.1"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/ssh_bf",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "5h",
|
||||
"origin": "test",
|
||||
"scenario": "crowdsecurity/longest",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.1",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.1",
|
||||
"range": "127.0.0.1/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.1"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/longest",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "3h",
|
||||
"origin": "test",
|
||||
"scenario": "crowdsecurity/test",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.2",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.2",
|
||||
"range": "127.0.0.2/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.2"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/test",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "3h",
|
||||
"origin": "test",
|
||||
"scenario": "crowdsecurity/ssh_bf",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.2",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.2",
|
||||
"range": "127.0.0.2/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.2"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/ssh_bf",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "1h",
|
||||
"origin": "test",
|
||||
"scenario": "crowdsecurity/ssh_bf",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.2",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.2",
|
||||
"range": "127.0.0.2/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.2"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/ssh_bf",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "2h",
|
||||
"origin": "another_origin",
|
||||
"scenario": "crowdsecurity/test",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.2",
|
||||
"type": "ban"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.2",
|
||||
"range": "127.0.0.2/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.2"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/test",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"machine_id": "test",
|
||||
"created_at": "2020-10-09T10:00:10Z",
|
||||
"decisions": [
|
||||
{
|
||||
"duration": "3h",
|
||||
"origin": "test",
|
||||
"scenario": "crowdsecurity/test",
|
||||
"scope": "Ip",
|
||||
"value": "127.0.0.2",
|
||||
"type": "captcha"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"ip": "127.0.0.2",
|
||||
"range": "127.0.0.2/32",
|
||||
"scope": "ip",
|
||||
"value": "127.0.0.2"
|
||||
},
|
||||
"Events": [
|
||||
],
|
||||
"events_count": 1,
|
||||
"leakspeed": "0.5s",
|
||||
"message": "test",
|
||||
"scenario_hash": "hashtest",
|
||||
"scenario_version": "v1",
|
||||
"simulated": false,
|
||||
"capacity": 1,
|
||||
"scenario": "crowdsecurity/test",
|
||||
"start_at": "2020-10-09T10:00:01Z",
|
||||
"stop_at": "2020-10-09T10:00:05Z"
|
||||
}
|
||||
]
|
|
@ -15,16 +15,19 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string][]string) (*ent.DecisionQuery, error) {
|
||||
func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string][]string) (*ent.DecisionQuery, []*sql.Predicate, error) {
|
||||
|
||||
var err error
|
||||
var start_ip, start_sfx, end_ip, end_sfx int64
|
||||
var ip_sz int
|
||||
var contains bool = true
|
||||
/*if contains is true, return bans that *contains* the given value (value is the inner)
|
||||
else, return bans that are *contained* by the given value (value is the outer)*/
|
||||
|
||||
/*the simulated filter is a bit different : if it's not present *or* set to false, specifically exclude records with simulated to true */
|
||||
// contains == true -> return bans that *contain* the given value (value is the inner)
|
||||
// contains == false or missing -> return bans *contained* in the given value (value is the outer)
|
||||
|
||||
// simulated == true -> include simulated rows
|
||||
// simulated == false or missing -> exclude simulated rows
|
||||
|
||||
if v, ok := filter["simulated"]; ok {
|
||||
if v[0] == "false" {
|
||||
query = query.Where(decision.SimulatedEQ(false))
|
||||
|
@ -33,13 +36,14 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
|
|||
} else {
|
||||
query = query.Where(decision.SimulatedEQ(false))
|
||||
}
|
||||
|
||||
t := sql.Table(decision.Table)
|
||||
joinPredicate := make([]*sql.Predicate, 0)
|
||||
for param, value := range filter {
|
||||
switch param {
|
||||
case "contains":
|
||||
contains, err = strconv.ParseBool(value[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
|
||||
return nil, nil, errors.Wrapf(InvalidFilter, "invalid contains value : %s", err)
|
||||
}
|
||||
case "scopes":
|
||||
scopes := strings.Split(value[0], ",")
|
||||
|
@ -64,9 +68,24 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
|
|||
query = query.Where(
|
||||
decision.OriginIn(strings.Split(value[0], ",")...),
|
||||
)
|
||||
origins := strings.Split(value[0], ",")
|
||||
originsContainsPredicate := make([]*sql.Predicate, 0)
|
||||
for _, origin := range origins {
|
||||
pred := sql.EqualFold(t.C(decision.FieldOrigin), origin)
|
||||
originsContainsPredicate = append(originsContainsPredicate, pred)
|
||||
}
|
||||
joinPredicate = append(joinPredicate, sql.Or(originsContainsPredicate...))
|
||||
case "scenarios_containing":
|
||||
predicates := decisionPredicatesFromStr(value[0], decision.ScenarioContainsFold)
|
||||
query = query.Where(decision.Or(predicates...))
|
||||
|
||||
scenarios := strings.Split(value[0], ",")
|
||||
scenariosContainsPredicate := make([]*sql.Predicate, 0)
|
||||
for _, scenario := range scenarios {
|
||||
pred := sql.ContainsFold(t.C(decision.FieldScenario), scenario)
|
||||
scenariosContainsPredicate = append(scenariosContainsPredicate, pred)
|
||||
}
|
||||
joinPredicate = append(joinPredicate, sql.Or(scenariosContainsPredicate...))
|
||||
case "scenarios_not_containing":
|
||||
predicates := decisionPredicatesFromStr(value[0], decision.ScenarioContainsFold)
|
||||
query = query.Where(decision.Not(
|
||||
|
@ -74,10 +93,17 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
|
|||
predicates...,
|
||||
),
|
||||
))
|
||||
scenarios := strings.Split(value[0], ",")
|
||||
scenariosContainsPredicate := make([]*sql.Predicate, 0)
|
||||
for _, scenario := range scenarios {
|
||||
pred := sql.ContainsFold(t.C(decision.FieldScenario), scenario)
|
||||
scenariosContainsPredicate = append(scenariosContainsPredicate, sql.Not(pred))
|
||||
}
|
||||
joinPredicate = append(joinPredicate, sql.Or(scenariosContainsPredicate...))
|
||||
case "ip", "range":
|
||||
ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(value[0])
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
|
||||
return nil, nil, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", value[0], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,9 +175,9 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
|
|||
))
|
||||
}
|
||||
} else if ip_sz != 0 {
|
||||
return nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
|
||||
return nil, nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
|
||||
}
|
||||
return query, nil
|
||||
return query, joinPredicate, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryDecisionWithFilter(filter map[string][]string) ([]*ent.Decision, error) {
|
||||
|
@ -161,7 +187,7 @@ func (c *Client) QueryDecisionWithFilter(filter map[string][]string) ([]*ent.Dec
|
|||
decisions := c.Ent.Decision.Query().
|
||||
Where(decision.UntilGTE(time.Now().UTC()))
|
||||
|
||||
decisions, err = BuildDecisionRequestWithFilter(decisions, filter)
|
||||
decisions, _, err = BuildDecisionRequestWithFilter(decisions, filter)
|
||||
if err != nil {
|
||||
return []*ent.Decision{}, err
|
||||
}
|
||||
|
@ -185,86 +211,89 @@ func (c *Client) QueryDecisionWithFilter(filter map[string][]string) ([]*ent.Dec
|
|||
return data, nil
|
||||
}
|
||||
|
||||
// ent translation of https://stackoverflow.com/a/28090544
|
||||
func longestDecisionForScopeTypeValue(s *sql.Selector) {
|
||||
t := sql.Table(decision.Table)
|
||||
s.LeftJoin(t).OnP(sql.And(
|
||||
sql.ColumnsEQ(
|
||||
t.C(decision.FieldValue),
|
||||
s.C(decision.FieldValue),
|
||||
),
|
||||
sql.ColumnsEQ(
|
||||
t.C(decision.FieldType),
|
||||
s.C(decision.FieldType),
|
||||
),
|
||||
sql.ColumnsEQ(
|
||||
t.C(decision.FieldScope),
|
||||
s.C(decision.FieldScope),
|
||||
),
|
||||
sql.ColumnsGT(
|
||||
t.C(decision.FieldUntil),
|
||||
s.C(decision.FieldUntil),
|
||||
),
|
||||
))
|
||||
s.Where(
|
||||
sql.IsNull(
|
||||
t.C(decision.FieldUntil),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) QueryAllDecisionsWithFilters(filters map[string][]string) ([]*ent.Decision, error) {
|
||||
query := c.Ent.Decision.Query().Where(
|
||||
decision.UntilGT(time.Now().UTC()),
|
||||
longestDecisionForScopeTypeValue,
|
||||
)
|
||||
query, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
|
||||
query, _, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryAllDecisionsWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "get all decisions with filters")
|
||||
}
|
||||
|
||||
data, err := query.All(c.CTX)
|
||||
//Order is *very* important, the dedup assumes that decisions are sorted per IP and per time left
|
||||
data, err := query.Order(ent.Asc(decision.FieldValue), ent.Desc(decision.FieldUntil)).All(c.CTX)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryAllDecisionsWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "get all decisions with filters")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryExpiredDecisionsWithFilters(filters map[string][]string) ([]*ent.Decision, error) {
|
||||
now := time.Now().UTC()
|
||||
query := c.Ent.Decision.Query().Where(
|
||||
decision.UntilLT(time.Now().UTC()),
|
||||
longestDecisionForScopeTypeValue,
|
||||
)
|
||||
query, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
|
||||
query, predicates, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryExpiredDecisionsWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "get expired decisions with filters")
|
||||
}
|
||||
data, err := query.All(c.CTX)
|
||||
query = query.Where(func(s *sql.Selector) {
|
||||
t := sql.Table(decision.Table)
|
||||
|
||||
subQuery := sql.Select(t.C(decision.FieldValue)).From(t).Where(sql.GT(t.C(decision.FieldUntil), now))
|
||||
for _, predicate := range predicates {
|
||||
subQuery.Where(predicate)
|
||||
}
|
||||
s.Where(
|
||||
sql.NotIn(
|
||||
s.C(decision.FieldValue),
|
||||
subQuery,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
data, err := query.Order(ent.Asc(decision.FieldValue), ent.Desc(decision.FieldUntil)).All(c.CTX)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryExpiredDecisionsWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryExpiredDecisionsSinceWithFilters(since time.Time, filters map[string][]string) ([]*ent.Decision, error) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
query := c.Ent.Decision.Query().Where(
|
||||
decision.UntilLT(time.Now().UTC()),
|
||||
decision.UntilLT(now),
|
||||
decision.UntilGT(since),
|
||||
longestDecisionForScopeTypeValue,
|
||||
)
|
||||
query, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
query, predicates, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryExpiredDecisionsSinceWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions with filters")
|
||||
}
|
||||
|
||||
data, err := query.All(c.CTX)
|
||||
query = query.Where(func(s *sql.Selector) {
|
||||
t := sql.Table(decision.Table)
|
||||
|
||||
subQuery := sql.Select(t.C(decision.FieldValue)).From(t).Where(sql.GT(t.C(decision.FieldUntil), now))
|
||||
for _, predicate := range predicates {
|
||||
subQuery.Where(predicate)
|
||||
}
|
||||
s.Where(
|
||||
sql.NotIn(
|
||||
s.C(decision.FieldValue),
|
||||
subQuery,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
data, err := query.Order(ent.Asc(decision.FieldValue), ent.Desc(decision.FieldUntil)).All(c.CTX)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryExpiredDecisionsSinceWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions with filters")
|
||||
|
@ -277,14 +306,15 @@ func (c *Client) QueryNewDecisionsSinceWithFilters(since time.Time, filters map[
|
|||
query := c.Ent.Decision.Query().Where(
|
||||
decision.CreatedAtGT(since),
|
||||
decision.UntilGT(time.Now().UTC()),
|
||||
longestDecisionForScopeTypeValue,
|
||||
)
|
||||
query, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
query, _, err := BuildDecisionRequestWithFilter(query, filters)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrapf(QueryFail, "new decisions since '%s'", since.String())
|
||||
c.Log.Warningf("BuildDecisionRequestWithFilter : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrap(QueryFail, "expired decisions with filters")
|
||||
}
|
||||
data, err := query.All(c.CTX)
|
||||
|
||||
//Order is *very* important, the dedup assumes that decisions are sorted per IP and per time left
|
||||
data, err := query.Order(ent.Asc(decision.FieldValue), ent.Desc(decision.FieldUntil)).All(c.CTX)
|
||||
if err != nil {
|
||||
c.Log.Warningf("QueryNewDecisionsSinceWithFilters : %s", err)
|
||||
return []*ent.Decision{}, errors.Wrapf(QueryFail, "new decisions since '%s'", since.String())
|
||||
|
@ -521,17 +551,17 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
|
|||
}
|
||||
|
||||
//SoftDeleteDecisionByID set the expiration of a decision to now()
|
||||
func (c *Client) SoftDeleteDecisionByID(decisionID int) error {
|
||||
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 {
|
||||
c.Log.Warningf("SoftDeleteDecisionByID : %v (nb soft deleted: %d)", err, nbUpdated)
|
||||
return errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionID)
|
||||
return 0, errors.Wrapf(DeleteFail, "decision with id '%d' doesn't exist", decisionID)
|
||||
}
|
||||
|
||||
if nbUpdated == 0 {
|
||||
return ItemNotFound
|
||||
return 0, ItemNotFound
|
||||
}
|
||||
return nil
|
||||
return nbUpdated, nil
|
||||
}
|
||||
|
||||
func decisionPredicatesFromStr(s string, predicateFunc func(string) predicate.Decision) []predicate.Decision {
|
||||
|
|
233
tests/bats/99_lapi-stream-mode-scenario.bats
Normal file
233
tests/bats/99_lapi-stream-mode-scenario.bats
Normal file
|
@ -0,0 +1,233 @@
|
|||
#!/usr/bin/env bats
|
||||
# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
|
||||
|
||||
set -u
|
||||
|
||||
setup_file() {
|
||||
load "../lib/setup_file.sh"
|
||||
./instance-data load
|
||||
./instance-crowdsec start
|
||||
API_KEY=$(cscli bouncers add testbouncer -o raw)
|
||||
export API_KEY
|
||||
CROWDSEC_API_URL="http://localhost:8080"
|
||||
export CROWDSEC_API_URL
|
||||
}
|
||||
|
||||
teardown_file() {
|
||||
load "../lib/teardown_file.sh"
|
||||
}
|
||||
|
||||
setup() {
|
||||
load "../lib/setup.sh"
|
||||
skip
|
||||
}
|
||||
|
||||
#----------
|
||||
|
||||
api() {
|
||||
URI="$1"
|
||||
curl -s -H "X-Api-Key:${API_KEY}" "${CROWDSEC_API_URL}${URI}"
|
||||
}
|
||||
|
||||
output_new_decisions() {
|
||||
jq -c '.new | map(select(.origin!="CAPI")) | .[] | del(.id) | (.. | .duration?) |= capture("(?<d>[[:digit:]]+h[[:digit:]]+m)").d' <(output) | sort
|
||||
}
|
||||
|
||||
|
||||
@test "${FILE} adding decisions with different duration, scenario, origin" {
|
||||
# origin: test
|
||||
run -0 cscli decisions add -i 127.0.0.1 -d 1h -R crowdsecurity/test
|
||||
./instance-crowdsec stop
|
||||
run -0 ./instance-db exec_sql "update decisions set origin='test' where origin='cscli'"
|
||||
./instance-crowdsec start
|
||||
|
||||
run -0 cscli decisions add -i 127.0.0.1 -d 3h -R crowdsecurity/ssh_bf
|
||||
./instance-crowdsec stop
|
||||
run -0 ./instance-db exec_sql "update decisions set origin='another_origin' where origin='cscli'"
|
||||
./instance-crowdsec start
|
||||
|
||||
run -0 cscli decisions add -i 127.0.0.1 -d 5h -R crowdsecurity/longest
|
||||
run -0 cscli decisions add -i 127.0.0.2 -d 3h -R crowdsecurity/test
|
||||
run -0 cscli decisions add -i 127.0.0.2 -d 3h -R crowdsecurity/ssh_bf
|
||||
run -0 cscli decisions add -i 127.0.0.2 -d 1h -R crowdsecurity/ssh_bf
|
||||
./instance-crowdsec stop
|
||||
run -0 ./instance-db exec_sql "update decisions set origin='test' where origin='cscli'"
|
||||
./instance-crowdsec start
|
||||
|
||||
# origin: another_origin
|
||||
run -0 cscli decisions add -i 127.0.0.2 -d 2h -R crowdsecurity/test
|
||||
./instance-crowdsec stop
|
||||
run -0 ./instance-db exec_sql "update decisions set origin='another_origin' where origin='cscli'"
|
||||
./instance-crowdsec start
|
||||
}
|
||||
|
||||
@test "${FILE} test startup" {
|
||||
run -0 api "/v1/decisions/stream?startup=true"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"2h59m","origin":"test","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
{"duration":"4h59m","origin":"test","scenario":"crowdsecurity/longest","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with scenarios containing" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_containing=ssh_bf"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"2h59m","origin":"another_origin","scenario":"crowdsecurity/ssh_bf","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
{"duration":"2h59m","origin":"test","scenario":"crowdsecurity/ssh_bf","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with multiple scenarios containing" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_containing=ssh_bf,test"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"2h59m","origin":"another_origin","scenario":"crowdsecurity/ssh_bf","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
{"duration":"2h59m","origin":"test","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with unknown scenarios containing" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_containing=unknown"
|
||||
assert_output '{"deleted":null,"new":null}'
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with scenarios containing and not containing" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_containing=test&scenarios_not_containing=ssh_bf"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"2h59m","origin":"test","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
{"origin":"test","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with scenarios containing and not containing 2" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_containing=longest&scenarios_not_containing=ssh_bf,test"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"4h59m","origin":"test","scenario":"crowdsecurity/longest","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with scenarios not containing" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_not_containing=ssh_bf"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"2h59m","origin":"test","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
{"duration":"4h59m","origin":"test","scenario":"crowdsecurity/longest","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with multiple scenarios not containing" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&scenarios_not_containing=ssh_bf,test"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"4h59m","origin":"test","scenario":"crowdsecurity/longest","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with origins parameter" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&origins=another_origin"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"1h59m","origin":"another_origin","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
{"duration":"2h59m","origin":"another_origin","scenario":"crowdsecurity/ssh_bf","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with multiple origins parameter" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&origins=another_origin,test"
|
||||
run -0 output_new_decisions
|
||||
assert_output - <<-EOT
|
||||
{"duration":"2h59m","origin":"test","scenario":"crowdsecurity/test","scope":"Ip","type":"ban","value":"127.0.0.2"}
|
||||
{"duration":"4h59m","origin":"test","scenario":"crowdsecurity/longest","scope":"Ip","type":"ban","value":"127.0.0.1"}
|
||||
EOT
|
||||
}
|
||||
|
||||
@test "${FILE} test startup with unknown origins" {
|
||||
run -0 api "/v1/decisions/stream?startup=true&origins=unknown"
|
||||
assert_output '{"deleted":null,"new":null}'
|
||||
}
|
||||
|
||||
#@test "${FILE} delete decision 3 (127.0.0.1)" {
|
||||
#
|
||||
# {
|
||||
# TestName: "delete decisions 3 (127.0.0.1)",
|
||||
# Method: "DELETE",
|
||||
# Route: "/v1/decisions/3",
|
||||
# CheckCodeOnly: true,
|
||||
# Code: 200,
|
||||
# LenNew: 0,
|
||||
# LenDeleted: 0,
|
||||
# AuthType: PASSWORD,
|
||||
# DelChecks: []DecisionCheck{},
|
||||
# NewChecks: []DecisionCheck{},
|
||||
# TestName: "check that 127.0.0.1 is not in deleted IP",
|
||||
# Method: "GET",
|
||||
# Route: "/v1/decisions/stream?startup=true",
|
||||
# CheckCodeOnly: false,
|
||||
# Code: 200,
|
||||
# LenNew: 2,
|
||||
# LenDeleted: 0,
|
||||
# AuthType: APIKEY,
|
||||
# DelChecks: []DecisionCheck{},
|
||||
# NewChecks: []DecisionCheck{},
|
||||
# },
|
||||
# {
|
||||
# TestName: "delete decisions 2 (127.0.0.1)",
|
||||
# Method: "DELETE",
|
||||
# Route: "/v1/decisions/2",
|
||||
# CheckCodeOnly: true,
|
||||
# Code: 200,
|
||||
# LenNew: 0,
|
||||
# LenDeleted: 0,
|
||||
# AuthType: PASSWORD,
|
||||
# DelChecks: []DecisionCheck{},
|
||||
# NewChecks: []DecisionCheck{},
|
||||
# },
|
||||
# {
|
||||
# TestName: "check that 127.0.0.1 is not in deleted IP",
|
||||
# Method: "GET",
|
||||
# Route: "/v1/decisions/stream?startup=true",
|
||||
# CheckCodeOnly: false,
|
||||
# Code: 200,
|
||||
# LenNew: 2,
|
||||
# LenDeleted: 0,
|
||||
# AuthType: APIKEY,
|
||||
# DelChecks: []DecisionCheck{},
|
||||
# NewChecks: []DecisionCheck{},
|
||||
# },
|
||||
# {
|
||||
# TestName: "delete decisions 1 (127.0.0.1)",
|
||||
# Method: "DELETE",
|
||||
# Route: "/v1/decisions/1",
|
||||
# CheckCodeOnly: true,
|
||||
# Code: 200,
|
||||
# LenNew: 0,
|
||||
# LenDeleted: 0,
|
||||
# AuthType: PASSWORD,
|
||||
# DelChecks: []DecisionCheck{},
|
||||
# NewChecks: []DecisionCheck{},
|
||||
# },
|
||||
# TestName: "127.0.0.1 should be in deleted now",
|
||||
# Method: "GET",
|
||||
# Route: "/v1/decisions/stream?startup=true",
|
||||
# CheckCodeOnly: false,
|
||||
# Code: 200,
|
||||
# LenNew: 1,
|
||||
# LenDeleted: 1,
|
||||
# AuthType: APIKEY,
|
||||
# DelChecks: []DecisionCheck{
|
||||
# {
|
||||
# ID: int64(1),
|
||||
# Origin: "test",
|
||||
# Scenario: "crowdsecurity/test",
|
||||
# Value: "127.0.0.1",
|
||||
# Duration: "-", // we check that the time is negative
|
||||
# },
|
||||
# },
|
||||
# NewChecks: []DecisionCheck{},
|
||||
# },
|
||||
#}
|
||||
|
|
@ -115,7 +115,13 @@ case "$1" in
|
|||
;;
|
||||
exec_sql)
|
||||
shift
|
||||
exec_sql "$@"
|
||||
#
|
||||
# This command is meant to run a query against the the crowdsec database.
|
||||
# The exec_sql() function is more generic and is also used for database setup and backups.
|
||||
#
|
||||
# For this reason, we select the database here.
|
||||
#
|
||||
exec_sql "use crowdsec_test; $@"
|
||||
;;
|
||||
*)
|
||||
about
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
# https://github.com/bats-core/bats-core/blob/master/docs/source/warnings/BW02.rst
|
||||
bats_require_minimum_version 1.5.0
|
||||
|
||||
# this should have effect globally, for all tests
|
||||
# https://github.com/bats-core/bats-core/blob/master/docs/source/warnings/BW02.rst
|
||||
bats_require_minimum_version 1.5.0
|
||||
|
||||
debug() {
|
||||
echo 'exec 1<&-; exec 2<&-; exec 1>&3; exec 2>&1'
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue