Files
trufflehog/pkg/detectors/azureappconfigconnectionstring/azureappconfigconnectionstring.go
T
Amaan Ullah 0fa069c12f Enable errcheck and staticcheck for golangci-lint v2 and resolve all issues (#4924)
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues

* skip lint on intentional reference of deprecated DetectorType values
2026-05-15 17:07:14 +05:00

168 lines
5.2 KiB
Go

package azureappconfigconnectionstring
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
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/pb/detector_typepb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = common.SaneHttpClient()
connectionStringPat = regexp.MustCompile(`Endpoint=(https:\/\/[a-zA-Z0-9-]+\.azconfig\.io);Id=([a-zA-Z0-9+\/=]+);Secret=([a-zA-Z0-9+\/=]+)`)
)
// 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{".azconfig.io"}
}
// FromData will find and optionally verify Azure Management API keys 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)
keyMatchesUnique := make(map[string][]string)
for _, keyMatch := range connectionStringPat.FindAllStringSubmatch(dataStr, -1) {
keyMatchesUnique[strings.TrimSpace(keyMatch[0])] = keyMatch // keep all the matched groups for verification
}
for connectionString, connectionInfo := range keyMatchesUnique {
endpoint := connectionInfo[1] // Endpoint
id := connectionInfo[2] // Id
secret := connectionInfo[3] // Secret
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_AzureAppConfigConnectionString,
Raw: []byte(id),
RawV2: []byte(connectionString),
SecretParts: map[string]string{
"endpoint": endpoint,
"id": id,
"secret": secret,
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := s.verifyMatch(ctx, client, endpoint, id, secret)
s1.Verified = isVerified
if verificationErr != nil && !strings.Contains(verificationErr.Error(), "no such host") { // ignore no such host errors
s1.SetVerificationError(verificationErr, connectionString)
}
}
results = append(results, s1)
}
return results, nil
}
func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_AzureAppConfigConnectionString
}
func (s Scanner) Description() string {
return "Azure App Configuration is a managed service that centralizes application settings and feature flags, enabling dynamic updates without redeploying applications. Its connection string, which includes the endpoint URL and an access key, securely connects applications to the configuration store."
}
// generateHMACSignature creates the HMAC-SHA256 signature
func generateHMACSignature(secret, stringToSign string) (string, error) {
decodedSecret, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return "", fmt.Errorf("failed to decode secret: %w", err)
}
h := hmac.New(sha256.New, decodedSecret)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return signature, nil
}
// verifyMatch sends a request to the Azure App Configuration REST API to verify the provided credentials
// https://learn.microsoft.com/en-us/azure/azure-app-configuration/rest-api-authentication-hmac
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, endpoint, id, secret string) (bool, error) {
apiVersion := "1.0"
requestPath := "/kv"
query := fmt.Sprintf("?api-version=%s", apiVersion)
url := fmt.Sprintf("%s%s%s", endpoint, requestPath, query)
// Prepare request
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return false, fmt.Errorf("failed to create request: %w", err)
}
// Set required headers
host := strings.TrimPrefix(strings.TrimPrefix(endpoint, "https://"), "http://")
date := time.Now().UTC().Format(http.TimeFormat)
contentHash := base64.StdEncoding.EncodeToString(sha256.New().Sum(nil)) // SHA256 hash of an empty body
req.Header.Set("Host", host)
req.Header.Set("Date", date)
req.Header.Set("x-ms-content-sha256", contentHash)
// Create the string to sign
stringToSign := fmt.Sprintf("%s\n%s%s\n%s;%s;%s",
http.MethodGet,
requestPath,
query,
date,
host,
contentHash,
)
// Generate the HMAC signature
signature, err := generateHMACSignature(secret, stringToSign)
if err != nil {
return false, fmt.Errorf("failed to generate HMAC signature: %w", err)
}
// Set the Authorization header
authorizationHeader := fmt.Sprintf(
"HMAC-SHA256 Credential=%s&SignedHeaders=date;host;x-ms-content-sha256&Signature=%s",
id,
signature,
)
req.Header.Set("Authorization", authorizationHeader)
// Send the request
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
// Check the response status
switch resp.StatusCode {
case http.StatusOK:
return true, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("got unexpected status code: %d", resp.StatusCode)
}
}