Skip to content

Commit 7a501f4

Browse files
rgmzRichard Gomez
authored and
Richard Gomez
committed
feat(azure): improve connstring matching
1 parent 9176e25 commit 7a501f4

File tree

5 files changed

+519
-146
lines changed

5 files changed

+519
-146
lines changed
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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 = http.DefaultClient
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+
}

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)