From 15bc3e59afad7b022dd3c9f693a479501f691c73 Mon Sep 17 00:00:00 2001 From: Kashif Khan <70996046+kashifkhan0771@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:19:24 +0500 Subject: [PATCH] Added support for additional validation rules in custom detector (#4413) * Added support for additional validation rules in custom detector * refactored the approach to support validations for each regex in config * resolved comments --- examples/generic_with_filters.yml | 4 + pkg/custom_detectors/CUSTOM_DETECTORS.md | 7 + pkg/custom_detectors/custom_detectors.go | 22 +- pkg/custom_detectors/custom_detectors_test.go | 332 ++++++++++++++++++ pkg/custom_detectors/validation.go | 44 +++ pkg/custom_detectors/validation_test.go | 120 ++++++- .../custom_detectorspb/custom_detectors.pb.go | 203 ++++++++--- .../custom_detectors.pb.validate.go | 154 ++++++++ proto/custom_detectors.proto | 8 + 9 files changed, 850 insertions(+), 44 deletions(-) diff --git a/examples/generic_with_filters.yml b/examples/generic_with_filters.yml index be3f9386e..2af7e097d 100644 --- a/examples/generic_with_filters.yml +++ b/examples/generic_with_filters.yml @@ -11,6 +11,10 @@ detectors: regex: secret: |- (?i)[\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\x60'"\s;]|\\[nr]|$) + validations: + secret: # name of the regex to apply these validations to + contains_digit: true + contains_special_char: true entropy: 3.5 # exclude_regexes_capture: # - |- diff --git a/pkg/custom_detectors/CUSTOM_DETECTORS.md b/pkg/custom_detectors/CUSTOM_DETECTORS.md index aa3519edc..4b447cfd5 100644 --- a/pkg/custom_detectors/CUSTOM_DETECTORS.md +++ b/pkg/custom_detectors/CUSTOM_DETECTORS.md @@ -43,6 +43,13 @@ This guide will walk you through setting up a custom detector in TruffleHog to i - **`exclude_regexes_match`**: This parameter enables you to define regex patterns to exclude entire matches from being reported as secrets. This applies to the entire matched string, not just the token. - **`entropy`**: This parameter is used to assess the randomness of detected strings. High entropy often indicates that a string is a potential secret, such as an API key or password, due to its complexity and unpredictability. It helps in filtering false-positives. While an entropy threshold of `3` can be a starting point, it's essential to adjust this value based on your project's specific requirements and the nature of the data you have. - **`exclude_words`**: This parameter allows you to specify a list of words that, if present in a detected string, will cause TruffleHog to ignore that string. This is a substring match and does not enforce word boundaries. It applies only to the token. + - **`validations`**: This parameter lets you define extra validation rules for each regex specified in the regex option. These rules address limitations of Go's RE2 engine, such as the lack of lookahead support, and are applied after a regex match to help reduce false positives. + Available validation options: + - **`contains_digit`**: Ensures the match contains at least one numeric digit (0-9). Useful for API keys or tokens that must include numbers. + - **`contains_lowercase`**: Ensures the match contains at least one lowercase letter (a-z). Common requirement for passwords and mixed-case tokens. + - **`contains_uppercase`**: Ensures the match contains at least one uppercase letter (A-Z). Helps validate tokens that follow mixed-case conventions. + - **`contains_special_char`**: Ensures the match contains at least one special character from the set `!@#$%^&*()_+-=[]{}|;:,.<>?`. Useful for complex passwords or encoded tokens. + [Here](/examples/generic_with_filters.yml) is an example of a custom detector using these parameters. diff --git a/pkg/custom_detectors/custom_detectors.go b/pkg/custom_detectors/custom_detectors.go index 2880d856f..c0b6638cc 100644 --- a/pkg/custom_detectors/custom_detectors.go +++ b/pkg/custom_detectors/custom_detectors.go @@ -116,7 +116,7 @@ func (c *CustomRegexWebhook) FromData(ctx context.Context, verify bool, data []b MatchLoop: for _, match := range matches { - for _, values := range match { + for key, values := range match { // attempt to use capture group secret := values[0] if len(values) > 1 { @@ -150,6 +150,26 @@ MatchLoop: continue MatchLoop } } + + if validations := c.GetValidations(); validations != nil { + validationRules := []struct { + enabled bool + validator func(string) bool + }{ + {validations[key].GetContainsDigit(), ContainsDigit}, + {validations[key].GetContainsLowercase(), ContainsLowercase}, + {validations[key].GetContainsUppercase(), ContainsUppercase}, + {validations[key].GetContainsSpecialChar(), ContainsSpecialChar}, + } + + for _, rule := range validationRules { + if rule.enabled && !rule.validator(secret) { + // skip this match if a validation rule is enabled but missing from the secret + continue MatchLoop + } + } + } + } g.Go(func() error { diff --git a/pkg/custom_detectors/custom_detectors_test.go b/pkg/custom_detectors/custom_detectors_test.go index 4861be96b..05ada041b 100644 --- a/pkg/custom_detectors/custom_detectors_test.go +++ b/pkg/custom_detectors/custom_detectors_test.go @@ -4,9 +4,13 @@ import ( "context" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/custom_detectorspb" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" "github.com/trufflesecurity/trufflehog/v3/pkg/protoyaml" ) @@ -227,6 +231,334 @@ func TestDetectorPrimarySecret(t *testing.T) { assert.Equal(t, "secret_YI7C90ACY1_yy", results[0].GetPrimarySecretValue()) } +func TestDetectorValidations(t *testing.T) { + type args struct { + CustomRegex *custom_detectorspb.CustomRegex + Data string + } + + tests := []struct { + name string + input args + want []detectors.Result + }{ + { + name: "custom validation - contains digit", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsDigit: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStr0ngP@ssword! + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("MyStr0ngP@ssword!"), + }, + }, + }, + { + name: "custom validation - does not contains digit", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsDigit: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongPassword! + End of file`, + }, + want: nil, + }, + { + name: "custom validation - contains lowercase", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsLowercase: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongPassword! + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("MyStrongPassword!"), + }, + }, + }, + { + name: "custom validation - does not contains lowercase", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsLowercase: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MYSTRONGPASSWORD! + End of file`, + }, + want: nil, + }, + { + name: "custom validation - contains uppercase", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsUppercase: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongPassword! + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("MyStrongPassword!"), + }, + }, + }, + { + name: "custom validation - does not contains uppercase", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsUppercase: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: mystrongpassword! + End of file`, + }, + want: nil, + }, + { + name: "custom validation - contains special character", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsSpecialChar: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStr@ngP@ssword! + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("MyStr@ngP@ssword!"), + }, + }, + }, + { + name: "custom validation - does not contains special character", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsSpecialChar: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongPassword + End of file`, + }, + want: nil, + }, + { + name: "custom validation - contains uppercase and special characters", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsUppercase: true, + ContainsSpecialChar: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongP@ssword + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("MyStrongP@ssword"), + }, + }, + }, + { + name: "custom validation - contains uppercase but does not contain special characters", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsUppercase: true, + ContainsSpecialChar: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongPassword + End of file`, + }, + want: nil, + }, + { + name: "custom validation - wrong regex name in validations", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password"}, + Regex: map[string]string{"password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`}, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "wrong": { + ContainsUppercase: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: mystrongp@ssword + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("mystrongp@ssword"), + }, + }, + }, + { + name: "custom validation - multiple regex validations", + input: args{ + CustomRegex: &custom_detectorspb.CustomRegex{ + Name: "test", + Keywords: []string{"password", "api_key"}, + Regex: map[string]string{ + "password": `([A-Za-z0-9!@#$%^&*()_+=\-]{12,})`, + "api_key": `([a-f0-9_-]{32})`, + }, + Validations: map[string]*custom_detectorspb.ValidationConfig{ + "password": { + ContainsUppercase: true, + ContainsSpecialChar: true, + }, + "api_key": { + ContainsSpecialChar: true, + }, + }, + }, + Data: `This is custom example + This file has a random text and maybe a secret + Password: MyStrongP@ssword + API_Key: c392c9837d69b44c764cbf260b-e6184 // should be detected + API_Key: c392c9837d69b44c764cbf260be6184 // should be filtered by validation + End of file`, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_CustomRegex, + DetectorName: "test", + Verified: false, + Raw: []byte("MyStrongP@sswordc392c9837d69b44c764cbf260b-e6184"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + detector, err := NewWebhookCustomRegex(tt.input.CustomRegex) + assert.NoError(t, err) + results, err := detector.FromData(context.Background(), false, []byte(tt.input.Data)) + assert.NoError(t, err) + + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "ExtraData", "verificationError", "primarySecret") + if diff := cmp.Diff(results, tt.want, ignoreOpts); diff != "" { + t.Errorf("CustomDetector.FromData() %s diff: (-got +want)\n%s", tt.name, diff) + } + }) + } +} + func BenchmarkProductIndices(b *testing.B) { for i := 0; i < b.N; i++ { _ = productIndices(3, 2, 6) diff --git a/pkg/custom_detectors/validation.go b/pkg/custom_detectors/validation.go index 35f720297..d7edd9356 100644 --- a/pkg/custom_detectors/validation.go +++ b/pkg/custom_detectors/validation.go @@ -107,3 +107,47 @@ func ValidateRegexVars(regex map[string]string, body ...string) error { } return nil } + +// === Custom Validations === + +// ContainsDigit checks if string contains at least one digit +func ContainsDigit(s string) bool { + for i := 0; i < len(s); i++ { + char := s[i] + if char >= '0' && char <= '9' { + return true + } + } + + return false +} + +// ContainsLowercase checks if string contains at least one lowercase letter +func ContainsLowercase(s string) bool { + for i := 0; i < len(s); i++ { + char := s[i] + if char >= 'a' && char <= 'z' { + return true + } + } + + return false +} + +// ContainsUppercase checks if string contains at least one uppercase letter +func ContainsUppercase(s string) bool { + for i := 0; i < len(s); i++ { + char := s[i] + if char >= 'A' && char <= 'Z' { + return true + } + } + + return false +} + +// ContainsSpecialChar checks if string contains at least one special character +func ContainsSpecialChar(s string) bool { + specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?" + return strings.ContainsAny(s, specialChars) +} diff --git a/pkg/custom_detectors/validation_test.go b/pkg/custom_detectors/validation_test.go index c5f7f56d6..39d74cdd8 100644 --- a/pkg/custom_detectors/validation_test.go +++ b/pkg/custom_detectors/validation_test.go @@ -1,6 +1,8 @@ package custom_detectors -import "testing" +import ( + "testing" +) func TestCustomDetectorsKeywordValidation(t *testing.T) { tests := []struct { @@ -232,3 +234,119 @@ func TestCustomDetectorsVerifyRegexVarsValidation(t *testing.T) { }) } } + +func TestContainsDigit(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "contains digit", + args: args{s: "lzscqf&60M"}, + want: true, + }, + { + name: "does not contains digit", + args: args{s: "ZlDQOdaM*vsT"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsDigit(tt.args.s); got != tt.want { + t.Errorf("ContainsDigit() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContainsLowercase(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "contains lower case", + args: args{s: "g0AJBHdnhRG2"}, + want: true, + }, + { + name: "does not contains lower case", + args: args{s: "V7T#MEA6@+TN"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsLowercase(tt.args.s); got != tt.want { + t.Errorf("ContainsDigit() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContainsUppercase(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "contains upper case", + args: args{s: "G1sKkJeKlSQf"}, + want: true, + }, + { + name: "does not contains upper case", + args: args{s: "pq6-14ydz1@d"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsUppercase(tt.args.s); got != tt.want { + t.Errorf("ContainsDigit() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestContainsSpecialChar(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "contains upper case", + args: args{s: "HP$gE7s=do0B"}, + want: true, + }, + { + name: "does not contains upper case", + args: args{s: "w9gvBYctrSjB"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ContainsSpecialChar(tt.args.s); got != tt.want { + t.Errorf("ContainsDigit() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/pb/custom_detectorspb/custom_detectors.pb.go b/pkg/pb/custom_detectorspb/custom_detectors.pb.go index 245d36033..284bf8546 100644 --- a/pkg/pb/custom_detectorspb/custom_detectors.pb.go +++ b/pkg/pb/custom_detectorspb/custom_detectors.pb.go @@ -73,16 +73,17 @@ type CustomRegex struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Keywords []string `protobuf:"bytes,2,rep,name=keywords,proto3" json:"keywords,omitempty"` - Regex map[string]string `protobuf:"bytes,3,rep,name=regex,proto3" json:"regex,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Verify []*VerifierConfig `protobuf:"bytes,4,rep,name=verify,proto3" json:"verify,omitempty"` - Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` - ExcludeRegexesCapture []string `protobuf:"bytes,6,rep,name=exclude_regexes_capture,json=excludeRegexesCapture,proto3" json:"exclude_regexes_capture,omitempty"` - ExcludeWords []string `protobuf:"bytes,7,rep,name=exclude_words,json=excludeWords,proto3" json:"exclude_words,omitempty"` - Entropy float32 `protobuf:"fixed32,8,opt,name=entropy,proto3" json:"entropy,omitempty"` - ExcludeRegexesMatch []string `protobuf:"bytes,9,rep,name=exclude_regexes_match,json=excludeRegexesMatch,proto3" json:"exclude_regexes_match,omitempty"` - PrimaryRegexName string `protobuf:"bytes,10,opt,name=primary_regex_name,json=primaryRegexName,proto3" json:"primary_regex_name,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Keywords []string `protobuf:"bytes,2,rep,name=keywords,proto3" json:"keywords,omitempty"` + Regex map[string]string `protobuf:"bytes,3,rep,name=regex,proto3" json:"regex,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Verify []*VerifierConfig `protobuf:"bytes,4,rep,name=verify,proto3" json:"verify,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + ExcludeRegexesCapture []string `protobuf:"bytes,6,rep,name=exclude_regexes_capture,json=excludeRegexesCapture,proto3" json:"exclude_regexes_capture,omitempty"` + ExcludeWords []string `protobuf:"bytes,7,rep,name=exclude_words,json=excludeWords,proto3" json:"exclude_words,omitempty"` + Entropy float32 `protobuf:"fixed32,8,opt,name=entropy,proto3" json:"entropy,omitempty"` + ExcludeRegexesMatch []string `protobuf:"bytes,9,rep,name=exclude_regexes_match,json=excludeRegexesMatch,proto3" json:"exclude_regexes_match,omitempty"` + PrimaryRegexName string `protobuf:"bytes,10,opt,name=primary_regex_name,json=primaryRegexName,proto3" json:"primary_regex_name,omitempty"` + Validations map[string]*ValidationConfig `protobuf:"bytes,11,rep,name=validations,proto3" json:"validations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *CustomRegex) Reset() { @@ -187,6 +188,13 @@ func (x *CustomRegex) GetPrimaryRegexName() string { return "" } +func (x *CustomRegex) GetValidations() map[string]*ValidationConfig { + if x != nil { + return x.Validations + } + return nil +} + type VerifierConfig struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -258,6 +266,77 @@ func (x *VerifierConfig) GetSuccessRanges() []string { return nil } +type ValidationConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ContainsDigit bool `protobuf:"varint,1,opt,name=contains_digit,json=containsDigit,proto3" json:"contains_digit,omitempty"` + ContainsLowercase bool `protobuf:"varint,2,opt,name=contains_lowercase,json=containsLowercase,proto3" json:"contains_lowercase,omitempty"` + ContainsUppercase bool `protobuf:"varint,3,opt,name=contains_uppercase,json=containsUppercase,proto3" json:"contains_uppercase,omitempty"` + ContainsSpecialChar bool `protobuf:"varint,4,opt,name=contains_special_char,json=containsSpecialChar,proto3" json:"contains_special_char,omitempty"` +} + +func (x *ValidationConfig) Reset() { + *x = ValidationConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_custom_detectors_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ValidationConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidationConfig) ProtoMessage() {} + +func (x *ValidationConfig) ProtoReflect() protoreflect.Message { + mi := &file_custom_detectors_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidationConfig.ProtoReflect.Descriptor instead. +func (*ValidationConfig) Descriptor() ([]byte, []int) { + return file_custom_detectors_proto_rawDescGZIP(), []int{3} +} + +func (x *ValidationConfig) GetContainsDigit() bool { + if x != nil { + return x.ContainsDigit + } + return false +} + +func (x *ValidationConfig) GetContainsLowercase() bool { + if x != nil { + return x.ContainsLowercase + } + return false +} + +func (x *ValidationConfig) GetContainsUppercase() bool { + if x != nil { + return x.ContainsUppercase + } + return false +} + +func (x *ValidationConfig) GetContainsSpecialChar() bool { + if x != nil { + return x.ContainsSpecialChar + } + return false +} + var File_custom_detectors_proto protoreflect.FileDescriptor var file_custom_detectors_proto_rawDesc = []byte{ @@ -270,7 +349,7 @@ var file_custom_detectors_proto_rawDesc = []byte{ 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x67, 0x65, 0x78, 0x52, 0x09, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x73, 0x22, 0xec, 0x03, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, + 0x6f, 0x72, 0x73, 0x22, 0xa2, 0x05, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x67, 0x65, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x77, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x77, 0x6f, @@ -297,25 +376,49 @@ var file_custom_detectors_proto_rawDesc = []byte{ 0x67, 0x65, 0x78, 0x65, 0x73, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x52, - 0x65, 0x67, 0x65, 0x78, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x38, 0x0a, 0x0a, 0x52, 0x65, 0x67, 0x65, - 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x8e, 0x01, 0x0a, 0x0e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, - 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, - 0x6e, 0x73, 0x61, 0x66, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x6e, 0x73, - 0x61, 0x66, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x24, 0x0a, - 0x0d, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x61, 0x6e, - 0x67, 0x65, 0x73, 0x42, 0x44, 0x5a, 0x42, 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, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x64, 0x65, - 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x65, 0x67, 0x65, 0x78, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x50, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, + 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x67, 0x65, 0x78, 0x2e, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x38, 0x0a, 0x0a, 0x52, 0x65, + 0x67, 0x65, 0x78, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x62, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8e, 0x01, 0x0a, 0x0e, 0x56, 0x65, 0x72, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x08, 0x65, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xfa, + 0x42, 0x05, 0x72, 0x03, 0x90, 0x01, 0x01, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x75, 0x6e, 0x73, 0x61, 0x66, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x52, 0x61, + 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0xcb, 0x01, 0x0a, 0x10, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, + 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x5f, 0x64, 0x69, 0x67, 0x69, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, + 0x44, 0x69, 0x67, 0x69, 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x73, 0x5f, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x63, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x4c, 0x6f, 0x77, 0x65, 0x72, + 0x63, 0x61, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, + 0x5f, 0x75, 0x70, 0x70, 0x65, 0x72, 0x63, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x55, 0x70, 0x70, 0x65, 0x72, 0x63, + 0x61, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x5f, + 0x73, 0x70, 0x65, 0x63, 0x69, 0x61, 0x6c, 0x5f, 0x63, 0x68, 0x61, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x13, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x73, 0x53, 0x70, 0x65, 0x63, + 0x69, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x72, 0x42, 0x44, 0x5a, 0x42, 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, 0x63, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x5f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -330,22 +433,26 @@ func file_custom_detectors_proto_rawDescGZIP() []byte { return file_custom_detectors_proto_rawDescData } -var file_custom_detectors_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_custom_detectors_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_custom_detectors_proto_goTypes = []interface{}{ - (*CustomDetectors)(nil), // 0: custom_detectors.CustomDetectors - (*CustomRegex)(nil), // 1: custom_detectors.CustomRegex - (*VerifierConfig)(nil), // 2: custom_detectors.VerifierConfig - nil, // 3: custom_detectors.CustomRegex.RegexEntry + (*CustomDetectors)(nil), // 0: custom_detectors.CustomDetectors + (*CustomRegex)(nil), // 1: custom_detectors.CustomRegex + (*VerifierConfig)(nil), // 2: custom_detectors.VerifierConfig + (*ValidationConfig)(nil), // 3: custom_detectors.ValidationConfig + nil, // 4: custom_detectors.CustomRegex.RegexEntry + nil, // 5: custom_detectors.CustomRegex.ValidationsEntry } var file_custom_detectors_proto_depIdxs = []int32{ 1, // 0: custom_detectors.CustomDetectors.detectors:type_name -> custom_detectors.CustomRegex - 3, // 1: custom_detectors.CustomRegex.regex:type_name -> custom_detectors.CustomRegex.RegexEntry + 4, // 1: custom_detectors.CustomRegex.regex:type_name -> custom_detectors.CustomRegex.RegexEntry 2, // 2: custom_detectors.CustomRegex.verify:type_name -> custom_detectors.VerifierConfig - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 5, // 3: custom_detectors.CustomRegex.validations:type_name -> custom_detectors.CustomRegex.ValidationsEntry + 3, // 4: custom_detectors.CustomRegex.ValidationsEntry.value:type_name -> custom_detectors.ValidationConfig + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_custom_detectors_proto_init() } @@ -390,6 +497,18 @@ func file_custom_detectors_proto_init() { return nil } } + file_custom_detectors_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ValidationConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -397,7 +516,7 @@ func file_custom_detectors_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_custom_detectors_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/pb/custom_detectorspb/custom_detectors.pb.validate.go b/pkg/pb/custom_detectorspb/custom_detectors.pb.validate.go index 080cec35f..9e0a31eb4 100644 --- a/pkg/pb/custom_detectorspb/custom_detectors.pb.validate.go +++ b/pkg/pb/custom_detectorspb/custom_detectors.pb.validate.go @@ -235,6 +235,52 @@ func (m *CustomRegex) validate(all bool) error { // no validation rules for PrimaryRegexName + { + sorted_keys := make([]string, len(m.GetValidations())) + i := 0 + for key := range m.GetValidations() { + sorted_keys[i] = key + i++ + } + sort.Slice(sorted_keys, func(i, j int) bool { return sorted_keys[i] < sorted_keys[j] }) + for _, key := range sorted_keys { + val := m.GetValidations()[key] + _ = val + + // no validation rules for Validations[key] + + if all { + switch v := interface{}(val).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CustomRegexValidationError{ + field: fmt.Sprintf("Validations[%v]", key), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CustomRegexValidationError{ + field: fmt.Sprintf("Validations[%v]", key), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(val).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CustomRegexValidationError{ + field: fmt.Sprintf("Validations[%v]", key), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + } + if len(errors) > 0 { return CustomRegexMultiError(errors) } @@ -425,3 +471,111 @@ var _ interface { Cause() error ErrorName() string } = VerifierConfigValidationError{} + +// Validate checks the field values on ValidationConfig with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *ValidationConfig) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ValidationConfig with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// ValidationConfigMultiError, or nil if none found. +func (m *ValidationConfig) ValidateAll() error { + return m.validate(true) +} + +func (m *ValidationConfig) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for ContainsDigit + + // no validation rules for ContainsLowercase + + // no validation rules for ContainsUppercase + + // no validation rules for ContainsSpecialChar + + if len(errors) > 0 { + return ValidationConfigMultiError(errors) + } + + return nil +} + +// ValidationConfigMultiError is an error wrapping multiple validation errors +// returned by ValidationConfig.ValidateAll() if the designated constraints +// aren't met. +type ValidationConfigMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ValidationConfigMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ValidationConfigMultiError) AllErrors() []error { return m } + +// ValidationConfigValidationError is the validation error returned by +// ValidationConfig.Validate if the designated constraints aren't met. +type ValidationConfigValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ValidationConfigValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ValidationConfigValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ValidationConfigValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ValidationConfigValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ValidationConfigValidationError) ErrorName() string { return "ValidationConfigValidationError" } + +// Error satisfies the builtin error interface +func (e ValidationConfigValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sValidationConfig.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ValidationConfigValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ValidationConfigValidationError{} diff --git a/proto/custom_detectors.proto b/proto/custom_detectors.proto index 67d53b467..6c72a415b 100644 --- a/proto/custom_detectors.proto +++ b/proto/custom_detectors.proto @@ -21,6 +21,7 @@ message CustomRegex { float entropy = 8; repeated string exclude_regexes_match = 9; string primary_regex_name = 10; + map validations = 11; } @@ -30,3 +31,10 @@ message VerifierConfig { repeated string headers = 3; repeated string successRanges = 4; } + +message ValidationConfig { + bool contains_digit = 1; + bool contains_lowercase = 2; + bool contains_uppercase = 3; + bool contains_special_char = 4; +}