-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
690 additions
and
105 deletions.
There are no files selected for viewing
90 changes: 0 additions & 90 deletions
90
pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package serviceprincipal | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/golang-jwt/jwt/v5" | ||
) | ||
|
||
var ( | ||
ErrSecretInvalid = errors.New("invalid client secret provided") | ||
ErrSecretExpired = errors.New("the provided secret is expired") | ||
ErrTenantNotFound = errors.New("tenant not found") | ||
ErrClientNotFoundInTenant = errors.New("application was not found in tenant") | ||
) | ||
|
||
type TokenOkResponse struct { | ||
AccessToken string `json:"access_token"` | ||
} | ||
|
||
type TokenErrResponse struct { | ||
Error string `json:"error"` | ||
Description string `json:"error_description"` | ||
} | ||
|
||
// VerifyCredentials attempts to get a token using the provided client credentials. | ||
// See: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#get-a-token | ||
func VerifyCredentials(ctx context.Context, client *http.Client, tenantId string, clientId string, clientSecret string) (bool, map[string]string, error) { | ||
data := url.Values{} | ||
data.Set("client_id", clientId) | ||
//data.Set("scope", "https://management.core.windows.net/.default") | ||
data.Set("scope", "https://graph.microsoft.com/.default") | ||
data.Set("client_secret", clientSecret) | ||
data.Set("grant_type", "client_credentials") | ||
|
||
tokenUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantId) | ||
encodedData := data.Encode() | ||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenUrl, strings.NewReader(encodedData)) | ||
if err != nil { | ||
return false, nil, nil | ||
} | ||
req.Header.Set("Accept", "application/json") | ||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
req.Header.Set("Content-Length", strconv.Itoa(len(encodedData))) | ||
|
||
res, err := client.Do(req) | ||
if err != nil { | ||
return false, nil, err | ||
} | ||
defer func() { | ||
_, _ = io.Copy(io.Discard, res.Body) | ||
_ = res.Body.Close() | ||
}() | ||
|
||
if res.StatusCode == http.StatusOK { | ||
var okResp TokenOkResponse | ||
|
||
if err := json.NewDecoder(res.Body).Decode(&okResp); err != nil { | ||
return false, nil, err | ||
} | ||
|
||
extraData := map[string]string{ | ||
"rotation_guide": "https://howtorotate.com/docs/tutorials/azure/", | ||
"tenant": tenantId, | ||
"client": clientId, | ||
} | ||
|
||
// Add claims from the access token. | ||
if token, _ := jwt.Parse(okResp.AccessToken, nil); token != nil { | ||
claims := token.Claims.(jwt.MapClaims) | ||
|
||
if app := claims["app_displayname"]; app != nil { | ||
extraData["application"] = fmt.Sprint(app) | ||
} | ||
} | ||
return true, extraData, nil | ||
} else { | ||
var errResp TokenErrResponse | ||
if err := json.NewDecoder(res.Body).Decode(&errResp); err != nil { | ||
return false, nil, err | ||
} | ||
|
||
switch res.StatusCode { | ||
case http.StatusBadRequest, http.StatusUnauthorized: | ||
// Error codes can be looked up by removing the `AADSTS` prefix. | ||
// https://login.microsoftonline.com/error?code=9002313 | ||
d := errResp.Description | ||
switch { | ||
case strings.HasPrefix(d, "AADSTS700016:"): | ||
// https://login.microsoftonline.com/error?code=700016 | ||
return false, nil, ErrClientNotFoundInTenant | ||
case strings.HasPrefix(d, "AADSTS7000215:"): | ||
// https://login.microsoftonline.com/error?code=7000215 | ||
return false, nil, ErrSecretInvalid | ||
case strings.HasPrefix(d, "AADSTS7000222:"): | ||
// The secret has expired. | ||
// https://login.microsoftonline.com/error?code=7000222 | ||
return false, nil, ErrSecretExpired | ||
case strings.HasPrefix(d, "AADSTS90002:"): | ||
// https://login.microsoftonline.com/error?code=90002 | ||
return false, nil, ErrTenantNotFound | ||
default: | ||
return false, nil, fmt.Errorf("unexpected error '%s': %s", errResp.Error, errResp.Description) | ||
} | ||
default: | ||
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package v1 | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
|
||
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/detectors/azure_entra" | ||
v2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal/v2" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
) | ||
|
||
type Scanner struct { | ||
client *http.Client | ||
detectors.DefaultMultiPartCredentialProvider | ||
} | ||
|
||
// Ensure the Scanner satisfies the interface at compile time. | ||
var _ interface { | ||
detectors.Detector | ||
detectors.Versioner | ||
} = (*Scanner)(nil) | ||
|
||
var ( | ||
defaultClient = common.SaneHttpClient() | ||
// TODO: Azure storage access keys and investigate other types of creds. | ||
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate | ||
// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential | ||
//clientSecretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,40}?([a-z0-9~@_\-[\]:.?]{32,34})`) | ||
secretPat = regexp.MustCompile(`(?i)(?:secret|password| -p[ =]).{0,80}[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]([A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]{31,34})[^A-Za-z0-9!#$%&()*+,\-./:;<=>?@[\\\]^_{|}~]`) | ||
) | ||
|
||
func (s Scanner) Version() int { | ||
return 1 | ||
} | ||
|
||
// Keywords are used for efficiently pre-filtering chunks. | ||
// Use identifiers in the secret preferably, or the provider name. | ||
func (s Scanner) Keywords() []string { | ||
return []string{"azure", "az", "entra", "msal", "login.microsoftonline.com", ".onmicrosoft.com"} | ||
} | ||
|
||
// FromData will find and optionally verify Azure secrets in a given set of bytes. | ||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { | ||
dataStr := string(data) | ||
|
||
clientSecrets := findSecretMatches(dataStr) | ||
if len(clientSecrets) == 0 { | ||
return | ||
} | ||
clientIds := azure_entra.FindClientIdMatches(dataStr) | ||
if len(clientIds) == 0 { | ||
return | ||
} | ||
tenantIds := azure_entra.FindTenantIdMatches(dataStr) | ||
|
||
client := s.client | ||
if client == nil { | ||
client = defaultClient | ||
} | ||
processedResults := v2.ProcessData(ctx, clientSecrets, clientIds, tenantIds, verify, client) | ||
for _, result := range processedResults { | ||
results = append(results, result) | ||
} | ||
return results, nil | ||
} | ||
|
||
func (s Scanner) Type() detectorspb.DetectorType { | ||
return detectorspb.DetectorType_Azure | ||
} | ||
|
||
// region Helper methods. | ||
func findSecretMatches(data string) map[string]struct{} { | ||
uniqueMatches := make(map[string]struct{}) | ||
for _, match := range secretPat.FindAllStringSubmatch(data, -1) { | ||
m := match[1] | ||
// Ignore secrets that are handled by the V2 detector. | ||
if v2.SecretPat.MatchString(m) { | ||
continue | ||
} | ||
uniqueMatches[m] = struct{}{} | ||
} | ||
return uniqueMatches | ||
} | ||
|
||
//endregion |
2 changes: 1 addition & 1 deletion
2
...serviceprincipal/serviceprincipal_test.go → ...viceprincipal/v1/spv1_integration_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
//go:build detectors | ||
// +build detectors | ||
|
||
package azure_entra_serviceprincipal | ||
package v1 | ||
|
||
import ( | ||
"context" | ||
|
Oops, something went wrong.