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 19, 2024
1 parent 7f06be0 commit 6750421
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 105 deletions.
90 changes: 0 additions & 90 deletions pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go

This file was deleted.

17 changes: 17 additions & 0 deletions pkg/detectors/azure_entra/serviceprincipal/sp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package serviceprincipal

import (
"context"

"github.com/Azure/go-autorest/autorest/azure/auth"
)

func VerifyCredentials(ctx context.Context, tenantId string, clientId string, clientSecret string) bool {
cred := auth.NewClientCredentialsConfig(clientId, clientSecret, tenantId)
token, err := cred.ServicePrincipalToken()
if err != nil {
return false
}
err = token.Refresh()
return err != nil
}
94 changes: 94 additions & 0 deletions pkg/detectors/azure_entra/serviceprincipal/v1/spv1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package v1

import (
"context"
"fmt"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}

// Ensure the Scanner satisfies the interface at compile time.
var _ interface {
detectors.Detector
detectors.Versioner
} = (*Scanner)(nil)

var (
// TODO: Azure storage access keys and investigate other types of creds.
clientSecretPat = regexp.MustCompile(`(?i)(?:secret).{0,20}?([a-z0-9~@_\-.?]{32,34})`)
)

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", "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)

secretMatches := findSecretMatches(dataStr)
if len(secretMatches) == 0 {
return
}
clientMatches := azure_entra.FindClientIdMatches(dataStr)
if len(clientMatches) == 0 {
return
}
tenantMatches := azure_entra.FindTenantIdMatches(dataStr)

for clientSecret := range secretMatches {
for clientId := range clientMatches {
for tenantId := range tenantMatches {
s := detectors.Result{
DetectorType: detectorspb.DetectorType_Azure,
Raw: []byte(clientSecret),
RawV2: []byte(fmt.Sprintf(`{"tenantId":"%s",clientId":"%s","clientSecret":"%s"}`, tenantId, clientId, clientSecret)),
Redacted: clientSecret[:5] + "...",
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/azure/",
},
}

if verify {
isVerified := serviceprincipal.VerifyCredentials(ctx, tenantId, clientId, clientSecret)
s.Verified = isVerified
}

results = append(results, s)

}
}
}

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 clientSecretPat.FindAllStringSubmatch(data, -1) {
uniqueMatches[match[1]] = 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
65 changes: 65 additions & 0 deletions pkg/detectors/azure_entra/serviceprincipal/v1/spv1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package v1

import (
"testing"

"github.com/google/go-cmp/cmp"
)

type testCase struct {
Input string
Expected map[string]struct{}
}

func Test_FindClientSecretMatches(t *testing.T) {
cases := map[string]testCase{
"client_secret": {
Input: ` "TenantId": "3d7e0652-b03d-4ed2-bf86-f1299cecde17",
"ClientSecret": "gHduiL_j6t4b6DG?Qr-G6M@IOS?mX3B9",
"Resource": "62d94f6c-d599-489b-a797-3e10e42fbe22",`,
Expected: map[string]struct{}{
"gHduiL_j6t4b6DG?Qr-G6M@IOS?mX3B9": {},
},
},
"client_secret1": {
Input: ` public static string clientId = "413ff05b-6d54-41a7-9271-9f964bc10624";
public static string clientSecret = "k72~odcN_6TbVh5D~19_1Qkj~87trteArL";
private const string `,
Expected: map[string]struct{}{
"k72~odcN_6TbVh5D~19_1Qkj~87trteArL": {},
},
},
"client_secret2": {
Input: ` "azClientSecret": "2bWD_tu3~9B0_.R0W3BFJN-Hu_xjfR8EL5",
"kvVaultUri": "https://corp.vault.azure.net/",`,
Expected: map[string]struct{}{
"2bWD_tu3~9B0_.R0W3BFJN-Hu_xjfR8EL5": {},
},
},
//"client_secret3": {
// Input: ``,
// Expected: map[string]struct{}{
// "": {},
// },
//},
}

for name, test := range cases {
t.Run(name, func(t *testing.T) {
matches := findSecretMatches(test.Input)
if len(matches) == 0 {
if len(test.Expected) != 0 {
t.Fatalf("no matches found, expected: %v", test.Expected)
return
} else {
return
}
}

if diff := cmp.Diff(test.Expected, matches); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", name, diff)
}
})
}
}
Loading

0 comments on commit 6750421

Please sign in to comment.