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.).
317 lines
10 KiB
Go
317 lines
10 KiB
Go
package jdbc
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/kylelemons/godebug/pretty"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
|
|
)
|
|
|
|
func TestJdbc_Pattern(t *testing.T) {
|
|
d := Scanner{}
|
|
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want []string
|
|
}{
|
|
{
|
|
// examples from: https://github.com/trufflesecurity/trufflehog/issues/3704
|
|
name: "valid patterns",
|
|
input: `
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<project version="4">
|
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
|
<data-source source="LOCAL" name="PostgreSQL - postgres@localhost" uuid="18f0f64d-b804-471d-9351-e98a67c8389f">
|
|
<driver-ref>postgresql</driver-ref>
|
|
<synchronize>true</synchronize>
|
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
|
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
|
|
<jdbc-url>jdbc:sqlserver:</jdbc-url>
|
|
<jdbc-url>jdbc:postgresql://#{uri.host}#{uri.path}?user=#{uri.user}</jdbc-url>
|
|
<jdbc-url>postgresql://postgres:postgres@<your-connection-ip-address>:5432</jdbc-url>
|
|
<jdbc-url>jdbc:mysql:localhost:3306/mydatabase</jdbc-url>
|
|
<jdbc-url>jdbc:sqlserver://x.x.x.x:1433;databaseName=MY-DB;user=MY-USER;password=MY-PASSWORD;encrypt=false</jdbc-url>
|
|
<jdbc-url>jdbc:sqlserver://localhost:1433;databaseName=AdventureWorks</jdbc-url>
|
|
<jdbc-url>(jdbc:mysql://testuser:testpassword@tcp(localhost:1521)/testdb)</jdbc-url>
|
|
<jdbc-url>jdbc:postgresql://localhost:1521/testdb?sslmode=disable&password=testpassword&user=testuser&</jdbc-url>
|
|
<working-dir>$ProjectFileDir$</working-dir>
|
|
</data-source>
|
|
</component>
|
|
</project>
|
|
`,
|
|
want: []string{
|
|
"jdbc:postgresql://localhost:5432/postgres",
|
|
"jdbc:mysql:localhost:3306/mydatabase",
|
|
"jdbc:sqlserver://x.x.x.x:1433;databaseName=MY-DB;user=MY-USER;password=MY-PASSWORD;encrypt=false",
|
|
"jdbc:sqlserver://localhost:1433;databaseName=AdventureWorks",
|
|
"jdbc:mysql://testuser:testpassword@tcp(localhost:1521)/testdb",
|
|
"jdbc:postgresql://localhost:1521/testdb?sslmode=disable&password=testpassword&user=testuser",
|
|
},
|
|
},
|
|
{
|
|
name: "valid pattern - true positives",
|
|
input: `
|
|
{
|
|
"detector": "jdbc",
|
|
"potential_matches": [
|
|
"jdbc:postgresql://localhost:5432/mydb",
|
|
"jdbc:mysql://user:pass@host:3306/db?param=1",
|
|
"jdbc:sqlite:/data/test.db",
|
|
"jdbc:oracle:thin:@host:1521:db",
|
|
"jdbc:mysql://host:3306/db,other_param",
|
|
"jdbc:db2://host:50000/db?param=1"
|
|
"jdbc:postgresql://localhost:1521/testdb?sslmode=disable&password=testpassword&user=testuser"
|
|
"jdbc:mysql://testuser:testpassword@tcp(localhost:1521)/testdb"
|
|
]
|
|
}`,
|
|
want: []string{
|
|
"jdbc:postgresql://localhost:5432/mydb",
|
|
"jdbc:mysql://user:pass@host:3306/db?param=1",
|
|
"jdbc:sqlite:/data/test.db",
|
|
"jdbc:oracle:thin:@host:1521:db",
|
|
"jdbc:mysql://host:3306/db",
|
|
"jdbc:db2://host:50000/db?param=1",
|
|
"jdbc:postgresql://localhost:1521/testdb?sslmode=disable&password=testpassword&user=testuser",
|
|
"jdbc:mysql://testuser:testpassword@tcp(localhost:1521)/testdb",
|
|
},
|
|
},
|
|
{
|
|
name: "trailing non-alphanumeric characters in password",
|
|
input: `jdbc:hive9://foo.example.com:10191/default;user=MyRoleUser;password=MyPa$$w0rd...`,
|
|
want: []string{
|
|
"jdbc:hive9://foo.example.com:10191/default;user=MyRoleUser;password=MyPa$$w0rd...",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid pattern - false positives",
|
|
input: `
|
|
{
|
|
"detector": "jdbc",
|
|
"false_positives": [
|
|
"jdbc:xyz:short",
|
|
"somejdbc:mysql://host/db",
|
|
"jdbc:invalid_driver:test",
|
|
"jdbc:mysql://host/db>next",
|
|
"adjdbc:mysql://host/db",
|
|
"jdbc:my?ql:localhost:3306/my database"
|
|
]
|
|
}`,
|
|
want: []string{},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
|
|
if len(matchedDetectors) == 0 {
|
|
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
|
|
return
|
|
}
|
|
|
|
results, err := d.FromData(context.Background(), false, []byte(test.input))
|
|
if err != nil {
|
|
t.Errorf("error = %v", err)
|
|
return
|
|
}
|
|
|
|
if len(results) != len(test.want) {
|
|
if len(results) == 0 {
|
|
t.Errorf("did not receive result")
|
|
} else {
|
|
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
|
|
}
|
|
return
|
|
}
|
|
|
|
actual := make(map[string]struct{}, len(results))
|
|
for _, r := range results {
|
|
if len(r.RawV2) > 0 {
|
|
actual[string(r.RawV2)] = struct{}{}
|
|
} else {
|
|
actual[string(r.Raw)] = struct{}{}
|
|
}
|
|
}
|
|
expected := make(map[string]struct{}, len(test.want))
|
|
for _, v := range test.want {
|
|
expected[v] = struct{}{}
|
|
}
|
|
|
|
if diff := cmp.Diff(expected, actual); diff != "" {
|
|
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJdbc_ExtraData(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
wantHost string
|
|
wantUsername string
|
|
wantDatabase string
|
|
}{
|
|
{
|
|
name: "mysql with basic auth",
|
|
data: `jdbc:mysql://root:password@localhost:3306/testdb`,
|
|
wantHost: "tcp(localhost:3306)",
|
|
wantUsername: "root",
|
|
wantDatabase: "testdb",
|
|
},
|
|
{
|
|
name: "postgresql with basic auth",
|
|
data: `jdbc:postgresql://postgres:secret@dbhost:5432/mydb`,
|
|
wantHost: "dbhost:5432",
|
|
wantUsername: "postgres",
|
|
wantDatabase: "mydb",
|
|
},
|
|
{
|
|
name: "sqlserver with semicolon params",
|
|
data: `jdbc:sqlserver://server.example.com:1433;database=testdb;user=sa;password=Pass123`,
|
|
wantHost: "server.example.com:1433",
|
|
wantUsername: "sa",
|
|
wantDatabase: "testdb",
|
|
},
|
|
{
|
|
name: "mysql with query params for credentials",
|
|
data: `jdbc:mysql://dbhost:3307/testdb?user=admin&password=secret`,
|
|
wantHost: "tcp(dbhost:3307)",
|
|
wantUsername: "admin",
|
|
wantDatabase: "testdb",
|
|
},
|
|
{
|
|
name: "postgresql with query params for credentials",
|
|
data: `jdbc:postgresql://localhost:1521/testdb?sslmode=disable&password=testpassword&user=testuser`,
|
|
wantHost: "localhost:1521",
|
|
wantUsername: "testuser",
|
|
wantDatabase: "testdb",
|
|
},
|
|
}
|
|
|
|
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"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJdbc_ExtraData_UnsupportedSubprotocol(t *testing.T) {
|
|
// For unsupported subprotocols (e.g., sqlite), ExtraData should be nil
|
|
// because we can't parse connection info, but the result should still be returned.
|
|
s := Scanner{}
|
|
results, err := s.FromData(context.Background(), false, []byte(`jdbc:sqlite:/data/test.db`))
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, results, "expected at least one result")
|
|
assert.Nil(t, results[0].ExtraData, "expected nil ExtraData for unsupported subprotocol")
|
|
}
|
|
|
|
func TestJdbc_FromDataWithIgnorePattern(t *testing.T) {
|
|
type args struct {
|
|
ctx context.Context
|
|
data []byte
|
|
verify bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want []detectors.Result
|
|
ignorePatterns []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "not found",
|
|
args: args{
|
|
ctx: context.Background(),
|
|
data: []byte("jdbc:sqlite::secretpattern:"),
|
|
verify: false,
|
|
},
|
|
want: nil,
|
|
ignorePatterns: []string{
|
|
".*secretpattern.*",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := New(WithIgnorePattern(tt.ignorePatterns))
|
|
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Jdbc.FromDataWithConfig() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if os.Getenv("FORCE_PASS_DIFF") == "true" {
|
|
return
|
|
}
|
|
for i := range got {
|
|
if len(got[i].Raw) == 0 {
|
|
t.Fatal("no raw secret present")
|
|
}
|
|
got[i].Raw = nil
|
|
}
|
|
if diff := pretty.Compare(got, tt.want); diff != "" {
|
|
t.Errorf("Jdbc.FromDataWithConfig() %s diff: (-got +want)\n%s", tt.name, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseJDBCURL_EdgeCases(t *testing.T) {
|
|
t.Run("MySQL with special characters in password", func(t *testing.T) {
|
|
// Special chars: @ # $ % ^ & * ( )
|
|
jdbcURL := "jdbc:mysql://user:p@ss%23word@localhost:3306/testdb"
|
|
jdbc, err := NewJDBC(logContext.Background(), jdbcURL)
|
|
require.NoError(t, err)
|
|
|
|
info := jdbc.GetConnectionInfo()
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, info)
|
|
assert.Equal(t, "user", info.User)
|
|
// URL encoding should be handled by url.Parse
|
|
})
|
|
|
|
t.Run("PostgreSQL with empty database", func(t *testing.T) {
|
|
jdbcURL := "jdbc:postgresql://user:pass@localhost:5432"
|
|
jdbc, err := NewJDBC(logContext.Background(), jdbcURL)
|
|
require.NoError(t, err)
|
|
|
|
info := jdbc.GetConnectionInfo()
|
|
assert.Equal(t, "postgres", info.Database) // default
|
|
})
|
|
|
|
t.Run("SQL Server with multiple semicolon params", func(t *testing.T) {
|
|
jdbcURL := "jdbc:sqlserver://localhost:1433;database=testdb;user=sa;password=Pass123;encrypt=true;trustServerCertificate=false"
|
|
jdbc, err := NewJDBC(logContext.Background(), jdbcURL)
|
|
require.NoError(t, err)
|
|
|
|
info := jdbc.GetConnectionInfo()
|
|
assert.Equal(t, "testdb", info.Database)
|
|
assert.Equal(t, "sa", info.User)
|
|
assert.Equal(t, "Pass123", info.Password)
|
|
})
|
|
|
|
t.Run("MySQL missing host", func(t *testing.T) {
|
|
// Missing // after prefix - will trigger error
|
|
jdbcURL := "jdbc:mysql:/testdb"
|
|
_, err := NewJDBC(logContext.Background(), jdbcURL)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "expected host to start with //")
|
|
})
|
|
}
|