proposal: SqlServer connection string detector (#867)

* sqlserver added to detectors.proto

* make protos

* boilerplate detector generated

* wireup

* initial
This commit is contained in:
Alexandr Marchenko 2022-10-26 14:46:13 +00:00 committed by GitHub
parent d7d614cc5f
commit 60464da3ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 245 additions and 4 deletions

View file

@ -0,0 +1,89 @@
package sqlserver
import (
"context"
"database/sql"
"regexp"
"github.com/denisenkom/go-mssqldb/msdsn"
log "github.com/sirupsen/logrus"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)
type Scanner struct{}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
// SQLServer connection string is a semicolon delimited set of case-insensitive parameters which may go in any order.
pattern = regexp.MustCompile("(?:\n|`|'|\"| )?((?:[A-Za-z0-9_ ]+=[^;$'`\"$]+;?){3,})(?:'|`|\"|\r\n|\n)?")
)
// 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{"sqlserver"}
}
// FromData will find and optionally verify SpotifyKey secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
matches := pattern.FindAllStringSubmatch(string(data), -1)
for _, match := range matches {
params, _, err := msdsn.Parse(match[1])
if err != nil {
log.Debugf("sqlserver: unable to parse connection string '%s' because '%s'", match[1], err.Error())
continue
}
if params.Password == "" {
log.Debugf("sqlserver: skip connection string '%s' because it does not contain password", match[1])
continue
}
detected := detectors.Result{
DetectorType: detectorspb.DetectorType_SQLServer,
Raw: []byte(params.Password),
}
if verify {
verified, err := ping(params)
if err != nil {
log.Debugf("sqlserver: unable to verify '%s' because '%s'", params.URL(), err.Error())
} else {
detected.Verified = verified
}
}
results = append(results, detected)
}
return detectors.CleanResults(results), nil
}
var ping = func(config msdsn.Config) (bool, error) {
url := config.URL()
query := url.Query()
query.Set("dial timeout", "3")
query.Set("connection timeout", "3")
url.RawQuery = query.Encode()
conn, err := sql.Open("mssql", url.String())
if err != nil {
return false, err
}
err = conn.Ping()
if err != nil {
return false, err
}
err = conn.Close()
if err != nil {
log.Debugf("sqlserver: unable to close connection '%s' because '%s'", url.String(), err.Error())
}
return true, nil
}

View file

@ -0,0 +1,145 @@
//go:build detectors
// +build detectors
package sqlserver
import (
"context"
"fmt"
"github.com/denisenkom/go-mssqldb/msdsn"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"testing"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
func TestSQLServer_FromChunk(t *testing.T) {
secret := "Server=localhost;Initial Catalog=Demo;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true;"
inactiveSecret := "Server=localhost;User ID=sa;Password=123"
type args struct {
ctx context.Context
data []byte
verify bool
}
tests := []struct {
name string
s Scanner
args args
want []detectors.Result
wantErr bool
mockFunc func()
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a sqlserver secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SQLServer,
Verified: true,
},
},
wantErr: false,
mockFunc: func() {
ping = func(config msdsn.Config) (bool, error) {
return true, nil
}
},
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a sqlserver secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SQLServer,
Verified: false,
},
},
wantErr: false,
mockFunc: func() {
ping = func(config msdsn.Config) (bool, error) {
return false, nil
}
},
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
mockFunc: func() {},
},
}
// preserve the original function
originalPing := ping
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockFunc()
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("SQLServer.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "RawV2")
if diff := cmp.Diff(tt.want, got, ignoreOpts); diff != "" {
t.Errorf("SQLServer.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
ping = originalPing
}
func TestSQLServer_pattern(t *testing.T) {
if !pattern.Match([]byte(`builder.Services.AddDbContext<Database>(optionsBuilder => optionsBuilder.UseSqlServer("Server=localhost;Initial Catalog=master;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true;"));`)) {
t.Errorf("SQLServer.pattern: did not catched connection string from Program.cs")
}
if !pattern.Match([]byte(`{"ConnectionStrings": {"Demo": "Server=localhost;Initial Catalog=master;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true;"}}`)) {
t.Errorf("SQLServer.pattern: did not catched connection string from appsettings.json")
}
if !pattern.Match([]byte(`CONNECTION_STRING: Server=localhost;Initial Catalog=master;User ID=sa;Password=P@ssw0rd!;Persist Security Info=true;MultipleActiveResultSets=true`)) {
t.Errorf("SQLServer.pattern: did not catched connection string from .env")
}
}
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)
}
}
})
}
}

View file

@ -593,6 +593,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/spoonacular"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sportradar"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sportsmonk"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sqlserver"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/square"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/squareapp"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/squarespace"
@ -1482,5 +1483,6 @@ func DefaultDetectors() []detectors.Detector {
digitaloceanv2.Scanner{},
npmtoken.Scanner{},
npmtokenv2.Scanner{},
sqlserver.Scanner{},
}
}

View file

@ -966,6 +966,7 @@ const (
DetectorType_MongoDB DetectorType = 895
DetectorType_NGC DetectorType = 896
DetectorType_DigitalOceanV2 DetectorType = 897
DetectorType_SQLServer DetectorType = 898
)
// Enum value maps for DetectorType.
@ -1865,6 +1866,7 @@ var (
895: "MongoDB",
896: "NGC",
897: "DigitalOceanV2",
898: "SQLServer",
}
DetectorType_value = map[string]int32{
"Alibaba": 0,
@ -2761,6 +2763,7 @@ var (
"MongoDB": 895,
"NGC": 896,
"DigitalOceanV2": 897,
"SQLServer": 898,
}
)
@ -3125,7 +3128,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, 0xb1, 0x70, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79,
0x02, 0x2a, 0xc1, 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,
@ -4024,7 +4027,8 @@ var file_detectors_proto_rawDesc = []byte{
0x72, 0x6b, 0x64, 0x61, 0x79, 0x10, 0xfe, 0x06, 0x12, 0x0c, 0x0a, 0x07, 0x4d, 0x6f, 0x6e, 0x67,
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, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
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,

View file

@ -905,6 +905,7 @@ enum DetectorType {
MongoDB = 895;
NGC = 896;
DigitalOceanV2 = 897;
SQLServer = 898;
}
message Result {