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
116 changes: 116 additions & 0 deletions pkg/detectors/newrelicmobileapptoken/newrelicmobileapptoken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package newrelicmobileapptoken

import (
"context"
"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
}

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

var (
defaultClient = common.SaneHttpClient()
// US region keys start with AA, followed by a 40 characters hexadecimal string, end with "-NRMA"
// EU region keys start with eu01xx, followed by a 36 characters hexadecimal string, end with "-NRMA"
keyPat = regexp.MustCompile(`\b((AA[0-9a-f]{40}|eu01xx[0-9a-f]{36})-NRMA)\b`)
)

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{"-nrma"} }

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

func (s Scanner) Description() string {
return "A New Relic Mobile App Token is an authentication key used to send mobile application telemetry data (such as performance metrics, crashes, and events) from iOS and Android apps to New Relic for monitoring and analysis. It is specific to each mobile app and ensures secure data ingestion."
}

func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

matches := keyPat.FindAllStringSubmatch(dataStr, -1)
for _, match := range matches {
resMatch := strings.TrimSpace(match[1])

s1 := detectors.Result{
DetectorType: s.Type(),
Raw: []byte(resMatch),
Redacted: resMatch[:8] + "...",
}

if verify {
isVerified, extraData, verificationErr := s.verify(ctx, resMatch)
s1.Verified = isVerified
s1.ExtraData = extraData
s1.SetVerificationError(verificationErr)
}

results = append(results, s1)
}

return results, nil
}

// verify checks if the provided key is valid by making a request to the New Relic Android Agent internal API.
// A POST request is made to the /mobile/v5/connect endpoint. If the response status code is 400,
// it indicates that the key is valid but the request is malformed (since we're not sending a proper payload),
// while a 401 status code indicates that the key is invalid. Any other status code is treated as an error.
// This API is not documented, and was discovered by digging into New Relic's Android agent SDK code:
// https://github.com/newrelic/newrelic-android-agent
func (s Scanner) verify(ctx context.Context, key string) (bool, map[string]string, error) {
host := "https://mobile-collector.newrelic.com"
region := "us"
if strings.HasPrefix(key, "eu01xx") {
// EU region keys have a different host
host = "https://mobile-collector.eu01.nr-data.net"
region = "eu"
}
req, err := http.NewRequestWithContext(
ctx, http.MethodPost, host+"/mobile/v5/connect", http.NoBody)
if err != nil {
return false, nil, fmt.Errorf("error constructing request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-App-License-Key", key)

client := s.getClient()
res, err := client.Do(req)
if err != nil {
return false, nil, fmt.Errorf("error making request: %w", err)
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()

switch res.StatusCode {
case http.StatusBadRequest:
return true, map[string]string{"region": region}, nil
case http.StatusUnauthorized:
return false, map[string]string{"region": region}, nil
default:
return false, nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//go:build detectors
// +build detectors

package newrelicmobileapptoken

import (
"context"
"fmt"
"testing"
"time"

"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"

"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

func TestNewRelicMobileAppToken_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}

key := testSecrets.MustGetField("NEW_RELIC_MOBILE_APP_TOKEN")
keyEU := testSecrets.MustGetField("NEW_RELIC_MOBILE_APP_TOKEN_EU")
keyInactive := "AAcc7eb96551e8cd65818865695f35bb109455d623-NRMA"

type args struct {
ctx context.Context
data []byte
verify bool
}
tests := []struct {
name string
s Scanner
args args
want []detectors.Result
wantErr bool
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a new relic mobile app token %s within", key)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_NewRelicMobileAppToken,
Verified: true,
ExtraData: map[string]string{
"region": "us",
},
},
},
wantErr: false,
},
{
name: "found eu, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a new EU relic mobile app token %s within", keyEU)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_NewRelicMobileAppToken,
Verified: true,
ExtraData: map[string]string{
"region": "eu",
},
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a new relic mobile app token %s within", keyInactive)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_NewRelicMobileAppToken,
Verified: false,
ExtraData: map[string]string{
"region": "us",
},
},
},
wantErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("NewRelicMobileAppToken.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
if len(got[i].Redacted) == 0 {
t.Fatalf("no redacted secret present: \n %+v", got[i])
}
got[i].Redacted = ""
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("NewRelicMobileAppToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
}

func BenchmarkFromData(benchmark *testing.B) {
ctx := context.Background()
s := Scanner{}
for name, data := range detectors.MustGetBenchmarkData() {
benchmark.Run(name, func(b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := s.FromData(ctx, false, data)
if err != nil {
b.Fatal(err)
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package newrelicmobileapptoken

import (
"context"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

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

var (
validPattern = "AAcc7eb96551e8cd65818865695f35bb109455d623-NRMA"
validPatternEU = "eu01xxbfd4a807e4099453ba160493119a126319cb-NRMA"
invalidPattern = "AAcc7eb96551e8cd65818865695f35bb109455d6-NRMA"
)

func TestNewRelicMobileAppToken_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern",
input: fmt.Sprintf("new relic mobile app token = '%s'", validPattern),
want: []string{validPattern},
},
{
name: "valid pattern EU",
input: fmt.Sprintf("new relic mobile app token EU = '%s'", validPatternEU),
want: []string{validPatternEU},
},
{
name: "invalid pattern",
input: fmt.Sprintf("new relic mobile app token = '%s'", invalidPattern),
want: []string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
return
}

results, err := d.FromData(context.Background(), false, []byte(test.input))
if err != nil {
t.Errorf("error = %v", err)
return
}

if len(results) != len(test.want) {
if len(results) == 0 {
t.Errorf("did not receive result")
} else {
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
}
return
}

actual := make(map[string]struct{}, len(results))
for _, r := range results {
if len(r.RawV2) > 0 {
actual[string(r.RawV2)] = struct{}{}
} else {
actual[string(r.Raw)] = struct{}{}
}
}
expected := make(map[string]struct{}, len(test.want))
for _, v := range test.want {
expected[v] = struct{}{}
}

if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ import (
netlifyv2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/netlify/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/netsuite"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/neutrinoapi"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/newrelicmobileapptoken"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/newrelicpersonalapikey"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/newsapi"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/newscatcher"
Expand Down Expand Up @@ -1373,6 +1374,7 @@ func buildDetectorList() []detectors.Detector {
&netlifyv2.Scanner{},
&netsuite.Scanner{},
&neutrinoapi.Scanner{},
&newrelicmobileapptoken.Scanner{},
&newrelicpersonalapikey.Scanner{},
&newsapi.Scanner{},
&newscatcher.Scanner{},
Expand Down
Loading
Loading