Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Detector implementation for Azure API Management Direct Management Key #3938

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
cleanup some unnecessary logic for expiry formatting.
code refactoring, remove unnecessary code.
updated the description of detector.
reduce the expiry to 5 seconds.
  • Loading branch information
abmussani committed Feb 28, 2025
commit 13b3967b6fe76c50afc441ee93205e07281c01ca
Original file line number Diff line number Diff line change
@@ -17,6 +17,8 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

const RFC3339WithoutMicroseconds = "2006-01-02T15:04:05"

type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
@@ -28,7 +30,7 @@ var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
urlPat = regexp.MustCompile(`https://([a-z0-9][a-z0-9-]{0,48}[a-z0-9])\.management\.azure-api\.net`) // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.APIM.Name/
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([a-zA-Z0-9+\/-]{86,88}\b={0,2})`) // Base64-encoded primary key
primaryKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"azure"}) + `\b([a-zA-Z0-9+\/-]{86,88}\b={0,2})`) // Base64-encoded primary key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why - at the end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rgmz Initially I copied that regex from another detector, Azure Storage. After your comment, I generated 20 times a key and confidently say that all of them ended with [a-zA-Z0-9]==. Updated the regex.

)

// Keywords are used for efficiently pre-filtering chunks.
@@ -41,30 +43,21 @@ func (s Scanner) Keywords() []string {
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

urlMatches := urlPat.FindAllStringSubmatch(dataStr, -1)
keyMatches := keyPat.FindAllStringSubmatch(dataStr, -1)

urlMatchesUnique := make(map[string]string)
for _, urlMatch := range urlMatches {
urlMatchesUnique[urlMatch[0]] = urlMatch[1]
for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
urlMatchesUnique[urlMatch[0]] = urlMatch[1] // urlMatch[0] is the full url, urlMatch[1] is the service name
}
keyMatchesUnique := make(map[string]struct{})
for _, keyMatch := range keyMatches {
keyMatchesUnique[keyMatch[1]] = struct{}{}
primaryKeyMatchesUnique := make(map[string]struct{})
for _, keyMatch := range primaryKeyPat.FindAllStringSubmatch(dataStr, -1) {
primaryKeyMatchesUnique[strings.TrimSpace(keyMatch[1])] = struct{}{}
}

for baseUrl, serviceName := range urlMatchesUnique {
for key, _ := range keyMatchesUnique {
resMatch := strings.TrimSpace(key)
url := fmt.Sprintf(
"%s/subscriptions/default/resourceGroups/default/providers/Microsoft.ApiManagement/service/%s/apis?api-version=2024-05-01",
baseUrl, serviceName,
)
for primaryKey := range primaryKeyMatchesUnique {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_AzureDirectManagementKey,
Raw: []byte(baseUrl),
RawV2: []byte(baseUrl + resMatch),
Redacted: baseUrl,
RawV2: []byte(baseUrl + primaryKey),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we start making RawV2 structured in new detectors? #3634 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rgmz I am also not in favor of using RawV2 as structured secret. As @ahrav mentioned that it is tightly coupled with Enterprise and (in future) if we need to restructure it, due to any reason, We wont be able to do it.

My suggestion is to have another Map for this purpose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... it is tightly coupled with Enterprise and (in future) if we need to restructure it, due to any reason, We wont be able to do it.

That is the case regardless. Making it structured at least makes it parseable and human-readable, unlike doing a simple concatenation.

}

if verify {
@@ -73,9 +66,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
client = defaultClient
}

isVerified, verificationErr := s.verifyMatch(ctx, client, url, resMatch)
isVerified, verificationErr := s.verifyMatch(ctx, client, baseUrl, serviceName, primaryKey)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
s1.SetVerificationError(verificationErr, primaryKey)
}

results = append(results, s1)
@@ -88,19 +81,19 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
return results, nil
}

func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
return false, ""
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_AzureDirectManagementKey
}

func (s Scanner) Description() string {
return "The Azure Management API is a RESTful interface for managing Azure resources programmatically through Azure Resource Manager (ARM), supporting automation with tools like Azure CLI and PowerShell. An Azure Management Direct Access API Key enables secure, non-interactive authentication, allowing direct access to manage resources via Azure Active Directory (AAD)."
return "Azure API Management provides a direct management REST API for performing operations on selected entities, such as users, groups, products, and subscriptions."
}

func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, url, key string) (bool, error) {
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, baseUrl, serviceName, key string) (bool, error) {
url := fmt.Sprintf(
"%s/subscriptions/default/resourceGroups/default/providers/Microsoft.ApiManagement/service/%s/apis?api-version=2024-05-01",
baseUrl, serviceName,
)
accessToken, err := generateAccessToken(key)
if err != nil {
return false, err
@@ -129,8 +122,8 @@ func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, url, key

// https://learn.microsoft.com/en-us/rest/api/apimanagement/apimanagementrest/azure-api-management-rest-api-authentication
func generateAccessToken(key string) (string, error) {
expiry := time.Now().UTC().Add(time.Minute).Format(time.RFC3339Nano)
expiry = expiry[:27] + "Z" // 7 decimals precision for miliseconds
expiry := time.Now().UTC().Add(5 * time.Second).Format(RFC3339WithoutMicroseconds) // expires in 5 seconds
expiry = expiry + ".0000000Z" // 7 decimals microsecond's precision is must for access token

// Construct the string-to-sign
stringToSign := fmt.Sprintf("integration\n%s", expiry)
Original file line number Diff line number Diff line change
@@ -25,9 +25,9 @@ func TestAzureDirectManagementAPIKey_FromChunk(t *testing.T) {
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
url := testSecrets.MustGetField("AZUREMANAGEMENTAPI_URL")
secret := testSecrets.MustGetField("AZUREMANAGEMENTAPI_KEY")
inactiveSecret := testSecrets.MustGetField("AZUREMANAGEMENTAPI_KEY_INACTIVE")
url := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_URL")
secret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_KEY")
inactiveSecret := testSecrets.MustGetField("AZUREDIRECTMANAGEMENTAPI_KEY_INACTIVE")

type args struct {
ctx context.Context
@@ -104,8 +104,7 @@ func TestAzureDirectManagementAPIKey_FromChunk(t *testing.T) {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "RawV2", "Raw", "Redacted", "verificationError")

ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "Redacted", "verificationError")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("AzureDirectManagementAPIKey.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}