Skip to content

Commit 2b3eff9

Browse files
committed
feat(azure): improve connstring matching
1 parent e6d786a commit 2b3eff9

File tree

5 files changed

+523
-150
lines changed

5 files changed

+523
-150
lines changed
+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package azure_storage
2+
3+
import (
4+
"context"
5+
"crypto/hmac"
6+
"crypto/sha256"
7+
"encoding/base64"
8+
"encoding/xml"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
"strings"
13+
"time"
14+
15+
regexp "github.com/wasilibs/go-re2"
16+
17+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
18+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
19+
)
20+
21+
type Scanner struct {
22+
client *http.Client
23+
}
24+
25+
var _ detectors.Detector = (*Scanner)(nil)
26+
27+
var (
28+
defaultClient = detectors.DetectorHttpClientWithNoLocalAddresses
29+
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.
30+
keyPat = regexp.MustCompile(`(?i:(?:Access|Account|Storage)[_.-]?Key)(?:.|\s){0,25}?([a-zA-Z0-9+\/-]{86,88}={0,2})`)
31+
32+
// https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator
33+
testNames = map[string]struct{}{
34+
"devstoreaccount1": {},
35+
"storagesample": {},
36+
}
37+
testKeys = map[string]struct{}{
38+
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==": {},
39+
}
40+
)
41+
42+
type storageResponse struct {
43+
Containers struct {
44+
Container []container `xml:"Container"`
45+
} `xml:"Containers"`
46+
}
47+
48+
type container struct {
49+
Name string `xml:"Name"`
50+
}
51+
52+
func (s Scanner) Keywords() []string {
53+
return []string{
54+
"DefaultEndpointsProtocol=http", "EndpointSuffix", "core.windows.net",
55+
"AccountName", "Account_Name", "Account.Name", "Account-Name",
56+
"StorageAccount", "Storage_Account", "Storage.Account", "Storage-Account",
57+
"AccountKey", "Account_Key", "Account.Key", "Account-Key",
58+
}
59+
}
60+
61+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
62+
dataStr := string(data)
63+
64+
// Deduplicate results.
65+
names := make(map[string]struct{})
66+
for _, matches := range namePat.FindAllStringSubmatch(dataStr, -1) {
67+
var name string
68+
if matches[1] != "" {
69+
name = matches[1]
70+
} else {
71+
name = matches[2]
72+
}
73+
if _, ok := testNames[name]; ok {
74+
continue
75+
}
76+
names[name] = struct{}{}
77+
}
78+
if len(names) == 0 {
79+
return results, nil
80+
}
81+
82+
keys := make(map[string]struct{})
83+
for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) {
84+
key := matches[1]
85+
if _, ok := testKeys[key]; ok {
86+
continue
87+
}
88+
keys[key] = struct{}{}
89+
}
90+
if len(keys) == 0 {
91+
return results, nil
92+
}
93+
94+
// Check results.
95+
for name := range names {
96+
var s1 detectors.Result
97+
for key := range keys {
98+
s1 = detectors.Result{
99+
DetectorType: s.Type(),
100+
Raw: []byte(key),
101+
RawV2: []byte(fmt.Sprintf(`{"accountName":"%s","accountKey":"%s"}`, name, key)),
102+
ExtraData: map[string]string{
103+
"Account_name": name,
104+
},
105+
}
106+
107+
if verify {
108+
client := s.client
109+
if client == nil {
110+
client = defaultClient
111+
}
112+
113+
isVerified, verificationErr := s.verifyMatch(ctx, client, name, key, s1.ExtraData)
114+
s1.Verified = isVerified
115+
s1.SetVerificationError(verificationErr, key)
116+
117+
if s1.Verified {
118+
break
119+
}
120+
}
121+
results = append(results, s1)
122+
}
123+
}
124+
125+
return results, nil
126+
}
127+
128+
func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, name string, key string, extraData map[string]string) (bool, error) {
129+
// https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
130+
now := time.Now().UTC().Format(http.TimeFormat)
131+
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"
132+
accountKeyBytes, _ := base64.StdEncoding.DecodeString(key)
133+
h := hmac.New(sha256.New, accountKeyBytes)
134+
h.Write([]byte(stringToSign))
135+
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
136+
137+
url := "https://" + name + ".blob.core.windows.net/?comp=list"
138+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
139+
if err != nil {
140+
return false, err
141+
}
142+
143+
req.Header.Set("x-ms-date", now)
144+
req.Header.Set("x-ms-version", "2019-12-12")
145+
req.Header.Set("Authorization", "SharedKey "+name+":"+signature)
146+
147+
res, err := client.Do(req)
148+
if err != nil {
149+
// If the host is not found, we can assume that the accountName is not valid
150+
if strings.Contains(err.Error(), "no such host") {
151+
return false, nil
152+
}
153+
return false, err
154+
}
155+
defer func() {
156+
_, _ = io.Copy(io.Discard, res.Body)
157+
_ = res.Body.Close()
158+
}()
159+
160+
switch res.StatusCode {
161+
case http.StatusOK:
162+
// parse response
163+
response := storageResponse{}
164+
if err := xml.NewDecoder(res.Body).Decode(&response); err != nil {
165+
return false, err
166+
}
167+
168+
// update the extra data with container names only
169+
var containerNames []string
170+
for _, c := range response.Containers.Container {
171+
containerNames = append(containerNames, c.Name)
172+
}
173+
extraData["container_names"] = strings.Join(containerNames, ", ")
174+
175+
return true, nil
176+
case http.StatusForbidden:
177+
// 403 if account id or key is invalid, or if the account is disabled
178+
return false, nil
179+
default:
180+
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
181+
}
182+
}
183+
184+
func (s Scanner) Type() detectorspb.DetectorType {
185+
return detectorspb.DetectorType_AzureStorage
186+
}
187+
188+
func (s Scanner) Description() string {
189+
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."
190+
}

pkg/detectors/azurestorage/azurestorage_test.go renamed to pkg/detectors/azure_storage/storage_integration_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
//go:build detectors
22
// +build detectors
33

4-
package azurestorage
4+
package azure_storage
55

66
import (
77
"context"
88
"fmt"
9-
regexp "github.com/wasilibs/go-re2"
109
"strings"
1110
"testing"
1211
"time"
1312

13+
regexp "github.com/wasilibs/go-re2"
14+
1415
"github.com/google/go-cmp/cmp"
1516
"github.com/google/go-cmp/cmp/cmpopts"
1617

0 commit comments

Comments
 (0)