Files
Miccah ad2f9a4b98 Populate SecretParts on all detectors (#4919)
* Populate SecretParts on single-part detectors

Adds SecretParts: map[string]string{"key": <secret>} to every detector
package that constructs detectors.Result with a single captured secret
value. This is the single-part half of the SecretParts migration (the
linter's common case, ~695 packages).

* Populate SecretParts on multi-part detectors
2026-04-24 08:32:51 -07:00

202 lines
5.5 KiB
Go

package azure_storage
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/http"
"strings"
"time"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)
type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}
var _ detectors.Detector = (*Scanner)(nil)
var (
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
namePat = regexp.MustCompile(`(?i:Account[_.-]?Name|Storage[_.-]?(?:Account|Name))(?:.|\s){0,20}?\b([a-z0-9]{3,24})\b|([a-z0-9]{3,24})(?i:\.blob\.core\.windows\.net)`) // Names can only be lowercase alphanumeric.
keyPat = regexp.MustCompile(`(?i:(?:Access|Account|Storage)[_.-]?Key)(?:.|\s){0,25}?([a-zA-Z0-9+\/-]{86,88}={0,2})`)
// https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator
testNames = map[string]struct{}{
"devstoreaccount1": {},
"storagesample": {},
}
testKeys = map[string]struct{}{
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==": {},
}
)
func (s Scanner) Keywords() []string {
return []string{
"DefaultEndpointsProtocol=http", "EndpointSuffix", "core.windows.net",
"AccountName", "Account_Name", "Account.Name", "Account-Name",
"StorageAccount", "Storage_Account", "Storage.Account", "Storage-Account",
"AccountKey", "Account_Key", "Account.Key", "Account-Key",
}
}
func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_AzureStorage
}
func (s Scanner) Description() string {
return "Azure Storage is a Microsoft-managed cloud service that provides storage that is highly available, secure, durable, scalable, and redundant. Azure Storage Account keys can be used to access and manage data within storage accounts."
}
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)
// Deduplicate results.
names := make(map[string]struct{})
for _, matches := range namePat.FindAllStringSubmatch(dataStr, -1) {
var name string
if matches[1] != "" {
name = matches[1]
} else {
name = matches[2]
}
if _, ok := testNames[name]; ok {
continue
}
names[name] = struct{}{}
}
if len(names) == 0 {
return results, nil
}
keys := make(map[string]struct{})
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
key := matches[1]
if _, ok := testKeys[key]; ok {
continue
}
keys[key] = struct{}{}
}
if len(keys) == 0 {
return results, nil
}
// Check results.
for name := range names {
var s1 detectors.Result
for key := range keys {
s1 = detectors.Result{
DetectorType: s.Type(),
Raw: []byte(key),
RawV2: []byte(fmt.Sprintf(`{"accountName":"%s","accountKey":"%s"}`, name, key)),
SecretParts: map[string]string{
"account_name": name,
"account_key": key,
},
ExtraData: map[string]string{
"Account_name": name,
},
}
if verify {
client := s.client
if client == nil {
client = defaultClient
}
isVerified, verificationErr := s.verifyMatch(ctx, client, name, key, s1.ExtraData)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, key)
}
results = append(results, s1)
if s1.Verified {
break
}
}
}
return results, nil
}
type storageResponse struct {
Containers struct {
Container []container `xml:"Container"`
} `xml:"Containers"`
}
type container struct {
Name string `xml:"Name"`
}
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, name string, key string, extraData map[string]string) (bool, error) {
// https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
now := time.Now().UTC().Format(http.TimeFormat)
stringToSign := "GET\n\n\n\n\n\n\n\n\n\n\n\nx-ms-date:" + now + "\nx-ms-version:2019-12-12\n/" + name + "/\ncomp:list"
accountKeyBytes, _ := base64.StdEncoding.DecodeString(key)
h := hmac.New(sha256.New, accountKeyBytes)
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
url := "https://" + name + ".blob.core.windows.net/?comp=list"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Set("x-ms-date", now)
req.Header.Set("x-ms-version", "2019-12-12")
req.Header.Set("Authorization", "SharedKey "+name+":"+signature)
res, err := client.Do(req)
if err != nil {
// If the host is not found, we can assume that the accountName is not valid
if strings.Contains(err.Error(), "no such host") {
return false, nil
}
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
// parse response
response := storageResponse{}
if err := xml.NewDecoder(res.Body).Decode(&response); err != nil {
return false, err
}
// update the extra data with container names only
if len(response.Containers.Container) > 0 {
var b strings.Builder
for i, c := range response.Containers.Container {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(c.Name)
}
extraData["container_names"] = b.String()
}
return true, nil
case http.StatusForbidden:
// 403 if account id or key is invalid, or if the account is disabled
return false, nil
default:
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}