Skip to content

Commit a1243a4

Browse files
authored
[feat] Addition of Detector - Azure Subsciprtion Keys (#3998)
* azureapimanagementsubscriptionkey detector implementation * move test patterns to their respective tests
1 parent de874d3 commit a1243a4

File tree

6 files changed

+398
-50
lines changed

6 files changed

+398
-50
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package azureapimanagementsubscriptionkey
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
10+
regexp "github.com/wasilibs/go-re2"
11+
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
14+
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
17+
)
18+
19+
type Scanner struct {
20+
client *http.Client
21+
detectors.DefaultMultiPartCredentialProvider
22+
}
23+
24+
// Ensure the Scanner satisfies the interface at compile time.
25+
var _ detectors.Detector = (*Scanner)(nil)
26+
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
27+
28+
var (
29+
defaultClient = common.SaneHttpClient()
30+
urlPat = regexp.MustCompile(`https://([a-z0-9][a-z0-9-]{0,48}[a-z0-9])\.azure-api\.net`) // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.APIM.Name/
31+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure", ".azure-api.net", "subscription", "key"}) + `([a-zA-Z-0-9]{32})`) // pattern for both Primary and secondary key
32+
33+
invalidHosts = simple.NewCache[struct{}]()
34+
noSuchHostErr = errors.New("no such host")
35+
)
36+
37+
// Keywords are used for efficiently pre-filtering chunks.
38+
// Use identifiers in the secret preferably, or the provider name.
39+
func (s Scanner) Keywords() []string {
40+
return []string{".azure-api.net"}
41+
}
42+
43+
// FromData will find and optionally verify Azure Subscription keys in a given set of bytes.
44+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
45+
logger := logContext.AddLogger(ctx).Logger().WithName("azureapimanagementsubscriptionkey")
46+
dataStr := string(data)
47+
48+
urlMatchesUnique := make(map[string]struct{})
49+
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
50+
urlMatchesUnique[urlMatch[0]] = struct{}{}
51+
}
52+
keyMatchesUnique := make(map[string]struct{})
53+
for _, keyMatch := range keyPat.FindAllStringSubmatch(dataStr, -1) {
54+
keyMatchesUnique[strings.TrimSpace(keyMatch[1])] = struct{}{}
55+
}
56+
57+
EndpointLoop:
58+
for baseUrl := range urlMatchesUnique {
59+
for key := range keyMatchesUnique {
60+
s1 := detectors.Result{
61+
DetectorType: detectorspb.DetectorType_AzureAPIManagementSubscriptionKey,
62+
Raw: []byte(baseUrl),
63+
RawV2: []byte(baseUrl + ":" + key),
64+
}
65+
66+
if verify {
67+
if invalidHosts.Exists(baseUrl) {
68+
logger.V(3).Info("Skipping invalid registry", "baseUrl", baseUrl)
69+
continue EndpointLoop
70+
}
71+
72+
client := s.client
73+
if client == nil {
74+
client = defaultClient
75+
}
76+
77+
isVerified, verificationErr := s.verifyMatch(ctx, client, baseUrl, key)
78+
s1.Verified = isVerified
79+
if verificationErr != nil {
80+
if errors.Is(verificationErr, noSuchHostErr) {
81+
invalidHosts.Set(baseUrl, struct{}{})
82+
continue EndpointLoop
83+
}
84+
s1.SetVerificationError(verificationErr, baseUrl)
85+
}
86+
}
87+
88+
results = append(results, s1)
89+
}
90+
}
91+
92+
return results, nil
93+
}
94+
95+
func (s Scanner) Type() detectorspb.DetectorType {
96+
return detectorspb.DetectorType_AzureAPIManagementSubscriptionKey
97+
}
98+
99+
func (s Scanner) Description() string {
100+
return "Azure API Management provides a direct management REST API for performing operations on selected entities, such as users, groups, products, and subscriptions."
101+
}
102+
103+
func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
104+
return false, ""
105+
}
106+
107+
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, baseUrl, key string) (bool, error) {
108+
url := baseUrl + "/echo/resource" // default testing endpoint for api management services
109+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
110+
if err != nil {
111+
return false, err
112+
}
113+
req.Header.Set("Content-Type", "application/json")
114+
req.Header.Set("Ocp-Apim-Subscription-Key", key)
115+
resp, err := client.Do(req)
116+
if err != nil {
117+
return false, nil
118+
}
119+
defer resp.Body.Close()
120+
121+
switch resp.StatusCode {
122+
case http.StatusOK:
123+
return true, nil
124+
case http.StatusUnauthorized:
125+
return false, nil
126+
default:
127+
return false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
128+
}
129+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package azureapimanagementsubscriptionkey
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
14+
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
17+
18+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
19+
)
20+
21+
func TestAzureAPIManagementSubscriptionKey_FromChunk(t *testing.T) {
22+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
23+
defer cancel()
24+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
25+
if err != nil {
26+
t.Fatalf("could not get test secrets from GCP: %s", err)
27+
}
28+
url := testSecrets.MustGetField("AZUREAPIMANAGEMENTSUBSCRIPTIONKEY_URL")
29+
secret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPISUBSCRIPTIONKEY")
30+
inactiveSecret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPISUBSCRIPTIONKEY_INACTIVE")
31+
32+
type args struct {
33+
ctx context.Context
34+
data []byte
35+
verify bool
36+
}
37+
tests := []struct {
38+
name string
39+
s Scanner
40+
args args
41+
want []detectors.Result
42+
wantErr bool
43+
wantVerificationErr bool
44+
}{
45+
{
46+
name: "found, verified",
47+
s: Scanner{},
48+
args: args{
49+
ctx: ctx,
50+
data: []byte(fmt.Sprintf("You can find a azure api management gateway url %s and subscription key %s within", url, secret)),
51+
verify: true,
52+
},
53+
want: []detectors.Result{
54+
{
55+
DetectorType: detectorspb.DetectorType_AzureAPIManagementSubscriptionKey,
56+
Verified: true,
57+
},
58+
},
59+
wantErr: false,
60+
wantVerificationErr: false,
61+
},
62+
{
63+
name: "found, unverified",
64+
s: Scanner{},
65+
args: args{
66+
ctx: ctx,
67+
data: []byte(fmt.Sprintf("You can find a azure api management gateway url %s and subscription key %s within but not valid", url, inactiveSecret)), // the secret would satisfy the regex but not pass validation
68+
verify: true,
69+
},
70+
want: []detectors.Result{
71+
{
72+
DetectorType: detectorspb.DetectorType_AzureAPIManagementSubscriptionKey,
73+
Verified: false,
74+
},
75+
},
76+
wantErr: false,
77+
wantVerificationErr: false,
78+
},
79+
{
80+
name: "not found",
81+
s: Scanner{},
82+
args: args{
83+
ctx: ctx,
84+
data: []byte("You cannot find the secret within"),
85+
verify: true,
86+
},
87+
want: nil,
88+
wantErr: false,
89+
wantVerificationErr: false,
90+
},
91+
}
92+
for _, tt := range tests {
93+
t.Run(tt.name, func(t *testing.T) {
94+
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
95+
if (err != nil) != tt.wantErr {
96+
t.Errorf("AzureDirectManagementAPIKey.FromData() error = %v, wantErr %v", err, tt.wantErr)
97+
return
98+
}
99+
for i := range got {
100+
if len(got[i].Raw) == 0 {
101+
t.Fatalf("no raw secret present: \n %+v", got[i])
102+
}
103+
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
104+
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
105+
}
106+
}
107+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "Redacted", "verificationError")
108+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
109+
t.Errorf("AzureDirectManagementAPIKey.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
110+
}
111+
})
112+
}
113+
}
114+
115+
func BenchmarkFromData(benchmark *testing.B) {
116+
ctx := context.Background()
117+
s := Scanner{}
118+
for name, data := range detectors.MustGetBenchmarkData() {
119+
benchmark.Run(name, func(b *testing.B) {
120+
b.ResetTimer()
121+
for n := 0; n < b.N; n++ {
122+
_, err := s.FromData(ctx, false, data)
123+
if err != nil {
124+
b.Fatal(err)
125+
}
126+
}
127+
})
128+
}
129+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package azureapimanagementsubscriptionkey
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
11+
)
12+
13+
func TestAzureAPIManagementSubscriptionKey_Pattern(t *testing.T) {
14+
d := Scanner{}
15+
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
16+
17+
tests := []struct {
18+
name string
19+
input string
20+
want []string
21+
}{
22+
{
23+
name: "valid pattern",
24+
input: `
25+
AZURE_API_MANAGEMENT_GATEWAY_URL=https://trufflesecuritytest.azure-api.net
26+
AZURE_API_MANAGEMENT_SUBSCRIPTION_KEY=2c69j0dc327c4929b74d3a832a04266b
27+
`,
28+
want: []string{"https://trufflesecuritytest.azure-api.net:2c69j0dc327c4929b74d3a832a04266b"},
29+
},
30+
{
31+
name: "invalid pattern",
32+
input: `
33+
AZURE_API_MANAGEMENT_GATEWAY_URL=https://trufflesecuritytest.azure-api.net
34+
AZURE_API_MANAGEMENT_SUBSCRIPTION_KEY=2c69j2dc3f7c4929b74d3a832a042
35+
`,
36+
want: nil,
37+
},
38+
}
39+
40+
for _, test := range tests {
41+
t.Run(test.name, func(t *testing.T) {
42+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
43+
if len(matchedDetectors) == 0 {
44+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
45+
return
46+
}
47+
48+
results, err := d.FromData(context.Background(), false, []byte(test.input))
49+
if err != nil {
50+
t.Errorf("error = %v", err)
51+
return
52+
}
53+
54+
if len(results) != len(test.want) {
55+
if len(results) == 0 {
56+
t.Errorf("did not receive result")
57+
} else {
58+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
59+
}
60+
return
61+
}
62+
63+
actual := make(map[string]struct{}, len(results))
64+
for _, r := range results {
65+
if len(r.RawV2) > 0 {
66+
actual[string(r.RawV2)] = struct{}{}
67+
} else {
68+
actual[string(r.Raw)] = struct{}{}
69+
}
70+
}
71+
expected := make(map[string]struct{}, len(test.want))
72+
for _, v := range test.want {
73+
expected[v] = struct{}{}
74+
}
75+
76+
if diff := cmp.Diff(expected, actual); diff != "" {
77+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
78+
}
79+
})
80+
}
81+
}

pkg/engine/defaults/defaults.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import (
7373
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_openai"
7474
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_storage"
7575
azurerepositorykey "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azureapimanagement/repositorykey"
76+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azureapimanagementsubscriptionkey"
7677
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azurecontainerregistry"
7778
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuredevopspersonalaccesstoken"
7879
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresastoken"
@@ -900,6 +901,7 @@ func buildDetectorList() []detectors.Detector {
900901
&axonaut.Scanner{},
901902
&aylien.Scanner{},
902903
&ayrshare.Scanner{},
904+
&azureapimanagementsubscriptionkey.Scanner{},
903905
&azure_entra_refreshtoken.Scanner{},
904906
&azure_entra_serviceprincipal_v1.Scanner{},
905907
&azure_entra_serviceprincipal_v2.Scanner{},

0 commit comments

Comments
 (0)