feat(azurecr): update detector (#3632)

This commit is contained in:
Richard Gomez
2024-11-20 13:24:55 -08:00
committed by GitHub
parent f19e2c14cd
commit 3dd52edaec
2 changed files with 145 additions and 51 deletions
@@ -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,