[INS-170] Unify JDBC URL Parsing Across Detector and Analyzer (Continued) (#4606)
* changes to unify with analyzer
This commit is contained in:
@@ -83,7 +83,7 @@ matchLoop:
|
||||
}
|
||||
|
||||
if verify {
|
||||
j, err := newJDBC(logCtx, jdbcConn)
|
||||
j, err := NewJDBC(logCtx, jdbcConn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -206,31 +206,13 @@ func tryRedactRegex(conn string) (string, bool) {
|
||||
return newConn, true
|
||||
}
|
||||
|
||||
var supportedSubprotocols = map[string]func(logContext.Context, string) (jdbc, error){
|
||||
"mysql": ParseMySQL,
|
||||
"postgresql": ParsePostgres,
|
||||
"sqlserver": ParseSqlServer,
|
||||
var supportedSubprotocols = map[string]func(logContext.Context, string) (JDBC, error){
|
||||
"mysql": parseMySQL,
|
||||
"postgresql": parsePostgres,
|
||||
"sqlserver": parseSqlServer,
|
||||
}
|
||||
|
||||
type pingResult struct {
|
||||
err error
|
||||
determinate bool
|
||||
}
|
||||
|
||||
// ConnectionInfo holds parsed connection information
|
||||
type ConnectionInfo struct {
|
||||
Host string // includes port if specified, e.g., "host:port"
|
||||
Database string
|
||||
User string
|
||||
Password string
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
type jdbc interface {
|
||||
ping(context.Context) pingResult
|
||||
}
|
||||
|
||||
func newJDBC(ctx logContext.Context, conn string) (jdbc, error) {
|
||||
func NewJDBC(ctx logContext.Context, conn string) (JDBC, error) {
|
||||
// expected format: "jdbc:{subprotocol}:{subname}"
|
||||
if !strings.HasPrefix(strings.ToLower(conn), "jdbc:") {
|
||||
return nil, errors.New("expected jdbc prefix")
|
||||
@@ -242,11 +224,11 @@ func newJDBC(ctx logContext.Context, conn string) (jdbc, error) {
|
||||
return nil, errors.New("expected a colon separated subprotocol and subname")
|
||||
}
|
||||
|
||||
// get the subprotocol parser
|
||||
parser, ok := supportedSubprotocols[strings.ToLower(subprotocol)]
|
||||
if !ok {
|
||||
return nil, errors.New("unsupported subprotocol")
|
||||
return nil, fmt.Errorf("unsupported subprotocol: %s", subprotocol)
|
||||
}
|
||||
|
||||
return parser(ctx, subname)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -183,3 +186,46 @@ func TestJdbc_FromDataWithIgnorePattern(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 //")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package jdbc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type DatabaseType int
|
||||
|
||||
const (
|
||||
Unknown DatabaseType = iota
|
||||
MySQL
|
||||
PostgreSQL
|
||||
SQLServer
|
||||
)
|
||||
|
||||
func (dt DatabaseType) String() string {
|
||||
switch dt {
|
||||
case MySQL:
|
||||
return "mysql"
|
||||
case PostgreSQL:
|
||||
return "postgresql"
|
||||
case SQLServer:
|
||||
return "sqlserver"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type pingResult struct {
|
||||
err error
|
||||
determinate bool
|
||||
}
|
||||
|
||||
// ConnectionInfo holds parsed connection information
|
||||
type ConnectionInfo struct {
|
||||
Host string // includes port if specified, e.g., "host:port"
|
||||
Database string
|
||||
User string
|
||||
Password string
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
type jdbcPinger interface {
|
||||
ping(context.Context) pingResult
|
||||
}
|
||||
|
||||
// public interfaces for analyzer
|
||||
type JDBCParser interface {
|
||||
GetConnectionInfo() *ConnectionInfo
|
||||
GetDBType() DatabaseType
|
||||
BuildConnectionString() string
|
||||
}
|
||||
type JDBC interface {
|
||||
jdbcPinger
|
||||
JDBCParser
|
||||
}
|
||||
@@ -16,12 +16,26 @@ type MysqlJDBC struct {
|
||||
ConnectionInfo
|
||||
}
|
||||
|
||||
var _ JDBC = (*MysqlJDBC)(nil)
|
||||
|
||||
func (s *MysqlJDBC) ping(ctx context.Context) pingResult {
|
||||
return ping(ctx, "mysql", isMySQLErrorDeterminate,
|
||||
BuildMySQLConnectionString(s.Host, "", s.User, s.Password, s.Params))
|
||||
buildMySQLConnectionString(s.Host, "", s.User, s.Password, s.Params))
|
||||
}
|
||||
|
||||
func BuildMySQLConnectionString(host, database, user, password string, params map[string]string) string {
|
||||
func (s *MysqlJDBC) GetDBType() DatabaseType {
|
||||
return MySQL
|
||||
}
|
||||
|
||||
func (s *MysqlJDBC) GetConnectionInfo() *ConnectionInfo {
|
||||
return &s.ConnectionInfo
|
||||
}
|
||||
|
||||
func (s *MysqlJDBC) BuildConnectionString() string {
|
||||
return buildMySQLConnectionString(s.Host, s.Database, s.User, s.Password, s.Params)
|
||||
}
|
||||
|
||||
func buildMySQLConnectionString(host, database, user, password string, params map[string]string) string {
|
||||
conn := host + "/" + database
|
||||
userPass := user
|
||||
if password != "" {
|
||||
@@ -56,7 +70,7 @@ func isMySQLErrorDeterminate(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func ParseMySQL(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
func parseMySQL(ctx logContext.Context, subname string) (JDBC, error) {
|
||||
// expected form: [subprotocol:]//[user:password@]HOST[/DB][?key=val[&key=val]]
|
||||
if !strings.HasPrefix(subname, "//") {
|
||||
return nil, errors.New("expected host to start with //")
|
||||
@@ -86,7 +100,7 @@ func ParseMySQL(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseMySQLURI(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
func parseMySQLURI(ctx logContext.Context, subname string) (JDBC, error) {
|
||||
|
||||
// for standard URI format, which is all i've seen for JDBC
|
||||
u, err := url.Parse(subname)
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestMySQL(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
j, err := ParseMySQL(logContext.Background(), tt.input)
|
||||
j, err := parseMySQL(logContext.Background(), tt.input)
|
||||
|
||||
if err != nil {
|
||||
got := result{ParseErr: true}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
@@ -58,7 +60,7 @@ func TestParseMySQLMissingCredentials(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := logContext.AddLogger(context.Background())
|
||||
j, err := ParseMySQL(ctx, tt.subname)
|
||||
j, err := parseMySQL(ctx, tt.subname)
|
||||
|
||||
if tt.shouldBeNil {
|
||||
if j != nil {
|
||||
@@ -102,7 +104,7 @@ func TestParseMySQLUsernameRecognition(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := logContext.AddLogger(context.Background())
|
||||
j, err := ParseMySQL(ctx, tt.subname)
|
||||
j, err := parseMySQL(ctx, tt.subname)
|
||||
if err != nil {
|
||||
t.Fatalf("parseMySQL() error = %v", err)
|
||||
}
|
||||
@@ -115,3 +117,144 @@ func TestParseMySQLUsernameRecognition(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQL_ParseJDBCURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jdbcURL string
|
||||
wantHost string
|
||||
wantDB string
|
||||
wantUser string
|
||||
wantPass string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic URL with all parts",
|
||||
jdbcURL: "jdbc:mysql://root:password@localhost:3306/testdb",
|
||||
wantHost: "tcp(localhost:3306)",
|
||||
wantDB: "testdb",
|
||||
wantUser: "root",
|
||||
wantPass: "password",
|
||||
},
|
||||
{
|
||||
name: "URL with default port",
|
||||
jdbcURL: "jdbc:mysql://user:pass@dbhost/mydb",
|
||||
wantHost: "tcp(dbhost)",
|
||||
wantDB: "mydb",
|
||||
wantUser: "user",
|
||||
wantPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "URL with query params for credentials",
|
||||
jdbcURL: "jdbc:mysql://dbhost:3307/testdb?user=admin&password=secret",
|
||||
wantHost: "tcp(dbhost:3307)",
|
||||
wantDB: "testdb",
|
||||
wantUser: "admin",
|
||||
wantPass: "secret",
|
||||
},
|
||||
{
|
||||
name: "invalid URL - missing jdbc:mysql prefix",
|
||||
jdbcURL: "postgresql://user:pass@localhost/db",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid URL - missing //",
|
||||
jdbcURL: "jdbc:mysql:user:pass@localhost/db",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
jdbc, err := NewJDBC(logContext.Background(), tt.jdbcURL)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
info := jdbc.GetConnectionInfo()
|
||||
assert.Equal(t, tt.wantHost, info.Host)
|
||||
assert.Equal(t, tt.wantDB, info.Database)
|
||||
assert.Equal(t, tt.wantUser, info.User)
|
||||
assert.Equal(t, tt.wantPass, info.Password)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQL_ParseJDBCURL_DSNAddressParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jdbcURL string
|
||||
wantHost string
|
||||
}{
|
||||
{
|
||||
name: "DSN format with explicit port",
|
||||
jdbcURL: "jdbc:mysql://myuser:mypass@tcp(localhost:3307)/mydb",
|
||||
wantHost: "tcp(localhost:3307)",
|
||||
},
|
||||
{
|
||||
name: "DSN format with default port",
|
||||
jdbcURL: "jdbc:mysql://myuser:mypass@tcp(db.example.com:3306)/testdb",
|
||||
wantHost: "tcp(db.example.com:3306)",
|
||||
},
|
||||
{
|
||||
name: "DSN format without port",
|
||||
jdbcURL: "jdbc:mysql://myuser:mypass@tcp(myhost)/mydb",
|
||||
wantHost: "tcp(myhost:3306)",
|
||||
},
|
||||
{
|
||||
name: "Simple host:port format",
|
||||
jdbcURL: "jdbc:mysql://root:[email protected]:3308/database",
|
||||
wantHost: "tcp(mysql.server.com:3308)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
jdbc, err := NewJDBC(logContext.Background(), tt.jdbcURL)
|
||||
require.NoError(t, err)
|
||||
info := jdbc.GetConnectionInfo()
|
||||
assert.Equal(t, tt.wantHost, info.Host)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQL_BuildNativeConnectionString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info *ConnectionInfo
|
||||
wantUser string
|
||||
wantPass string
|
||||
wantHost string
|
||||
wantDB string
|
||||
}{
|
||||
{
|
||||
name: "basic connection",
|
||||
info: &ConnectionInfo{
|
||||
Host: "localhost",
|
||||
Database: "testdb",
|
||||
User: "root",
|
||||
Password: "secret",
|
||||
},
|
||||
wantUser: "root",
|
||||
wantPass: "secret",
|
||||
wantHost: "localhost",
|
||||
wantDB: "testdb",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mysqlJDBC := &MysqlJDBC{
|
||||
ConnectionInfo: *tt.info,
|
||||
}
|
||||
connStr := mysqlJDBC.BuildConnectionString()
|
||||
|
||||
// MySQL format: [user[:password]@]tcp(host:port)/database?timeout=10s
|
||||
assert.Contains(t, connStr, tt.wantUser)
|
||||
assert.Contains(t, connStr, tt.wantPass)
|
||||
assert.Contains(t, connStr, tt.wantHost)
|
||||
assert.Contains(t, connStr, "/"+tt.wantDB)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ type PostgresJDBC struct {
|
||||
ConnectionInfo
|
||||
}
|
||||
|
||||
var _ JDBC = (*PostgresJDBC)(nil)
|
||||
|
||||
func (s *PostgresJDBC) ping(ctx context.Context) pingResult {
|
||||
// It is crucial that we try to build a connection string ourselves before using the one we found. This is because
|
||||
// if the found connection string doesn't include a username, the driver will attempt to connect using the current
|
||||
@@ -23,11 +25,23 @@ func (s *PostgresJDBC) ping(ctx context.Context) pingResult {
|
||||
// contrast, when we build a connection string ourselves, if there's no username, we try 'postgres' instead, which
|
||||
// actually has a chance of working.
|
||||
return ping(ctx, "postgres", isPostgresErrorDeterminate,
|
||||
BuildPostgresConnectionString(s.Host, s.User, s.Password, "postgres", s.Params, true),
|
||||
BuildPostgresConnectionString(s.Host, s.User, s.Password, "postgres", s.Params, false),
|
||||
buildPostgresConnectionString(s.Host, s.User, s.Password, "postgres", s.Params, true),
|
||||
buildPostgresConnectionString(s.Host, s.User, s.Password, "postgres", s.Params, false),
|
||||
)
|
||||
}
|
||||
|
||||
func (s *PostgresJDBC) GetDBType() DatabaseType {
|
||||
return PostgreSQL
|
||||
}
|
||||
|
||||
func (s *PostgresJDBC) GetConnectionInfo() *ConnectionInfo {
|
||||
return &s.ConnectionInfo
|
||||
}
|
||||
|
||||
func (s *PostgresJDBC) BuildConnectionString() string {
|
||||
return buildPostgresConnectionString(s.Host, s.User, s.Password, s.Database, s.Params, true)
|
||||
}
|
||||
|
||||
func isPostgresErrorDeterminate(err error) bool {
|
||||
// Postgres codes from https://www.postgresql.org/docs/current/errcodes-appendix.html
|
||||
if pqErr, isPostgresError := err.(*pq.Error); isPostgresError {
|
||||
@@ -58,7 +72,7 @@ func joinKeyValues(m map[string]string, sep string) string {
|
||||
return strings.Join(data, sep)
|
||||
}
|
||||
|
||||
func ParsePostgres(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
func parsePostgres(ctx logContext.Context, subname string) (JDBC, error) {
|
||||
// expected form: [subprotocol:]//[user:password@]HOST[/DB][?key=val[&key=val]]
|
||||
|
||||
if !strings.HasPrefix(subname, "//") {
|
||||
@@ -122,7 +136,7 @@ func ParsePostgres(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
return postgresJDBC, nil
|
||||
}
|
||||
|
||||
func BuildPostgresConnectionString(host string, user string, password string, dbName string, params map[string]string, includeDbName bool) string {
|
||||
func buildPostgresConnectionString(host string, user string, password string, dbName string, params map[string]string, includeDbName bool) string {
|
||||
data := map[string]string{
|
||||
// default user
|
||||
"user": "postgres",
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestPostgres(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j, err := ParsePostgres(logContext.Background(), tt.input)
|
||||
j, err := parsePostgres(logContext.Background(), tt.input)
|
||||
if err != nil {
|
||||
got := result{ParseErr: true}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
@@ -47,7 +49,7 @@ func TestParsePostgresMissingCredentials(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := logContext.AddLogger(context.Background())
|
||||
j, err := ParsePostgres(ctx, tt.subname)
|
||||
j, err := parsePostgres(ctx, tt.subname)
|
||||
|
||||
if tt.shouldBeNil {
|
||||
if j != nil {
|
||||
@@ -86,7 +88,7 @@ func TestParsePostgresUsernameRecognition(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := logContext.AddLogger(context.Background())
|
||||
j, err := ParsePostgres(ctx, tt.subname)
|
||||
j, err := parsePostgres(ctx, tt.subname)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePostgres() error = %v", err)
|
||||
}
|
||||
@@ -98,3 +100,141 @@ func TestParsePostgresUsernameRecognition(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLHandler_ParseJDBCURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jdbcURL string
|
||||
wantHost string
|
||||
wantDB string
|
||||
wantUser string
|
||||
wantPass string
|
||||
wantSSLMode string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic URL with all parts",
|
||||
jdbcURL: "jdbc:postgresql://postgres:secret@localhost:5432/mydb",
|
||||
wantHost: "localhost:5432",
|
||||
wantDB: "mydb",
|
||||
wantUser: "postgres",
|
||||
wantPass: "secret",
|
||||
},
|
||||
{
|
||||
name: "URL with default port",
|
||||
jdbcURL: "jdbc:postgresql://user:pass@dbhost/testdb",
|
||||
wantHost: "dbhost",
|
||||
wantDB: "testdb",
|
||||
wantUser: "user",
|
||||
wantPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "URL with default database",
|
||||
jdbcURL: "jdbc:postgresql://user:pass@dbhost:5433",
|
||||
wantHost: "dbhost:5433",
|
||||
wantDB: "postgres",
|
||||
wantUser: "user",
|
||||
wantPass: "pass",
|
||||
},
|
||||
{
|
||||
name: "URL with SSL mode",
|
||||
jdbcURL: "jdbc:postgresql://user:pass@dbhost:5432/mydb?sslmode=require",
|
||||
wantHost: "dbhost:5432",
|
||||
wantDB: "mydb",
|
||||
wantUser: "user",
|
||||
wantPass: "pass",
|
||||
wantSSLMode: "require",
|
||||
},
|
||||
{
|
||||
name: "invalid URL - missing jdbc:postgresql prefix",
|
||||
jdbcURL: "mysql://user:pass@localhost/db",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid URL - missing //",
|
||||
jdbcURL: "jdbc:postgresql:user:pass@localhost/db",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
jdbc, err := NewJDBC(logContext.Background(), tt.jdbcURL)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
info := jdbc.GetConnectionInfo()
|
||||
assert.Equal(t, tt.wantHost, info.Host)
|
||||
assert.Equal(t, tt.wantDB, info.Database)
|
||||
assert.Equal(t, tt.wantUser, info.User)
|
||||
assert.Equal(t, tt.wantPass, info.Password)
|
||||
|
||||
if tt.wantSSLMode != "" {
|
||||
assert.Equal(t, tt.wantSSLMode, info.Params["sslmode"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLHandler_BuildNativeConnectionString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info *ConnectionInfo
|
||||
want map[string]string // key-value pairs that should be in the connection string
|
||||
}{
|
||||
{
|
||||
name: "basic connection",
|
||||
info: &ConnectionInfo{
|
||||
Host: "localhost",
|
||||
Database: "testdb",
|
||||
User: "postgres",
|
||||
Password: "secret",
|
||||
Params: map[string]string{
|
||||
"connect_timeout": "10",
|
||||
},
|
||||
},
|
||||
want: map[string]string{
|
||||
"host": "localhost",
|
||||
"dbname": "testdb",
|
||||
"user": "postgres",
|
||||
"password": "secret",
|
||||
"connect_timeout": "10",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with SSL mode",
|
||||
info: &ConnectionInfo{
|
||||
Host: "dbhost:5433",
|
||||
Database: "mydb",
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
Params: map[string]string{"sslmode": "require"},
|
||||
},
|
||||
want: map[string]string{
|
||||
"host": "dbhost",
|
||||
"port": "5433",
|
||||
"dbname": "mydb",
|
||||
"sslmode": "require",
|
||||
"user": "user",
|
||||
"password": "pass",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
jdbc := &PostgresJDBC{
|
||||
ConnectionInfo: *tt.info,
|
||||
}
|
||||
|
||||
connStr := jdbc.BuildConnectionString()
|
||||
// Verify all expected key-value pairs are in the connection string
|
||||
for key, expectedValue := range tt.want {
|
||||
expectedPair := key + "=" + expectedValue
|
||||
assert.Contains(t, connStr, expectedPair)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,23 @@ type SqlServerJDBC struct {
|
||||
ConnectionInfo
|
||||
}
|
||||
|
||||
var _ JDBC = (*SqlServerJDBC)(nil)
|
||||
|
||||
func (s *SqlServerJDBC) ping(ctx context.Context) pingResult {
|
||||
return ping(ctx, "mssql", isSqlServerErrorDeterminate,
|
||||
BuildSQLServerConnectionString(s.Host, s.User, s.Password, "master", map[string]string{"connection+timeout": "5"}))
|
||||
buildSQLServerConnectionString(s.Host, s.User, s.Password, "master", map[string]string{"connection+timeout": "5"}))
|
||||
}
|
||||
|
||||
func (s *SqlServerJDBC) GetDBType() DatabaseType {
|
||||
return SQLServer
|
||||
}
|
||||
|
||||
func (s *SqlServerJDBC) GetConnectionInfo() *ConnectionInfo {
|
||||
return &s.ConnectionInfo
|
||||
}
|
||||
|
||||
func (s *SqlServerJDBC) BuildConnectionString() string {
|
||||
return buildSQLServerConnectionString(s.Host, s.User, s.Password, s.Database, s.Params)
|
||||
}
|
||||
|
||||
func isSqlServerErrorDeterminate(err error) bool {
|
||||
@@ -34,7 +48,7 @@ func isSqlServerErrorDeterminate(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func ParseSqlServer(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
func parseSqlServer(ctx logContext.Context, subname string) (JDBC, error) {
|
||||
if !strings.HasPrefix(subname, "//") {
|
||||
return nil, errors.New("expected connection to start with //")
|
||||
}
|
||||
@@ -93,7 +107,7 @@ func ParseSqlServer(ctx logContext.Context, subname string) (jdbc, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildSQLServerConnectionString(host, user, password, database string, params map[string]string) string {
|
||||
func buildSQLServerConnectionString(host, user, password, database string, params map[string]string) string {
|
||||
conn := fmt.Sprintf("sqlserver://%s:%s@%s?database=%s", user, password, host, database)
|
||||
if len(params) > 0 {
|
||||
for k, v := range params {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
@@ -54,7 +56,7 @@ func TestParseSqlServerMissingCredentials(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := logContext.AddLogger(context.Background())
|
||||
|
||||
j, err := ParseSqlServer(ctx, tt.subname)
|
||||
j, err := parseSqlServer(ctx, tt.subname)
|
||||
|
||||
if tt.shouldBeNil {
|
||||
if j != nil {
|
||||
@@ -101,7 +103,7 @@ func TestParseSqlServerUserIgnoredBug2(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := logContext.AddLogger(context.Background())
|
||||
|
||||
j, err := ParseSqlServer(ctx, tt.subname)
|
||||
j, err := parseSqlServer(ctx, tt.subname)
|
||||
if err != nil {
|
||||
t.Fatalf("parseSqlServer() error = %v", err)
|
||||
}
|
||||
@@ -115,3 +117,106 @@ func TestParseSqlServerUserIgnoredBug2(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLServerHandler_ParseJDBCURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jdbcURL string
|
||||
wantHost string
|
||||
wantDB string
|
||||
wantUser string
|
||||
wantPass string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "basic URL with semicolon params",
|
||||
jdbcURL: "jdbc:sqlserver://localhost:1433;database=testdb;user=sa;password=Pass123",
|
||||
wantHost: "localhost:1433",
|
||||
wantDB: "testdb",
|
||||
wantUser: "sa",
|
||||
wantPass: "Pass123",
|
||||
},
|
||||
{
|
||||
name: "URL with default port and database",
|
||||
jdbcURL: "jdbc:sqlserver://dbhost;user=testuser;password=secret",
|
||||
wantHost: "dbhost:1433",
|
||||
wantDB: "master",
|
||||
wantUser: "testuser",
|
||||
wantPass: "secret",
|
||||
},
|
||||
{
|
||||
name: "URL with port in host",
|
||||
jdbcURL: "jdbc:sqlserver://server.example.com:1434;databaseName=mydb;userId=admin;pwd=admin123",
|
||||
wantHost: "server.example.com:1434",
|
||||
wantDB: "mydb",
|
||||
wantUser: "admin",
|
||||
wantPass: "admin123",
|
||||
},
|
||||
{
|
||||
name: "invalid URL - missing jdbc:sqlserver prefix",
|
||||
jdbcURL: "jdbc:mysql://localhost/db",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid URL - missing //",
|
||||
jdbcURL: "jdbc:sqlserver:localhost;database=test",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
jdbc, err := NewJDBC(logContext.Background(), tt.jdbcURL)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
info := jdbc.GetConnectionInfo()
|
||||
assert.Equal(t, tt.wantHost, info.Host)
|
||||
assert.Equal(t, tt.wantDB, info.Database)
|
||||
assert.Equal(t, tt.wantUser, info.User)
|
||||
assert.Equal(t, tt.wantPass, info.Password)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLServerHandler_BuildNativeConnectionString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info *ConnectionInfo
|
||||
wantUser string
|
||||
wantPass string
|
||||
wantHost string
|
||||
wantDB string
|
||||
}{
|
||||
{
|
||||
name: "basic connection",
|
||||
info: &ConnectionInfo{
|
||||
Host: "localhost",
|
||||
Database: "testdb",
|
||||
User: "sa",
|
||||
Password: "Pass123",
|
||||
},
|
||||
wantUser: "sa",
|
||||
wantPass: "Pass123",
|
||||
wantHost: "localhost",
|
||||
wantDB: "testdb",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
jdbc := &SqlServerJDBC{
|
||||
ConnectionInfo: *tt.info,
|
||||
}
|
||||
connStr := jdbc.BuildConnectionString()
|
||||
|
||||
// SQL Server format: sqlserver://user:password@host:port?database=db&connection+timeout=10
|
||||
assert.Contains(t, connStr, tt.wantUser)
|
||||
assert.Contains(t, connStr, tt.wantPass)
|
||||
assert.Contains(t, connStr, tt.wantHost)
|
||||
assert.Contains(t, connStr, "database="+tt.wantDB)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user