diff --git a/ee/tables/secretscan/config.toml b/ee/tables/secretscan/config.toml new file mode 100644 index 000000000..616bc1030 --- /dev/null +++ b/ee/tables/secretscan/config.toml @@ -0,0 +1,15 @@ +# This config extends the default gitleaks config with some additional settings +# to avoid false positives. +title = "Kolide secretscan config" + +[extend] +useDefault = true + +# Ignore K8s Sealed Secrets +# https://github.com/gitleaks/gitleaks/issues/1728 +[[rules]] +id = "generic-api-key" +[[rules.allowlists]] +condition = "AND" +regexes = ['''Ag[a-zA-Z0-9+/]{500,}={0,2}'''] +paths = ['''(?i).*\.ya?ml$'''] diff --git a/ee/tables/secretscan/table.go b/ee/tables/secretscan/table.go index 28bdfd2d1..7539da7fe 100644 --- a/ee/tables/secretscan/table.go +++ b/ee/tables/secretscan/table.go @@ -2,6 +2,7 @@ package secretscan import ( "context" + _ "embed" "encoding/base64" "encoding/json" "fmt" @@ -31,30 +32,43 @@ const ( directoryScanConcurrency = 4 ) -func newDefaultConfig() (config.Config, error) { +var ( + //go:embed config.toml + rawKolideConfig string + kolideConfig *config.Config + configErr error +) + +// newConfigOnce sets up our config, which pulls in the default gitleaks config as our base. +// When gitleaks pulls in the default config, it updates multiple global vars (`viper.SetConfigType("toml")`, +// and a private variable extendDepth). We use a OnceFunc here to avoid both the data race +// and undesirable behavior resulting from extendDepth being modified multiple times. +var newConfigOnce = sync.OnceFunc(func() { v := viper.New() // init viper here so we don't update a global var v.SetConfigType("toml") - err := v.ReadConfig(strings.NewReader(config.DefaultConfig)) + err := v.ReadConfig(strings.NewReader(rawKolideConfig)) if err != nil { - return config.Config{}, err + configErr = fmt.Errorf("reading config: %w", err) + return } var vc config.ViperConfig err = v.Unmarshal(&vc) if err != nil { - return config.Config{}, err + configErr = fmt.Errorf("unmarshalling config: %w", err) + return } + cfg, err := vc.Translate() if err != nil { - return config.Config{}, err + configErr = fmt.Errorf("translating: %w", err) + return } - return cfg, nil -} + + kolideConfig = &cfg +}) type Table struct { - slogger *slog.Logger - defaultConfig *config.Config - configOnce sync.Once - configErr error + slogger *slog.Logger } func TablePlugin(flags types.Flags, slogger *slog.Logger) *table.Plugin { @@ -83,16 +97,9 @@ func TablePlugin(flags types.Flags, slogger *slog.Logger) *table.Plugin { } func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { - t.configOnce.Do(func() { - cfg, err := newDefaultConfig() - if err != nil { - t.configErr = fmt.Errorf("creating default config: %w", err) - return - } - t.defaultConfig = &cfg - }) - if t.configErr != nil { - return nil, t.configErr + newConfigOnce() + if configErr != nil { + return nil, configErr } var results []map[string]string @@ -161,7 +168,7 @@ func (t *Table) scanPath(ctx context.Context, argon2idSalts []string, targetPath } // Fresh detector per scan - gitleaks accumulates findings internally - detector := detect.NewDetector(*t.defaultConfig) + detector := detect.NewDetector(*kolideConfig) var source sources.Source var file *os.File @@ -199,7 +206,7 @@ func (t *Table) scanPath(ctx context.Context, argon2idSalts []string, targetPath func (t *Table) scanContent(ctx context.Context, argon2idSalts []string, content []byte) ([]map[string]string, error) { // Fresh detector per scan - gitleaks accumulates findings internally - detector := detect.NewDetector(*t.defaultConfig) + detector := detect.NewDetector(*kolideConfig) fileSource := &sources.File{ Content: strings.NewReader(string(content)), diff --git a/ee/tables/secretscan/table_posix_test.go b/ee/tables/secretscan/table_posix_test.go index cf68f3d15..d2c335262 100644 --- a/ee/tables/secretscan/table_posix_test.go +++ b/ee/tables/secretscan/table_posix_test.go @@ -20,14 +20,13 @@ func TestScanPathFIFO(t *testing.T) { } // Initialize config for direct scanPath calls - cfg, err := newDefaultConfig() - require.NoError(t, err) - tbl.defaultConfig = &cfg + newConfigOnce() + require.NoError(t, configErr) tempDir := t.TempDir() fifoPath := filepath.Join(tempDir, "test_fifo") - err = unix.Mkfifo(fifoPath, 0644) + err := unix.Mkfifo(fifoPath, 0644) require.NoError(t, err, "creating FIFO") // Scanning a FIFO should return an error diff --git a/ee/tables/secretscan/table_test.go b/ee/tables/secretscan/table_test.go index 477aaec62..b5e54766c 100644 --- a/ee/tables/secretscan/table_test.go +++ b/ee/tables/secretscan/table_test.go @@ -417,13 +417,9 @@ func Test_isEncryptedJWTFamilyValue(t *testing.T) { func Test_isEmptyVariable(t *testing.T) { t.Parallel() - // Set up one table for use for all test cases - tbl := &Table{ - slogger: multislogger.NewNopLogger(), - } - cfg, err := newDefaultConfig() - require.NoError(t, err) - tbl.defaultConfig = &cfg + // Make sure config exists + newConfigOnce() + require.NoError(t, configErr) for _, tt := range []struct { testCaseName string @@ -482,7 +478,7 @@ func Test_isEmptyVariable(t *testing.T) { t.Run(tt.testCaseName, func(t *testing.T) { t.Parallel() - detector := detect.NewDetector(*tbl.defaultConfig) + detector := detect.NewDetector(*kolideConfig) fileSource := &sources.File{ Content: strings.NewReader(tt.rawData), Config: &detector.Config, @@ -502,6 +498,58 @@ func Test_isEmptyVariable(t *testing.T) { } } +// Test_kolideConfig confirms that our overrides in config.toml work as expected +func Test_kolideConfig(t *testing.T) { + t.Parallel() + + // Make sure config exists + newConfigOnce() + require.NoError(t, configErr) + + for _, tt := range []struct { + testCaseName string + pathName string + rawData string + }{ + { + testCaseName: "K8s sealed secrets", + pathName: "k8s.yaml", + rawData: ` +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + creationTimestamp: null + name: basic-auth + namespace: default +spec: + encryptedData: + password: AgAYsM4W6e5MMp90oqj7sIolG+//xxJTwRh8ke50CUFiZ8M/r5BoV/vHh3zKnlskt/s4jK058M1i4Y3ETgfsrbiYD/3ADLKRHjr2TO5O6xUrWtfFh6aDR8fOSbVpm1qCSmxFGMaHq5QLJ3Ab0FQTKF/eFehogkzXb8opY/PlI9r+9DcUJ1epAjbNjzvHDDSEZxwi/i5kCNfxqnQNZHQ4uIgEysOk/kjBEtfISxyeD2PGYiyMYk9zMtEUk9LoUTR3/EXiFcJc4iw83DUVICbaCdMPh2ZRsv3A1CWx608rtIyF+qqKnmfX5me7njnYi0vGVDY97T4cV5rKrdZTTOZVggur2l+sPG+BJSYmEVqz3cZ81mVOr4znwU6w3f2e5HxD7ivdJJEz70xgPFX8pFruQulAOohd8qXakqdtA+ew+tr0h3M+cWvOu6VNXQlbijRgC4R89CclHW4/GX3j0OulJUotrV29rTpmVZ7MTGrnZkwJbNUYAe1GEFo7LNws+GJTcNM9R6QA/AYvJ2eE3SjE2G7VaIUh2RvSe2O094Ln7yIJctzEK8afiCiIQQnIgy/M+YheoC+TzogvLZGuNsvrt/oiiilUNa6WODTr1DmJGIyM4cg0pZuVKJ8dx+zf4l7+efkBOa/2mzU9DakvoRQK8/ClR6tuOAVXHZksPehcI3eTX/ZI0MtM2CLJouoi86hPbgwEorBt+nLClkw= + user: AgAMrlzJFHS+mqUhv2ZpG57VNoTSRrewuu6FZVPZcV35dCmdZwesz3MSrmweNHXAlJbVMSMRlINIEBQZKgjw0szEh2ZKkXjGv1926p34GJJSB5/rqYBIDUFkIRY3aJsijMN6etjLRQi68sbYIQAZQM4pGdN23++CfNmXoQgDm9ItspcSYAcOeKP8tZ799pQTdM1pMMur5EyrYWxckORCz+OT1+buCL9+5DJkjU7JuQCk5QkwXRE1sm76FvmmP+a3FbNCIgqsBpD42AqqD4/ex0PogKr7gDkG27MWZNIvCWDd2iF4x1cwJgOtNZJEzQ7tDr+Mf4438w03sJQPMtCEUuuzX1I7SPuT6D9eRSV00GA/IS0/fmNbPf2pPHFxj8t1RMsGI2ZLdZBOXapn3P0SLYZ2Xh6QIqdxzb3VR37WS/Ir4c8v86ZzDTbnqVdT/rwb7U4Iy2k3nDj+/ghxD+7HQbmBx4zzFVYe70Sb1QIWthzZHtuvoX7+FeSa6iU6ipUj4g0U9r53vD+AYt7ntJtCI3EdX+Fh9yJe4AAL4ToHjnt3s2EG4K5i0/21KwAy9WX2rkwBb8GD3POT3zZq2b4uB5gUYyF467kw0J7MfEsPJjAfhd72+IMM9BZDU4tlrFBJPRubVsmRJKelM/o1YTkbl+eFNyWBE1t5IQ9DFjHpcppgUm1CUiDdI0RbIc2goHfFWNePxZZQBg== + template: + metadata: + creationTimestamp: null + name: basic-auth + namespace: default +`, + }, + } { + t.Run(tt.testCaseName, func(t *testing.T) { + t.Parallel() + + detector := detect.NewDetector(*kolideConfig) + fileSource := &sources.File{ + Content: strings.NewReader(tt.rawData), + Path: tt.pathName, + Config: &detector.Config, + } + + findings, err := detector.DetectSource(t.Context(), fileSource) + require.NoError(t, err) + require.Equal(t, 0, len(findings)) + }) + } +} + // Benchmarks func BenchmarkSecretScanDirectory(b *testing.B) {