Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -31,7 +31,6 @@ var (
// 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+)?)`)
)

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, []string{"atlassian", "bitbucket"}, 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()
Comment on lines 85 to 92
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of all this and do

baseURL + "/rest/api/1.0/projects?limit=1"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to make as minimal changes as possible to the detectors' specific logic. Also we generally prefer to use net/url to work with URLs and paths. This is valid for your other comment as well.


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
117 changes: 117 additions & 0 deletions pkg/detectors/atlassiandatacenter/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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)`,
)
}

// FindEndpoints extracts all URLs from data that are near the given keywords,
// 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, keywords []string, resolve func(...string) []string) []string {
urlPat := regexp.MustCompile(detectors.PrefixRegex(keywords) + `(https?://[a-zA-Z0-9][a-zA-Z0-9.\-]*(?::\d{1,5})?)`)
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
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