* Add Type() to detector interface The goal here is to allow the detector type information to be used without the need for reflection. This could possibly allow us to more easily inject information into detectors or filter them out if necessary. Co-authored-by: ahmed <[email protected]> * remove test detector --------- Co-authored-by: ahmed <[email protected]>
209 lines
5.0 KiB
Go
209 lines
5.0 KiB
Go
package jdbc
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
|
)
|
|
|
|
type Scanner struct{}
|
|
|
|
// Ensure the Scanner satisfies the interface at compile time.
|
|
var _ detectors.Detector = (*Scanner)(nil)
|
|
|
|
var (
|
|
keyPat = regexp.MustCompile(`(?i)jdbc:[\w]{3,10}:[^\s"']{0,512}`)
|
|
)
|
|
|
|
// 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{"jdbc"}
|
|
}
|
|
|
|
// FromData will find and optionally verify Jdbc secrets in a given set of bytes.
|
|
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
|
dataStr := string(data)
|
|
|
|
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
|
|
for _, match := range matches {
|
|
jdbcConn := match[0]
|
|
|
|
s := detectors.Result{
|
|
DetectorType: detectorspb.DetectorType_JDBC,
|
|
Raw: []byte(jdbcConn),
|
|
Redacted: tryRedactAnonymousJDBC(jdbcConn),
|
|
}
|
|
|
|
if verify {
|
|
s.Verified = false
|
|
j, err := newJDBC(jdbcConn)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
s.Verified = j.ping(ctx)
|
|
// TODO: specialized redaction
|
|
}
|
|
|
|
if !s.Verified && detectors.IsKnownFalsePositive(string(s.Raw), detectors.DefaultFalsePositives, false) {
|
|
continue
|
|
}
|
|
|
|
results = append(results, s)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func tryRedactAnonymousJDBC(conn string) string {
|
|
if s, ok := tryRedactBasicAuth(conn); ok {
|
|
return s
|
|
}
|
|
if s, ok := tryRedactURLParams(conn); ok {
|
|
return s
|
|
}
|
|
if s, ok := tryRedactODBC(conn); ok {
|
|
return s
|
|
}
|
|
if s, ok := tryRedactRegex(conn); ok {
|
|
return s
|
|
}
|
|
return conn
|
|
}
|
|
|
|
// Basic authentication "username:password@host" style
|
|
func tryRedactBasicAuth(conn string) (string, bool) {
|
|
userPass, postfix, found := strings.Cut(conn, "@")
|
|
if !found {
|
|
return "", false
|
|
}
|
|
index := strings.LastIndex(userPass, ":")
|
|
if index == -1 {
|
|
return "", false
|
|
}
|
|
prefix, pass := userPass[:index], userPass[index+1:]
|
|
return prefix + ":" + strings.Repeat("*", len(pass)) + "@" + postfix, true
|
|
}
|
|
|
|
// URL param "?password=password" style
|
|
func tryRedactURLParams(conn string) (string, bool) {
|
|
prefix, paramString, found := strings.Cut(conn, "?")
|
|
if !found {
|
|
return "", false
|
|
}
|
|
var newParams []string
|
|
found = false
|
|
for _, param := range strings.Split(paramString, "&") {
|
|
key, val, _ := strings.Cut(param, "=")
|
|
if strings.Contains(strings.ToLower(key), "pass") {
|
|
newParams = append(newParams, key+"="+strings.Repeat("*", len(val)))
|
|
found = true
|
|
continue
|
|
}
|
|
newParams = append(newParams, param)
|
|
}
|
|
if !found {
|
|
return "", false
|
|
}
|
|
return prefix + "?" + strings.Join(newParams, "&"), true
|
|
}
|
|
|
|
// ODBC params ";password=password" style
|
|
func tryRedactODBC(conn string) (string, bool) {
|
|
var found bool
|
|
var newParams []string
|
|
for _, param := range strings.Split(conn, ";") {
|
|
key, val, _ := strings.Cut(param, "=")
|
|
if strings.Contains(strings.ToLower(key), "pass") {
|
|
newParams = append(newParams, key+"="+strings.Repeat("*", len(val)))
|
|
found = true
|
|
continue
|
|
}
|
|
newParams = append(newParams, param)
|
|
}
|
|
if !found {
|
|
return "", false
|
|
}
|
|
return strings.Join(newParams, ";"), true
|
|
}
|
|
|
|
// Naively search the string for "pass="
|
|
func tryRedactRegex(conn string) (string, bool) {
|
|
pattern := regexp.MustCompile(`(?i)pass.*?=(.+?)\b`)
|
|
var found bool
|
|
newConn := pattern.ReplaceAllStringFunc(conn, func(s string) string {
|
|
index := strings.Index(s, "=")
|
|
if index == -1 {
|
|
// unreachable due to regex containing '='
|
|
return s
|
|
}
|
|
found = true
|
|
return s[:index+1] + strings.Repeat("*", len(s[index+1:]))
|
|
})
|
|
if !found {
|
|
return "", false
|
|
}
|
|
return newConn, true
|
|
}
|
|
|
|
var supportedSubprotocols = map[string]func(string) (jdbc, error){
|
|
"sqlite": parseSqlite,
|
|
"mysql": parseMySQL,
|
|
"postgresql": parsePostgres,
|
|
"sqlserver": parseSqlServer,
|
|
}
|
|
|
|
type jdbc interface {
|
|
ping(context.Context) bool
|
|
}
|
|
|
|
func newJDBC(conn string) (jdbc, error) {
|
|
// expected format: "jdbc:{subprotocol}:{subname}"
|
|
if !strings.HasPrefix(strings.ToLower(conn), "jdbc:") {
|
|
return nil, errors.New("expected jdbc prefix")
|
|
}
|
|
conn = conn[len("jdbc:"):]
|
|
subprotocol, subname, found := strings.Cut(conn, ":")
|
|
if !found {
|
|
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 parser(subname)
|
|
}
|
|
|
|
func ping(ctx context.Context, driverName, conn string) bool {
|
|
if err := pingErr(ctx, driverName, conn); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func pingErr(ctx context.Context, driverName, conn string) error {
|
|
db, err := sql.Open(driverName, conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := db.PingContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s Scanner) Type() detectorspb.DetectorType {
|
|
return detectorspb.DetectorType_JDBC
|
|
}
|