Populate ExtraData with parsed fields for all database connection string detectors (MongoDB, PostgreSQL, Redis, JDBC). This surfaces useful metadata about detected credentials. The parsing logic already existed in each detector — this change exposes the extracted values in the result's ExtraData map alongside any pre-existing fields (rotation_guide, sslmode, etc.).
281 lines
11 KiB
Go
281 lines
11 KiB
Go
package mongodb
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMongoDB_ExtraData(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
wantHost string
|
|
wantUsername string
|
|
wantDatabase string
|
|
}{
|
|
{
|
|
name: "single host with port",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]:27017`,
|
|
wantHost: "mongodb0.example.com:27017",
|
|
wantUsername: "myDBReader",
|
|
},
|
|
{
|
|
name: "single host without port",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]`,
|
|
wantHost: "mongodb0.example.com",
|
|
wantUsername: "myDBReader",
|
|
},
|
|
{
|
|
name: "with options and no database",
|
|
data: `mongodb://username:[email protected]:27018/?authMechanism=PLAIN&tls=true`,
|
|
wantHost: "host.docker.internal:27018",
|
|
wantUsername: "username",
|
|
},
|
|
{
|
|
name: "cosmos db style with database",
|
|
data: `mongodb://agenda-live:m21w7PFfRXQwfHZU1Fgx0rTX29ZBQaWMODLeAjsmyslVcMmcmy6CnLyu3byVDtdLYcCokze8lIE4KyAgSCGZxQ==@agenda-live.mongo.cosmos.azure.com:10255/csb-db?retryWrites=false&ssl=true&replicaSet=globaldb&maxIdleTimeMS=120000&appName=@agenda-live@`,
|
|
wantHost: "agenda-live.mongo.cosmos.azure.com:10255",
|
|
wantUsername: "agenda-live",
|
|
wantDatabase: "csb-db",
|
|
},
|
|
{
|
|
name: "with database in path",
|
|
data: `mongodb://db-user:db-password@mongodb-instance:27017/db-name`,
|
|
wantHost: "mongodb-instance:27017",
|
|
wantUsername: "db-user",
|
|
wantDatabase: "db-name",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := Scanner{}
|
|
results, err := s.FromData(context.Background(), false, []byte(tt.data))
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, results, "expected at least one result")
|
|
|
|
r := results[0]
|
|
assert.Equal(t, tt.wantHost, r.ExtraData["host"])
|
|
assert.Equal(t, tt.wantUsername, r.ExtraData["username"])
|
|
assert.Equal(t, tt.wantDatabase, r.ExtraData["database"])
|
|
assert.NotEmpty(t, r.ExtraData["rotation_guide"], "ExtraData[rotation_guide] should still be present")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMongoDB_Pattern(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
shouldMatch bool
|
|
match string
|
|
skip bool
|
|
}{
|
|
// True positives
|
|
{
|
|
name: "long_password",
|
|
data: `mongodb://agenda-live:m21w7PFfRXQwfHZU1Fgx0rTX29ZBQaWMODLeAjsmyslVcMmcmy6CnLyu3byVDtdLYcCokze8lIE4KyAgSCGZxQ==@agenda-live.mongo.cosmos.azure.com:10255/?retryWrites=false&ssl=true&replicaSet=globaldb&maxIdleTimeMS=120000&appName=@agenda-live@`,
|
|
shouldMatch: true,
|
|
match: `mongodb://agenda-live:m21w7PFfRXQwfHZU1Fgx0rTX29ZBQaWMODLeAjsmyslVcMmcmy6CnLyu3byVDtdLYcCokze8lIE4KyAgSCGZxQ==@agenda-live.mongo.cosmos.azure.com:10255/?appName=%40agenda-live%40&maxIdleTimeMS=120000&replicaSet=globaldb&retryWrites=false&ssl=true`,
|
|
},
|
|
{
|
|
name: "long_password2",
|
|
data: `mongodb://csb0230eada-2354-4c73-b3e4-8a1aaa996894:AiNtEyASbdXR5neJmTStMzKGItX2xvKuyEkcy65rviKD0ggZR19E1iVFIJ5ZAIY1xvvAiS5tOXsmACDbKDJIhQ==@csb0230eada-2354-4c73-b3e4-8a1aaa996894.mongo.cosmos.cloud-hostname.com:10255/csb-db0230eada-2354-4c73-b3e4-8a1aaa996894?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@csb0230eada-2354-4c73-b3e4-8a1aaa996894@`,
|
|
shouldMatch: true,
|
|
match: `mongodb://csb0230eada-2354-4c73-b3e4-8a1aaa996894:AiNtEyASbdXR5neJmTStMzKGItX2xvKuyEkcy65rviKD0ggZR19E1iVFIJ5ZAIY1xvvAiS5tOXsmACDbKDJIhQ==@csb0230eada-2354-4c73-b3e4-8a1aaa996894.mongo.cosmos.cloud-hostname.com:10255/csb-db0230eada-2354-4c73-b3e4-8a1aaa996894?appName=%40csb0230eada-2354-4c73-b3e4-8a1aaa996894%40&maxIdleTimeMS=120000&replicaSet=globaldb&retrywrites=false&ssl=true`,
|
|
},
|
|
{
|
|
name: "long_password3",
|
|
data: `mongodb://amsdfasfsadfdfdfpshot:6xNRRsdfsdfafd9NodO8vAFFBEHidfdfdfa87QDKXdCMubACDbhfQH1g==@amssdfafdafdadbsnapshot.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@amssadfasdfdbsnsdfadfapshot@`,
|
|
shouldMatch: true,
|
|
match: `mongodb://amsdfasfsadfdfdfpshot:6xNRRsdfsdfafd9NodO8vAFFBEHidfdfdfa87QDKXdCMubACDbhfQH1g==@amssdfafdafdadbsnapshot.mongo.cosmos.azure.com:10255/?appName=%40amssadfasdfdbsnsdfadfapshot%40&maxIdleTimeMS=120000&replicaSet=globaldb&retrywrites=false&ssl=true`,
|
|
},
|
|
{
|
|
name: "single_host",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "single_host+port",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]:27017`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "single_host+port+authdb",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]:27017/?authSource=admin`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "single_host_ip",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "single_host_ip+port",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]:27017`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "multiple_hosts_ip",
|
|
data: `mongodb://root:[email protected]:27018,192.168.74.143:27019`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "multiple_hosts_ip+slash",
|
|
data: `mongodb://root:[email protected]:27018,192.168.74.143:27019/`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "multiple_hosts+port+authdb",
|
|
data: `mongodb://myDBReader:D1fficultP%[email protected]:27017,mongodb0.example.com:27017,mongodb0.example.com:27017/?authSource=admin`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "multiple_hosts+options",
|
|
data: `mongodb://username:[email protected]:27317,mongodb2.example.com,mongodb2.example.com:270/?connectTimeoutMS=300000&replicaSet=mySet&authSource=aDifferentAuthDB`,
|
|
shouldMatch: true,
|
|
match: `mongodb://username:[email protected]:27317,mongodb2.example.com,mongodb2.example.com:270/?authSource=aDifferentAuthDB&connectTimeoutMS=300000&replicaSet=mySet`,
|
|
},
|
|
{
|
|
name: "multiple_hosts2",
|
|
data: `mongodb://prisma:[email protected]:27017,srv2.bu2lt.mongodb.net:27017,srv3.bu2lt.mongodb.net:27017/test?retryWrites=true&w=majority`,
|
|
shouldMatch: true,
|
|
},
|
|
// TODO: These fail because the Go driver doesn't explicitly support `authMechanism=DEFAULT`[1].
|
|
// However, this seems like a valid option[2] and I'm going to try to get that behaviour changed.
|
|
//
|
|
// [1] https://github.com/mongodb/mongo-go-driver/blob/master/x/mongo/driver/connstring/connstring.go#L450-L451
|
|
// [2] https://www.mongodb.com/docs/drivers/node/current/fundamentals/authentication/mechanisms/
|
|
{
|
|
name: "encoded_options1",
|
|
data: `mongodb://dave:password@localhost:27017/?authMechanism=DEFAULT&authSource=db&ssl=true"`,
|
|
shouldMatch: true,
|
|
match: "mongodb://dave:password@localhost:27017/?authMechanism=DEFAULT&authSource=db&ssl=true",
|
|
},
|
|
{
|
|
name: "encoded_options2",
|
|
data: `mongodb://cefapp:MdTc8Kc8DzlTE1RUl1JVDGS4zw1U1t6145sPWqeStWA50xEUKPfUCGlnk3ACkfqH6qLAwpnm9awpY1m8dg0YlQ==@cefapp.documents.azure.com:10250/?ssl=true&sslverifycertificate=false`,
|
|
shouldMatch: true,
|
|
match: "mongodb://cefapp:MdTc8Kc8DzlTE1RUl1JVDGS4zw1U1t6145sPWqeStWA50xEUKPfUCGlnk3ACkfqH6qLAwpnm9awpY1m8dg0YlQ==@cefapp.documents.azure.com:10250/?ssl=true&sslverifycertificate=false",
|
|
},
|
|
// TODO: `%2Ftmp%2Fmongodb-27017.sock` fails with url.Parse.
|
|
// Then again, TruffleHog will never be able to verify a local socket on a remote machine.
|
|
{
|
|
name: "unix_socket",
|
|
data: `mongodb://u%24ername:pa%24%24w%7B%7Drd@%2Ftmp%2Fmongodb-27017.sock/test`,
|
|
shouldMatch: true,
|
|
skip: true,
|
|
},
|
|
{
|
|
name: "dashes",
|
|
data: `mongodb://db-user:db-password@mongodb-instance:27017/db-name`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "protocol+srv",
|
|
// TODO: Figure out how to handle `mongodb+srv`. It performs a DNS lookup, which fails if the host doesn't exist.
|
|
//data: `mongodb+srv://root:[email protected]/mydb?retryWrites=true&w=majority`,
|
|
data: `mongodb://root:[email protected]/mydb?retryWrites=true&w=majority`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "0.0.0.0_host",
|
|
data: `mongodb://username:[email protected]:27017/?authSource=admin`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "localhost_host",
|
|
data: `mongodb://username:password@localhost:27017/?authSource=admin`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "127.0.0.1_host",
|
|
data: `mongodb://username:[email protected]:27017/?authSource=admin`,
|
|
shouldMatch: true,
|
|
},
|
|
{
|
|
name: "docker_internal_host",
|
|
data: `mongodb://username:[email protected]:27018/?authMechanism=PLAIN&tls=true&tlsCertificateKeyFile=/etc/certs/client.pem&tlsCaFile=/etc/certs/rootCA-cert.pem`,
|
|
shouldMatch: true,
|
|
match: `mongodb://username:[email protected]:27018/?authMechanism=PLAIN&tls=true&tlsCaFile=%2Fetc%2Fcerts%2FrootCA-cert.pem&tlsCertificateKeyFile=%2Fetc%2Fcerts%2Fclient.pem`,
|
|
},
|
|
{
|
|
name: "options_authsource_external",
|
|
data: `mongodb://AKIAAAAAAAAAAAA:t9t2mawssecretkey@localhost:27017/?authMechanism=MONGODB-AWS&authsource=$external`,
|
|
shouldMatch: true,
|
|
match: `mongodb://AKIAAAAAAAAAAAA:t9t2mawssecretkey@localhost:27017/?authMechanism=MONGODB-AWS&authsource=%24external`,
|
|
},
|
|
{
|
|
name: "generic1",
|
|
data: `mongodb://root:[email protected]:27017/`,
|
|
shouldMatch: true,
|
|
},
|
|
|
|
// False positives
|
|
{
|
|
name: "no_password",
|
|
data: `mongodb://mongodb0.example.com:27017/?replicaSet=myRepl`,
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "empty",
|
|
data: `mongodb://username:@mongodb0.example.com:27017/?replicaSet=myRepl`,
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "invalid_userinfo",
|
|
data: `mongodb+srv://<user>:<password>@myMongoCluster.mongocluster.cosmos.azure.com`,
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "placeholders_x+single_host",
|
|
data: `mongodb://xxxx:xxxxx@xxxxxxx:3717/zkquant?replicaSet=mgset-3017917`,
|
|
shouldMatch: false,
|
|
},
|
|
{
|
|
name: "placeholders_x+multiple_hosts",
|
|
data: `mongodb://xxxx:xxxxx@xxxxxxx:3717,xxxxxxx:3717/zkquant?replicaSet=mgset-3017917`,
|
|
shouldMatch: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.skip {
|
|
t.SkipNow()
|
|
}
|
|
s := Scanner{}
|
|
|
|
results, err := s.FromData(context.Background(), false, []byte(test.data))
|
|
if err != nil {
|
|
t.Errorf("MongoDB.FromData() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if test.shouldMatch {
|
|
if len(results) == 0 {
|
|
t.Errorf("%s: did not receive a match for '%v' when one was expected", test.name, test.data)
|
|
return
|
|
}
|
|
expected := test.data
|
|
if test.match != "" {
|
|
expected = test.match
|
|
}
|
|
result := string(results[0].Raw)
|
|
if result != expected {
|
|
t.Errorf("%s: did not receive expected match.\n\texpected: '%s'\n\t actual: '%s'", test.name, expected, result)
|
|
return
|
|
}
|
|
} else {
|
|
if len(results) > 0 {
|
|
t.Errorf("%s: received a match for '%v' when one wasn't wanted", test.name, test.data)
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|