Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package bitbucketdatacenter
import (
"context"
"fmt"
"io"
"net/http"
"strings"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/atlassiandatacenter"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

Expand All @@ -30,8 +30,7 @@ var (
// and are usually between the length of 40-50 character
// consisting of both alphanumeric and some special character like +, _, @ and etc
userPat = regexp.MustCompile(`\b(BBDC-[A-Za-z0-9+/@_-]{40,50})(?:[^A-Za-z0-9+/@_-]|$)`)

urlPat = regexp.MustCompile(detectors.PrefixRegex([]string{"atlassian", "bitbucket"}) + `(https?://[a-zA-Z0-9.-]+(?::\d+)?)`)
urlPat = atlassiandatacenter.GetURLPat([]string{"atlassian", "bitbucket"})
)

func (s Scanner) Keywords() []string {
Expand All @@ -52,24 +51,11 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
return results, nil
}

foundURLs := make(map[string]struct{})
for _, match := range urlPat.FindAllStringSubmatch(dataStr, -1) {
foundURLs[match[1]] = struct{}{}
}

endpoints := make([]string, 0, len(foundURLs))
for endpoint := range foundURLs {
endpoints = append(endpoints, endpoint)
}

var uniqueUrls = make(map[string]struct{})
for _, endpoint := range s.Endpoints(endpoints...) {
uniqueUrls[endpoint] = struct{}{}
}
endpoints := atlassiandatacenter.FindEndpoints(dataStr, urlPat, s.Endpoints)

// create combination results that can be verified
for secret := range uniqueSecretPat {
for bitBucketURL := range uniqueUrls {
for _, bitBucketURL := range endpoints {
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_BitbucketDataCenter,
Raw: []byte(secret),
Expand Down Expand Up @@ -105,30 +91,8 @@ func verifyMatch(ctx context.Context, client *http.Client, secretPat, baseURL st
q.Set("limit", "1")
u.RawQuery = q.Encode()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody)
if err != nil {
return false, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", secretPat))
resp, err := client.Do(req)
if err != nil {
return false, err
}

defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()

switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
isVerified, _, err := atlassiandatacenter.MakeVerifyRequest(ctx, client, u.String(), secretPat)
return isVerified, err
}

func (s Scanner) Type() detector_typepb.DetectorType {
Expand Down
124 changes: 124 additions & 0 deletions pkg/detectors/atlassiandatacenter/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package atlassiandatacenter

import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
)

// GetDCTokenPat returns a compiled regex that matches Atlassian Data Center PATs
// (Jira DC and Confluence DC style) scoped to the given keyword prefixes.
//
// PATs are 44-char base64 strings decoding to "<numeric-id>:<random-bytes>".
// The first character is always M, N, or O because the numeric ID begins with
// an ASCII digit (0x30–0x39). The trailing boundary prevents matching substrings
// of longer base64 strings or base64-padded tokens.
//
// This does not apply to Bitbucket DC tokens, which use a BBDC- prefix format.
func GetDCTokenPat(prefixes []string) *regexp.Regexp {
return regexp.MustCompile(
detectors.PrefixRegex(prefixes) + `\b([MNO][A-Za-z0-9+/]{43})(?:[^A-Za-z0-9+/=]|\z)`,
)
}

// GetURLPat returns a compiled regex that matches self-hosted Atlassian instance
// URLs (scheme + alphanumeric-starting host + optional port up to 5 digits),
// scoped to the given keyword prefixes. Callers should store the result in a
// package-level var so the regex is compiled once at init time rather than per chunk.
func GetURLPat(prefixes []string) *regexp.Regexp {
return regexp.MustCompile(detectors.PrefixRegex(prefixes) + `(https?://[a-zA-Z0-9][a-zA-Z0-9.\-]*(?::\d{1,5})?)`)
}

// FindEndpoints extracts all URLs matching urlPat from data, passes them through
// the resolve function (typically s.Endpoints), deduplicates the results, and
// returns them as a slice with trailing slashes stripped.
func FindEndpoints(data string, urlPat *regexp.Regexp, resolve func(...string) []string) []string {
seen := make(map[string]struct{})
for _, m := range urlPat.FindAllStringSubmatch(data, -1) {
seen[m[1]] = struct{}{}
}

raw := make([]string, 0, len(seen))
for u := range seen {
raw = append(raw, u)
}

resolved := make(map[string]struct{})
for _, u := range resolve(raw...) {
resolved[strings.TrimRight(u, "/")] = struct{}{}
}

result := make([]string, 0, len(resolved))
for u := range resolved {
result = append(result, u)
}
return result
}

// IsStructuralPAT decodes a candidate base64 string and checks that it matches
// the "<numeric id>:<random bytes>" structure used by Jira and Confluence DC PATs:
// one or more ASCII digits, a colon, then at least one more byte.
func IsStructuralPAT(candidate string) bool {
raw, err := base64.StdEncoding.DecodeString(candidate)
if err != nil {
return false
}
colon := bytes.IndexByte(raw, ':')
if colon <= 0 || colon == len(raw)-1 {
return false
}
for _, b := range raw[:colon] {
if b < '0' || b > '9' {
return false
}
}
return true
}

// MakeVerifyRequest sends a Bearer-authenticated GET request to fullURL and
// interprets the response:
// - 200: returns (true, decoded JSON body as map or nil if unparseable, nil)
// - 401: returns (false, nil, nil)
// - other: returns (false, nil, error describing the unexpected status)
//
// A non-nil error is also returned for network failures.
// Callers that need fields from the response body (e.g. display name, email)
// can read them from the returned map; callers that don't need the body can
// ignore it.
func MakeVerifyRequest(ctx context.Context, client *http.Client, fullURL, token string) (bool, map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, http.NoBody)
if err != nil {
return false, nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+token)

resp, err := client.Do(req)
if err != nil {
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()

switch resp.StatusCode {
case http.StatusOK:
var body map[string]any
_ = json.NewDecoder(resp.Body).Decode(&body)
return true, body, nil
case http.StatusUnauthorized:
return false, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}
Loading
Loading