Skip to content

Commit 4a02d1a

Browse files
rgmzRichard Gomez
authored and
Richard Gomez
committed
feat(azure): update sp detector
1 parent eedbcff commit 4a02d1a

File tree

8 files changed

+683
-93
lines changed

8 files changed

+683
-93
lines changed

pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go

-90
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package serviceprincipal
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/url"
11+
"strconv"
12+
"strings"
13+
14+
"github.com/golang-jwt/jwt/v5"
15+
)
16+
17+
var (
18+
ErrSecretInvalid = errors.New("invalid client secret provided")
19+
ErrSecretExpired = errors.New("the provided secret is expired")
20+
ErrTenantNotFound = errors.New("tenant not found")
21+
ErrClientNotFoundInTenant = errors.New("application was not found in tenant")
22+
)
23+
24+
type TokenOkResponse struct {
25+
AccessToken string `json:"access_token"`
26+
}
27+
28+
type TokenErrResponse struct {
29+
Error string `json:"error"`
30+
Description string `json:"error_description"`
31+
}
32+
33+
// VerifyCredentials attempts to get a token using the provided client credentials.
34+
// See: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#get-a-token
35+
func VerifyCredentials(ctx context.Context, client *http.Client, tenantId string, clientId string, clientSecret string) (bool, map[string]string, error) {
36+
data := url.Values{}
37+
data.Set("client_id", clientId)
38+
//data.Set("scope", "https://management.core.windows.net/.default")
39+
data.Set("scope", "https://graph.microsoft.com/.default")
40+
data.Set("client_secret", clientSecret)
41+
data.Set("grant_type", "client_credentials")
42+
43+
tokenUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantId)
44+
encodedData := data.Encode()
45+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenUrl, strings.NewReader(encodedData))
46+
if err != nil {
47+
return false, nil, nil
48+
}
49+
req.Header.Set("Accept", "application/json")
50+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
51+
req.Header.Set("Content-Length", strconv.Itoa(len(encodedData)))
52+
53+
res, err := client.Do(req)
54+
if err != nil {
55+
return false, nil, err
56+
}
57+
defer func() {
58+
_, _ = io.Copy(io.Discard, res.Body)
59+
_ = res.Body.Close()
60+
}()
61+
62+
if res.StatusCode == http.StatusOK {
63+
var okResp TokenOkResponse
64+
65+
if err := json.NewDecoder(res.Body).Decode(&okResp); err != nil {
66+
return false, nil, err
67+
}
68+
69+
extraData := map[string]string{
70+
"rotation_guide": "https://howtorotate.com/docs/tutorials/azure/",
71+
"tenant": tenantId,
72+
"client": clientId,
73+
}
74+
75+
// Add claims from the access token.
76+
if token, _ := jwt.Parse(okResp.AccessToken, nil); token != nil {
77+
claims := token.Claims.(jwt.MapClaims)
78+
79+
if app := claims["app_displayname"]; app != nil {
80+
extraData["application"] = fmt.Sprint(app)
81+
}
82+
}
83+
return true, extraData, nil
84+
} else {
85+
var errResp TokenErrResponse
86+
if err := json.NewDecoder(res.Body).Decode(&errResp); err != nil {
87+
return false, nil, err
88+
}
89+
90+
switch res.StatusCode {
91+
case http.StatusBadRequest, http.StatusUnauthorized:
92+
// Error codes can be looked up by removing the `AADSTS` prefix.
93+
// https://login.microsoftonline.com/error?code=9002313
94+
d := errResp.Description
95+
switch {
96+
case strings.HasPrefix(d, "AADSTS700016:"):
97+
// https://login.microsoftonline.com/error?code=700016
98+
return false, nil, ErrClientNotFoundInTenant
99+
case strings.HasPrefix(d, "AADSTS7000215:"):
100+
// https://login.microsoftonline.com/error?code=7000215
101+
return false, nil, ErrSecretInvalid
102+
case strings.HasPrefix(d, "AADSTS7000222:"):
103+
// The secret has expired.
104+
// https://login.microsoftonline.com/error?code=7000222
105+
return false, nil, ErrSecretExpired
106+
case strings.HasPrefix(d, "AADSTS90002:"):
107+
// https://login.microsoftonline.com/error?code=90002
108+
return false, nil, ErrTenantNotFound
109+
default:
110+
return false, nil, fmt.Errorf("unexpected error '%s': %s", errResp.Error, errResp.Description)
111+
}
112+
default:
113+
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
114+
}
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
regexp "github.com/wasilibs/go-re2"
8+
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra"
12+
v2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal/v2"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
14+
)
15+
16+
type Scanner struct {
17+
client *http.Client
18+
detectors.DefaultMultiPartCredentialProvider
19+
}
20+
21+
// Ensure the Scanner satisfies the interface at compile time.
22+
var _ interface {
23+
detectors.Detector
24+
detectors.Versioner
25+
} = (*Scanner)(nil)
26+
27+
var (
28+
defaultClient = common.SaneHttpClient()
29+
// TODO: Azure storage access keys and investigate other types of creds.
30+
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate
31+
// 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+
secretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,80}[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]([A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]{31,34})[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]`)
34+
)
35+
36+
func (s Scanner) Version() int {
37+
return 1
38+
}
39+
40+
// Keywords are used for efficiently pre-filtering chunks.
41+
// Use identifiers in the secret preferably, or the provider name.
42+
func (s Scanner) Keywords() []string {
43+
return []string{"azure", "az", "entra", "msal", "login.microsoftonline.com", ".onmicrosoft.com"}
44+
}
45+
46+
// FromData will find and optionally verify Azure secrets in a given set of bytes.
47+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
48+
dataStr := string(data)
49+
50+
clientSecrets := findSecretMatches(dataStr)
51+
if len(clientSecrets) == 0 {
52+
return
53+
}
54+
clientIds := azure_entra.FindClientIdMatches(dataStr)
55+
if len(clientIds) == 0 {
56+
return
57+
}
58+
tenantIds := azure_entra.FindTenantIdMatches(dataStr)
59+
60+
client := s.client
61+
if client == nil {
62+
client = defaultClient
63+
}
64+
processedResults := v2.ProcessData(ctx, clientSecrets, clientIds, tenantIds, verify, client)
65+
for _, result := range processedResults {
66+
results = append(results, result)
67+
}
68+
return results, nil
69+
}
70+
71+
func (s Scanner) Type() detectorspb.DetectorType {
72+
return detectorspb.DetectorType_Azure
73+
}
74+
75+
// region Helper methods.
76+
func findSecretMatches(data string) map[string]struct{} {
77+
uniqueMatches := make(map[string]struct{})
78+
for _, match := range secretPat.FindAllStringSubmatch(data, -1) {
79+
m := match[1]
80+
// Ignore secrets that are handled by the V2 detector.
81+
if v2.SecretPat.MatchString(m) {
82+
continue
83+
}
84+
uniqueMatches[m] = struct{}{}
85+
}
86+
return uniqueMatches
87+
}
88+
89+
//endregion

pkg/detectors/azure_entra/serviceprincipal/serviceprincipal_test.go renamed to pkg/detectors/azure_entra/serviceprincipal/v1/spv1_integration_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//go:build detectors
22
// +build detectors
33

4-
package azure_entra_serviceprincipal
4+
package v1
55

66
import (
77
"context"

0 commit comments

Comments
 (0)