Skip to content

Commit d69f4c2

Browse files
committed
feat(azure): finish pending items for review
1 parent 20b3f37 commit d69f4c2

File tree

7 files changed

+64
-151
lines changed

7 files changed

+64
-151
lines changed

pkg/detectors/azure_entra/common.go

+41-2
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"
@@ -59,19 +61,56 @@ func FindClientIdMatches(data string) map[string]struct{} {
5961
return uniqueMatches
6062
}
6163

64+
var (
65+
tenantCache = simple.NewCache[bool]()
66+
tenantGroup singleflight.Group
67+
)
68+
6269
// TenantExists returns whether the tenant exists according to Microsoft's well-known OpenID endpoint.
6370
func TenantExists(ctx context.Context, client *http.Client, tenant string) bool {
71+
// Use cached value where possible.
72+
if tenantExists, isCached := tenantCache.Get(tenant); isCached {
73+
return tenantExists
74+
}
75+
76+
// https://www.codingexplorations.com/blog/understanding-singleflight-in-golang-a-solution-for-eliminating-redundant-work
77+
tenantExists, _, _ := tenantGroup.Do(tenant, func() (interface{}, error) {
78+
result := queryTenant(ctx, client, tenant)
79+
tenantCache.Set(tenant, result)
80+
return result, nil
81+
})
82+
83+
return tenantExists.(bool)
84+
}
85+
86+
func queryTenant(ctx context.Context, client *http.Client, tenant string) bool {
87+
logger := ctx.Logger().WithName("azure").WithValues("tenant", tenant)
88+
6489
tenantUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/.well-known/openid-configuration", tenant)
6590
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tenantUrl, nil)
6691
if err != nil {
6792
return false
6893
}
6994

7095
res, err := client.Do(req)
96+
if err != nil {
97+
logger.Error(err, "Failed to check if tenant exists")
98+
return false
99+
}
71100
defer func() {
72101
_, _ = io.Copy(io.Discard, res.Body)
73102
_ = res.Body.Close()
74103
}()
75104

76-
return res.StatusCode == http.StatusOK
105+
switch res.StatusCode {
106+
case http.StatusOK:
107+
return true
108+
case http.StatusBadRequest:
109+
logger.V(4).Info("Tenant does not exist.")
110+
return false
111+
default:
112+
bodyBytes, _ := io.ReadAll(res.Body)
113+
logger.V(3).Info("WARNING: Unexpected response when checking if tenant exists", "status_code", res.StatusCode, "body", string(bodyBytes))
114+
return false
115+
}
77116
}

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{})

pkg/detectors/azure_entra/serviceprincipal/v2/spv2.go

+7-98
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ package v2
33
import (
44
"context"
55
"errors"
6-
"fmt"
7-
"io"
6+
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
87
"net/http"
98
"regexp"
109
"strings"
@@ -32,7 +31,6 @@ var (
3231
defaultClient = common.SaneHttpClient()
3332

3433
SecretPat = regexp.MustCompile(`(?:[^a-zA-Z0-9_~.-]|\A)([a-zA-Z0-9_~.-]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:[^a-zA-Z0-9_~.-]|\z)`)
35-
//clientSecretPat = regexp.MustCompile(`(?:[^a-zA-Z0-9_~.-]|\A)([a-zA-Z0-9_~.-]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:[^a-zA-Z0-9_~.-]|\z)|(?:secret|password| -p[ =]).{0,80}[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]([A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]{31,34})[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]`)
3634
)
3735

3836
func (s Scanner) Version() int {
@@ -69,6 +67,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
6967
}
7068

7169
func ProcessData(ctx context.Context, clientSecrets, clientIds, tenantIds map[string]struct{}, verify bool, client *http.Client) (results []detectors.Result) {
70+
logCtx := logContext.AddLogger(ctx)
7271
invalidClientsForTenant := make(map[string]map[string]struct{})
7372

7473
SecretLoop:
@@ -96,7 +95,7 @@ SecretLoop:
9695
}
9796

9897
if verify {
99-
if !isValidTenant(ctx, client, tenantId) {
98+
if !azure_entra.TenantExists(logCtx, client, tenantId) {
10099
// Tenant doesn't exist
101100
delete(tenantIds, tenantId)
102101
continue
@@ -126,78 +125,6 @@ SecretLoop:
126125
r = createResult(tenantId, clientId, clientSecret, isVerified, extraData, verificationErr)
127126
break ClientLoop
128127
}
129-
130-
// The result may be valid for another client/tenant.
131-
//
132-
//
133-
//// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-client_secret
134-
//cred := auth.NewClientCredentialsConfig(clientId, clientSecret, tenantId)
135-
//token, err := cred.ServicePrincipalToken()
136-
//if err != nil {
137-
// // This can only fail if a value is empty, which shouldn't be possible.
138-
// continue
139-
//}
140-
//
141-
//err = token.Refresh()
142-
//if err != nil {
143-
// var refreshError adal.TokenRefreshError
144-
// if ok := errors.As(err, &refreshError); ok {
145-
// resp := refreshError.Response()
146-
// defer func() {
147-
// // Ensure we drain the response body so this connection can be reused.
148-
// _, _ = io.Copy(io.Discard, resp.Body)
149-
// _ = resp.Body.Close()
150-
// }()
151-
//
152-
// status := resp.StatusCode
153-
// errStr := refreshError.Error()
154-
// if status == 400 {
155-
// if strings.Contains(errStr, `"error_description":"AADSTS90002:`) {
156-
// // Tenant doesn't exist
157-
// delete(tenantIds, tenantId)
158-
// continue
159-
// } else if strings.Contains(errStr, `"error_description":"AADSTS700016:`) {
160-
// // Tenant is valid but the ClientID doesn't exist.
161-
// invalidTenantsForClientId[clientId] = append(invalidTenantsForClientId[clientId], tenantId)
162-
// continue
163-
// } else {
164-
// // Unexpected error.
165-
// r.SetVerificationError(refreshError, clientSecret)
166-
// break
167-
// }
168-
// } else if status == 401 {
169-
// // Tenant exists and the clientID is valid, but something is wrong.
170-
// if strings.Contains(errStr, `"error_description":"AADSTS7000215:`) {
171-
// // Secret is not valid.
172-
// setValidTenantIdForClientId(clientId, tenantId, tenantIds, invalidTenantsForClientId)
173-
// continue IdLoop
174-
// } else if strings.Contains(errStr, `"error_description":"AADSTS7000222:`) {
175-
// // The secret is expired.
176-
// setValidTenantIdForClientId(clientId, tenantId, tenantIds, invalidTenantsForClientId)
177-
// continue SecretLoop
178-
// } else {
179-
// // TODO: Investigate if it's possible to get a 401 with a valid id/secret.
180-
// r.SetVerificationError(refreshError, clientSecret)
181-
// break
182-
// }
183-
// } else {
184-
// // Unexpected status code.
185-
// r.SetVerificationError(refreshError, clientSecret)
186-
// break
187-
// }
188-
// } else {
189-
// // Unexpected error.
190-
// r.SetVerificationError(err, clientSecret)
191-
// break
192-
// }
193-
//} else {
194-
// r.Verified = true
195-
// r.ExtraData = map[string]string{
196-
// "token": token.OAuthToken(),
197-
// }
198-
// setValidTenantIdForClientId(clientId, tenantId, tenantIds, invalidTenantsForClientId)
199-
// break
200-
//}
201128
}
202129
}
203130
}
@@ -243,32 +170,14 @@ func createResult(tenantId string, clientId string, clientSecret string, verifie
243170
return r
244171
}
245172

246-
func isValidTenant(ctx context.Context, client *http.Client, tenant string) bool {
247-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://login.microsoftonline.com/%s/.well-known/openid-configuration", tenant), nil)
248-
if err != nil {
249-
return false
250-
}
251-
res, err := client.Do(req)
252-
defer func() {
253-
_, _ = io.Copy(io.Discard, res.Body)
254-
_ = res.Body.Close()
255-
}()
256-
257-
if res.StatusCode == 200 {
258-
return true
259-
} else if res.StatusCode == 400 {
260-
fmt.Printf("Invalid tenant: %s\n", tenant)
261-
return false
262-
} else {
263-
fmt.Printf("[azure] Unexpected status code: %d for %s\n", res.StatusCode, tenant)
264-
return false
265-
}
266-
}
267-
268173
func (s Scanner) Type() detectorspb.DetectorType {
269174
return detectorspb.DetectorType_Azure
270175
}
271176

177+
func (s Scanner) Description() string {
178+
return serviceprincipal.Description
179+
}
180+
272181
// region Helper methods.
273182
func findSecretMatches(data string) map[string]struct{} {
274183
uniqueMatches := make(map[string]struct{})

pkg/detectors/uuids.txt

-37
This file was deleted.

pkg/pb/detectorspb/detectors.pb.go

+6-11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/detectors.proto

-1
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,6 @@ enum DetectorType {
10101010
PyPI = 998;
10111011
RailwayApp = 999;
10121012
Meraki = 1000;
1013-
AzureRefreshToken = 1001;
10141013
}
10151014

10161015
message Result {

0 commit comments

Comments
 (0)