Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 57 additions & 5 deletions pkg/acquisition/modules/appsec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"os"
"strings"
"time"

yaml "github.com/goccy/go-yaml"
Expand All @@ -20,6 +21,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/apiclient/useragent"
"github.com/crowdsecurity/crowdsec/pkg/appsec"
"github.com/crowdsecurity/crowdsec/pkg/appsec/allowlists"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/metrics"
)

Expand Down Expand Up @@ -121,6 +124,41 @@ func loadCertPool(caCertPath string, logger log.FieldLogger) (*x509.CertPool, er
return caCertPool, nil
}

// resolveAppsecConfigEntry expands a single appsec_configs entry into a list of
// installed appsec-config item names. Entries without glob meta-characters are
// returned as-is so the per-name "no appsec-config found" error surfaces from
// AppsecConfig.Load. Entries containing '*' or '?' are matched against installed
// hub items using the same expression helper used elsewhere in the appsec stack.
func resolveAppsecConfigEntry(entry string, hub *cwhub.Hub) ([]string, error) {
if !strings.ContainsAny(entry, "*?") {
return []string{entry}, nil
}

matches := []string{}

for _, item := range hub.GetInstalledByType(cwhub.APPSEC_CONFIGS, true) {
tmpMatch, err := exprhelpers.Match(entry, item.Name)
if err != nil {
return nil, fmt.Errorf("unable to match %q against %q: %w", entry, item.Name, err)
}

matched, ok := tmpMatch.(bool)
if !ok {
return nil, fmt.Errorf("unexpected match result type %T for %q against %q", tmpMatch, entry, item.Name)
}

if matched {
matches = append(matches, item.Name)
}
}

if len(matches) == 0 {
return nil, fmt.Errorf("no installed appsec-config matches pattern %q", entry)
}

return matches, nil
}

func (w *Source) Configure(_ context.Context, yamlConfig []byte, logger *log.Entry, _ metrics.AcquisitionMetricsLevel) error {
if w.hub == nil {
return errors.New("appsec datasource requires a hub. this is a bug, please report")
Expand Down Expand Up @@ -178,15 +216,29 @@ func (w *Source) Configure(_ context.Context, yamlConfig []byte, logger *log.Ent
return fmt.Errorf("unable to load appsec_config: %w", err)
}
} else if w.config.AppsecConfig != "" {
if err := appsecCfg.Load(w.config.AppsecConfig, w.hub); err != nil {
return fmt.Errorf("unable to load appsec_config: %w", err)
names, err := resolveAppsecConfigEntry(w.config.AppsecConfig, w.hub)
if err != nil {
return fmt.Errorf("unable to resolve appsec_config entry %q: %w", w.config.AppsecConfig, err)
}
} else if len(w.config.AppsecConfigs) > 0 {
for _, appsecConfig := range w.config.AppsecConfigs {
if err := appsecCfg.Load(appsecConfig, w.hub); err != nil {

for _, name := range names {
if err := appsecCfg.Load(name, w.hub); err != nil {
return fmt.Errorf("unable to load appsec_config: %w", err)
}
}
} else if len(w.config.AppsecConfigs) > 0 {
for _, entry := range w.config.AppsecConfigs {
names, err := resolveAppsecConfigEntry(entry, w.hub)
if err != nil {
return fmt.Errorf("unable to resolve appsec_config entry %q: %w", entry, err)
}

for _, name := range names {
if err := appsecCfg.Load(name, w.hub); err != nil {
return fmt.Errorf("unable to load appsec_config: %w", err)
}
}
}
} else {
return errors.New("no appsec_config provided")
}
Expand Down
147 changes: 147 additions & 0 deletions pkg/acquisition/modules/appsec/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package appsecacquisition

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
)

// newTestHub builds an in-memory hub from a JSON index. It mirrors the
// `testHub` helper used in pkg/cwhub but is duplicated here because that helper
// is unexported.
func newTestHub(t *testing.T, indexJSON string) *cwhub.Hub {
t.Helper()

tempDir := t.TempDir()
local := &csconfig.LocalHubCfg{
HubDir: filepath.Join(tempDir, "hub"),
HubIndexFile: filepath.Join(tempDir, "hub", ".index.json"),
InstallDir: filepath.Join(tempDir, "install"),
InstallDataDir: filepath.Join(tempDir, "data"),
}

require.NoError(t, os.MkdirAll(local.HubDir, 0o755))
require.NoError(t, os.MkdirAll(local.InstallDir, 0o755))
require.NoError(t, os.MkdirAll(local.InstallDataDir, 0o755))
require.NoError(t, os.WriteFile(local.HubIndexFile, []byte(indexJSON), 0o644))

hub, err := cwhub.NewHub(local, nil)
require.NoError(t, err)
require.NoError(t, hub.Load())

return hub
}

// markInstalled flags an appsec-config hub item as installed by setting its
// LocalPath, which is what ItemState.IsInstalled() checks.
func markInstalled(t *testing.T, hub *cwhub.Hub, name string) {
t.Helper()

item := hub.GetItem(cwhub.APPSEC_CONFIGS, name)
require.NotNilf(t, item, "appsec-config %q missing from test hub", name)
item.State.LocalPath = filepath.Join("/", "fake", "install", item.Type, name+".yaml")

Check failure on line 48 in pkg/acquisition/modules/appsec/config_test.go

View workflow job for this annotation

GitHub Actions / Build + tests

filepathJoin: "/" contains a path separator (gocritic)

Check failure on line 48 in pkg/acquisition/modules/appsec/config_test.go

View workflow job for this annotation

GitHub Actions / Build + tests

filepathJoin: "/" contains a path separator (gocritic)
}

// hubFixture sets up a hub with three appsec-configs under crowdsecurity/* and
// one under custom/*. Two of the crowdsecurity items are marked installed; the
// custom one is also installed; one crowdsecurity item is left uninstalled to
// verify wildcard expansion ignores it.
const hubFixtureIndex = `{
"appsec-configs": {
"crowdsecurity/vpatch": {
"path": "appsec-configs/crowdsecurity/vpatch.yaml",
"version": "1.0",
"versions": {"1.0": {"digest": "aa"}}
},
"crowdsecurity/generic": {
"path": "appsec-configs/crowdsecurity/generic.yaml",
"version": "1.0",
"versions": {"1.0": {"digest": "bb"}}
},
"crowdsecurity/uninstalled": {
"path": "appsec-configs/crowdsecurity/uninstalled.yaml",
"version": "1.0",
"versions": {"1.0": {"digest": "cc"}}
},
"custom/my-config": {
"path": "appsec-configs/custom/my-config.yaml",
"version": "1.0",
"versions": {"1.0": {"digest": "dd"}}
}
}
}`

func newPopulatedHub(t *testing.T) *cwhub.Hub {
t.Helper()
hub := newTestHub(t, hubFixtureIndex)
markInstalled(t, hub, "crowdsecurity/vpatch")
markInstalled(t, hub, "crowdsecurity/generic")
markInstalled(t, hub, "custom/my-config")
return hub
}

func TestResolveAppsecConfigEntry_LiteralPassThrough(t *testing.T) {
hub := newPopulatedHub(t)

// Literal entries are returned as-is without consulting the hub. This
// preserves the existing per-name "no appsec-config found for X" error
// path inside AppsecConfig.Load for typos.
got, err := resolveAppsecConfigEntry("crowdsecurity/vpatch", hub)
require.NoError(t, err)
assert.Equal(t, []string{"crowdsecurity/vpatch"}, got)

got, err = resolveAppsecConfigEntry("does/not-exist", hub)
require.NoError(t, err)
assert.Equal(t, []string{"does/not-exist"}, got)
}

func TestResolveAppsecConfigEntry_WildcardExpands(t *testing.T) {
hub := newPopulatedHub(t)

got, err := resolveAppsecConfigEntry("crowdsecurity/*", hub)
require.NoError(t, err)
// Only installed items match; sorted (case-insensitive) order from the hub.
assert.Equal(t, []string{"crowdsecurity/generic", "crowdsecurity/vpatch"}, got)
}

func TestResolveAppsecConfigEntry_WildcardMatchesAll(t *testing.T) {
hub := newPopulatedHub(t)

got, err := resolveAppsecConfigEntry("*", hub)
require.NoError(t, err)
assert.Equal(t, []string{"crowdsecurity/generic", "crowdsecurity/vpatch", "custom/my-config"}, got)
}

func TestResolveAppsecConfigEntry_QuestionMark(t *testing.T) {
hub := newPopulatedHub(t)

// '?' matches a single character, so this should match neither
// "crowdsecurity/vpatch" nor "crowdsecurity/generic".
_, err := resolveAppsecConfigEntry("crowdsecurity/?", hub)
require.Error(t, err)
assert.Contains(t, err.Error(), "no installed appsec-config matches pattern")
}

func TestResolveAppsecConfigEntry_NoMatchErrors(t *testing.T) {
hub := newPopulatedHub(t)

_, err := resolveAppsecConfigEntry("nope/*", hub)
require.Error(t, err)
assert.Contains(t, err.Error(), `no installed appsec-config matches pattern "nope/*"`)
}

func TestResolveAppsecConfigEntry_WildcardSkipsUninstalled(t *testing.T) {
hub := newPopulatedHub(t)

got, err := resolveAppsecConfigEntry("crowdsecurity/*", hub)
require.NoError(t, err)
// "crowdsecurity/uninstalled" is in the index but not installed, so it
// must not appear in the expansion.
assert.NotContains(t, got, "crowdsecurity/uninstalled")
}
Loading