Skip to content

Commit

Permalink
feat(azure): update sp detector
Browse files Browse the repository at this point in the history
  • Loading branch information
rgmz authored and Richard Gomez committed Jun 21, 2024
1 parent 7f06be0 commit edf16dd
Show file tree
Hide file tree
Showing 10 changed files with 690 additions and 105 deletions.
90 changes: 0 additions & 90 deletions pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go

This file was deleted.

116 changes: 116 additions & 0 deletions pkg/detectors/azure_entra/serviceprincipal/sp.go
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)
}
}
}
89 changes: 89 additions & 0 deletions pkg/detectors/azure_entra/serviceprincipal/v1/spv1.go
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
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"
Expand Down
Loading

0 comments on commit edf16dd

Please sign in to comment.