Files
trufflehog/pkg/detectors/ldap/ldap.go
Miccah ad2f9a4b98 Populate SecretParts on all detectors (#4919)
* Populate SecretParts on single-part detectors

Adds SecretParts: map[string]string{"key": <secret>} to every detector
package that constructs detectors.Result with a single captured secret
value. This is the single-part half of the SecretParts migration (the
linter's common case, ~695 packages).

* Populate SecretParts on multi-part detectors
2026-04-24 08:32:51 -07:00

193 lines
5.3 KiB
Go

package ldap
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"
ldap "github.com/trufflesecurity/ldap-verify"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
func init() {
ldap.DefaultTimeout = 5 * time.Second
}
var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
uriPat = regexp.MustCompile(`\b(?i)ldaps?://[\S]+\b`)
// ldap://127.0.0.1:389
// ldap://127.0.0.1
// ldap://mydomain.test
// ldaps://[fe80:4049:92ff:fe44:4bd1]:5060
// ldap://[fe80::4bd1]:5060
// ldap://ds.example.com:389/dc=example,dc=com?givenName,sn,cn?sub?(uid=john.doe)
usernamePat = regexp.MustCompile(detectors.PrefixRegex([]string{"user", "bind"}) + `["']([a-zA-Z=,]{4,150})["']`)
passwordPat = regexp.MustCompile(detectors.PrefixRegex([]string{"pass"}) + `["']([\S]{4,48})["']`)
// https://learn.microsoft.com/en-us/windows/win32/api/iads/nf-iads-iadsopendsobject-opendsobject?redirectedfrom=MSDN
// I.E. Set ou = dso.OpenDSObject("LDAP://DC.business.com/OU=IT,DC=Business,DC=com", "Business\administrator", "Pa$$word01", 1)
iadPat = regexp.MustCompile(`OpenDSObject\(\"(?i)(ldaps?://[\S]+)\", ?\"([\S]+)\", ?\"([\S]+)\",[ \d]+\)`)
)
// 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{"ldaps://", "ldap://"}
}
// FromData will find and optionally verify Ldap 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)
// Check for matches in the URI + username + password format
uriMatches := uriPat.FindAllString(dataStr, -1)
for _, uri := range uriMatches {
ldapURL, err := url.Parse(uri)
if err != nil {
continue
}
usernameMatches := usernamePat.FindAllStringSubmatch(dataStr, -1)
for _, username := range usernameMatches {
passwordMatches := passwordPat.FindAllStringSubmatch(dataStr, -1)
for _, password := range passwordMatches {
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_LDAP,
Raw: []byte(strings.Join([]string{ldapURL.String(), username[1], password[1]}, "\t")),
SecretParts: map[string]string{
"url": ldapURL.String(),
"username": username[1],
"password": password[1],
},
}
if verify {
verificationErr := verifyLDAP(ctx, username[1], password[1], ldapURL)
s1.Verified = verificationErr == nil
if !isErrDeterminate(verificationErr) {
s1.SetVerificationError(verificationErr, password[1])
}
}
results = append(results, s1)
}
}
}
// Check for matches for the IAD library format
iadMatches := iadPat.FindAllStringSubmatch(dataStr, -1)
for _, iad := range iadMatches {
uri := iad[1]
username := iad[2]
password := iad[3]
ldapURL, err := url.Parse(uri)
if err != nil {
continue
}
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_LDAP,
Raw: []byte(strings.Join([]string{ldapURL.String(), username, password}, "\t")),
SecretParts: map[string]string{
"url": ldapURL.String(),
"username": username,
"password": password,
},
}
if verify {
verificationError := verifyLDAP(ctx, username, password, ldapURL)
s1.Verified = verificationError == nil
if !isErrDeterminate(verificationError) {
s1.SetVerificationError(verificationError, password)
}
}
results = append(results, s1)
}
return results, nil
}
func isErrDeterminate(err error) bool {
var neterr *net.OpError
if errors.As(err, &neterr) ||
errors.Is(err, context.DeadlineExceeded) ||
errors.Is(err, context.Canceled) {
return false
}
return true
}
func verifyLDAP(ctx context.Context, username, password string, ldapURL *url.URL) error {
// Tests with non-TLS, TLS, and STARTTLS
uri := ldapURL.String()
switch ldapURL.Scheme {
case "ldap":
// Non-TLS dial
l, err := ldap.DialURL(uri, ldap.DialWithContext(ctx))
if err != nil {
return err
}
defer l.Close()
// Non-TLS verify
err = l.BindContext(ctx, username, password)
if err == nil {
return nil
}
// STARTTLS
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
return err
}
// STARTTLS verify
return l.BindContext(ctx, username, password)
case "ldaps":
// TLS dial
l, err := ldap.DialURL(
uri,
ldap.DialWithContext(ctx),
ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}),
)
if err != nil {
return err
}
defer l.Close()
// TLS verify
return l.BindContext(ctx, username, password)
default:
return fmt.Errorf("unknown ldap scheme %q", ldapURL.Scheme)
}
}
func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_LDAP
}
func (s Scanner) Description() string {
return "LDAP (Lightweight Directory Access Protocol) is an open, vendor-neutral, industry standard application protocol for accessing and maintaining distributed directory information services over an Internet Protocol (IP) network."
}