Skip to content

Commit 959b872

Browse files
authored
allowlists: automatically expire current matching decisions on update (#3601)
1 parent f8f0b2a commit 959b872

File tree

8 files changed

+186
-7
lines changed

8 files changed

+186
-7
lines changed

cmd/crowdsec-cli/cliallowlists/allowlists.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,14 @@ func (cli *cliAllowLists) add(ctx context.Context, db *database.Client, name str
488488
fmt.Printf("added %d values to allowlist %s\n", added, name)
489489
}
490490

491+
deleted, err := db.ApplyAllowlistsToExistingDecisions(ctx)
492+
if err != nil {
493+
return fmt.Errorf("unable to apply allowlists to existing decisions: %w", err)
494+
}
495+
if deleted > 0 {
496+
fmt.Printf("%d decisions deleted by allowlists\n", deleted)
497+
}
498+
491499
return nil
492500
}
493501

pkg/apiserver/apic.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,8 @@ func fillAlertsWithDecisions(alerts []*models.Alert, decisions []*models.Decisio
588588
func (a *apic) PullTop(ctx context.Context, forcePull bool) error {
589589
var err error
590590

591+
hasPulledAllowlists := false
592+
591593
// A mutex with TryLock would be a bit simpler
592594
// But go does not guarantee that TryLock will be able to acquire the lock even if it is available
593595
select {
@@ -649,16 +651,17 @@ func (a *apic) PullTop(ctx context.Context, forcePull bool) error {
649651
// process deleted decisions
650652
nbDeleted, err := a.HandleDeletedDecisionsV3(ctx, data.Deleted, deleteCounters)
651653
if err != nil {
652-
return err
654+
log.Errorf("could not delete decisions from CAPI: %s", err)
653655
}
654656

655657
log.Printf("capi/community-blocklist : %d explicit deletions", nbDeleted)
656658

657659
// Update allowlists before processing decisions
658660
if data.Links != nil {
659661
if len(data.Links.Allowlists) > 0 {
662+
hasPulledAllowlists = true
660663
if err := a.UpdateAllowlists(ctx, data.Links.Allowlists, forcePull); err != nil {
661-
return fmt.Errorf("while updating allowlists: %w", err)
664+
log.Errorf("could not update allowlists from CAPI: %s", err)
662665
}
663666
}
664667
}
@@ -675,7 +678,7 @@ func (a *apic) PullTop(ctx context.Context, forcePull bool) error {
675678

676679
err = a.SaveAlerts(ctx, alertsFromCapi, addCounters, deleteCounters)
677680
if err != nil {
678-
return fmt.Errorf("while saving alerts: %w", err)
681+
log.Errorf("could not save alert for CAPI pull: %s", err)
679682
}
680683
} else {
681684
if a.pullCommunity {
@@ -689,11 +692,21 @@ func (a *apic) PullTop(ctx context.Context, forcePull bool) error {
689692
if data.Links != nil {
690693
if len(data.Links.Blocklists) > 0 {
691694
if err := a.UpdateBlocklists(ctx, data.Links.Blocklists, addCounters, forcePull); err != nil {
692-
return fmt.Errorf("while updating blocklists: %w", err)
695+
log.Errorf("could not update blocklists from CAPI: %s", err)
693696
}
694697
}
695698
}
696699

700+
if hasPulledAllowlists {
701+
deleted, err := a.dbClient.ApplyAllowlistsToExistingDecisions(ctx)
702+
if err != nil {
703+
log.Errorf("could not apply allowlists to existing decisions: %s", err)
704+
}
705+
if deleted > 0 {
706+
log.Infof("deleted %d decisions from allowlists", deleted)
707+
}
708+
}
709+
697710
return nil
698711
}
699712

pkg/apiserver/papi_cmd.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ func ManagementCmd(message *Message, p *Papi, sync bool) error {
264264
if err != nil {
265265
return fmt.Errorf("failed to force pull operation: %w", err)
266266
}
267+
268+
deleted, err := p.DBClient.ApplyAllowlistsToExistingDecisions(ctx)
269+
if err != nil {
270+
log.Errorf("could not apply allowlists to existing decisions: %s", err)
271+
}
272+
if deleted > 0 {
273+
log.Infof("deleted %d decisions from allowlists", deleted)
274+
}
267275
}
268276
case "allowlist_unsubscribe":
269277
data, err := json.Marshal(message.Data)

pkg/database/allowlists.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
1313
"github.com/crowdsecurity/crowdsec/pkg/database/ent/allowlist"
1414
"github.com/crowdsecurity/crowdsec/pkg/database/ent/allowlistitem"
15+
"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
16+
1517
"github.com/crowdsecurity/crowdsec/pkg/models"
1618
"github.com/crowdsecurity/crowdsec/pkg/types"
1719
)
@@ -389,3 +391,96 @@ func (c *Client) GetAllowlistsContentForAPIC(ctx context.Context) ([]net.IP, []*
389391

390392
return ips, nets, nil
391393
}
394+
395+
func (c *Client) ApplyAllowlistsToExistingDecisions(ctx context.Context) (int, error) {
396+
// Soft delete (set expiration to now) all decisions that matches any allowlist
397+
398+
totalCount := 0
399+
400+
// Get all non-expired allowlist items
401+
// We will match them one by one against all decisions
402+
allowlistItems, err := c.Ent.AllowListItem.Query().
403+
Where(
404+
allowlistitem.Or(
405+
allowlistitem.ExpiresAtGTE(time.Now().UTC()),
406+
allowlistitem.ExpiresAtIsNil(),
407+
),
408+
).All(ctx)
409+
if err != nil {
410+
return 0, fmt.Errorf("unable to get allowlist items: %w", err)
411+
}
412+
413+
now := time.Now().UTC()
414+
415+
for _, item := range allowlistItems {
416+
updateQuery := c.Ent.Decision.Update().SetUntil(now).Where(decision.UntilGTE(now))
417+
switch item.IPSize {
418+
case 4:
419+
updateQuery = updateQuery.Where(
420+
decision.And(
421+
decision.IPSizeEQ(4),
422+
decision.Or(
423+
decision.And(
424+
decision.StartIPLTE(item.StartIP),
425+
decision.EndIPGTE(item.EndIP),
426+
),
427+
decision.And(
428+
decision.StartIPGTE(item.StartIP),
429+
decision.EndIPLTE(item.EndIP),
430+
),
431+
)))
432+
case 16:
433+
updateQuery = updateQuery.Where(
434+
decision.And(
435+
decision.IPSizeEQ(16),
436+
decision.Or(
437+
decision.And(
438+
decision.Or(
439+
decision.StartIPLT(item.StartIP),
440+
decision.And(
441+
decision.StartIPEQ(item.StartIP),
442+
decision.StartSuffixLTE(item.StartSuffix),
443+
)),
444+
decision.Or(
445+
decision.EndIPGT(item.EndIP),
446+
decision.And(
447+
decision.EndIPEQ(item.EndIP),
448+
decision.EndSuffixGTE(item.EndSuffix),
449+
),
450+
),
451+
),
452+
decision.And(
453+
decision.Or(
454+
decision.StartIPGT(item.StartIP),
455+
decision.And(
456+
decision.StartIPEQ(item.StartIP),
457+
decision.StartSuffixGTE(item.StartSuffix),
458+
)),
459+
decision.Or(
460+
decision.EndIPLT(item.EndIP),
461+
decision.And(
462+
decision.EndIPEQ(item.EndIP),
463+
decision.EndSuffixLTE(item.EndSuffix),
464+
),
465+
),
466+
),
467+
),
468+
),
469+
)
470+
default:
471+
// This should never happen
472+
// But better safe than sorry and just skip it instead of expiring all decisions
473+
c.Log.Errorf("unexpected IP size %d for allowlist item %s", item.IPSize, item.Value)
474+
continue
475+
}
476+
// Update the decisions
477+
count, err := updateQuery.Save(ctx)
478+
if err != nil {
479+
c.Log.Errorf("unable to expire existing decisions: %s", err)
480+
continue
481+
}
482+
totalCount += count
483+
}
484+
485+
return totalCount, nil
486+
}

test/bats/cscli-allowlists.bats

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,58 @@ teardown() {
246246
rune -0 jq 'del(.created_at) | del(.updated_at) | del(.items.[].created_at) | del(.items.[].expiration)' <(output)
247247
assert_json '{"description":"a foo","items":[],"name":"foo"}'
248248
}
249+
250+
@test "allowlists expire active decisions" {
251+
rune -0 cscli decisions add -i 1.2.3.4
252+
rune -0 cscli decisions add -r 2.3.4.0/24
253+
rune -0 cscli decisions add -i 5.4.3.42
254+
rune -0 cscli decisions add -r 6.5.4.0/24
255+
rune -0 cscli decisions add -r 10.0.0.0/23
256+
257+
rune -0 cscli decisions list -o json
258+
rune -0 jq -r 'sort_by(.decisions[].value) | .[].decisions[0].value' <(output)
259+
assert_output - <<-EOT
260+
1.2.3.4
261+
10.0.0.0/23
262+
2.3.4.0/24
263+
5.4.3.42
264+
6.5.4.0/24
265+
EOT
266+
267+
rune -0 cscli allowlists create foo -d "foo"
268+
269+
# add an allowlist that matches exactly
270+
rune -0 cscli allowlists add foo 1.2.3.4
271+
if is_db_mysql; then sleep 2; fi
272+
# it should not be here anymore
273+
rune -0 cscli decisions list -o json
274+
rune -0 jq -e 'any(.[].decisions[]; .value == "1.2.3.4") | not' <(output)
275+
276+
# allowlist an IP belonging to a range
277+
rune -0 cscli allowlist add foo 2.3.4.42
278+
if is_db_mysql; then sleep 2; fi
279+
rune -0 cscli decisions list -o json
280+
rune -0 jq -e 'any(.[].decisions[]; .value == "2.3.4.0/24") | not' <(output)
281+
282+
# allowlist a range with an active decision inside
283+
rune -0 cscli allowlist add foo 5.4.3.0/24
284+
if is_db_mysql; then sleep 2; fi
285+
rune -0 cscli decisions list -o json
286+
rune -0 jq -e 'any(.[].decisions[]; .value == "5.4.3.42") | not' <(output)
287+
288+
# allowlist a range inside a range for which we have a decision
289+
rune -0 cscli allowlist add foo 6.5.4.0/25
290+
if is_db_mysql; then sleep 2; fi
291+
rune -0 cscli decisions list -o json
292+
rune -0 jq -e 'any(.[].decisions[]; .value == "6.5.4.0/24") | not' <(output)
293+
294+
# allowlist a range bigger than a range for which we have a decision
295+
rune -0 cscli allowlist add foo 10.0.0.0/24
296+
if is_db_mysql; then sleep 2; fi
297+
rune -0 cscli decisions list -o json
298+
rune -0 jq -e 'any(.[].decisions[]; .value == "10.0.0.0/24") | not' <(output)
299+
300+
# sanity check no more active decisions
301+
rune -0 cscli decisions list -o json
302+
assert_json []
303+
}

test/lib/config/config-global

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ load_init_data() {
116116

117117
dump_backend="$(cat "${LOCAL_INIT_DIR}/.backend")"
118118
if [[ "${DB_BACKEND}" != "${dump_backend}" ]]; then
119-
die "Can't run with backend '${DB_BACKEND}' because the test data was built with '${dump_backend}'"
119+
die "Can't run with backend '${DB_BACKEND}' because 'make bats-fixture' was ran with '${dump_backend}'"
120120
fi
121121

122122
remove_init_data

test/lib/config/config-local

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ load_init_data() {
168168

169169
dump_backend="$(cat "${LOCAL_INIT_DIR}/.backend")"
170170
if [[ "${DB_BACKEND}" != "${dump_backend}" ]]; then
171-
die "Can't run with backend '${DB_BACKEND}' because the test data was built with '${dump_backend}'"
171+
die "Can't run with backend '${DB_BACKEND}' because 'make bats-fixture' was ran with '${dump_backend}'"
172172
fi
173173

174174
remove_init_data

test/run-tests

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fi
2626

2727
dump_backend="$(cat "$LOCAL_INIT_DIR/.backend")"
2828
if [[ "$DB_BACKEND" != "$dump_backend" ]]; then
29-
die "Can't run with backend '$DB_BACKEND' because the test data was build with '$dump_backend'"
29+
die "Can't run with backend '$DB_BACKEND' because 'make bats-fixture' was ran with '$dump_backend'"
3030
fi
3131

3232
if [[ $# -ge 1 ]]; then

0 commit comments

Comments
 (0)