From e6de528e40d2c2ef03268a812d1391e331e12e42 Mon Sep 17 00:00:00 2001 From: Dustin Decker Date: Tue, 1 Nov 2022 15:40:09 -0700 Subject: [PATCH] split up detectors --- pkg/detectors/ftp/ftp.go | 103 ++++++++++++++++++ pkg/detectors/ftp/ftp_test.go | 109 ++++++++++++++++++++ pkg/detectors/mongodb/mongodb.go | 2 +- pkg/detectors/npmtokenv2/npmtokenv2_test.go | 2 +- pkg/detectors/redis/redis.go | 101 ++++++++++++++++++ pkg/detectors/redis/redis_test.go | 90 ++++++++++++++++ pkg/detectors/uri/uri.go | 70 ++----------- pkg/detectors/uri/uri_test.go | 36 ------- pkg/pb/detectorspb/detectors.pb.go | 20 ++-- proto/detectors.proto | 2 + 10 files changed, 428 insertions(+), 107 deletions(-) create mode 100644 pkg/detectors/ftp/ftp.go create mode 100644 pkg/detectors/ftp/ftp_test.go create mode 100644 pkg/detectors/redis/redis.go create mode 100644 pkg/detectors/redis/redis_test.go diff --git a/pkg/detectors/ftp/ftp.go b/pkg/detectors/ftp/ftp.go new file mode 100644 index 000000000000..b75cb6d9037c --- /dev/null +++ b/pkg/detectors/ftp/ftp.go @@ -0,0 +1,103 @@ +package uri + +import ( + "context" + "net/url" + "regexp" + "strings" + "time" + + "github.com/jlaffaye/ftp" + + "github.com/trufflesecurity/trufflehog/v3/pkg/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +type Scanner struct { + allowKnownTestSites bool +} + +// Ensure the Scanner satisfies the interface at compile time. +var _ detectors.Detector = (*Scanner)(nil) + +var ( + keyPat = regexp.MustCompile(`\bftp:\/\/[\S]{3,50}:([\S]{3,50})@[-.%\w\/:]+\b`) + + client = common.SaneHttpClient() +) + +// Keywords are used for efficiently pre-filtering chunks. +// Use identifiers in the secret preferably, or the provider name. +func (s Scanner) Keywords() []string { + return []string{"ftp://"} +} + +// FromData will find and optionally verify URI secrets in a given set of bytes. +func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { + dataStr := string(data) + + matches := keyPat.FindAllStringSubmatch(dataStr, -1) + + for _, match := range matches { + urlMatch := match[0] + password := match[1] + + // Skip findings where the password only has "*" characters, this is a redacted password + if strings.Trim(password, "*") == "" { + continue + } + + parsedURL, err := url.Parse(urlMatch) + if err != nil { + continue + } + if _, ok := parsedURL.User.Password(); !ok { + continue + } + + redact := strings.TrimSpace(strings.Replace(urlMatch, password, strings.Repeat("*", len(password)), -1)) + + s := detectors.Result{ + DetectorType: detectorspb.DetectorType_FTP, + Raw: []byte(urlMatch), + Redacted: redact, + } + + if verify { + s.Verified = verifyFTP(ctx, parsedURL) + } + + if !s.Verified { + // Skip unverified findings where the password starts with a `$` - it's almost certainly a variable. + if strings.HasPrefix(password, "$") { + continue + } + } + + if !s.Verified && detectors.IsKnownFalsePositive(string(s.Raw), detectors.DefaultFalsePositives, false) { + continue + } + + results = append(results, s) + } + + return detectors.CleanResults(results), nil +} + +func verifyFTP(ctx context.Context, u *url.URL) bool { + host := u.Host + if !strings.Contains(host, ":") { + host = host + ":21" + } + + c, err := ftp.Dial(host, ftp.DialWithTimeout(5*time.Second)) + if err != nil { + return false + } + + password, _ := u.User.Password() + err = c.Login(u.User.Username(), password) + + return err == nil +} diff --git a/pkg/detectors/ftp/ftp_test.go b/pkg/detectors/ftp/ftp_test.go new file mode 100644 index 000000000000..72821eb2dea5 --- /dev/null +++ b/pkg/detectors/ftp/ftp_test.go @@ -0,0 +1,109 @@ +//go:build detectors +// +build detectors + +package uri + +import ( + "context" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +func TestURI_FromChunk(t *testing.T) { + type args struct { + ctx context.Context + data []byte + verify bool + } + tests := []struct { + name string + s Scanner + args args + want []detectors.Result + wantErr bool + }{ + { + name: "bad scheme", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte("file://user:pass@foo.com:123/wh/at/ever"), + verify: true, + }, + wantErr: false, + }, + { + name: "verified FTP", + s: Scanner{}, + args: args{ + ctx: context.Background(), + // https://dlptest.com/ftp-test/ + data: []byte("ftp://dlpuser:rNrKYTX9g7z3RgJRmxWuGHbeu@ftp.dlptest.com"), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_FTP, + Verified: true, + Redacted: "ftp://dlpuser:*************************@ftp.dlptest.com", + }, + }, + wantErr: false, + }, + { + name: "unverified FTP", + s: Scanner{}, + args: args{ + ctx: context.Background(), + // https://dlptest.com/ftp-test/ + data: []byte("ftp://dlpuser:invalid@ftp.dlptest.com"), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_FTP, + Verified: false, + Redacted: "ftp://dlpuser:*******@ftp.dlptest.com", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Scanner{allowKnownTestSites: true} + got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("URI.FromData() error = %v, wantErr %v", err, tt.wantErr) + return + } + // if os.Getenv("FORCE_PASS_DIFF") == "true" { + // return + // } + for i := range got { + got[i].Raw = nil + } + if diff := pretty.Compare(got, tt.want); diff != "" { + t.Errorf("URI.FromData() %s diff: (-got +want)\n%s", tt.name, diff) + } + }) + } +} + +func BenchmarkFromData(benchmark *testing.B) { + ctx := context.Background() + s := Scanner{} + for name, data := range detectors.MustGetBenchmarkData() { + benchmark.Run(name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _, err := s.FromData(ctx, false, data) + if err != nil { + b.Fatal(err) + } + } + }) + } +} diff --git a/pkg/detectors/mongodb/mongodb.go b/pkg/detectors/mongodb/mongodb.go index 2841f54c38b1..3c1d4a0677fe 100644 --- a/pkg/detectors/mongodb/mongodb.go +++ b/pkg/detectors/mongodb/mongodb.go @@ -21,7 +21,7 @@ var _ detectors.Detector = (*Scanner)(nil) var ( // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. - keyPat = regexp.MustCompile(`\b(mongodb(\+srv)?://[-.%\w{}]{1,50}:([-.%\S]{3,50})@[-.%\w\/:]+)\b`) + keyPat = regexp.MustCompile(`\b(mongodb(\+srv)?://[\S]{3,50}:([\S]{3,50})@[-.%\w\/:]+)\b`) // TODO: Add support for sharded cluster, replica set and Atlas Deployment. ) diff --git a/pkg/detectors/npmtokenv2/npmtokenv2_test.go b/pkg/detectors/npmtokenv2/npmtokenv2_test.go index 4714e13b795a..4d8e35af23b0 100644 --- a/pkg/detectors/npmtokenv2/npmtokenv2_test.go +++ b/pkg/detectors/npmtokenv2/npmtokenv2_test.go @@ -1,7 +1,7 @@ //go:build detectors // +build detectors -package npmtoken_new +package npmtokenv2 import ( "context" diff --git a/pkg/detectors/redis/redis.go b/pkg/detectors/redis/redis.go new file mode 100644 index 000000000000..0e833bc78b19 --- /dev/null +++ b/pkg/detectors/redis/redis.go @@ -0,0 +1,101 @@ +package uri + +import ( + "context" + "net/url" + "regexp" + "strings" + + "github.com/go-redis/redis" + + "github.com/trufflesecurity/trufflehog/v3/pkg/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +type Scanner struct { + allowKnownTestSites bool +} + +// Ensure the Scanner satisfies the interface at compile time. +var _ detectors.Detector = (*Scanner)(nil) + +var ( + keyPat = regexp.MustCompile(`\bredis:\/\/[\S]{3,50}:([\S]{3,50})@[-.%\w\/:]+\b`) + + client = common.SaneHttpClient() +) + +// Keywords are used for efficiently pre-filtering chunks. +// Use identifiers in the secret preferably, or the provider name. +func (s Scanner) Keywords() []string { + return []string{"redis"} +} + +// FromData will find and optionally verify URI secrets in a given set of bytes. +func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { + dataStr := string(data) + + matches := keyPat.FindAllStringSubmatch(dataStr, -1) + + for _, match := range matches { + urlMatch := match[0] + password := match[1] + + // Skip findings where the password only has "*" characters, this is a redacted password + if strings.Trim(password, "*") == "" { + continue + } + + parsedURL, err := url.Parse(urlMatch) + if err != nil { + continue + } + if _, ok := parsedURL.User.Password(); !ok { + continue + } + + redact := strings.TrimSpace(strings.Replace(urlMatch, password, strings.Repeat("*", len(password)), -1)) + + s := detectors.Result{ + DetectorType: detectorspb.DetectorType_Redis, + Raw: []byte(urlMatch), + Redacted: redact, + } + + if verify { + s.Verified = verifyRedis(ctx, parsedURL) + } + + if !s.Verified { + // Skip unverified findings where the password starts with a `$` - it's almost certainly a variable. + if strings.HasPrefix(password, "$") { + continue + } + } + + if !s.Verified && detectors.IsKnownFalsePositive(string(s.Raw), detectors.DefaultFalsePositives, false) { + continue + } + + results = append(results, s) + } + + return detectors.CleanResults(results), nil +} + +func verifyRedis(ctx context.Context, u *url.URL) bool { + opt, err := redis.ParseURL(u.String()) + if err != nil { + return false + } + + client := redis.NewClient(opt) + + status, err := client.Ping().Result() + if err == nil && status == "PONG" { + return true + } + + return false +} diff --git a/pkg/detectors/redis/redis_test.go b/pkg/detectors/redis/redis_test.go new file mode 100644 index 000000000000..3b7224b16d33 --- /dev/null +++ b/pkg/detectors/redis/redis_test.go @@ -0,0 +1,90 @@ +//go:build detectors +// +build detectors + +package uri + +import ( + "context" + "testing" + + "github.com/kylelemons/godebug/pretty" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +func TestURI_FromChunk(t *testing.T) { + type args struct { + ctx context.Context + data []byte + verify bool + } + tests := []struct { + name string + s Scanner + args args + want []detectors.Result + wantErr bool + }{ + { + name: "bad scheme", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte("file://user:pass@foo.com:123/wh/at/ever"), + verify: true, + }, + wantErr: false, + }, + { + name: "unverified Redis", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte("redis://user:invalid@redis.com"), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Redis, + Verified: false, + Redacted: "redis://user:*******@redis.com", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Scanner{allowKnownTestSites: true} + got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("URI.FromData() error = %v, wantErr %v", err, tt.wantErr) + return + } + // if os.Getenv("FORCE_PASS_DIFF") == "true" { + // return + // } + for i := range got { + got[i].Raw = nil + } + if diff := pretty.Compare(got, tt.want); diff != "" { + t.Errorf("URI.FromData() %s diff: (-got +want)\n%s", tt.name, diff) + } + }) + } +} + +func BenchmarkFromData(benchmark *testing.B) { + ctx := context.Background() + s := Scanner{} + for name, data := range detectors.MustGetBenchmarkData() { + benchmark.Run(name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + _, err := s.FromData(ctx, false, data) + if err != nil { + b.Fatal(err) + } + } + }) + } +} diff --git a/pkg/detectors/uri/uri.go b/pkg/detectors/uri/uri.go index 3ebae84aa16b..492c8e1abfe8 100644 --- a/pkg/detectors/uri/uri.go +++ b/pkg/detectors/uri/uri.go @@ -8,9 +8,6 @@ import ( "strings" "time" - "github.com/go-redis/redis" - "github.com/jlaffaye/ftp" - "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -24,7 +21,7 @@ type Scanner struct { var _ detectors.Detector = (*Scanner)(nil) var ( - keyPat = regexp.MustCompile(`\b[a-zA-Z]{1,10}:?\/\/[-.%\w{}]{1,50}:([-.%\S]{3,50})@[-.%\w\/:]+\b`) + keyPat = regexp.MustCompile(`\b(?:http(?:s)?:)?\/\/[\S]{3,50}:([\S]{3,50})@[-.%\w\/:]+\b`) client = common.SaneHttpClient() ) @@ -39,16 +36,6 @@ func (s Scanner) Keywords() []string { return []string{"http"} } -func allowlistedProtos(scheme string) bool { - allowlisted := []string{"http", "https", "redis", "ftp"} - for _, s := range allowlisted { - if s == scheme { - return true - } - } - return false -} - // FromData will find and optionally verify URI secrets in a given set of bytes. func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) @@ -69,11 +56,6 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result urlMatch := match[0] password := match[1] - // Skip findings where the password starts with a `$` - it's almost certainly a variable. - if strings.HasPrefix(password, "$") { - continue - } - // Skip findings where the password only has "*" characters, this is a redacted password if strings.Trim(password, "*") == "" { continue @@ -86,9 +68,6 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result if _, ok := parsedURL.User.Password(); !ok { continue } - if !allowlistedProtos(parsedURL.Scheme) { - continue - } redact := strings.TrimSpace(strings.Replace(urlMatch, password, strings.Repeat("*", len(password)), -1)) @@ -99,14 +78,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - switch parsedURL.Scheme { - case "http", "https": - s.Verified = verifyURL(ctx, parsedURL) - case "redis": - s.Verified = verifyRedis(ctx, parsedURL) - case "ftp": - s.Verified = verifyFTP(ctx, parsedURL) - default: + s.Verified = verifyURL(ctx, parsedURL) + } + + if !s.Verified { + // Skip unverified findings where the password starts with a `$` - it's almost certainly a variable. + if strings.HasPrefix(password, "$") { continue } } @@ -121,39 +98,6 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return detectors.CleanResults(results), nil } -func verifyFTP(ctx context.Context, u *url.URL) bool { - host := u.Host - if !strings.Contains(host, ":") { - host = host + ":21" - } - - c, err := ftp.Dial(host, ftp.DialWithTimeout(5*time.Second)) - if err != nil { - return false - } - - password, _ := u.User.Password() - err = c.Login(u.User.Username(), password) - - return err == nil -} - -func verifyRedis(ctx context.Context, u *url.URL) bool { - opt, err := redis.ParseURL(u.String()) - if err != nil { - return false - } - - client := redis.NewClient(opt) - - status, err := client.Ping().Result() - if err == nil && status == "PONG" { - return true - } - - return false -} - func verifyURL(ctx context.Context, u *url.URL) bool { // defuse most SSRF payloads u.Path = strings.TrimSuffix(u.Path, "/") diff --git a/pkg/detectors/uri/uri_test.go b/pkg/detectors/uri/uri_test.go index ddd5cc02abc3..012b5d84621e 100644 --- a/pkg/detectors/uri/uri_test.go +++ b/pkg/detectors/uri/uri_test.go @@ -87,42 +87,6 @@ func TestURI_FromChunk(t *testing.T) { }, wantErr: false, }, - { - name: "verified FTP", - s: Scanner{}, - args: args{ - ctx: context.Background(), - // https://dlptest.com/ftp-test/ - data: []byte("ftp://dlpuser:rNrKYTX9g7z3RgJRmxWuGHbeu@ftp.dlptest.com"), - verify: true, - }, - want: []detectors.Result{ - { - DetectorType: detectorspb.DetectorType_URI, - Verified: true, - Redacted: "ftp://dlpuser:*************************@ftp.dlptest.com", - }, - }, - wantErr: false, - }, - { - name: "unverified FTP", - s: Scanner{}, - args: args{ - ctx: context.Background(), - // https://dlptest.com/ftp-test/ - data: []byte("ftp://dlpuser:invalid@ftp.dlptest.com"), - verify: true, - }, - want: []detectors.Result{ - { - DetectorType: detectorspb.DetectorType_URI, - Verified: false, - Redacted: "ftp://dlpuser:*******@ftp.dlptest.com", - }, - }, - wantErr: false, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/pb/detectorspb/detectors.pb.go b/pkg/pb/detectorspb/detectors.pb.go index 108890eb9139..724bb8dd76a5 100644 --- a/pkg/pb/detectorspb/detectors.pb.go +++ b/pkg/pb/detectorspb/detectors.pb.go @@ -967,6 +967,8 @@ const ( DetectorType_NGC DetectorType = 896 DetectorType_DigitalOceanV2 DetectorType = 897 DetectorType_SQLServer DetectorType = 898 + DetectorType_FTP DetectorType = 899 + DetectorType_Redis DetectorType = 900 ) // Enum value maps for DetectorType. @@ -1867,6 +1869,8 @@ var ( 896: "NGC", 897: "DigitalOceanV2", 898: "SQLServer", + 899: "FTP", + 900: "Redis", } DetectorType_value = map[string]int32{ "Alibaba": 0, @@ -2764,6 +2768,8 @@ var ( "NGC": 896, "DigitalOceanV2": 897, "SQLServer": 898, + "FTP": 899, + "Redis": 900, } ) @@ -3128,7 +3134,7 @@ var file_detectors_proto_rawDesc = []byte{ 0x0a, 0x0b, 0x44, 0x65, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34, 0x10, - 0x02, 0x2a, 0xc1, 0x70, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, + 0x02, 0x2a, 0xd7, 0x70, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x10, 0x03, 0x12, 0x0a, 0x0a, @@ -4028,11 +4034,13 @@ var file_detectors_proto_rawDesc = []byte{ 0x6f, 0x44, 0x42, 0x10, 0xff, 0x06, 0x12, 0x08, 0x0a, 0x03, 0x4e, 0x47, 0x43, 0x10, 0x80, 0x07, 0x12, 0x13, 0x0a, 0x0e, 0x44, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, 0x4f, 0x63, 0x65, 0x61, 0x6e, 0x56, 0x32, 0x10, 0x81, 0x07, 0x12, 0x0e, 0x0a, 0x09, 0x53, 0x51, 0x4c, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x10, 0x82, 0x07, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, - 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x72, 0x10, 0x82, 0x07, 0x12, 0x08, 0x0a, 0x03, 0x46, 0x54, 0x50, 0x10, 0x83, 0x07, 0x12, + 0x0a, 0x0a, 0x05, 0x52, 0x65, 0x64, 0x69, 0x73, 0x10, 0x84, 0x07, 0x42, 0x3d, 0x5a, 0x3b, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, + 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, + 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, + 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/proto/detectors.proto b/proto/detectors.proto index e7e809165f62..289ea950a58c 100644 --- a/proto/detectors.proto +++ b/proto/detectors.proto @@ -906,6 +906,8 @@ enum DetectorType { NGC = 896; DigitalOceanV2 = 897; SQLServer = 898; + FTP = 899; + Redis = 900; } message Result {