Files
trufflehog/pkg/detectors/atlassiandatacenter/common.go
Mustansir 07a860596f [INS-455] Unify common logic in Atlassian Data Center detectors (#4907)
* unify common logic in atlassian data center detectors

* initialize url pat once

* engine_test fix

* remove bitbucketdatacenter from defaults test exclude list
2026-05-12 18:29:20 +05:00

125 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}