feat(azurecr): update detector (#3632)
This commit is contained in:
@@ -2,13 +2,17 @@ package azurecontainerregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
)
|
||||
@@ -24,9 +28,11 @@ var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
|
||||
|
||||
var (
|
||||
defaultClient = common.SaneHttpClient()
|
||||
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
|
||||
url = regexp.MustCompile(`([a-zA-Z0-9-]{1,100})\.azurecr\.io`)
|
||||
password = regexp.MustCompile(`\b[A-Za-z0-9+/=]{52}\b`)
|
||||
|
||||
urlPat = regexp.MustCompile(`([a-z0-9][a-z0-9-]{1,100}[a-z0-9])\.azurecr\.io`)
|
||||
passwordPat = regexp.MustCompile(`\b[a-zA-Z0-9+/]{42}\+ACR[a-zA-Z0-9]{6}\b`)
|
||||
|
||||
invalidHosts = simple.NewCache[struct{}]()
|
||||
)
|
||||
|
||||
// Keywords are used for efficiently pre-filtering chunks.
|
||||
@@ -35,59 +41,75 @@ func (s Scanner) Keywords() []string {
|
||||
return []string{".azurecr.io"}
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_AzureContainerRegistry
|
||||
}
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Azure's container registry is used to store docker containers. An API key can be used to override existing containers, read sensitive data, and do other operations inside the container registry."
|
||||
}
|
||||
|
||||
// FromData will find and optionally verify Azurecontainerregistry secrets in a given set of bytes.
|
||||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
|
||||
logger := logContext.AddLogger(ctx).Logger().WithName("azurecr")
|
||||
dataStr := string(data)
|
||||
|
||||
urlMatches := url.FindAllStringSubmatch(dataStr, -1)
|
||||
passwordMatches := password.FindAllStringSubmatch(dataStr, -1)
|
||||
// Deduplicate matches.
|
||||
registryMatches := make(map[string]struct{})
|
||||
for _, matches := range urlPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
u := matches[1]
|
||||
// Ignore https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link
|
||||
if u == "privatelink" || u == "myacr" {
|
||||
continue
|
||||
}
|
||||
registryMatches[u] = struct{}{}
|
||||
}
|
||||
passwordMatches := make(map[string]struct{})
|
||||
for _, matches := range passwordPat.FindAllStringSubmatch(dataStr, -1) {
|
||||
p := matches[0]
|
||||
if detectors.StringShannonEntropy(p) < 4 {
|
||||
continue
|
||||
}
|
||||
passwordMatches[p] = struct{}{}
|
||||
}
|
||||
|
||||
for _, urlMatch := range urlMatches {
|
||||
for _, passwordMatch := range passwordMatches {
|
||||
|
||||
endpoint := urlMatch[0]
|
||||
username := urlMatch[1]
|
||||
password := passwordMatch[0]
|
||||
|
||||
s1 := detectors.Result{
|
||||
EndpointLoop:
|
||||
for username := range registryMatches {
|
||||
for password := range passwordMatches {
|
||||
r := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_AzureContainerRegistry,
|
||||
Raw: []byte(endpoint),
|
||||
RawV2: []byte(endpoint + password),
|
||||
Redacted: endpoint,
|
||||
Raw: []byte(password),
|
||||
RawV2: []byte(`{"username":"` + username + `","password":"` + password + `"}`),
|
||||
Redacted: username,
|
||||
}
|
||||
|
||||
if verify {
|
||||
if invalidHosts.Exists(username) {
|
||||
logger.V(3).Info("Skipping invalid registry", "username", username)
|
||||
continue EndpointLoop
|
||||
}
|
||||
|
||||
client := s.client
|
||||
if client == nil {
|
||||
client = defaultClient
|
||||
}
|
||||
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
|
||||
url := fmt.Sprintf("https://%s/v2/", endpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
isVerified, verificationErr := verifyMatch(ctx, client, username, password)
|
||||
if isVerified {
|
||||
delete(passwordMatches, password)
|
||||
r.Verified = true
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth))
|
||||
res, err := client.Do(req)
|
||||
if err == nil {
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= 200 && res.StatusCode < 300 {
|
||||
s1.Verified = true
|
||||
} else if res.StatusCode == 401 {
|
||||
// The secret is determinately not verified (nothing to do)
|
||||
} else {
|
||||
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
s1.SetVerificationError(err, password)
|
||||
if verificationErr != nil {
|
||||
if errors.Is(verificationErr, noSuchHostErr) {
|
||||
invalidHosts.Set(username, struct{}{})
|
||||
continue EndpointLoop
|
||||
}
|
||||
} else {
|
||||
s1.SetVerificationError(err, username, password)
|
||||
r.SetVerificationError(verificationErr, password)
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, s1)
|
||||
if s1.Verified {
|
||||
results = append(results, r)
|
||||
if r.Verified {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -100,10 +122,35 @@ func (s Scanner) IsFalsePositive(_ detectors.Result) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (s Scanner) Type() detectorspb.DetectorType {
|
||||
return detectorspb.DetectorType_AzureContainerRegistry
|
||||
}
|
||||
var noSuchHostErr = errors.New("no such host")
|
||||
|
||||
func (s Scanner) Description() string {
|
||||
return "Azure's container registry is used to store docker containers. An API key can be used to override existing containers, read sensitive data, and do other operations inside the container registry."
|
||||
func verifyMatch(ctx context.Context, client *http.Client, username string, password string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s.azurecr.io/v2/", username), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(username, password)
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
// lookup foo.azurecr.io: no such host
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
return false, noSuchHostErr
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusUnauthorized:
|
||||
// The secret is determinately not verified.
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
validPattern = `
|
||||
azure:
|
||||
url: 02r0TG8OBkyxWIhSrcCMUQCazXEpVwdrTSVKCstwytZjAa4b2fKJFCy9n.azurecr.io
|
||||
secret: np5kXaH6Qu9zVE5nudGcsm+3/qSOWt/hQ/4bP0kvk6aw98pJEjE3
|
||||
`
|
||||
invalidPattern = `
|
||||
azure:
|
||||
url: http://invalid.azurecr.io.azure.com
|
||||
@@ -33,10 +28,62 @@ func TestAzureContainerRegistry_Pattern(t *testing.T) {
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "valid pattern",
|
||||
input: validPattern,
|
||||
want: []string{"02r0TG8OBkyxWIhSrcCMUQCazXEpVwdrTSVKCstwytZjAa4b2fKJFCy9n.azurecr.ionp5kXaH6Qu9zVE5nudGcsm+3/qSOWt/hQ/4bP0kvk6aw98pJEjE3"},
|
||||
name: "pwd",
|
||||
input: `source storage.env
|
||||
|
||||
ACR=smpldev.azurecr.io
|
||||
ACRUSER=smpldev
|
||||
ACRPWD=Cw8xeDNK6Bub3p61aq5ij/TiVvtBicpTj5rverVezj+ACRBPkEcx
|
||||
CONTAINER=storage-svc:latest`,
|
||||
want: []string{`{"username":"smpldev","password":"Cw8xeDNK6Bub3p61aq5ij/TiVvtBicpTj5rverVezj+ACRBPkEcx"}`},
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
input: ` - name: Deploy to ARC
|
||||
uses: azure/docker-login@v1
|
||||
with:
|
||||
login-server: crmshopacr.azurecr.io
|
||||
username: crmshopacr
|
||||
password: o9uXSjWlUdRwAeGP2xGSfGy+25vetsONo3Mq13fksa+ACRBXyFsY
|
||||
- run: |`,
|
||||
want: []string{`{"username":"crmshopacr","password":"o9uXSjWlUdRwAeGP2xGSfGy+25vetsONo3Mq13fksa+ACRBXyFsY"}`},
|
||||
},
|
||||
{
|
||||
name: "docker cli login",
|
||||
input: `docker login dvacr00.azurecr.io -u dvacr00 -p Ljc+1lq0U0+c3jHlMHxSxAhCipKt6zU43HfMle/Ymj+ACRAKcPHy
|
||||
docker push dvacr00.azurecr.io/foo-alpine:3.18`,
|
||||
want: []string{`{"username":"dvacr00","password":"Ljc+1lq0U0+c3jHlMHxSxAhCipKt6zU43HfMle/Ymj+ACRAKcPHy"}`},
|
||||
},
|
||||
{
|
||||
name: "request body",
|
||||
input: `"registries":[{"identity":"","passwordSecretRef":"registry-password","server":"cr2bxwtqgom2oo.azurecr.io","username":"cr2bxwtqgom2oo"}],"secrets":[{"name":"registry-password","value":"VP2rvkuld42mr3jNjM+rVbvIzVuZxwncKWyVU5UIad+ACRBivL0B"}]}`,
|
||||
want: []string{`{"username":"cr2bxwtqgom2oo","password":"VP2rvkuld42mr3jNjM+rVbvIzVuZxwncKWyVU5UIad+ACRBivL0B"}`},
|
||||
},
|
||||
{
|
||||
name: "README",
|
||||
input: `# AZURE-CICD-Deployment-with-Github-Actions
|
||||
|
||||
## Save pass:
|
||||
|
||||
s3cEZKH3yytiVnJ3h+eI3qhhzf9l1vNwEi1+q+WGdd+ACRCZ7JD6
|
||||
|
||||
|
||||
## Run from terminal:
|
||||
|
||||
docker build -t testapp.azurecr.io/chicken:latest .
|
||||
`,
|
||||
want: []string{`{"username":"testapp","password":"s3cEZKH3yytiVnJ3h+eI3qhhzf9l1vNwEi1+q+WGdd+ACRCZ7JD6"}`},
|
||||
},
|
||||
// TODO:
|
||||
//{
|
||||
// name: "az cli login",
|
||||
// input: `az acr login --name tstcopilotacr --username tstcopilotacr --password 9iZkJiOTKeEsQDfgoobtCYU47EEDs9UvU4L8NErLV+ACRACptmc`,
|
||||
// want: []string{},
|
||||
//},
|
||||
//{
|
||||
// name: "",
|
||||
// input: ``,
|
||||
// want: []string{},
|
||||
{
|
||||
name: "invalid pattern",
|
||||
input: invalidPattern,
|
||||
|
||||
Reference in New Issue
Block a user