-
Notifications
You must be signed in to change notification settings - Fork 2.3k
[INS-402] Add Jira Data Center PAT Detector #4872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
195f1a9
8b966dd
6850849
d425a74
0fea204
9989b87
6d540d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| package jiradatacenterpat | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| regexp "github.com/wasilibs/go-re2" | ||
|
|
||
| "github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
| "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb" | ||
| ) | ||
|
|
||
| type Scanner struct { | ||
| client *http.Client | ||
| detectors.EndpointSetter | ||
| } | ||
|
|
||
| // Ensure the Scanner satisfies the interfaces at compile time. | ||
| var ( | ||
| _ detectors.Detector = (*Scanner)(nil) | ||
| _ detectors.EndpointCustomizer = (*Scanner)(nil) | ||
| ) | ||
|
|
||
| var ( | ||
| defaultClient = common.SaneHttpClient() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth switching from |
||
|
|
||
| // PATs are base64-encoded strings of the form <12-digit-id>:<20-random-bytes> (33 bytes, 44 chars, no padding). | ||
| // Since the first byte is always an ASCII digit (0x30–0x39), the first base64 character is always M, N, or O. | ||
| // This is also verified by generating 25+ tokens. | ||
| // The trailing boundary (?:[^A-Za-z0-9+/=]|\z) is used instead of \b to correctly handle tokens ending in + or /. | ||
| patPat = regexp.MustCompile(detectors.PrefixRegex([]string{"jira", "atlassian"}) + `\b([MNO][A-Za-z0-9+/]{43})(?:[^A-Za-z0-9+/=]|\z)`) | ||
| urlPat = regexp.MustCompile(detectors.PrefixRegex([]string{"jira", "atlassian"}) + `(https?://[A-Za-z0-9][A-Za-z0-9.\-]*(?::\d{1,5})?)`) | ||
| ) | ||
|
|
||
| func (s Scanner) getClient() *http.Client { | ||
| if s.client != nil { | ||
| return s.client | ||
| } | ||
| return defaultClient | ||
| } | ||
|
|
||
| // Keywords are used for efficiently pre-filtering chunks. | ||
| func (s Scanner) Keywords() []string { | ||
| return []string{"jira", "atlassian"} | ||
| } | ||
|
|
||
| // FromData will find and optionally verify Jira Data Center PAT 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) | ||
|
|
||
| var tokens []string | ||
| for _, match := range patPat.FindAllStringSubmatch(dataStr, -1) { | ||
| tokens = append(tokens, match[1]) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tokens not deduplicated unlike comparable detectorsLow Severity Matched tokens are collected into a plain Reviewed by Cursor Bugbot for commit 6d540d2. Configure here. |
||
|
|
||
| foundURLs := make(map[string]struct{}) | ||
| for _, match := range urlPat.FindAllStringSubmatch(dataStr, -1) { | ||
| foundURLs[match[1]] = struct{}{} | ||
| } | ||
| uniqueURLs := make([]string, 0, len(foundURLs)) | ||
| for url := range foundURLs { | ||
| uniqueURLs = append(uniqueURLs, url) | ||
| } | ||
|
|
||
| for _, token := range tokens { | ||
| for _, endpoint := range s.Endpoints(uniqueURLs...) { | ||
| s1 := detectors.Result{ | ||
| DetectorType: detector_typepb.DetectorType_JiraDataCenterPAT, | ||
| Raw: []byte(token), | ||
| RawV2: []byte(token + endpoint), | ||
| Redacted: token[:3] + "..." + token[len(token)-3:], | ||
| } | ||
|
|
||
| if verify { | ||
| isVerified, extraData, verificationErr := verifyPAT(ctx, s.getClient(), endpoint, token) | ||
| s1.Verified = isVerified | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had a look at Jira Analyzer and it seems it does support custom domains. Can you verify if the analyzer works for this detector too? If yes, we should add AnalysisInfo |
||
| s1.ExtraData = extraData | ||
| s1.SetVerificationError(verificationErr, token) | ||
| } | ||
|
|
||
| results = append(results, s1) | ||
|
|
||
| if s1.Verified { | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return results, nil | ||
| } | ||
|
|
||
| // verifyPAT checks whether the token is valid by calling the /rest/api/2/myself endpoint, | ||
| // which returns the currently authenticated user. | ||
| // Docs: https://developer.atlassian.com/server/jira/platform/rest/v10002/api-group-myself/#api-api-2-myself-get | ||
| func verifyPAT(ctx context.Context, client *http.Client, baseURL, token string) (bool, map[string]string, error) { | ||
| u, err := detectors.ParseURLAndStripPathAndParams(baseURL) | ||
| if err != nil { | ||
| return false, nil, err | ||
| } | ||
| u.Path = "/rest/api/2/myself" | ||
|
|
||
| req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody) | ||
| if err != nil { | ||
| return false, nil, err | ||
| } | ||
|
|
||
| req.Header.Set("Accept", "application/json") | ||
| req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", 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 result map[string]any | ||
| if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { | ||
| // 200 confirms the token is valid; failing to decode only means we can't extract extra data. | ||
| return true, nil, nil | ||
| } | ||
| extraData := map[string]string{} | ||
| if name, ok := result["displayName"].(string); ok { | ||
| extraData["display_name"] = name | ||
| } | ||
| if email, ok := result["emailAddress"].(string); ok { | ||
| extraData["email_address"] = email | ||
| } | ||
| return true, extraData, nil | ||
| case http.StatusUnauthorized: | ||
| return false, nil, nil | ||
| default: | ||
| return false, nil, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode) | ||
| } | ||
| } | ||
|
|
||
| func (s Scanner) Type() detector_typepb.DetectorType { | ||
| return detector_typepb.DetectorType_JiraDataCenterPAT | ||
| } | ||
|
|
||
| func (s Scanner) Description() string { | ||
| return "Jira Data Center is a self-hosted version of Jira. Personal Access Tokens (PATs) are used to authenticate API requests to Jira Data Center instances." | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think this detector might be a candidate for
detectors.DefaultMultiPartCredentialProvider?