Skip to content

Commit 438e11d

Browse files
committed
feat(azure): finish pending items for review
1 parent 80b03ad commit 438e11d

File tree

9 files changed

+313
-204
lines changed

9 files changed

+313
-204
lines changed

pkg/detectors/azure_entra/common.go

+54-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package azure_entra
22

33
import (
4-
"context"
54
"fmt"
5+
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
6+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
7+
"golang.org/x/sync/singleflight"
68
"io"
79
"net/http"
810
"strings"
@@ -21,7 +23,11 @@ var (
2123
// https://learn.microsoft.com/en-us/partner-center/account-settings/find-ids-and-domain-names#find-the-microsoft-azure-ad-tenant-id-and-primary-domain-name
2224
// https://learn.microsoft.com/en-us/microsoft-365/admin/setup/domains-faq?view=o365-worldwide#why-do-i-have-an--onmicrosoft-com--domain
2325
tenantIdPat = regexp.MustCompile(fmt.Sprintf(
24-
`(?i)(?:login\.microsoftonline\.com/|sts\.windows\.net/|(?:t[ae]n[ae]nt(?:[ ._-]?id)?|\btid)(?:.|\s){0,45}?)(%s)`, uuidStr,
26+
//language=regexp
27+
`(?i)(?:(?:login\.microsoftonline\.com/|(?:login|sts)\.windows\.net/|(?:t[ae]n[ae]nt(?:[ ._-]?id)?|\btid)(?:.|\s){0,60}?)(%s)|https?://(%s)|X-AnchorMailbox(?:.|\s){0,60}?@(%s))`,
28+
uuidStr,
29+
uuidStr,
30+
uuidStr,
2531
))
2632
tenantOnMicrosoftPat = regexp.MustCompile(`([\w-]+\.onmicrosoft\.com)`)
2733

@@ -34,7 +40,14 @@ func FindTenantIdMatches(data string) map[string]struct{} {
3440
uniqueMatches := make(map[string]struct{})
3541

3642
for _, match := range tenantIdPat.FindAllStringSubmatch(data, -1) {
37-
m := strings.ToLower(match[1])
43+
var m string
44+
if match[1] != "" {
45+
m = strings.ToLower(match[1])
46+
} else if match[2] != "" {
47+
m = strings.ToLower(match[2])
48+
} else if match[3] != "" {
49+
m = strings.ToLower(match[3])
50+
}
3851
if _, ok := detectors.UuidFalsePositives[detectors.FalsePositive(m)]; ok {
3952
continue
4053
}
@@ -59,19 +72,56 @@ func FindClientIdMatches(data string) map[string]struct{} {
5972
return uniqueMatches
6073
}
6174

75+
var (
76+
tenantCache = simple.NewCache[bool]()
77+
tenantGroup singleflight.Group
78+
)
79+
6280
// TenantExists returns whether the tenant exists according to Microsoft's well-known OpenID endpoint.
6381
func TenantExists(ctx context.Context, client *http.Client, tenant string) bool {
82+
// Use cached value where possible.
83+
if tenantExists, isCached := tenantCache.Get(tenant); isCached {
84+
return tenantExists
85+
}
86+
87+
// https://www.codingexplorations.com/blog/understanding-singleflight-in-golang-a-solution-for-eliminating-redundant-work
88+
tenantExists, _, _ := tenantGroup.Do(tenant, func() (interface{}, error) {
89+
result := queryTenant(ctx, client, tenant)
90+
tenantCache.Set(tenant, result)
91+
return result, nil
92+
})
93+
94+
return tenantExists.(bool)
95+
}
96+
97+
func queryTenant(ctx context.Context, client *http.Client, tenant string) bool {
98+
logger := ctx.Logger().WithName("azure").WithValues("tenant", tenant)
99+
64100
tenantUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/.well-known/openid-configuration", tenant)
65101
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tenantUrl, nil)
66102
if err != nil {
67103
return false
68104
}
69105

70106
res, err := client.Do(req)
107+
if err != nil {
108+
logger.Error(err, "Failed to check if tenant exists")
109+
return false
110+
}
71111
defer func() {
72112
_, _ = io.Copy(io.Discard, res.Body)
73113
_ = res.Body.Close()
74114
}()
75115

76-
return res.StatusCode == http.StatusOK
116+
switch res.StatusCode {
117+
case http.StatusOK:
118+
return true
119+
case http.StatusBadRequest:
120+
logger.V(4).Info("Tenant does not exist.")
121+
return false
122+
default:
123+
bodyBytes, _ := io.ReadAll(res.Body)
124+
logger.Error(nil, "WARNING: Unexpected response when checking if tenant exists", "status_code", res.StatusCode, "body", string(bodyBytes))
125+
return false
126+
}
77127
}

pkg/detectors/azure_entra/common_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ func runPatTest(t *testing.T, tests map[string]testCase, matchFunc func(data str
3535
func Test_FindTenantIdMatches(t *testing.T) {
3636
cases := map[string]testCase{
3737
// Tenant ID
38+
"audience": {
39+
Input: `az offazure hyperv site create --location "eastus" --service-principal-identity-details \
40+
application-id="cbcfc473-97da-45dd-8a00-3612d1ddf35a" \
41+
audience="https://bced5192-08c4-4470-9a94-666fea59efb07/aadapp" `,
42+
Expected: map[string]struct{}{
43+
"bced5192-08c4-4470-9a94-666fea59efb0": {},
44+
},
45+
},
3846
"tenant": {
3947
Input: ` "cas.authn.azure-active-directory.login-url=https://login.microsoftonline.com/common/",
4048
"cas.authn.azure-active-directory.tenant=8e439f30-da7a-482c-bd23-e45d0a732000"`,
@@ -99,6 +107,12 @@ tenant_id = "57aabdfc-6ce0-4828-94a2-9abe277892ec"`,
99107
"7bb339cb-e94c-4a85-884c-48ebd9bb28c3": {},
100108
},
101109
},
110+
"login.windows.net": {
111+
Input: `az offazure hyperv site create --location "eastus" --service-principal-identity-details aad-authority="https://login.windows.net/7bb339cb-e94c-4a85-884c-48ebd9bb28c3" application-id="e9f013df-2a2a-4871-b766-e79867f30348" \'`,
112+
Expected: map[string]struct{}{
113+
"7bb339cb-e94c-4a85-884c-48ebd9bb28c3": {},
114+
},
115+
},
102116
"sts.windows.net": {
103117
Input: `{
104118
"aud": "00000003-0000-0000-c000-000000000000",
@@ -108,6 +122,20 @@ tenant_id = "57aabdfc-6ce0-4828-94a2-9abe277892ec"`,
108122
"974fde14-c3a4-481b-9b03-cfce182c3a07": {},
109123
},
110124
},
125+
"x-anchor-mailbox": {
126+
// The tenantID can be encoded in this parameter.
127+
// https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/95a63a7fe97d91b99979e5bf78e03f6acf40a286/msal/application.py#L185-L186
128+
// https://github.com/silverhack/monkey365/blob/b3f43c4a2d014fcc3aae0a4103c8f2610fbb4980/core/utils/Get-MonkeySecCompBackendUri.ps1#L70
129+
Input: ` User-Agent:
130+
- python-requests/2.31.0
131+
X-AnchorMailbox:
132+
- Oid:2b9b0cb5-d707-42e3-9504-d9b76ac7bec5@86843c34-863b-44d3-bb14-4f14e7c0564d
133+
x-client-current-telemetry:
134+
- 4|84,3|`,
135+
Expected: map[string]struct{}{
136+
"86843c34-863b-44d3-bb14-4f14e7c0564d": {},
137+
},
138+
},
111139

112140
// Tenant onmicrosoft.com
113141
"onmicrosoft tenant": {
@@ -145,6 +173,17 @@ tenant_id = "57aabdfc-6ce0-4828-94a2-9abe277892ec"`,
145173
"12fc345b-0c67-4cde-8902-dabf2cad34b5": {},
146174
},
147175
},
176+
"newline": {
177+
Input: ` {\n \"mode\": \"Manual\"\n },\n \"bootstrapProfile\": {\n \"artifactSource\":
178+
\"Direct\"\n }\n },\n \"identity\": {\n \"type\": \"SystemAssigned\",\n
179+
\ \"principalId\":\"00000000-0000-0000-0000-000000000001\",\n \"tenantId\":
180+
\"d0a69dfd-9b9e-4833-9c33-c7903dd2e012\"\n },\n \"sku\": {\n \"name\": \"Base\",\n
181+
\ \"tier\": \"Free\"\n }\n}"
182+
headers:`,
183+
Expected: map[string]struct{}{
184+
"d0a69dfd-9b9e-4833-9c33-c7903dd2e012": {},
185+
},
186+
},
148187

149188
// False positives
150189
"tid shouldn't match clientId": {

pkg/detectors/azure_entra/serviceprincipal/sp.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
)
1616

1717
var (
18+
Description = "Azure is a cloud service offering a wide range of services including compute, analytics, storage, and networking. Azure credentials can be used to access and manage these services."
1819
ErrSecretInvalid = errors.New("invalid client secret provided")
1920
ErrSecretExpired = errors.New("the provided secret is expired")
2021
ErrTenantNotFound = errors.New("tenant not found")
@@ -35,7 +36,6 @@ type TokenErrResponse struct {
3536
func VerifyCredentials(ctx context.Context, client *http.Client, tenantId string, clientId string, clientSecret string) (bool, map[string]string, error) {
3637
data := url.Values{}
3738
data.Set("client_id", clientId)
38-
//data.Set("scope", "https://management.core.windows.net/.default")
3939
data.Set("scope", "https://graph.microsoft.com/.default")
4040
data.Set("client_secret", clientSecret)
4141
data.Set("grant_type", "client_credentials")
@@ -91,6 +91,7 @@ func VerifyCredentials(ctx context.Context, client *http.Client, tenantId string
9191
case http.StatusBadRequest, http.StatusUnauthorized:
9292
// Error codes can be looked up by removing the `AADSTS` prefix.
9393
// https://login.microsoftonline.com/error?code=9002313
94+
// TODO: Handle AADSTS900382 (https://github.com/Azure/azure-sdk-for-js/issues/30557)
9495
d := errResp.Description
9596
switch {
9697
case strings.HasPrefix(d, "AADSTS700016:"):

pkg/detectors/azure_entra/serviceprincipal/v1/spv1.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v1
22

33
import (
44
"context"
5+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal"
56
"net/http"
67

78
regexp "github.com/wasilibs/go-re2"
@@ -29,7 +30,8 @@ var (
2930
// TODO: Azure storage access keys and investigate other types of creds.
3031
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate
3132
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential
32-
//clientSecretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,40}?([a-z0-9~@_\-[\]:.?]{32,34})`)
33+
//clientSecretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,80}?([\w~@[\]:.?*/+=-]{31,34}`)
34+
// TODO: Tighten this regex and replace it with above.
3335
secretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,80}[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]([A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]{31,34})[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]`)
3436
)
3537

@@ -61,6 +63,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
6163
if client == nil {
6264
client = defaultClient
6365
}
66+
// The handling logic is identical for both versions.
6467
processedResults := v2.ProcessData(ctx, clientSecrets, clientIds, tenantIds, verify, client)
6568
for _, result := range processedResults {
6669
results = append(results, result)
@@ -72,6 +75,10 @@ func (s Scanner) Type() detectorspb.DetectorType {
7275
return detectorspb.DetectorType_Azure
7376
}
7477

78+
func (s Scanner) Description() string {
79+
return serviceprincipal.Description
80+
}
81+
7582
// region Helper methods.
7683
func findSecretMatches(data string) map[string]struct{} {
7784
uniqueMatches := make(map[string]struct{})

0 commit comments

Comments
 (0)