Skip to content

Commit

Permalink
feat(algolia): upgrade detector (#3613)
Browse files Browse the repository at this point in the history
  • Loading branch information
rgmz authored Nov 21, 2024
1 parent 457b129 commit 735bac0
Showing 1 changed file with 85 additions and 73 deletions.
158 changes: 85 additions & 73 deletions pkg/detectors/algoliaadminkey/algoliaadminkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ package algoliaadminkey

import (
"context"
"fmt"
"encoding/json"
"fmt"
regexp "github.com/wasilibs/go-re2"
"io"
"net/http"
"slices"
"strings"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

type Scanner struct{
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}

Expand All @@ -24,8 +26,8 @@ var (
client = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "apiKey"}) + `\b([a-zA-Z0-9]{32})\b`)
idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "appId"}) + `\b([A-Z0-9]{10})\b`)
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"algolia", "docsearch", "apiKey"}) + `\b([a-zA-Z0-9]{32})\b`)
)

// Keywords are used for efficiently pre-filtering chunks.
Expand All @@ -38,105 +40,115 @@ func (s Scanner) Keywords() []string {
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

matches := keyPat.FindAllStringSubmatch(dataStr, -1)
idMatches := idPat.FindAllStringSubmatch(dataStr, -1)

for _, match := range matches {
if len(match) != 2 {
continue
// Deduplicate matches.
idMatches := make(map[string]struct{})
for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) {
id := match[1]
if detectors.StringShannonEntropy(id) > 2 {
idMatches[id] = struct{}{}
}
resMatch := strings.TrimSpace(match[1])
for _, idMatch := range idMatches {
if len(idMatch) != 2 {
continue
}
resIdMatch := strings.TrimSpace(idMatch[1])
}
keyMatches := make(map[string]struct{})
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
key := match[1]
if detectors.StringShannonEntropy(key) > 3 {
keyMatches[key] = struct{}{}
}
}

s1 := detectors.Result{
// Test matches.
for key := range keyMatches {
for id := range idMatches {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_AlgoliaAdminKey,
Raw: []byte(resMatch),
RawV2: []byte(resMatch + resIdMatch),
Raw: []byte(key),
RawV2: []byte(id + ":" + key),
}

if verify {
// Verify if the key is a valid Algolia Admin Key.
isVerified, verificationErr := verifyAlgoliaKey(ctx, resIdMatch, resMatch)

// Verify if the key has sensitive permissions, even if it's not an Admin Key.
if !isVerified {
isVerified, verificationErr = verifyAlgoliaKeyACL(ctx, resIdMatch, resMatch)
}

s1.SetVerificationError(verificationErr, resMatch)
s1.Verified = isVerified
isVerified, extraData, verificationErr := verifyMatch(ctx, id, key)
r.Verified = isVerified
r.ExtraData = extraData
r.SetVerificationError(verificationErr, key)
}

results = append(results, s1)
results = append(results, r)
if r.Verified {
break
}
}
}
return results, nil
}

func verifyAlgoliaKey(ctx context.Context, appId, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+appId+"-dsn.algolia.net/1/keys", nil)
if err != nil {
return false, err
}

req.Header.Add("X-Algolia-Application-Id", appId)
req.Header.Add("X-Algolia-API-Key", apiKey)

res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()

if res.StatusCode == 403 {
return false, nil
} else if res.StatusCode < 200 || res.StatusCode > 299 {
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}

return true, nil
// https://www.algolia.com/doc/guides/security/api-keys/#access-control-list-acl
var nonSensitivePermissions = map[string]struct{}{
"listIndexes": {},
"search": {},
"settings": {},
}

func verifyAlgoliaKeyACL(ctx context.Context, appId, apiKey string) (bool, error) {
func verifyMatch(ctx context.Context, appId, apiKey string) (bool, map[string]string, error) {
// https://www.algolia.com/doc/rest-api/search/#section/Base-URLs
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://"+appId+".algolia.net/1/keys/"+apiKey, nil)
if err != nil {
return false, err
return false, nil, err
}

req.Header.Add("X-Algolia-Application-Id", appId)
req.Header.Add("X-Algolia-API-Key", apiKey)
req.Header.Set("X-Algolia-Application-Id", appId)
req.Header.Set("X-Algolia-API-Key", apiKey)

res, err := client.Do(req)
if err != nil {
return false, err
}
defer res.Body.Close()

if res.StatusCode == 403 {
return false, nil
} else if res.StatusCode < 200 || res.StatusCode > 299 {
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}

var jsonResponse struct {
ACL []string `json:"acl"`
return false, nil, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()

switch res.StatusCode {
case http.StatusOK:
var keyRes keyResponse
if err := json.NewDecoder(res.Body).Decode(&keyRes); err != nil {
return false, nil, err
}

if err := json.NewDecoder(res.Body).Decode(&jsonResponse); err != nil {
return false, err
}
// Check if the key has sensitive permissions, even if it's not an Admin Key.
hasSensitivePerms := false
for _, acl := range keyRes.ACL {
if _, ok := nonSensitivePermissions[acl]; !ok {
hasSensitivePerms = true
break
}
}
if !hasSensitivePerms {
return false, nil, nil
}

for _, acl := range jsonResponse.ACL {
if acl != "search" && acl != "listIndexes" && acl != "settings" {
return true, nil // Other permissions are sensitive.
slices.Sort(keyRes.ACL)
extraData := map[string]string{
"acl": strings.Join(keyRes.ACL, ","),
}
if keyRes.Description != "" && keyRes.Description != "<redacted>" {
extraData["description"] = keyRes.Description
}
return true, extraData, nil
case http.StatusUnauthorized:
return false, nil, nil
case http.StatusForbidden:
// Key is valid but lacks permissions.
return true, nil, nil
default:
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}
}

return false, nil
// https://www.algolia.com/doc/rest-api/search/#tag/Api-Keys/operation/getApiKey
type keyResponse struct {
ACL []string `json:"acl"`
Description string `json:"description"`
}

func (s Scanner) Type() detectorspb.DetectorType {
Expand Down

0 comments on commit 735bac0

Please sign in to comment.