Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for centralized allowlists #3355

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dbb9adc
Centralized allowlists support
blotus Nov 7, 2024
804dfc4
conflicts
blotus Dec 9, 2024
5af9706
update allowlists before blocklists from CAPI
blotus Dec 9, 2024
468c822
ci
blotus Dec 9, 2024
3e08ab9
lint
blotus Dec 9, 2024
35095f7
fix test mode with no LAPI running
blotus Dec 9, 2024
198bc91
lint
blotus Dec 9, 2024
dc66afd
trim scenarios description
blotus Dec 9, 2024
c87b817
fix tests
blotus Dec 9, 2024
832868d
fix expiration date with cscli
blotus Dec 9, 2024
13a9686
fix allowlists for CAPI
blotus Dec 9, 2024
d7f8d2b
lint
blotus Dec 9, 2024
28ba0c9
move the allowlist check for manual decisions in cscli
blotus Dec 10, 2024
809f4f5
merge from master
blotus Dec 12, 2024
97fac0a
cscli allowlist add: support days for expiration
blotus Dec 12, 2024
c9bf246
lint
blotus Dec 12, 2024
85ab85d
Merge branch 'master' into centralized-allowlists
buixor Feb 4, 2025
426d6d4
inspect: also show basic data about the allowlist
blotus Feb 10, 2025
183b28d
mention allowlists in capi pull log
blotus Feb 10, 2025
4d7d75e
remove debug
blotus Feb 10, 2025
a5beb49
comment
blotus Feb 10, 2025
a541363
cscli allowlists: add raw output format
blotus Feb 10, 2025
3baf633
lint
blotus Feb 10, 2025
d48ff39
use in-memory dbclient for tests
blotus Feb 10, 2025
c832ed2
lint
blotus Feb 10, 2025
01ea040
more explicit log when console takes over an existing allowlist
blotus Feb 10, 2025
37a09e0
add test for allowlist alert creation
blotus Feb 10, 2025
e49693f
add test for allowlist pull from CAPI
blotus Feb 10, 2025
9f8d286
lint
blotus Feb 10, 2025
11cd0d3
Merge branch 'master' into centralized-allowlists
mmetc Feb 11, 2025
4ea0b1d
lint
mmetc Feb 11, 2025
3953474
lint
mmetc Feb 11, 2025
a34e802
when managed from the console, name/description are updatable
blotus Feb 11, 2025
61a79f0
rebaes
blotus Feb 11, 2025
fb313c0
show allowlist console URL when attempting to edit with cscli
blotus Feb 11, 2025
aea932d
check for nil pointer in allowlist received from CAPI
blotus Feb 11, 2025
b65da6f
lint
blotus Feb 11, 2025
3ff1538
revert capi dev usage
blotus Feb 11, 2025
aca0ea6
remove debug
blotus Feb 11, 2025
a692267
up
blotus Feb 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
648 changes: 648 additions & 0 deletions cmd/crowdsec-cli/cliallowlists/allowlists.go

Large diffs are not rendered by default.

30 changes: 21 additions & 9 deletions cmd/crowdsec-cli/clidecision/decisions.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@
return cmd
}

func (cli *cliDecisions) add(ctx context.Context, addIP, addRange, addDuration, addValue, addScope, addReason, addType string) error {
//nolint:revive // we'll reduce the number of args later
func (cli *cliDecisions) add(ctx context.Context, addIP, addRange, addDuration, addValue, addScope, addReason, addType string, bypassAllowlist bool) error {
alerts := models.AddAlertsRequest{}
origin := types.CscliOrigin
capacity := int32(0)
Expand Down Expand Up @@ -350,6 +351,15 @@
addReason = fmt.Sprintf("manual '%s' from '%s'", addType, cli.cfg().API.Client.Credentials.Login)
}

if !bypassAllowlist && (addScope == types.Ip || addScope == types.Range) {
resp, _, err := cli.client.Allowlists.CheckIfAllowlistedWithReason(ctx, addValue)
if err != nil {
log.Errorf("Cannot check if %s is in allowlist: %s", addValue, err)
} else if resp.Allowlisted {
return fmt.Errorf("%s is allowlisted by item %s, use --bypass-allowlist to add the decision anyway", addValue, resp.Reason)
}

Check warning on line 360 in cmd/crowdsec-cli/clidecision/decisions.go

View check run for this annotation

Codecov / codecov/patch

cmd/crowdsec-cli/clidecision/decisions.go#L359-L360

Added lines #L359 - L360 were not covered by tests
}

decision := models.Decision{
Duration: &addDuration,
Scope: &addScope,
Expand Down Expand Up @@ -398,13 +408,14 @@

func (cli *cliDecisions) newAddCmd() *cobra.Command {
var (
addIP string
addRange string
addDuration string
addValue string
addScope string
addReason string
addType string
addIP string
addRange string
addDuration string
addValue string
addScope string
addReason string
addType string
bypassAllowlist bool
)

cmd := &cobra.Command{
Expand All @@ -419,7 +430,7 @@
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.add(cmd.Context(), addIP, addRange, addDuration, addValue, addScope, addReason, addType)
return cli.add(cmd.Context(), addIP, addRange, addDuration, addValue, addScope, addReason, addType, bypassAllowlist)
},
}

Expand All @@ -432,6 +443,7 @@
flags.StringVar(&addScope, "scope", types.Ip, "Decision scope (ie. ip,range,username)")
flags.StringVarP(&addReason, "reason", "R", "", "Decision reason (ie. scenario-name)")
flags.StringVarP(&addType, "type", "t", "ban", "Decision type (ie. ban,captcha,throttle)")
flags.BoolVarP(&bypassAllowlist, "bypass-allowlist", "B", false, "Add decision even if value is in allowlist")

return cmd
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/crowdsec-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/trace"

"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clialert"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cliallowlists"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clibouncer"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clicapi"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cliconfig"
Expand Down Expand Up @@ -166,6 +168,8 @@ func (cli *cliRoot) initialize() error {
}
}

csConfig.DbConfig.LogLevel = ptr.Of(cli.wantedLogLevel())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated change ?


return nil
}

Expand Down Expand Up @@ -282,6 +286,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
cmd.AddCommand(cliitem.NewContext(cli.cfg).NewCommand())
cmd.AddCommand(cliitem.NewAppsecConfig(cli.cfg).NewCommand())
cmd.AddCommand(cliitem.NewAppsecRule(cli.cfg).NewCommand())
cmd.AddCommand(cliallowlists.New(cli.cfg).NewCommand())

cli.addSetup(cmd)

Expand Down
16 changes: 14 additions & 2 deletions cmd/crowdsec/crowdsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
"github.com/crowdsecurity/crowdsec/pkg/alertcontext"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
Expand All @@ -23,7 +24,7 @@ import (
)

// initCrowdsec prepares the log processor service
func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, []acquisition.DataSource, error) {
func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub, testMode bool) (*parser.Parsers, []acquisition.DataSource, error) {
var err error

if err = alertcontext.LoadConsoleContext(cConfig, hub); err != nil {
Expand Down Expand Up @@ -51,6 +52,17 @@ func initCrowdsec(cConfig *csconfig.Config, hub *cwhub.Hub) (*parser.Parsers, []
return nil, nil, err
}

if !testMode {
err = apiclient.InitLAPIClient(
context.TODO(), cConfig.API.Client.Credentials.URL, cConfig.API.Client.Credentials.PapiURL,
cConfig.API.Client.Credentials.Login, cConfig.API.Client.Credentials.Password,
hub.GetInstalledListForAPI())

if err != nil {
return nil, nil, fmt.Errorf("while initializing LAPIClient: %w", err)
}
}

datasources, err := LoadAcquisition(cConfig)
if err != nil {
return nil, nil, fmt.Errorf("while loading acquisition config: %w", err)
Expand Down Expand Up @@ -116,7 +128,7 @@ func runCrowdsec(cConfig *csconfig.Config, parsers *parser.Parsers, hub *cwhub.H
})
bucketWg.Wait()

apiClient, err := AuthenticatedLAPIClient(context.TODO(), *cConfig.API.Client.Credentials, hub)
apiClient, err := apiclient.GetLAPIClient()
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/crowdsec/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
return nil, err
}

csParsers, datasources, err := initCrowdsec(cConfig, hub)
csParsers, datasources, err := initCrowdsec(cConfig, hub, false)

Check warning on line 97 in cmd/crowdsec/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/crowdsec/serve.go#L97

Added line #L97 was not covered by tests
if err != nil {
return nil, fmt.Errorf("unable to init crowdsec: %w", err)
}
Expand Down Expand Up @@ -396,7 +396,7 @@
return err
}

csParsers, datasources, err := initCrowdsec(cConfig, hub)
csParsers, datasources, err := initCrowdsec(cConfig, hub, flags.TestMode)
if err != nil {
return fmt.Errorf("crowdsec init: %w", err)
}
Expand Down
116 changes: 104 additions & 12 deletions pkg/acquisition/modules/appsec/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"github.com/crowdsecurity/go-cs-lib/trace"

"github.com/crowdsecurity/crowdsec/pkg/acquisition/configuration"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/appsec"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/types"
Expand All @@ -31,6 +32,8 @@
)

var DefaultAuthCacheDuration = (1 * time.Minute)
var negativeAllowlistCacheDuration = (5 * time.Minute)
var positiveAllowlistCacheDuration = (5 * time.Minute)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like a lot to me, but it depends if SE gets a force_pull via PAPI when the allowlist is updated ?


// configuration structure of the acquis for the application security engine
type AppsecSourceConfig struct {
Expand All @@ -49,18 +52,20 @@

// runtime structure of AppsecSourceConfig
type AppsecSource struct {
metricsLevel int
config AppsecSourceConfig
logger *log.Entry
mux *http.ServeMux
server *http.Server
outChan chan types.Event
InChan chan appsec.ParsedRequest
AppsecRuntime *appsec.AppsecRuntimeConfig
AppsecConfigs map[string]appsec.AppsecConfig
lapiURL string
AuthCache AuthCache
AppsecRunners []AppsecRunner // one for each go-routine
metricsLevel int
config AppsecSourceConfig
logger *log.Entry
mux *http.ServeMux
server *http.Server
outChan chan types.Event
InChan chan appsec.ParsedRequest
AppsecRuntime *appsec.AppsecRuntimeConfig
AppsecConfigs map[string]appsec.AppsecConfig
lapiURL string
AuthCache AuthCache
AppsecRunners []AppsecRunner // one for each go-routine
allowlistCache allowlistCache
apiClient *apiclient.ApiClient
}

// Struct to handle cache of authentication
Expand All @@ -69,6 +74,17 @@
mu sync.RWMutex
}

// FIXME: auth and allowlist should probably be merged to a common structure
type allowlistCache struct {
mu sync.RWMutex
allowlist map[string]allowlistCacheEntry
}

type allowlistCacheEntry struct {
allowlisted bool
expiration time.Time
}

func NewAuthCache() AuthCache {
return AuthCache{
APIKeys: make(map[string]time.Time, 0),
Expand All @@ -90,6 +106,30 @@
return expiration, exists
}

func NewAllowlistCache() allowlistCache {
return allowlistCache{
allowlist: make(map[string]allowlistCacheEntry, 0),
mu: sync.RWMutex{},
}

Check warning on line 113 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L109-L113

Added lines #L109 - L113 were not covered by tests
}

func (ac *allowlistCache) Set(value string, allowlisted bool, expiration time.Time) {
ac.mu.Lock()
ac.allowlist[value] = allowlistCacheEntry{
allowlisted: allowlisted,
expiration: expiration,
}
ac.mu.Unlock()

Check warning on line 122 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L116-L122

Added lines #L116 - L122 were not covered by tests
}

func (ac *allowlistCache) Get(value string) (bool, time.Time, bool) {
ac.mu.RLock()
entry, exists := ac.allowlist[value]
ac.mu.RUnlock()

return entry.allowlisted, entry.expiration, exists

Check warning on line 130 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L125-L130

Added lines #L125 - L130 were not covered by tests
}

// @tko + @sbl : we might want to get rid of that or improve it
type BodyResponse struct {
Action string `json:"action"`
Expand Down Expand Up @@ -246,6 +286,12 @@
// We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec
w.mux.HandleFunc(w.config.Path, w.appsecHandler)

w.apiClient, err = apiclient.GetLAPIClient()
if err != nil {
return fmt.Errorf("unable to get authenticated LAPI client: %w", err)
}
w.allowlistCache = NewAllowlistCache()

Check warning on line 294 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L289-L294

Added lines #L289 - L294 were not covered by tests
return nil
}

Expand Down Expand Up @@ -377,6 +423,33 @@
return resp.StatusCode == http.StatusOK
}

func (w *AppsecSource) isAllowlisted(ctx context.Context, value string, query bool) bool {
var err error

allowlisted, expiration, exists := w.allowlistCache.Get(value)
if exists && !time.Now().After(expiration) {
return allowlisted
}

Check warning on line 432 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L426-L432

Added lines #L426 - L432 were not covered by tests

if !query {
return false
}

Check warning on line 436 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L434-L436

Added lines #L434 - L436 were not covered by tests

allowlisted, _, err = w.apiClient.Allowlists.CheckIfAllowlisted(ctx, value)
if err != nil {
w.logger.Errorf("unable to check if %s is allowlisted: %s", value, err)
return false
}

Check warning on line 442 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L438-L442

Added lines #L438 - L442 were not covered by tests

if allowlisted {
w.allowlistCache.Set(value, allowlisted, time.Now().Add(positiveAllowlistCacheDuration))
} else {
w.allowlistCache.Set(value, allowlisted, time.Now().Add(negativeAllowlistCacheDuration))
}

Check warning on line 448 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L444-L448

Added lines #L444 - L448 were not covered by tests

return allowlisted

Check warning on line 450 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L450

Added line #L450 was not covered by tests
}

// should this be in the runner ?
func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
w.logger.Debugf("Received request from '%s' on %s", r.RemoteAddr, r.URL.Path)
Expand Down Expand Up @@ -406,6 +479,25 @@
w.AuthCache.Set(apiKey, time.Now().Add(*w.config.AuthCacheDuration))
}

// check if the client IP is allowlisted
if w.isAllowlisted(r.Context(), clientIP, false) {
w.logger.Infof("%s is allowlisted by LAPI, not processing", clientIP)
statusCode, appsecResponse := w.AppsecRuntime.GenerateResponse(appsec.AppsecTempResponse{
InBandInterrupt: false,
OutOfBandInterrupt: false,
Action: appsec.AllowRemediation,
}, w.logger)
body, err := json.Marshal(appsecResponse)
if err != nil {
w.logger.Errorf("unable to serialize response: %s", err)
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(statusCode)
rw.Write(body)
return

Check warning on line 498 in pkg/acquisition/modules/appsec/appsec.go

View check run for this annotation

Codecov / codecov/patch

pkg/acquisition/modules/appsec/appsec.go#L483-L498

Added lines #L483 - L498 were not covered by tests
}

// parse the request only once
parsedRequest, err := appsec.NewParsedRequestFromRequest(r, w.logger)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func (lc *LokiClient) getURLFor(endpoint string, params map[string]string) strin
func (lc *LokiClient) Ready(ctx context.Context) error {
tick := time.NewTicker(500 * time.Millisecond)
url := lc.getURLFor("ready", nil)
lc.Logger.Debugf("Using url: %s for ready check", url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated change?

for {
select {
case <-ctx.Done():
Expand Down
Loading
Loading