mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2026-05-16 13:20:35 +00:00
0fa069c12f
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues * skip lint on intentional reference of deprecated DetectorType values
168 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|