mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-09-20 06:31:57 +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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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{},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -905,6 +905,7 @@ enum DetectorType {
|
|||
MongoDB = 895;
|
||||
NGC = 896;
|
||||
DigitalOceanV2 = 897;
|
||||
SQLServer = 898;
|
||||
}
|
||||
|
||||
message Result {
|
||||
|
|
Loading…
Reference in a new issue