trufflehog/pkg/detectors/jdbc/jdbc.go
dylanTruffle 0f427b3c6a
Adding Descriptions (#3258)
* adding AI generated descriptions of the key types and their capabilities

* removing empty file

* Update abbysale.go

* update to interface

* fixes

* fix

* small cleanup

---------

Co-authored-by: Dylan Ayrey <dxa4481@rit.edu>
Co-authored-by: Dustin Decker <dustin@trufflesec.com>
2024-09-24 16:10:16 -07:00

271 lines
6.9 KiB
Go

package jdbc
import (
"context"
"database/sql"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
ignorePatterns []regexp.Regexp
}
func New(opts ...func(*Scanner)) *Scanner {
scanner := &Scanner{
ignorePatterns: []regexp.Regexp{},
}
for _, opt := range opts {
opt(scanner)
}
return scanner
}
func WithIgnorePattern(ignoreStrings []string) func(*Scanner) {
return func(s *Scanner) {
var ignorePatterns []regexp.Regexp
for _, ignoreString := range ignoreStrings {
ignorePattern, err := regexp.Compile(ignoreString)
if err != nil {
panic(fmt.Sprintf("%s is not a valid regex, error received: %v", ignoreString, err))
}
ignorePatterns = append(ignorePatterns, *ignorePattern)
}
s.ignorePatterns = ignorePatterns
}
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*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)
matchLoop:
for _, match := range matches {
if len(s.ignorePatterns) != 0 {
for _, ignore := range s.ignorePatterns {
if ignore.MatchString(match[0]) {
continue matchLoop
}
}
}
jdbcConn := match[0]
result := detectors.Result{
DetectorType: detectorspb.DetectorType_JDBC,
Raw: []byte(jdbcConn),
Redacted: tryRedactAnonymousJDBC(jdbcConn),
}
if verify {
result.Verified = false
j, err := newJDBC(jdbcConn)
if err != nil {
continue
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
pingRes := j.ping(ctx)
result.Verified = pingRes.err == nil
// If there's a ping error that is marked as "determinate" we throw it away. We do this because this was the
// behavior before tri-state verification was introduced and preserving it allows us to gradually migrate
// detectors to use tri-state verification.
if pingRes.err != nil && !pingRes.determinate {
err = pingRes.err
result.SetVerificationError(err, jdbcConn)
}
result.AnalysisInfo = map[string]string{
"connection_string": jdbcConn,
}
// TODO: specialized redaction
}
results = append(results, result)
}
return
}
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}
func tryRedactAnonymousJDBC(conn string) string {
if s, ok := tryRedactURLParams(conn); ok {
return s
}
if s, ok := tryRedactODBC(conn); ok {
return s
}
if s, ok := tryRedactBasicAuth(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, isKvp := strings.Cut(param, "=")
if isKvp && 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){
"mysql": parseMySQL,
"postgresql": parsePostgres,
"sqlserver": parseSqlServer,
}
type pingResult struct {
err error
determinate bool
}
type jdbc interface {
ping(context.Context) pingResult
}
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 string, isDeterminate func(error) bool, candidateConns ...string) pingResult {
var indeterminateErrors []error
for _, c := range candidateConns {
err := pingErr(ctx, driverName, c)
if err == nil || isDeterminate(err) {
return pingResult{err, true}
}
indeterminateErrors = append(indeterminateErrors, err)
}
return pingResult{errors.Join(indeterminateErrors...), false}
}
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
}
func (s Scanner) Description() string {
return "JDBC (Java Database Connectivity) is an API for connecting and executing queries with databases. JDBC connection strings can be used to access and manipulate databases."
}