Skip to content

Commit 50aa469

Browse files
authored
[INS-242] Add more validations to Custom Detector config (#4642)
1 parent a633174 commit 50aa469

3 files changed

Lines changed: 177 additions & 0 deletions

File tree

pkg/custom_detectors/custom_detectors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ func NewWebhookCustomRegex(pb *custom_detectorspb.CustomRegex) (*CustomRegexWebh
4747
if err := ValidateRegex(pb.Regex); err != nil {
4848
return nil, err
4949
}
50+
if err := ValidateRegexSlice(pb.ExcludeRegexesCapture); err != nil {
51+
return nil, err
52+
}
53+
if err := ValidateRegexSlice(pb.ExcludeRegexesMatch); err != nil {
54+
return nil, err
55+
}
56+
if err := ValidatePrimaryRegexName(pb.PrimaryRegexName, pb.Regex); err != nil {
57+
return nil, err
58+
}
5059

5160
for _, verify := range pb.Verify {
5261
if err := ValidateVerifyEndpoint(verify.Endpoint, verify.Unsafe); err != nil {

pkg/custom_detectors/custom_detectors_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package custom_detectors
22

33
import (
44
"context"
5+
"strings"
56
"testing"
67

78
"github.com/google/go-cmp/cmp"
@@ -559,6 +560,153 @@ func TestDetectorValidations(t *testing.T) {
559560
}
560561
}
561562

563+
func TestNewWebhookCustomRegex_Validation(t *testing.T) {
564+
t.Parallel()
565+
566+
// A known-good baseline; each test case mutates exactly one thing to trigger a specific validator.
567+
base := func() *custom_detectorspb.CustomRegex {
568+
return &custom_detectorspb.CustomRegex{
569+
Name: "ok",
570+
Keywords: []string{"kw"},
571+
Regex: map[string]string{
572+
"main": `\btoken_[a-z]+\b`,
573+
},
574+
PrimaryRegexName: "main",
575+
ExcludeRegexesCapture: []string{
576+
`^skip_.*$`,
577+
},
578+
ExcludeRegexesMatch: []string{
579+
`^ignore_.*$`,
580+
},
581+
Verify: []*custom_detectorspb.VerifierConfig{
582+
{
583+
Endpoint: "https://example.com/verify",
584+
Unsafe: false,
585+
Headers: []string{"Authorization: Bearer x"},
586+
},
587+
},
588+
}
589+
}
590+
591+
tests := []struct {
592+
name string
593+
mutate func(*custom_detectorspb.CustomRegex)
594+
wantErr bool
595+
wantErrSubstr string // substring expected in error
596+
}{
597+
{
598+
name: "Validate everything ok",
599+
mutate: func(pb *custom_detectorspb.CustomRegex) {},
600+
},
601+
{
602+
name: "ValidateKeywords: no keywords",
603+
mutate: func(pb *custom_detectorspb.CustomRegex) {
604+
pb.Keywords = nil
605+
},
606+
wantErr: true,
607+
wantErrSubstr: "no keywords",
608+
},
609+
{
610+
name: "ValidateKeywords: empty keyword",
611+
mutate: func(pb *custom_detectorspb.CustomRegex) {
612+
pb.Keywords = []string{""}
613+
},
614+
wantErr: true,
615+
wantErrSubstr: "empty keyword",
616+
},
617+
{
618+
name: "ValidateRegex: no regex",
619+
mutate: func(pb *custom_detectorspb.CustomRegex) {
620+
pb.Regex = nil
621+
},
622+
wantErr: true,
623+
wantErrSubstr: "no regex",
624+
},
625+
{
626+
name: "ValidateRegex: invalid regex in map",
627+
mutate: func(pb *custom_detectorspb.CustomRegex) {
628+
pb.Regex = map[string]string{"main": "("} // invalid
629+
},
630+
wantErr: true,
631+
wantErrSubstr: "regex 'main':",
632+
},
633+
{
634+
name: "ValidateRegexSlice: invalid exclude_regexes_capture",
635+
mutate: func(pb *custom_detectorspb.CustomRegex) {
636+
pb.ExcludeRegexesCapture = []string{"("} // invalid
637+
},
638+
wantErr: true,
639+
wantErrSubstr: "regex '1':",
640+
},
641+
{
642+
name: "ValidateRegexSlice: invalid exclude_regexes_match",
643+
mutate: func(pb *custom_detectorspb.CustomRegex) {
644+
pb.ExcludeRegexesMatch = []string{"("} // invalid
645+
},
646+
wantErr: true,
647+
wantErrSubstr: "regex '1':",
648+
},
649+
{
650+
name: "ValidatePrimaryRegexName: unknown primary regex name",
651+
mutate: func(pb *custom_detectorspb.CustomRegex) {
652+
pb.PrimaryRegexName = "does-not-exist"
653+
},
654+
wantErr: true,
655+
wantErrSubstr: `unknown primary regex name: "does-not-exist"`,
656+
},
657+
{
658+
name: "ValidateVerifyEndpoint: empty endpoint",
659+
mutate: func(pb *custom_detectorspb.CustomRegex) {
660+
pb.Verify = []*custom_detectorspb.VerifierConfig{
661+
{Endpoint: "", Unsafe: false, Headers: []string{"A: b"}},
662+
}
663+
},
664+
wantErr: true,
665+
wantErrSubstr: "no endpoint",
666+
},
667+
{
668+
name: "ValidateVerifyEndpoint: http endpoint without unsafe=true",
669+
mutate: func(pb *custom_detectorspb.CustomRegex) {
670+
pb.Verify = []*custom_detectorspb.VerifierConfig{
671+
{Endpoint: "http://example.com/verify", Unsafe: false, Headers: []string{"A: b"}},
672+
}
673+
},
674+
wantErr: true,
675+
wantErrSubstr: "http endpoint must have unsafe=true",
676+
},
677+
{
678+
name: "ValidateVerifyHeaders: header missing colon",
679+
mutate: func(pb *custom_detectorspb.CustomRegex) {
680+
pb.Verify = []*custom_detectorspb.VerifierConfig{
681+
{Endpoint: "https://example.com/verify", Unsafe: false, Headers: []string{"Authorization Bearer x"}},
682+
}
683+
},
684+
wantErr: true,
685+
wantErrSubstr: `must contain a colon`,
686+
},
687+
}
688+
689+
for _, tt := range tests {
690+
t.Run(tt.name, func(t *testing.T) {
691+
t.Parallel()
692+
693+
pb := base()
694+
tt.mutate(pb)
695+
696+
got, err := NewWebhookCustomRegex(pb)
697+
if (err != nil) != tt.wantErr {
698+
t.Fatalf("expected error=%v, got error=%v (result=%#v)", tt.wantErr, err != nil, got)
699+
}
700+
if tt.wantErr && got != nil {
701+
t.Fatalf("expected nil result on error, got=%#v", got)
702+
}
703+
if tt.wantErr && !strings.Contains(err.Error(), tt.wantErrSubstr) {
704+
t.Fatalf("error mismatch:\n got: %q\n want substring: %q", err.Error(), tt.wantErrSubstr)
705+
}
706+
})
707+
}
708+
}
709+
562710
func BenchmarkProductIndices(b *testing.B) {
563711
for i := 0; i < b.N; i++ {
564712
_ = productIndices(3, 2, 6)

pkg/custom_detectors/validation.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,26 @@ func ValidateRegex(regex map[string]string) error {
3232
return nil
3333
}
3434

35+
func ValidateRegexSlice(regex []string) error {
36+
for i, reg := range regex {
37+
if _, err := regexp.Compile(reg); err != nil {
38+
return fmt.Errorf("regex '%d': %w", i+1, err)
39+
}
40+
}
41+
return nil
42+
}
43+
44+
// validates if a provided non-empty primary regex name exists in the map of regexes
45+
func ValidatePrimaryRegexName(primaryRegexName string, regexes map[string]string) error {
46+
if primaryRegexName == "" {
47+
return nil
48+
}
49+
if _, ok := regexes[primaryRegexName]; !ok {
50+
return fmt.Errorf("unknown primary regex name: %q", primaryRegexName)
51+
}
52+
return nil
53+
}
54+
3555
func ValidateVerifyEndpoint(endpoint string, unsafe bool) error {
3656
if len(endpoint) == 0 {
3757
return fmt.Errorf("no endpoint")

0 commit comments

Comments
 (0)