mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-10 07:04:24 +00:00
proposal: SqlServer connection string detector (#867)
* sqlserver added to detectors.proto * make protos * boilerplate detector generated * wireup * initial
This commit is contained in:
parent
d7d614cc5f
commit
60464da3ce
5 changed files with 245 additions and 4 deletions
89
pkg/detectors/sqlserver/sqlserver.go
Normal file
89
pkg/detectors/sqlserver/sqlserver.go
Normal 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
|
||||||
|
}
|
145
pkg/detectors/sqlserver/sqlserver_test.go
Normal file
145
pkg/detectors/sqlserver/sqlserver_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -426,8 +426,8 @@ import (
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/noticeable"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/noticeable"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/notion"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/notion"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nozbeteams"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nozbeteams"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/npmtoken"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/npmtoken"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/npmtokenv2"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/npmtokenv2"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/numverify"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/numverify"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nutritionix"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nutritionix"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nylas"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nylas"
|
||||||
|
@ -593,6 +593,7 @@ import (
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/spoonacular"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/spoonacular"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sportradar"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sportradar"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/sportsmonk"
|
"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/square"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/squareapp"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/squareapp"
|
||||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/squarespace"
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/squarespace"
|
||||||
|
@ -1482,5 +1483,6 @@ func DefaultDetectors() []detectors.Detector {
|
||||||
digitaloceanv2.Scanner{},
|
digitaloceanv2.Scanner{},
|
||||||
npmtoken.Scanner{},
|
npmtoken.Scanner{},
|
||||||
npmtokenv2.Scanner{},
|
npmtokenv2.Scanner{},
|
||||||
|
sqlserver.Scanner{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -966,6 +966,7 @@ const (
|
||||||
DetectorType_MongoDB DetectorType = 895
|
DetectorType_MongoDB DetectorType = 895
|
||||||
DetectorType_NGC DetectorType = 896
|
DetectorType_NGC DetectorType = 896
|
||||||
DetectorType_DigitalOceanV2 DetectorType = 897
|
DetectorType_DigitalOceanV2 DetectorType = 897
|
||||||
|
DetectorType_SQLServer DetectorType = 898
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enum value maps for DetectorType.
|
// Enum value maps for DetectorType.
|
||||||
|
@ -1865,6 +1866,7 @@ var (
|
||||||
895: "MongoDB",
|
895: "MongoDB",
|
||||||
896: "NGC",
|
896: "NGC",
|
||||||
897: "DigitalOceanV2",
|
897: "DigitalOceanV2",
|
||||||
|
898: "SQLServer",
|
||||||
}
|
}
|
||||||
DetectorType_value = map[string]int32{
|
DetectorType_value = map[string]int32{
|
||||||
"Alibaba": 0,
|
"Alibaba": 0,
|
||||||
|
@ -2761,6 +2763,7 @@ var (
|
||||||
"MongoDB": 895,
|
"MongoDB": 895,
|
||||||
"NGC": 896,
|
"NGC": 896,
|
||||||
"DigitalOceanV2": 897,
|
"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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f,
|
||||||
|
|
|
@ -905,6 +905,7 @@ enum DetectorType {
|
||||||
MongoDB = 895;
|
MongoDB = 895;
|
||||||
NGC = 896;
|
NGC = 896;
|
||||||
DigitalOceanV2 = 897;
|
DigitalOceanV2 = 897;
|
||||||
|
SQLServer = 898;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Result {
|
message Result {
|
||||||
|
|
Loading…
Reference in a new issue