Skip to content

Commit

Permalink
fixed sentry auth token detector (#3827)
Browse files Browse the repository at this point in the history
* fixed sentry auth token detector

* not sure why this is failing

* Updated http client logic

Co-authored-by: Richard Gomez <[email protected]>

* resolved comments

* improved decoding logic

* removed response type

* splitted to two versions

* splitted to two versions

* this is confusing error

---------

Co-authored-by: Richard Gomez <[email protected]>
  • Loading branch information
kashifkhan0771 and rgmz authored Jan 20, 2025
1 parent 1fc8961 commit 39d2453
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 196 deletions.
132 changes: 0 additions & 132 deletions pkg/detectors/sentrytoken/sentrytoken.go

This file was deleted.

137 changes: 137 additions & 0 deletions pkg/detectors/sentrytoken/v1/sentrytoken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package sentrytoken

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

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/detectorspb"
)

type Scanner struct {
client *http.Client
}

type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
}

// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.Versioner = (*Scanner)(nil)

var (
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sentry"}) + `\b([a-f0-9]{64})\b`)

forbiddenError = "You do not have permission to perform this action."
)

func (s Scanner) Version() int {
return 1
}

// 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{"sentry"}
}

// FromData will find and optionally verify SentryToken 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)

// find all unique auth tokens
var uniqueAuthTokens = make(map[string]struct{})

for _, authToken := range keyPat.FindAllStringSubmatch(dataStr, -1) {
uniqueAuthTokens[authToken[1]] = struct{}{}
}

for authToken := range uniqueAuthTokens {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_SentryToken,
Raw: []byte(authToken),
}

if verify {
if s.client == nil {
s.client = common.SaneHttpClient()
}
extraData, isVerified, verificationErr := VerifyToken(ctx, s.client, authToken)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, authToken)
s1.ExtraData = extraData
}

results = append(results, s1)
}

return results, nil
}

func VerifyToken(ctx context.Context, client *http.Client, token string) (map[string]string, bool, error) {
// api docs: https://docs.sentry.io/api/organizations/
// this api will return 200 for user auth tokens with scope of org:<>
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://sentry.io/api/0/organizations/", nil)
if err != nil {
return nil, false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

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

switch resp.StatusCode {
case http.StatusOK:
var organizations []Organization
if err = json.NewDecoder(resp.Body).Decode(&organizations); err != nil {
return nil, false, err
}

var extraData = make(map[string]string)
for _, org := range organizations {
extraData[fmt.Sprintf("orginzation_%s", org.ID)] = org.Name
}

return extraData, true, nil
case http.StatusForbidden:
var APIResp interface{}
if err = json.NewDecoder(resp.Body).Decode(&APIResp); err != nil {
return nil, false, err
}

// if response contain the forbiddenError message it means the token is active but does not have the right scope for this API call
if strings.Contains(fmt.Sprintf("%v", APIResp), forbiddenError) {
return nil, true, nil
}

return nil, false, nil
case http.StatusUnauthorized:
return nil, false, nil
default:
return nil, false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_SentryToken
}

func (s Scanner) Description() string {
return "Sentry is an error tracking service that helps developers monitor and fix crashes in real time. Sentry tokens can be used to access and manage projects and organizations within Sentry."
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func TestSentryToken_FromChunk(t *testing.T) {
{
DetectorType: detectorspb.DetectorType_SentryToken,
Verified: true,
ExtraData: map[string]string{"orginzation_4508567357947904": "Truffle Security"},
},
},
wantErr: false,
Expand Down Expand Up @@ -106,56 +107,6 @@ func TestSentryToken_FromChunk(t *testing.T) {
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, good key but wrong scope",
s: Scanner{client: common.ConstantResponseHttpClient(403, responseBody403)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a sentry super secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SentryToken,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, account deactivated",
s: Scanner{client: common.ConstantResponseHttpClient(200, responseAccountDeactivated)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a sentry super secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SentryToken,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "found, account deactivated",
s: Scanner{client: common.ConstantResponseHttpClient(200, responseEmpty)},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a sentry super secret %s within", secret)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_SentryToken,
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
},
{
name: "not found",
s: Scanner{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import (
)

var (
validPattern = "ad00eba0e2b5b057146e1b2b9373f86dbb0e712d106529111d97cb13f081de20"
invalidPattern = "ad00e?a0e2b5b057146e1b2b9373f86dbb0e712d106529111d97cb13f081de20"
keyword = "sentrytoken"
validPattern = `
sentry_token := ad00eba0e2b5b057146e1b2b9373f86dbb0e712d106529111d97cb13f081de20
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", sentry_token))
`
invalidPattern = "28ab769ecf2b465fake#0ea877d6494feffe5017a5824ec2920f24451423fake"
keyword = "sentry"
token = "ad00eba0e2b5b057146e1b2b9373f86dbb0e712d106529111d97cb13f081de20"
)

func TestSentryToken_Pattern(t *testing.T) {
Expand All @@ -27,18 +31,13 @@ func TestSentryToken_Pattern(t *testing.T) {
}{
{
name: "valid pattern - with keyword sentrytoken",
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern),
want: []string{validPattern},
input: validPattern,
want: []string{token},
},
{
name: "valid pattern - ignore duplicate",
input: fmt.Sprintf("%s token = '%s' | '%s'", keyword, validPattern, validPattern),
want: []string{validPattern},
},
{
name: "valid pattern - key out of prefix range",
input: fmt.Sprintf("%s keyword is not close to the real key in the data\n = '%s'", keyword, validPattern),
want: []string{},
want: []string{token},
},
{
name: "invalid pattern",
Expand Down
Loading

0 comments on commit 39d2453

Please sign in to comment.