mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-10 20:05:55 +02:00
486 lines
13 KiB
Go
486 lines
13 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
|
"github.com/crowdsecurity/crowdsec/pkg/database/ent/allowlist"
|
|
"github.com/crowdsecurity/crowdsec/pkg/database/ent/allowlistitem"
|
|
"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
|
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
|
)
|
|
|
|
func (c *Client) CreateAllowList(ctx context.Context, name string, description string, allowlistID string, fromConsole bool) (*ent.AllowList, error) {
|
|
allowlist, err := c.Ent.AllowList.Create().
|
|
SetName(name).
|
|
SetFromConsole(fromConsole).
|
|
SetDescription(description).
|
|
SetAllowlistID(allowlistID).
|
|
Save(ctx)
|
|
if err != nil {
|
|
if sqlgraph.IsUniqueConstraintError(err) {
|
|
return nil, fmt.Errorf("allowlist '%s' already exists", name)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to create allowlist: %w", err)
|
|
}
|
|
|
|
return allowlist, nil
|
|
}
|
|
|
|
func (c *Client) DeleteAllowList(ctx context.Context, name string, fromConsole bool) error {
|
|
nbDeleted, err := c.Ent.AllowListItem.Delete().Where(allowlistitem.HasAllowlistWith(allowlist.NameEQ(name), allowlist.FromConsoleEQ(fromConsole))).Exec(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete allowlist items: %w", err)
|
|
}
|
|
|
|
c.Log.Debugf("deleted %d items from allowlist %s", nbDeleted, name)
|
|
|
|
nbDeleted, err = c.Ent.AllowList.
|
|
Delete().
|
|
Where(allowlist.NameEQ(name), allowlist.FromConsoleEQ(fromConsole)).
|
|
Exec(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete allowlist: %w", err)
|
|
}
|
|
|
|
if nbDeleted == 0 {
|
|
return fmt.Errorf("allowlist %s not found", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) DeleteAllowListByID(ctx context.Context, name string, allowlistID string, fromConsole bool) error {
|
|
nbDeleted, err := c.Ent.AllowListItem.Delete().Where(allowlistitem.HasAllowlistWith(allowlist.AllowlistIDEQ(allowlistID), allowlist.FromConsoleEQ(fromConsole))).Exec(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete allowlist items: %w", err)
|
|
}
|
|
|
|
c.Log.Debugf("deleted %d items from allowlist %s", nbDeleted, name)
|
|
|
|
nbDeleted, err = c.Ent.AllowList.
|
|
Delete().
|
|
Where(allowlist.AllowlistIDEQ(allowlistID), allowlist.FromConsoleEQ(fromConsole)).
|
|
Exec(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to delete allowlist: %w", err)
|
|
}
|
|
|
|
if nbDeleted == 0 {
|
|
return fmt.Errorf("allowlist %s not found", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) ListAllowLists(ctx context.Context, withContent bool) ([]*ent.AllowList, error) {
|
|
q := c.Ent.AllowList.Query()
|
|
if withContent {
|
|
q = q.WithAllowlistItems()
|
|
}
|
|
|
|
result, err := q.All(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to list allowlists: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) GetAllowList(ctx context.Context, name string, withContent bool) (*ent.AllowList, error) {
|
|
q := c.Ent.AllowList.Query().Where(allowlist.NameEQ(name))
|
|
if withContent {
|
|
q = q.WithAllowlistItems()
|
|
}
|
|
|
|
result, err := q.First(ctx)
|
|
if err != nil {
|
|
if ent.IsNotFound(err) {
|
|
return nil, fmt.Errorf("allowlist '%s' not found", name)
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) GetAllowListByID(ctx context.Context, allowlistID string, withContent bool) (*ent.AllowList, error) {
|
|
q := c.Ent.AllowList.Query().Where(allowlist.AllowlistIDEQ(allowlistID))
|
|
if withContent {
|
|
q = q.WithAllowlistItems()
|
|
}
|
|
|
|
result, err := q.First(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) AddToAllowlist(ctx context.Context, list *ent.AllowList, items []*models.AllowlistItem) (int, error) {
|
|
added := 0
|
|
|
|
c.Log.Debugf("adding %d values to allowlist %s", len(items), list.Name)
|
|
c.Log.Tracef("values: %+v", items)
|
|
|
|
txClient, err := c.Ent.Tx(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("error creating transaction: %w", err)
|
|
}
|
|
|
|
for _, item := range items {
|
|
c.Log.Debugf("adding value %s to allowlist %s", item.Value, list.Name)
|
|
|
|
sz, start_ip, start_sfx, end_ip, end_sfx, err := types.Addr2Ints(item.Value)
|
|
if err != nil {
|
|
c.Log.Error(err)
|
|
continue
|
|
}
|
|
|
|
query := txClient.AllowListItem.Create().
|
|
SetValue(item.Value).
|
|
SetIPSize(int64(sz)).
|
|
SetStartIP(start_ip).
|
|
SetStartSuffix(start_sfx).
|
|
SetEndIP(end_ip).
|
|
SetEndSuffix(end_sfx).
|
|
SetComment(item.Description)
|
|
|
|
if !time.Time(item.Expiration).IsZero() {
|
|
query = query.SetExpiresAt(time.Time(item.Expiration).UTC())
|
|
}
|
|
|
|
content, err := query.Save(ctx)
|
|
if err != nil {
|
|
return 0, rollbackOnError(txClient, err, "unable to add value to allowlist")
|
|
}
|
|
|
|
c.Log.Debugf("Updating allowlist %s with value %s (exp: %s)", list.Name, item.Value, item.Expiration)
|
|
|
|
// We don't have a clean way to handle name conflict from the console, so use id
|
|
err = txClient.AllowList.Update().AddAllowlistItems(content).Where(allowlist.IDEQ(list.ID)).Exec(ctx)
|
|
if err != nil {
|
|
c.Log.Errorf("unable to add value to allowlist: %s", err)
|
|
continue
|
|
}
|
|
|
|
added++
|
|
}
|
|
|
|
err = txClient.Commit()
|
|
if err != nil {
|
|
return 0, rollbackOnError(txClient, err, "error committing transaction")
|
|
}
|
|
|
|
return added, nil
|
|
}
|
|
|
|
func (c *Client) RemoveFromAllowlist(ctx context.Context, list *ent.AllowList, values ...string) (int, error) {
|
|
c.Log.Debugf("removing %d values from allowlist %s", len(values), list.Name)
|
|
c.Log.Tracef("values: %v", values)
|
|
|
|
nbDeleted, err := c.Ent.AllowListItem.Delete().Where(
|
|
allowlistitem.HasAllowlistWith(allowlist.IDEQ(list.ID)),
|
|
allowlistitem.ValueIn(values...),
|
|
).Exec(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to remove values from allowlist: %w", err)
|
|
}
|
|
|
|
return nbDeleted, nil
|
|
}
|
|
|
|
func (c *Client) UpdateAllowlistMeta(ctx context.Context, allowlistID string, name string, description string) error {
|
|
c.Log.Debugf("updating allowlist %s meta", name)
|
|
|
|
err := c.Ent.AllowList.Update().Where(allowlist.AllowlistIDEQ(allowlistID)).SetName(name).SetDescription(description).Exec(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to update allowlist: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) ReplaceAllowlist(ctx context.Context, list *ent.AllowList, items []*models.AllowlistItem, fromConsole bool) (int, error) {
|
|
c.Log.Debugf("replacing values in allowlist %s", list.Name)
|
|
c.Log.Tracef("items: %+v", items)
|
|
|
|
_, err := c.Ent.AllowListItem.Delete().Where(allowlistitem.HasAllowlistWith(allowlist.IDEQ(list.ID))).Exec(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to delete allowlist contents: %w", err)
|
|
}
|
|
|
|
added, err := c.AddToAllowlist(ctx, list, items)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to add values to allowlist: %w", err)
|
|
}
|
|
|
|
if !list.FromConsole && fromConsole {
|
|
c.Log.Infof("marking allowlist %s as managed from console and replacing its content", list.Name)
|
|
|
|
err = c.Ent.AllowList.Update().SetFromConsole(fromConsole).Where(allowlist.IDEQ(list.ID)).Exec(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to update allowlist: %w", err)
|
|
}
|
|
}
|
|
|
|
return added, nil
|
|
}
|
|
|
|
func (c *Client) IsAllowlistedBy(ctx context.Context, value string) ([]string, error) {
|
|
/*
|
|
Few cases:
|
|
- value is an IP/range directly is in allowlist
|
|
- value is an IP/range in a range in allowlist
|
|
- value is a range and an IP/range belonging to it is in allowlist
|
|
*/
|
|
sz, start_ip, start_sfx, end_ip, end_sfx, err := types.Addr2Ints(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.Log.Debugf("checking if %s is allowlisted", value)
|
|
|
|
now := time.Now().UTC()
|
|
query := c.Ent.AllowListItem.Query().Where(
|
|
allowlistitem.Or(
|
|
allowlistitem.ExpiresAtGTE(now),
|
|
allowlistitem.ExpiresAtIsNil(),
|
|
),
|
|
allowlistitem.IPSizeEQ(int64(sz)),
|
|
)
|
|
|
|
if sz == 4 {
|
|
query = query.Where(
|
|
allowlistitem.Or(
|
|
// Value contained inside a range or exact match
|
|
allowlistitem.And(
|
|
allowlistitem.StartIPLTE(start_ip),
|
|
allowlistitem.EndIPGTE(end_ip),
|
|
),
|
|
// Value contains another allowlisted value
|
|
allowlistitem.And(
|
|
allowlistitem.StartIPGTE(start_ip),
|
|
allowlistitem.EndIPLTE(end_ip),
|
|
),
|
|
))
|
|
}
|
|
|
|
if sz == 16 {
|
|
query = query.Where(
|
|
// Value contained inside a range or exact match
|
|
allowlistitem.Or(
|
|
allowlistitem.And(
|
|
allowlistitem.Or(
|
|
allowlistitem.StartIPLT(start_ip),
|
|
allowlistitem.And(
|
|
allowlistitem.StartIPEQ(start_ip),
|
|
allowlistitem.StartSuffixLTE(start_sfx),
|
|
)),
|
|
allowlistitem.Or(
|
|
allowlistitem.EndIPGT(end_ip),
|
|
allowlistitem.And(
|
|
allowlistitem.EndIPEQ(end_ip),
|
|
allowlistitem.EndSuffixGTE(end_sfx),
|
|
),
|
|
),
|
|
),
|
|
// Value contains another allowlisted value
|
|
allowlistitem.And(
|
|
allowlistitem.Or(
|
|
allowlistitem.StartIPGT(start_ip),
|
|
allowlistitem.And(
|
|
allowlistitem.StartIPEQ(start_ip),
|
|
allowlistitem.StartSuffixGTE(start_sfx),
|
|
)),
|
|
allowlistitem.Or(
|
|
allowlistitem.EndIPLT(end_ip),
|
|
allowlistitem.And(
|
|
allowlistitem.EndIPEQ(end_ip),
|
|
allowlistitem.EndSuffixLTE(end_sfx),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
items, err := query.WithAllowlist().All(ctx)
|
|
if err != 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
|
|
}
|
|
|
|
reason := item.Value + " from " + item.Edges.Allowlist[0].Name
|
|
if item.Comment != "" {
|
|
reason += " (" + item.Comment + ")"
|
|
}
|
|
|
|
reasons = append(reasons, reason)
|
|
}
|
|
|
|
return reasons, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (c *Client) GetAllowlistsContentForAPIC(ctx context.Context) ([]net.IP, []*net.IPNet, error) {
|
|
allowlists, err := c.ListAllowLists(ctx, true)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to get allowlists: %w", err)
|
|
}
|
|
|
|
var (
|
|
ips []net.IP
|
|
nets []*net.IPNet
|
|
)
|
|
|
|
for _, allowlist := range allowlists {
|
|
for _, item := range allowlist.Edges.AllowlistItems {
|
|
if item.ExpiresAt.IsZero() || item.ExpiresAt.After(time.Now().UTC()) {
|
|
if strings.Contains(item.Value, "/") {
|
|
_, ipNet, err := net.ParseCIDR(item.Value)
|
|
if err != nil {
|
|
c.Log.Errorf("unable to parse CIDR %s: %s", item.Value, err)
|
|
continue
|
|
}
|
|
|
|
nets = append(nets, ipNet)
|
|
} else {
|
|
ip := net.ParseIP(item.Value)
|
|
if ip == nil {
|
|
c.Log.Errorf("unable to parse IP %s", item.Value)
|
|
continue
|
|
}
|
|
|
|
ips = append(ips, ip)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ips, nets, nil
|
|
}
|
|
|
|
func (c *Client) ApplyAllowlistsToExistingDecisions(ctx context.Context) (int, error) {
|
|
// Soft delete (set expiration to now) all decisions that matches any allowlist
|
|
|
|
totalCount := 0
|
|
|
|
// Get all non-expired allowlist items
|
|
// We will match them one by one against all decisions
|
|
allowlistItems, err := c.Ent.AllowListItem.Query().
|
|
Where(
|
|
allowlistitem.Or(
|
|
allowlistitem.ExpiresAtGTE(time.Now().UTC()),
|
|
allowlistitem.ExpiresAtIsNil(),
|
|
),
|
|
).All(ctx)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to get allowlist items: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
|
|
for _, item := range allowlistItems {
|
|
updateQuery := c.Ent.Decision.Update().SetUntil(now).Where(decision.UntilGTE(now))
|
|
switch item.IPSize {
|
|
case 4:
|
|
updateQuery = updateQuery.Where(
|
|
decision.And(
|
|
decision.IPSizeEQ(4),
|
|
decision.Or(
|
|
decision.And(
|
|
decision.StartIPLTE(item.StartIP),
|
|
decision.EndIPGTE(item.EndIP),
|
|
),
|
|
decision.And(
|
|
decision.StartIPGTE(item.StartIP),
|
|
decision.EndIPLTE(item.EndIP),
|
|
),
|
|
)))
|
|
case 16:
|
|
updateQuery = updateQuery.Where(
|
|
decision.And(
|
|
decision.IPSizeEQ(16),
|
|
decision.Or(
|
|
decision.And(
|
|
decision.Or(
|
|
decision.StartIPLT(item.StartIP),
|
|
decision.And(
|
|
decision.StartIPEQ(item.StartIP),
|
|
decision.StartSuffixLTE(item.StartSuffix),
|
|
)),
|
|
decision.Or(
|
|
decision.EndIPGT(item.EndIP),
|
|
decision.And(
|
|
decision.EndIPEQ(item.EndIP),
|
|
decision.EndSuffixGTE(item.EndSuffix),
|
|
),
|
|
),
|
|
),
|
|
decision.And(
|
|
decision.Or(
|
|
decision.StartIPGT(item.StartIP),
|
|
decision.And(
|
|
decision.StartIPEQ(item.StartIP),
|
|
decision.StartSuffixGTE(item.StartSuffix),
|
|
)),
|
|
decision.Or(
|
|
decision.EndIPLT(item.EndIP),
|
|
decision.And(
|
|
decision.EndIPEQ(item.EndIP),
|
|
decision.EndSuffixLTE(item.EndSuffix),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
default:
|
|
// This should never happen
|
|
// But better safe than sorry and just skip it instead of expiring all decisions
|
|
c.Log.Errorf("unexpected IP size %d for allowlist item %s", item.IPSize, item.Value)
|
|
continue
|
|
}
|
|
// Update the decisions
|
|
count, err := updateQuery.Save(ctx)
|
|
if err != nil {
|
|
c.Log.Errorf("unable to expire existing decisions: %s", err)
|
|
continue
|
|
}
|
|
totalCount += count
|
|
}
|
|
|
|
return totalCount, nil
|
|
}
|