From 502c15ce27ecbe83c43d0476bf44638d67a74b14 Mon Sep 17 00:00:00 2001 From: Richard Gomez Date: Fri, 14 Jun 2024 17:56:42 -0400 Subject: [PATCH] feat(detectors): create azure refresh token --- .../{azurebatch => azure_batch}/azurebatch.go | 0 .../azurebatch_test.go | 0 pkg/detectors/azure_entra/common.go | 77 +++++ pkg/detectors/azure_entra/common_test.go | 287 +++++++++++++++++ .../azure_entra/refreshtoken/refreshtoken.go | 300 ++++++++++++++++++ .../refreshtoken_integration_test.go | 161 ++++++++++ .../refreshtoken/refreshtoken_test.go | 110 +++++++ .../serviceprincipal/serviceprincipal.go} | 5 +- .../serviceprincipal_test.go} | 3 +- pkg/detectors/uuids.txt | 37 +++ pkg/engine/defaults.go | 8 +- pkg/pb/detectorspb/detectors.pb.go | 18 +- proto/detectors.proto | 1 + 13 files changed, 994 insertions(+), 13 deletions(-) rename pkg/detectors/{azurebatch => azure_batch}/azurebatch.go (100%) rename pkg/detectors/{azurebatch => azure_batch}/azurebatch_test.go (100%) create mode 100644 pkg/detectors/azure_entra/common.go create mode 100644 pkg/detectors/azure_entra/common_test.go create mode 100644 pkg/detectors/azure_entra/refreshtoken/refreshtoken.go create mode 100644 pkg/detectors/azure_entra/refreshtoken/refreshtoken_integration_test.go create mode 100644 pkg/detectors/azure_entra/refreshtoken/refreshtoken_test.go rename pkg/detectors/{azure/azure.go => azure_entra/serviceprincipal/serviceprincipal.go} (98%) rename pkg/detectors/{azure/azure_test.go => azure_entra/serviceprincipal/serviceprincipal_test.go} (98%) create mode 100644 pkg/detectors/uuids.txt diff --git a/pkg/detectors/azurebatch/azurebatch.go b/pkg/detectors/azure_batch/azurebatch.go similarity index 100% rename from pkg/detectors/azurebatch/azurebatch.go rename to pkg/detectors/azure_batch/azurebatch.go diff --git a/pkg/detectors/azurebatch/azurebatch_test.go b/pkg/detectors/azure_batch/azurebatch_test.go similarity index 100% rename from pkg/detectors/azurebatch/azurebatch_test.go rename to pkg/detectors/azure_batch/azurebatch_test.go diff --git a/pkg/detectors/azure_entra/common.go b/pkg/detectors/azure_entra/common.go new file mode 100644 index 0000000000000..b357e124ee847 --- /dev/null +++ b/pkg/detectors/azure_entra/common.go @@ -0,0 +1,77 @@ +package azure_entra + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + + regexp "github.com/wasilibs/go-re2" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" +) + +const uuidStr = `[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}` + +var ( + // Tenants can be identified with a UUID or an `*.onmicrosoft.com` domain. + // + // See: + // https://learn.microsoft.com/en-us/partner-center/account-settings/find-ids-and-domain-names#find-the-microsoft-azure-ad-tenant-id-and-primary-domain-name + // https://learn.microsoft.com/en-us/microsoft-365/admin/setup/domains-faq?view=o365-worldwide#why-do-i-have-an--onmicrosoft-com--domain + tenantIdPat = regexp.MustCompile(fmt.Sprintf( + `(?i)(?:login\.microsoftonline\.com/|sts\.windows\.net/|(?:t[ae]n[ae]nt(?:[ ._-]?id)?|\btid)(?:.|\s){0,45}?)(%s)`, uuidStr, + )) + tenantOnMicrosoftPat = regexp.MustCompile(`([\w-]+\.onmicrosoft\.com)`) + + clientIdPat = regexp.MustCompile(fmt.Sprintf( + `(?i)(?:(?:app(?:lication)?|client)(?:[ ._-]?id)?|username| -u)(?:.|\s){0,45}?(%s)`, uuidStr)) +) + +// FindTenantIdMatches returns a list of potential tenant IDs in the provided |data|. +func FindTenantIdMatches(data string) map[string]struct{} { + uniqueMatches := make(map[string]struct{}) + + for _, match := range tenantIdPat.FindAllStringSubmatch(data, -1) { + m := strings.ToLower(match[1]) + if _, ok := detectors.UuidFalsePositives[detectors.FalsePositive(m)]; ok { + continue + } + uniqueMatches[m] = struct{}{} + } + for _, match := range tenantOnMicrosoftPat.FindAllStringSubmatch(data, -1) { + uniqueMatches[match[1]] = struct{}{} + } + return uniqueMatches +} + +// FindClientIdMatches returns a list of potential client UUIDs in the provided |data|. +func FindClientIdMatches(data string) map[string]struct{} { + uniqueMatches := make(map[string]struct{}) + for _, match := range clientIdPat.FindAllStringSubmatch(data, -1) { + m := strings.ToLower(match[1]) + if _, ok := detectors.UuidFalsePositives[detectors.FalsePositive(m)]; ok { + continue + } + uniqueMatches[m] = struct{}{} + } + return uniqueMatches +} + +// TenantExists returns whether the tenant exists according to Microsoft's well-known OpenID endpoint. +func TenantExists(ctx context.Context, client *http.Client, tenant string) bool { + tenantUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/.well-known/openid-configuration", tenant) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, tenantUrl, nil) + if err != nil { + return false + } + + res, err := client.Do(req) + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + + return res.StatusCode == http.StatusOK +} diff --git a/pkg/detectors/azure_entra/common_test.go b/pkg/detectors/azure_entra/common_test.go new file mode 100644 index 0000000000000..c619a554f8504 --- /dev/null +++ b/pkg/detectors/azure_entra/common_test.go @@ -0,0 +1,287 @@ +package azure_entra + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +type testCase struct { + Input string + Expected map[string]struct{} +} + +func runPatTest(t *testing.T, tests map[string]testCase, matchFunc func(data string) map[string]struct{}) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + matches := matchFunc(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("expected: %s, actual: %s", test.Expected, matches) + return + } + }) + } +} + +func Test_FindTenantIdMatches(t *testing.T) { + cases := map[string]testCase{ + // Tenant ID + "tenant": { + Input: ` "cas.authn.azure-active-directory.login-url=https://login.microsoftonline.com/common/", + "cas.authn.azure-active-directory.tenant=8e439f30-da7a-482c-bd23-e45d0a732000"`, + Expected: map[string]struct{}{ + "8e439f30-da7a-482c-bd23-e45d0a732000": {}, + }, + }, + "tanentId": { + Input: `azure.grantType=client_credentials +azure.tanentId=029e3b51-60dd-47aa-81ad-3c15b389db86`, + Expected: map[string]struct{}{ + "029e3b51-60dd-47aa-81ad-3c15b389db86": {}, + }, + }, + "tenantid": { + Input: ` file: + folder-location: test + tenantid: ${vcap.services.user-authentication-service.credentials.tenantid:317fb200-a693-4062-a4fb-9d131fcd2d3c}`, + Expected: map[string]struct{}{ + "317fb200-a693-4062-a4fb-9d131fcd2d3c": {}, + }, + }, + "tenant id": { + Input: `1. Enter the tenant id "2ce99e96-b41b-47a0-b37c-16a22bceb8c0"`, + Expected: map[string]struct{}{ + "2ce99e96-b41b-47a0-b37c-16a22bceb8c0": {}, + }, + }, + "tenant_id": { + Input: `location = "eastus" +subscription_id = "47ab1364-000d-4a53-838d-1537b1e3b49f" +tenant_id = "57aabdfc-6ce0-4828-94a2-9abe277892ec"`, + Expected: map[string]struct{}{ + "57aabdfc-6ce0-4828-94a2-9abe277892ec": {}, + }, + }, + "tenant-id": { + Input: ` active-directory: + enabled: true + profile: + tenant-id: c32654ed-6931-4bae-bb23-a8b9e420e0f4 + credential:`, + Expected: map[string]struct{}{ + "c32654ed-6931-4bae-bb23-a8b9e420e0f4": {}, + }, + }, + "tid": { + Input: ` "sub": "jIzit1WEdXqAH9KZXz-e-UcqsVa1pyPoh-2hw3xjEO4", + "tenant_region_scope": "AS", + "tid": "974fde14-c3a4-481b-9b03-cfce18213a07", + "uti": "2Y26RWHsWEiqhD2vi_PFAg",`, + Expected: map[string]struct{}{ + "974fde14-c3a4-481b-9b03-cfce18213a07": {}, + }, + }, + "login.microsoftonline.com": { + Input: ` auth: { + authority: 'https://login.microsoftonline.com/7bb339cb-e94c-4a85-884c-48ebd9bb28c3', + redirectUri: 'http://localhost:8080/landing' +`, + Expected: map[string]struct{}{ + "7bb339cb-e94c-4a85-884c-48ebd9bb28c3": {}, + }, + }, + "sts.windows.net": { + Input: `{ + "aud": "00000003-0000-0000-c000-000000000000", + "iss": "https://sts.windows.net/974fde14-c3a4-481b-9b03-cfce182c3a07/", + "iat": 1641799220,`, + Expected: map[string]struct{}{ + "974fde14-c3a4-481b-9b03-cfce182c3a07": {}, + }, + }, + + // Tenant onmicrosoft.com + "onmicrosoft tenant": { + Input: ` "oid": "7be15f3a-d9b5-4080-ba37-95aa2e3d244e", + "platf": "3", + "puid": "10032001170600C8", + "scp": "Files.Read Files.Read.All Files.Read.Selected Files.ReadWrite Files.ReadWrite.All Files.ReadWrite.AppFolder Files.ReadWrite.Selected profile User.Export.All User.Invite.All User.ManageIdentities.All User.Read User.Read.All User.ReadBasic.All openid email", + "signin_state": [ + "kmsi" + ], + "sub": "jIzit1WEdXqAH9KZXz-e-UcqsVa1pyPoh-2hw3xjEO4", + "tenant_region_scope": "AS", + "unique_name": "ben@xhoaxiuqng.onmicrosoft.com", + "uti": "2Y26RWHsWEiqhD2vi_PFAg", + "ver": "1.0", + "wids": [ + "62e90394-69f5-4237-9190-012177145e10", + "b79fbf4d-3ef9-4689-8143-76b194e85509" + ],`, + Expected: map[string]struct{}{ + "xhoaxiuqng.onmicrosoft.com": {}, + }, + }, + + // Arbitrary test cases + "spacing": { + Input: `| Variable name | Description | Example value | +| ----------------- | ------------------------------------------------------------- | ------------------------------------- | +| AFASBaseUri | Base URI of the AFAS REST API endpoint for this environment | https://12345.rest.afas.online/ProfitRestServices | +| AFASToke | App token in XML format for this environment | \\1\\D5R324DD5F4TRD945E530ED3CDD70D94BBDEC4C732B43F285ECB12345678\\ | +| AADtenantID | Id of the Azure tenant | 12fc345b-0c67-4cde-8902-dabf2cad34b5 | +| AADAppId | Id of the Azure app | f12345c6-7890-1f23-b456-789eb0bb1c23 | +| AADAppSecret | Secret of the Azure app | G1X2HsBw-co3dTIB45RE6vY.mSU~6u.7.8 |`, + Expected: map[string]struct{}{ + "12fc345b-0c67-4cde-8902-dabf2cad34b5": {}, + }, + }, + + // False positives + "tid shouldn't match clientId": { + Input: `"userId": "jdoe@businesscorp.ca", "isUserIdDisplayable": true, "isMRRT": true, "_clientId": "d3590ed6-52b3-4102-aeff-aad2292ab01c", }`, + Expected: nil, + }, + "tid shouldn't match subscription_id": { + Input: `location = "eastus" +subscription_id = "47ab1364-000d-4a53-838d-1537b1e3b49f"`, + Expected: nil, + }, + } + + runPatTest(t, cases, FindTenantIdMatches) +} + +func Test_FindClientIdMatches(t *testing.T) { + cases := map[string]testCase{ + "app": { + Input: `var app = "4ba50db1-3f3f-4521-8a9a-1be0864d922a"`, + Expected: map[string]struct{}{ + "4ba50db1-3f3f-4521-8a9a-1be0864d922a": {}, + }, + }, + "appid": { + Input: `The output includes credentials that you must protect. Be sure that you do not include these credentials in your code or check the credentials into your source control. For more information, see https://aka.ms/azadsp-cli +{ + "appId": "4ba50db1-3f3f-4521-8a9a-1be0864d922a", + "displayName": "azure-cli-2022-12-02-15-40-24",`, + Expected: map[string]struct{}{ + "4ba50db1-3f3f-4521-8a9a-1be0864d922a": {}, + }, + }, + "app_id": { + Input: `msal: + app_id: 'b9cbc91c-c890-4824-a487-91611bb0615a'`, + Expected: map[string]struct{}{ + "b9cbc91c-c890-4824-a487-91611bb0615a": {}, + }, + }, + "application": { + Input: `const application = \x60902aeb6d-29c7-4f6e-849d-4b933117e320\x60`, + Expected: map[string]struct{}{ + "902aeb6d-29c7-4f6e-849d-4b933117e320": {}, + }, + }, + "applicationid": { + Input: `# Login using Service Principal +$ApplicationId = "1e002bca-c6e2-446e-a29e-a221909fe8aa"`, + Expected: map[string]struct{}{ + "1e002bca-c6e2-446e-a29e-a221909fe8aa": {}, + }, + }, + "application id": { + Input: `The application id is "029e3b51-60dd-47aa-81ad-3c15b389db86", you need to`, + Expected: map[string]struct{}{ + "029e3b51-60dd-47aa-81ad-3c15b389db86": {}, + }, + }, + "application_id": { + Input: ` credential: + application_id: | + bafe0126-03eb-4917-b3ff-4601c4e8f12f`, + Expected: map[string]struct{}{ + "bafe0126-03eb-4917-b3ff-4601c4e8f12f": {}, + }, + }, + "application-id": { + Input: `vcap.services.msal.application-id: 0704100e-7e76-4e62-bfb6-70bfd33906e2`, + Expected: map[string]struct{}{ + "0704100e-7e76-4e62-bfb6-70bfd33906e2": {}, + }, + }, + "client": { + Input: `String client = "902aeb6d-29c7-4f6e-849d-4b933117e320";`, + Expected: map[string]struct{}{ + "902aeb6d-29c7-4f6e-849d-4b933117e320": {}, + }, + }, + "clientid": { + Input: `export const msalConfig = { + auth: { + clientId: '82c54108-535c-40b2-87dc-2db599df3810',`, + Expected: map[string]struct{}{ + "82c54108-535c-40b2-87dc-2db599df3810": {}, + }, + }, + "client id": { + Input: `The client ID is: a54e584d-6fc4-464c-8479-dc67b5d87ab9`, + Expected: map[string]struct{}{ + "a54e584d-6fc4-464c-8479-dc67b5d87ab9": {}, + }, + }, + "client_id": { + Input: `location = "eastus" +client_id = "89d5bd08-0d51-42cd-8eab-382c3ce11199" +subscription_id = "47ab1364-000d-4a53-838d-1537b1e3b49f" +`, + Expected: map[string]struct{}{ + "89d5bd08-0d51-42cd-8eab-382c3ce11199": {}, + }, + }, + "client-id": { + Input: `@TestPropertySource(properties = { + "cas.authn.azure-active-directory.client-id=532c556b-1260-483f-9695-68d087fcd965", + "cas.authn.azure-active-directory.client-secret`, + Expected: map[string]struct{}{ + "532c556b-1260-483f-9695-68d087fcd965": {}, + }, + }, + "username": { + Input: `az login --service-principal --username "21e144ac-532d-49ad-ba15-1c40694ce8b1" --password`, + Expected: map[string]struct{}{ + "21e144ac-532d-49ad-ba15-1c40694ce8b1": {}, + }, + }, + "-u": { + Input: `az login --service-principal -u "21e144ac-532d-49ad-ba15-1c40694ce8b1" -p`, + Expected: map[string]struct{}{ + "21e144ac-532d-49ad-ba15-1c40694ce8b1": {}, + }, + }, + + // Arbitrary test cases + "spacing": { + Input: `| Variable name | Description | Example value | +| ----------------- | ------------------------------------------------------------- | ------------------------------------- | +| AFASBaseUri | Base URI of the AFAS REST API endpoint for this environment | https://12345.rest.afas.online/ProfitRestServices | +| AFASToke | App token in XML format for this environment | \\1\\D5R324DD5F4TRD945E530ED3CDD70D94BBDEC4C732B43F285ECB12345678\\ | +| AADtenantID | Id of the Azure tenant | 12fc345b-0c67-4cde-8902-dabf2cad34b5 | +| AADAppId | Id of the Azure app | f12345c6-7890-1f23-b456-789eb0bb1c23 | +| AADAppSecret | Secret of the Azure app | G1X2HsBw-co3dTIB45RE6vY.mSU~6u.7.8 |`, + Expected: map[string]struct{}{ + "f12345c6-7890-1f23-b456-789eb0bb1c23": {}, + }, + }, + } + + runPatTest(t, cases, FindClientIdMatches) +} diff --git a/pkg/detectors/azure_entra/refreshtoken/refreshtoken.go b/pkg/detectors/azure_entra/refreshtoken/refreshtoken.go new file mode 100644 index 0000000000000..99bd041afbbb4 --- /dev/null +++ b/pkg/detectors/azure_entra/refreshtoken/refreshtoken.go @@ -0,0 +1,300 @@ +package azure_entra_refreshtoken + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/golang-jwt/jwt/v4" + 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" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +type Scanner struct { + client *http.Client +} + +// Ensure the Scanner satisfies the interface at compile time. +var _ interface { + detectors.Detector + detectors.MaxSecretSizeProvider + detectors.StartOffsetProvider +} = (*Scanner)(nil) + +var ( + defaultClient = common.SaneHttpClient() + + refreshTokenPat = regexp.MustCompile(`\b0\.A[\w-]{50,}\.Ag[\w-]{250,}`) +) + +// 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{"0.A"} +} + +func (Scanner) MaxSecretSize() int64 { return 2048 } + +func (Scanner) StartOffset() int64 { return 4096 } + +// FromData will find and optionally verify Azure RefreshToken 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) + + tokenMatches := findTokenMatches(dataStr) + if len(tokenMatches) == 0 { + return + } + clientMatches := azure_entra.FindClientIdMatches(dataStr) + if len(clientMatches) == 0 { + clientMatches[defaultClientId] = struct{}{} + } + tenantMatches := azure_entra.FindTenantIdMatches(dataStr) + if len(tenantMatches) == 0 { + tenantMatches[defaultTenantId] = struct{}{} + } + + return s.processMatches(ctx, tokenMatches, clientMatches, tenantMatches, verify), err +} + +func (s Scanner) processMatches(ctx context.Context, refreshTokens, clientIds, tenantIds map[string]struct{}, verify bool) (results []detectors.Result) { + invalidClientsForTenant := make(map[string]map[string]struct{}) + validTenants := make(map[string]struct{}) + +TokenLoop: + for token := range refreshTokens { + var ( + r *detectors.Result + clientId string + tenantId string + ) + + ClientLoop: + for cId := range clientIds { + clientId = cId + for tId := range tenantIds { + tenantId = tId + + // Skip known invalid tenants. + invalidClients := invalidClientsForTenant[tenantId] + if invalidClients == nil { + invalidClients = map[string]struct{}{} + invalidClientsForTenant[tenantId] = invalidClients + } + if _, ok := invalidClients[clientId]; ok { + continue + } + + if verify { + client := s.client + if client == nil { + client = defaultClient + } + + if _, ok := validTenants[tenantId]; !ok { + if azure_entra.TenantExists(ctx, client, tenantId) { + validTenants[tenantId] = struct{}{} + } else { + fmt.Printf("Tenant doesn't exist: '%v'\n", tenantId) + delete(tenantIds, tenantId) + continue + } + } + + isVerified, extraData, verificationErr := verifyMatch(ctx, client, token, clientId, tenantId) + // Handle errors. + if verificationErr != nil { + if errors.Is(verificationErr, ErrTenantNotFound) { + // Tenant doesn't exist. This shouldn't happen with the check above. + delete(tenantIds, tenantId) + continue + } else if errors.Is(verificationErr, ErrClientNotFoundInTenant) { + // Tenant is valid but the ClientID doesn't exist. + invalidClients[clientId] = struct{}{} + continue + } else if errors.Is(verificationErr, ErrTokenExpired) { + continue TokenLoop + } + } + + // The result is verified or there's only one associated client and tenant. + if isVerified || (len(clientIds) == 1 && len(tenantIds) == 1) { + r = createResult(tenantId, clientId, token, isVerified, extraData, verificationErr) + break ClientLoop + } + + // The result may be valid for another client/tenant. + } + } + } + + if r == nil { + // Only include the clientId and tenantId if we're confident which one it is. + if len(clientIds) != 1 || clientId == defaultClientId { + clientId = "" + } + if len(tenantIds) != 1 || tenantId == defaultTenantId { + tenantId = "" + } + r = createResult(token, clientId, tenantId, false, nil, nil) + } + + results = append(results, *r) + } + return results +} + +const defaultTenantId = "common" +const defaultClientId = "d3590ed6-52b3-4102-aeff-aad2292ab01c" // Microsoft Office + +var ( + ErrTokenExpired = errors.New("token expired") + ErrTenantNotFound = errors.New("tenant not found") + ErrClientNotFoundInTenant = errors.New("application was not found in tenant") +) + +// https://learn.microsoft.com/en-us/advertising/guides/authentication-oauth-get-tokens?view=bingads-13#refresh-accesstoken +func verifyMatch(ctx context.Context, client *http.Client, refreshToken string, clientId string, tenantId string) (bool, map[string]string, error) { + data := url.Values{} + data.Set("client_id", clientId) + data.Set("scope", "https://graph.microsoft.com/.default") + data.Set("refresh_token", refreshToken) + data.Set("grant_type", "refresh_token") + + tokenUrl := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantId) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenUrl, bytes.NewBufferString(data.Encode())) + if err != nil { + return false, nil, nil + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + 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 successResponse + if err := json.NewDecoder(res.Body).Decode(&okResp); err != nil { + return false, nil, err + } + + extraData := map[string]string{ + "Tenant": tenantId, + "Client": clientId, + "Scope": okResp.Scope, + } + + // Add claims from the access token. + token, _ := jwt.Parse(okResp.AccessToken, nil) + if token != nil { + claims := token.Claims.(jwt.MapClaims) + + if app := fmt.Sprint(claims["app_displayname"]); app != "" { + extraData["Application"] = app + } + + // The user information can be in a few claims. + switch { + case claims["email"] != nil: + extraData["User"] = fmt.Sprint(claims["email"]) + case claims["upn"] != nil: + extraData["User"] = fmt.Sprint(claims["upn"]) + case claims["unique_name"]: + extraData["User"] = fmt.Sprint(claims["unique_name"]) + } + } + return true, extraData, nil + } else { + var errResp errorResponse + if err := json.NewDecoder(res.Body).Decode(&errResp); err != nil { + return false, nil, err + } + + switch res.StatusCode { + case http.StatusBadRequest: + // 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, "AADSTS70008:"): + // https://login.microsoftonline.com/error?code=70008 + return false, nil, ErrTokenExpired + case strings.HasPrefix(d, "AADSTS700016:"): + // https://login.microsoftonline.com/error?code=700016 + return false, nil, ErrClientNotFoundInTenant + 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) + } + case http.StatusUnauthorized: + // The secret is determinately not verified (nothing to do) + return false, nil, nil + default: + return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } + } +} + +type successResponse struct { + Scope string `json:"scope"` + AccessToken string `json:"access_token"` +} + +type errorResponse struct { + Error string `json:"error"` + Description string `json:"error_description"` +} + +func (s Scanner) Type() detectorspb.DetectorType { + return detectorspb.DetectorType_AzureRefreshToken +} + +// region Helper methods. +func findTokenMatches(data string) map[string]struct{} { + uniqueMatches := make(map[string]struct{}) + for _, match := range refreshTokenPat.FindAllStringSubmatch(data, -1) { + uniqueMatches[match[0]] = struct{}{} + } + return uniqueMatches +} + +func createResult(refreshToken, clientId, tenantId string, verified bool, extraData map[string]string, err error) *detectors.Result { + r := &detectors.Result{ + DetectorType: detectorspb.DetectorType_AzureRefreshToken, + Raw: []byte(refreshToken), + ExtraData: extraData, + Verified: verified, + } + r.SetVerificationError(err, refreshToken) + + if clientId != "" && tenantId != "" { + var sb strings.Builder + sb.WriteString(`{`) + sb.WriteString(`"refreshToken":"` + refreshToken + `"`) + sb.WriteString(`,"clientId":"` + clientId + `"`) + sb.WriteString(`,"tenantId":"` + tenantId + `"`) + sb.WriteString(`}`) + r.RawV2 = []byte(sb.String()) + } + + return r +} + +//endregion diff --git a/pkg/detectors/azure_entra/refreshtoken/refreshtoken_integration_test.go b/pkg/detectors/azure_entra/refreshtoken/refreshtoken_integration_test.go new file mode 100644 index 0000000000000..e9a85420bf279 --- /dev/null +++ b/pkg/detectors/azure_entra/refreshtoken/refreshtoken_integration_test.go @@ -0,0 +1,161 @@ +//go:build detectors +// +build detectors + +package azure_entra_refreshtoken + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/trufflesecurity/trufflehog/v3/pkg/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" +) + +func TestRefreshToken_FromChunk(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") + if err != nil { + t.Fatalf("could not get test secrets from GCP: %s", err) + } + secret := testSecrets.MustGetField("AZUREREFRESHTOKEN") + inactiveSecret := testSecrets.MustGetField("AZUREREFRESHTOKEN_INACTIVE") + + type args struct { + ctx context.Context + data []byte + verify bool + } + tests := []struct { + name string + s Scanner + args args + want []detectors.Result + wantErr bool + wantVerificationErr bool + }{ + { + name: "found, verified", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a azurerefreshtoken secret %s within", secret)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_AzureRefreshToken, + Verified: true, + }, + }, + wantErr: false, + wantVerificationErr: false, + }, + { + name: "found, unverified", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a azurerefreshtoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_AzureRefreshToken, + Verified: false, + }, + }, + wantErr: false, + wantVerificationErr: false, + }, + { + name: "not found", + s: Scanner{}, + args: args{ + ctx: context.Background(), + data: []byte("You cannot find the secret within"), + verify: true, + }, + want: nil, + wantErr: false, + wantVerificationErr: false, + }, + { + name: "found, would be verified if not for timeout", + s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a azurerefreshtoken secret %s within", secret)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_AzureRefreshToken, + Verified: false, + }, + }, + wantErr: false, + wantVerificationErr: true, + }, + { + name: "found, verified but unexpected api surface", + s: Scanner{client: common.ConstantResponseHttpClient(404, "")}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a azurerefreshtoken secret %s within", secret)), + verify: true, + }, + want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_AzureRefreshToken, + Verified: false, + }, + }, + wantErr: false, + wantVerificationErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("Azurerefreshtoken.FromData() error = %v, wantErr %v", err, tt.wantErr) + return + } + for i := range got { + if len(got[i].Raw) == 0 { + t.Fatalf("no raw secret present: \n %+v", got[i]) + } + if (got[i].VerificationError() != nil) != tt.wantVerificationErr { + t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError()) + } + } + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError") + if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { + t.Errorf("Azurerefreshtoken.FromData() %s diff: (-got +want)\n%s", tt.name, diff) + } + }) + } +} + +func BenchmarkFromData(benchmark *testing.B) { + ctx := context.Background() + s := Scanner{} + for name, data := range detectors.MustGetBenchmarkData() { + benchmark.Run(name, func(b *testing.B) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + _, err := s.FromData(ctx, false, data) + if err != nil { + b.Fatal(err) + } + } + }) + } +} diff --git a/pkg/detectors/azure_entra/refreshtoken/refreshtoken_test.go b/pkg/detectors/azure_entra/refreshtoken/refreshtoken_test.go new file mode 100644 index 0000000000000..ef85d4081b8d5 --- /dev/null +++ b/pkg/detectors/azure_entra/refreshtoken/refreshtoken_test.go @@ -0,0 +1,110 @@ +package azure_entra_refreshtoken + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" + "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" +) + +func TestRefreshToken_Pattern(t *testing.T) { + d := Scanner{} + ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) + tests := []struct { + name string + input string + want []string + }{ + { + name: "token only", + input: `"refresh_token": "0.AXEAFN5Pl6TDG0ibA8_OGCw6B-kFbFJoXnhBqmJD9wukrpZxAMc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9g0VCdz8smoWqJBpit_3P_ntszmbCH2-dGwpsamwQMbLl7QBa7tlfXH_NtpD1vNTGkacraUMyTM5lfg1AR1DLAxs-pNSpg8NfrHbNSRAIacCpOyqtU05Dg9l5LC7ZYwxT35dQWEK0EExLER-wxjW9DrDZNQV4J3Ktv1Z4ANT2N2rqAjPYqHTDPCCcOi980ptizeImgVYiVr37Ff0Hnr_lAi4Em0wGB7KDdu319sV9Sebe91FIRDs7GVvvv7GFvKjTeXJwHCpbhdqX4X2TRMryNrTNZ8QY7_Wa25MQm7v0qfFqDW_pRMxxohGhClSedZFnkzrreIhZ8ULJ9NCf8YENRHDP3LuOJP5gex-H0MUNsJQLxlDq3bH-i7Fz_cTEB3UN_bvgE9aNe-5gal-ykO_gSx-Kk5D-vZWpLDrFUdRSGYHmKr1zgEZvQjsFUj8pGWgUwssqN9SOPxTYIEzQaxPAul5AFKcxGYt2l4Kvhh58txUdayFAglWrkx1lrxnpIcjoRmHOo45AKlgH30bVOjjltwvD4L9SGMAHhni3F6mCB6aNLGpYCHjrbdsiWolHKV0leJmBYl2Ye4eosQf9YYdgPAbCQKqOJ6gfrxJJTcfrISqDVw1c6C9qPPdHbvdol_KfdJntyfuPpHovx7AfARBcjb6nMgYRBI0wFWsGuTNDcylicMFRcZx6v283wBv4U_0PrG1_Yd5ktfgaTVXF733C-ma_-s49tAvtDrJz2bmNFpotLyyQmwOiApLjeWFkH8EjBsBtpjhzzCIrOHuHR1I1gHChDMMDxfFT2k8dqxkvBpMLZ3zFWyJNl3LYbjgy9BkTIngvpQMSgRMl_VZ2eN_fZWk5wVOHjiUJJ9n4Y8IKQRM731vK_XEaK_BdtNLfC1Gw8hfLrIZpC6152zj6RhPn03gOK7G4RL6S21IfWKrw4kl6rdaPLgmMxlaI"`, + want: []string{"0.AXEAFN5Pl6TDG0ibA8_OGCw6B-kFbFJoXnhBqmJD9wukrpZxAMc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9g0VCdz8smoWqJBpit_3P_ntszmbCH2-dGwpsamwQMbLl7QBa7tlfXH_NtpD1vNTGkacraUMyTM5lfg1AR1DLAxs-pNSpg8NfrHbNSRAIacCpOyqtU05Dg9l5LC7ZYwxT35dQWEK0EExLER-wxjW9DrDZNQV4J3Ktv1Z4ANT2N2rqAjPYqHTDPCCcOi980ptizeImgVYiVr37Ff0Hnr_lAi4Em0wGB7KDdu319sV9Sebe91FIRDs7GVvvv7GFvKjTeXJwHCpbhdqX4X2TRMryNrTNZ8QY7_Wa25MQm7v0qfFqDW_pRMxxohGhClSedZFnkzrreIhZ8ULJ9NCf8YENRHDP3LuOJP5gex-H0MUNsJQLxlDq3bH-i7Fz_cTEB3UN_bvgE9aNe-5gal-ykO_gSx-Kk5D-vZWpLDrFUdRSGYHmKr1zgEZvQjsFUj8pGWgUwssqN9SOPxTYIEzQaxPAul5AFKcxGYt2l4Kvhh58txUdayFAglWrkx1lrxnpIcjoRmHOo45AKlgH30bVOjjltwvD4L9SGMAHhni3F6mCB6aNLGpYCHjrbdsiWolHKV0leJmBYl2Ye4eosQf9YYdgPAbCQKqOJ6gfrxJJTcfrISqDVw1c6C9qPPdHbvdol_KfdJntyfuPpHovx7AfARBcjb6nMgYRBI0wFWsGuTNDcylicMFRcZx6v283wBv4U_0PrG1_Yd5ktfgaTVXF733C-ma_-s49tAvtDrJz2bmNFpotLyyQmwOiApLjeWFkH8EjBsBtpjhzzCIrOHuHR1I1gHChDMMDxfFT2k8dqxkvBpMLZ3zFWyJNl3LYbjgy9BkTIngvpQMSgRMl_VZ2eN_fZWk5wVOHjiUJJ9n4Y8IKQRM731vK_XEaK_BdtNLfC1Gw8hfLrIZpC6152zj6RhPn03gOK7G4RL6S21IfWKrw4kl6rdaPLgmMxlaI"}, + }, + { + name: "token+client+tenant", + input: ` +{ + "tokenType": "Bearer", + "expiresIn": 4742, + "expiresOn": "2024-06-07 09:09:22.294640", + "resource": "https://graph.windows.net", + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + "refreshToken": "0.AUUAMe_N-B6jSkuT5F9XHpElWlj2JcxuFFnRLm_3awiSnuJQsa1.AgABAwEAAADnfolhJpSnRYB1SVj-Hgd8Agrf-wUA9P9oElBtlKe8a-5_1t2eEmBef50SCv8exOOrgjUFMLtPQj_XH1rq3Onj2dCFQaHzhm7DfoOxj5LH4kR9jPIbPf2yRI0CgxFLEGMf0biO9LxmvVwb_NKTScIc_MK4eBsXG-En_e3vaIJS5t-ghSvPAKzl3pxiYVvBdP1i_nUHPl4dsCkk9SKCexWnhi4tg9xVVIi-MIkGDJxThmuKfAko1VHMgx-tsHRKgPoXlJi51uNO0KQQUxnDnjiWmLapCe3hVtjfoINBlb3CpiHkfW5G9dzF4cmFOQJQG9RdW-CU6t4VmlamK9gSbNYfyd7fWr7Ebv9Bo06eWEwEBpQmJONJERNScnqMs5Ztba9kUHchXqJd9wZMH-NtWejuR92IqMmPoaY4DP52Yodu2hWZPv0pFEFsthPJ3YpViOaJnCoSQ7ba-qzVr8TnvFlkI8EfFKNbl47_WncwKXDrPk2FlZwG4ywX7s0dXYvXDJ-rMQHsDcJDMABQXrxaU0Z7ozCk_ftVgBQocWZHAkzBtWZNw9dS4ltux0GeAYekUjzE7UYrPw41DLWOLrr7V-kx5sZ6h66iiTi-zdsJ28LnRIX4aZ6IC7jxIG0FK-roPldOEjy0XJ-V6QmyjkEYT3PK23vUTHIz3EQ8JqGNJMJO5mWwbedlIl2xq-0CczybkR2MJgr4UAQKUBFMYuUYGWrVygte9d48usQ6-MhAavmkyZb5Mo_PeMnnNef-cl6c8RUzMAOpeiumFEG-gTzyDgaoM1eFjtYKTz0mr-0lPfrEavE4LfGXh87oDb0lNrbbkMNhAXjz2rJW8ex1REfeBH4oit0WeMWH-sIvpT3H8jsYIawfPp7rBN9z_TMX9AUbqROEY2Nv1jSJsXCX0sjLRweYiQnl-hHFfLcWwFIFjMfs7eOKSiOBKB3ZqjQw_A8OVDxhAQJybiVgW8U41IAjXGX0DNilrmE0PhDAqs5jQIBSO66G05yJj1RY3b2z8cYMG1lKAZ10IIDfo8f3FU-_m-w6zNVVkNZko89bX8tA91EjXpoUvmnPZKT84Qx9KvtRM561ABVEYnE152821Xy0HeObVue6M5WlF0puvqk1HnkfAUDxMk6qO1Xy7o0myTIV1R2yxFPpQX_pwCRB1IutSqz0s6E1XyfbRyv8TKxjX3_tGgvUy8KrZFeYJ9pRFsKIN_AJ9_a2GMG6h1b9aCIaA7jGlOkYlC-4LnhqoKxs4RpJJIpWWN6wZstGmIACwJS4", + "familyName": "Doe", + "givenName": "John", + "identityProvider": "live.com", + "tenantId": "16515984-9303-47f6-a59f-917611c8cb2b", + "userId": "john.doe@outlook.com", + "isUserIdDisplayable": true, + "isMRRT": true, + "_clientId": "1b730954-1685-4b74-9bfd-dac224a7b894", + "_authority": "https://login.microsoftonline.com/16515984-9303-47f6-a59f-917611c8cb2b" +}`, + want: []string{`{"refreshToken":"0.AUUAMe_N-B6jSkuT5F9XHpElWlj2JcxuFFnRLm_3awiSnuJQsa1.AgABAwEAAADnfolhJpSnRYB1SVj-Hgd8Agrf-wUA9P9oElBtlKe8a-5_1t2eEmBef50SCv8exOOrgjUFMLtPQj_XH1rq3Onj2dCFQaHzhm7DfoOxj5LH4kR9jPIbPf2yRI0CgxFLEGMf0biO9LxmvVwb_NKTScIc_MK4eBsXG-En_e3vaIJS5t-ghSvPAKzl3pxiYVvBdP1i_nUHPl4dsCkk9SKCexWnhi4tg9xVVIi-MIkGDJxThmuKfAko1VHMgx-tsHRKgPoXlJi51uNO0KQQUxnDnjiWmLapCe3hVtjfoINBlb3CpiHkfW5G9dzF4cmFOQJQG9RdW-CU6t4VmlamK9gSbNYfyd7fWr7Ebv9Bo06eWEwEBpQmJONJERNScnqMs5Ztba9kUHchXqJd9wZMH-NtWejuR92IqMmPoaY4DP52Yodu2hWZPv0pFEFsthPJ3YpViOaJnCoSQ7ba-qzVr8TnvFlkI8EfFKNbl47_WncwKXDrPk2FlZwG4ywX7s0dXYvXDJ-rMQHsDcJDMABQXrxaU0Z7ozCk_ftVgBQocWZHAkzBtWZNw9dS4ltux0GeAYekUjzE7UYrPw41DLWOLrr7V-kx5sZ6h66iiTi-zdsJ28LnRIX4aZ6IC7jxIG0FK-roPldOEjy0XJ-V6QmyjkEYT3PK23vUTHIz3EQ8JqGNJMJO5mWwbedlIl2xq-0CczybkR2MJgr4UAQKUBFMYuUYGWrVygte9d48usQ6-MhAavmkyZb5Mo_PeMnnNef-cl6c8RUzMAOpeiumFEG-gTzyDgaoM1eFjtYKTz0mr-0lPfrEavE4LfGXh87oDb0lNrbbkMNhAXjz2rJW8ex1REfeBH4oit0WeMWH-sIvpT3H8jsYIawfPp7rBN9z_TMX9AUbqROEY2Nv1jSJsXCX0sjLRweYiQnl-hHFfLcWwFIFjMfs7eOKSiOBKB3ZqjQw_A8OVDxhAQJybiVgW8U41IAjXGX0DNilrmE0PhDAqs5jQIBSO66G05yJj1RY3b2z8cYMG1lKAZ10IIDfo8f3FU-_m-w6zNVVkNZko89bX8tA91EjXpoUvmnPZKT84Qx9KvtRM561ABVEYnE152821Xy0HeObVue6M5WlF0puvqk1HnkfAUDxMk6qO1Xy7o0myTIV1R2yxFPpQX_pwCRB1IutSqz0s6E1XyfbRyv8TKxjX3_tGgvUy8KrZFeYJ9pRFsKIN_AJ9_a2GMG6h1b9aCIaA7jGlOkYlC-4LnhqoKxs4RpJJIpWWN6wZstGmIACwJS4","clientId":"1b730954-1685-4b74-9bfd-dac224a7b894","tenantId":"16515984-9303-47f6-a59f-917611c8cb2b"}`}, + }, + { + name: "README", + input: ` +### Connection settings + +The connection settings are defined in the automation variables. + 1. Create the following [user defined variables](https://docs.helloid.com/hc/en-us/articles/360014169933-How-to-Create-and-Manage-User-Defined-Variables) + +| Variable name | Description | Example value | +| ----------------- | ------------------------------------------------------------- | ------------------------------------- | +| AFASBaseUri | Base URI of the AFAS REST API endpoint for this environment | https://12345.rest.afas.online/ProfitRestServices | +| AFASToke | App token in XML format for this environment | \\1\\D5R324DD5F4TRD945E530ED3CDD70D94BBDEC4C732B43F285ECB12345678\\ | +| AADtenantID | Id of the Azure tenant | 12fc345b-0c67-4cde-8902-dabf2cad34b5 | +| AADAppId | Id of the Azure app | f12345c6-7890-1f23-b456-789eb0bb1c23 | +| AADRefreshToken | Refresh token of the Azure app | 0.ABCDEFGHIJKLMNOPQRS_PK0mtsE5afl5BYdPsASFbrS7jIZ0AAc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P-XOTtPMo2xp9vfbHGvVkHaBZh4D3YmTkx_WagBOk358QjDwHUsiuVvyKvP6FTbQQt8kCidfMC9cmIYesHG4Ft2B1HwJNX28OpiFPuFti1D4Is30GgQ685i_ovS4iXDCUgtm2zpI6ZQJVqoOidXZQW_lSupdcclMK_JCIb7LBuJBDXfy0-f75C734_nxL0nggS9mn-e_KuJpHvypvU8OS9MPDBArhUopZum2y-2oNE65Wr-xpKm_Zeyr3iUGSZg98nbaryHw-lbeyFC8LcNqqMB_T7BcgvJicHSnj6DtjjpMyjKMwsCAnxz2bUYoLLjGFHk8EhDUCuV9lzUW1BTko5_I31TQdX0XY94vHTU34N93t3QPrQFMf8UhDjfQKiCDj3r2b7YR9ndS8MNp9MIa1CbL8vI4EM8GO4wtVI30Dhca4HaMtpph6uJp3echt-q7AVNQ_7ZHgx_YFZNqDmJyYq3nrae7LYRo0kvM382ss7JpCylodwya89mC_SlnrFhLM_zbt1TQkOtZqiVHbdQk3z-MX1iZso5Mk17Yks1ao0mS0RJfWVWSlOq_Sp-2yaiCsP-lV1PVdvvY_AkuOulP1kPG_VfC0DN3pGjSQJ8J9Ot5hfyElWyPst9Nc-ODErLhEqIl-3IR6wPKFN2ffjt8-dtCVMlVdBd1QANQOFBiIGA-_BZdGLvzROrWCOE9dDtyBQ_LnxdnnOVdjUqJ-xdql1p13Xjy6ZTtcZtTDmFN5hSMffYuUtuwEOy_Xb91Y2tvwOxcSe9dj7ElOLZDo2C7fGsMgaIJ1gK8xt9OWsS1o1sQZKQADTZq5TTxJp7PY3tJsUnOlD4q8ZEyVBQAvRKinpajBRcbq2lTCVt0JgXAryWztqYTpAxiqaBr51vuR4pbVRtKv-h_10tYD-TUV1WeX2fY3GuZA4B5g | + +## contents`, + want: []string{`{"refreshToken":"0.ABCDEFGHIJKLMNOPQRS_PK0mtsE5afl5BYdPsASFbrS7jIZ0AAc.AgABAAAAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P-XOTtPMo2xp9vfbHGvVkHaBZh4D3YmTkx_WagBOk358QjDwHUsiuVvyKvP6FTbQQt8kCidfMC9cmIYesHG4Ft2B1HwJNX28OpiFPuFti1D4Is30GgQ685i_ovS4iXDCUgtm2zpI6ZQJVqoOidXZQW_lSupdcclMK_JCIb7LBuJBDXfy0-f75C734_nxL0nggS9mn-e_KuJpHvypvU8OS9MPDBArhUopZum2y-2oNE65Wr-xpKm_Zeyr3iUGSZg98nbaryHw-lbeyFC8LcNqqMB_T7BcgvJicHSnj6DtjjpMyjKMwsCAnxz2bUYoLLjGFHk8EhDUCuV9lzUW1BTko5_I31TQdX0XY94vHTU34N93t3QPrQFMf8UhDjfQKiCDj3r2b7YR9ndS8MNp9MIa1CbL8vI4EM8GO4wtVI30Dhca4HaMtpph6uJp3echt-q7AVNQ_7ZHgx_YFZNqDmJyYq3nrae7LYRo0kvM382ss7JpCylodwya89mC_SlnrFhLM_zbt1TQkOtZqiVHbdQk3z-MX1iZso5Mk17Yks1ao0mS0RJfWVWSlOq_Sp-2yaiCsP-lV1PVdvvY_AkuOulP1kPG_VfC0DN3pGjSQJ8J9Ot5hfyElWyPst9Nc-ODErLhEqIl-3IR6wPKFN2ffjt8-dtCVMlVdBd1QANQOFBiIGA-_BZdGLvzROrWCOE9dDtyBQ_LnxdnnOVdjUqJ-xdql1p13Xjy6ZTtcZtTDmFN5hSMffYuUtuwEOy_Xb91Y2tvwOxcSe9dj7ElOLZDo2C7fGsMgaIJ1gK8xt9OWsS1o1sQZKQADTZq5TTxJp7PY3tJsUnOlD4q8ZEyVBQAvRKinpajBRcbq2lTCVt0JgXAryWztqYTpAxiqaBr51vuR4pbVRtKv-h_10tYD-TUV1WeX2fY3GuZA4B5g","clientId":"f12345c6-7890-1f23-b456-789eb0bb1c23","tenantId":"12fc345b-0c67-4cde-8902-dabf2cad34b5"}`}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) + if len(matchedDetectors) == 0 { + t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input) + return + } + + results, err := d.FromData(context.Background(), false, []byte(test.input)) + if err != nil { + t.Errorf("error = %v", err) + return + } + + if len(results) != len(test.want) { + if len(results) == 0 { + t.Errorf("did not receive result") + } else { + t.Errorf("expected %d results, only received %d", len(test.want), len(results)) + } + return + } + + actual := make(map[string]struct{}, len(results)) + for _, r := range results { + if len(r.RawV2) > 0 { + actual[string(r.RawV2)] = struct{}{} + } else { + actual[string(r.Raw)] = struct{}{} + } + } + expected := make(map[string]struct{}, len(test.want)) + for _, v := range test.want { + expected[v] = struct{}{} + } + + if diff := cmp.Diff(expected, actual); diff != "" { + t.Errorf("%s diff: (-want +got)\n%s", test.name, diff) + } + }) + } +} diff --git a/pkg/detectors/azure/azure.go b/pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go similarity index 98% rename from pkg/detectors/azure/azure.go rename to pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go index 2319ce97c4aad..ad4c7831429c2 100644 --- a/pkg/detectors/azure/azure.go +++ b/pkg/detectors/azure_entra/serviceprincipal/serviceprincipal.go @@ -1,11 +1,12 @@ -package azure +package azure_entra_serviceprincipal import ( "context" "fmt" - regexp "github.com/wasilibs/go-re2" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" diff --git a/pkg/detectors/azure/azure_test.go b/pkg/detectors/azure_entra/serviceprincipal/serviceprincipal_test.go similarity index 98% rename from pkg/detectors/azure/azure_test.go rename to pkg/detectors/azure_entra/serviceprincipal/serviceprincipal_test.go index 9ecc18ab14a57..7e963b10ce604 100644 --- a/pkg/detectors/azure/azure_test.go +++ b/pkg/detectors/azure_entra/serviceprincipal/serviceprincipal_test.go @@ -1,7 +1,7 @@ //go:build detectors // +build detectors -package azure +package azure_entra_serviceprincipal import ( "context" @@ -10,6 +10,7 @@ import ( "time" "github.com/kylelemons/godebug/pretty" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" diff --git a/pkg/detectors/uuids.txt b/pkg/detectors/uuids.txt new file mode 100644 index 0000000000000..89a42a2efc36d --- /dev/null +++ b/pkg/detectors/uuids.txt @@ -0,0 +1,37 @@ +00000000-0000-0000-0000-000000000000 +11111111-1111-1111-1111-111111111111 +22222222-2222-2222-2222-222222222222 +33333333-3333-3333-3333-333333333333 +44444444-4444-4444-4444-444444444444 +55555555-5555-5555-5555-555555555555 +66666666-6666-6666-6666-666666666666 +77777777-7777-7777-7777-777777777777 +88888888-8888-8888-8888-888888888888 +99999999-9999-9999-9999-999999999999 +12345678-1234-1234-1234-123456789abc +23456789-2345-2345-2345-23456789abcd +34567890-3456-3456-3456-34567890bcde +45678901-4567-4567-4567-45678901cdef +56789012-5678-5678-5678-56789012def0 +aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb +cccccccc-cccc-cccc-cccc-cccccccccccc +dddddddd-dddd-dddd-dddd-dddddddddddd +eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee +ffffffff-ffff-ffff-ffff-ffffffffffff +deadbeef-dead-beef-dead-beefdeadbeef +cafebabe-cafe-babe-cafe-babecafebabe +badc0ffee-badc-0ffe-badc-0ffeebadc0f +deadface-dead-face-dead-facedeadface +feedface-feed-face-feed-facefeedface +a1b2c3d4-a1b2-c3d4-a1b2-c3d4a1b2c3d4 +98765432-9876-5432-9876-543298765432 +abcdefab-cdef-abcd-efab-cdefabcdefab +a0a0a0a0-a0a0-a0a0-a0a0-a0a0a0a0a0a0 +b0b0b0b0-b0b0-b0b0-b0b0-b0b0b0b0b0b0 +c0c0c0c0-c0c0-c0c0-c0c0-c0c0c0c0c0c0 +d0d0d0d0-d0d0-d0d0-d0d0-d0d0d0d0d0d0 +e0e0e0e0-e0e0-e0e0-e0e0-e0e0e0e0e0e0 +f0f0f0f0-f0f0-f0f0-f0f0-f0f0f0f0f0f0 +xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +-xxxx-xxxx-xxxx-xxxxxxxxxxxx diff --git a/pkg/engine/defaults.go b/pkg/engine/defaults.go index 9b588bbbff529..5f1762d24836d 100644 --- a/pkg/engine/defaults.go +++ b/pkg/engine/defaults.go @@ -61,8 +61,9 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/axonaut" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/aylien" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/ayrshare" - "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure" - "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azurebatch" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_batch" + azure_entra_refreshtoken "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/refreshtoken" + azure_entra_serviceprincipal "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azure_entra/serviceprincipal" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azurecontainerregistry" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuredevopspersonalaccesstoken" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/azuresearchadminkey" @@ -1592,7 +1593,7 @@ func DefaultDetectors() []detectors.Detector { portainertoken.Scanner{}, pagarme.Scanner{}, planetscaledb.Scanner{}, - azure.Scanner{}, + azure_entra_serviceprincipal.Scanner{}, azurestorage.Scanner{}, azurecontainerregistry.Scanner{}, azurebatch.Scanner{}, @@ -1610,6 +1611,7 @@ func DefaultDetectors() []detectors.Detector { groq.Scanner{}, twitterconsumerkey.Scanner{}, eraser.Scanner{}, + &azure_entra_refreshtoken.Scanner{}, } } diff --git a/pkg/pb/detectorspb/detectors.pb.go b/pkg/pb/detectorspb/detectors.pb.go index 146e38fe1e7ad..c8562af01b0a0 100644 --- a/pkg/pb/detectorspb/detectors.pb.go +++ b/pkg/pb/detectorspb/detectors.pb.go @@ -1090,6 +1090,7 @@ const ( DetectorType_Groq DetectorType = 988 DetectorType_TwitterConsumerkey DetectorType = 989 DetectorType_Eraser DetectorType = 990 + DetectorType_AzureRefreshToken DetectorType = 991 ) // Enum value maps for DetectorType. @@ -2082,6 +2083,7 @@ var ( 988: "Groq", 989: "TwitterConsumerkey", 990: "Eraser", + 991: "AzureRefreshToken", } DetectorType_value = map[string]int32{ "Alibaba": 0, @@ -3071,6 +3073,7 @@ var ( "Groq": 988, "TwitterConsumerkey": 989, "Eraser": 990, + "AzureRefreshToken": 991, } ) @@ -3524,7 +3527,7 @@ var file_detectors_proto_rawDesc = []byte{ 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x41, 0x53, 0x45, 0x36, 0x34, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x55, 0x54, 0x46, 0x31, 0x36, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x45, 0x53, 0x43, 0x41, 0x50, 0x45, 0x44, 0x5f, 0x55, 0x4e, 0x49, 0x43, 0x4f, 0x44, 0x45, - 0x10, 0x04, 0x2a, 0xba, 0x7e, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, + 0x10, 0x04, 0x2a, 0xd2, 0x7e, 0x0a, 0x0c, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x10, 0x03, 0x12, 0x0a, @@ -4535,12 +4538,13 @@ var file_detectors_proto_rawDesc = []byte{ 0x12, 0x0c, 0x0a, 0x07, 0x49, 0x6e, 0x74, 0x72, 0x61, 0x34, 0x32, 0x10, 0xdb, 0x07, 0x12, 0x09, 0x0a, 0x04, 0x47, 0x72, 0x6f, 0x71, 0x10, 0xdc, 0x07, 0x12, 0x17, 0x0a, 0x12, 0x54, 0x77, 0x69, 0x74, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x6b, 0x65, 0x79, 0x10, - 0xdd, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x45, 0x72, 0x61, 0x73, 0x65, 0x72, 0x10, 0xde, 0x07, 0x42, - 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, - 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, - 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, - 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0xdd, 0x07, 0x12, 0x0b, 0x0a, 0x06, 0x45, 0x72, 0x61, 0x73, 0x65, 0x72, 0x10, 0xde, 0x07, 0x12, + 0x16, 0x0a, 0x11, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x10, 0xdf, 0x07, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, + 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, + 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/detectors.proto b/proto/detectors.proto index f6f28968b797f..2feac42bba40a 100644 --- a/proto/detectors.proto +++ b/proto/detectors.proto @@ -1000,6 +1000,7 @@ enum DetectorType { Groq = 988; TwitterConsumerkey = 989; Eraser = 990; + AzureRefreshToken = 991; } message Result {