@@ -2,18 +2,20 @@ package algoliaadminkey
2
2
3
3
import (
4
4
"context"
5
- "fmt"
6
5
"encoding/json"
6
+ "fmt"
7
7
regexp "github.com/wasilibs/go-re2"
8
+ "io"
8
9
"net/http"
10
+ "slices"
9
11
"strings"
10
12
11
13
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12
14
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
13
15
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
14
16
)
15
17
16
- type Scanner struct {
18
+ type Scanner struct {
17
19
detectors.DefaultMultiPartCredentialProvider
18
20
}
19
21
24
26
client = common .SaneHttpClient ()
25
27
26
28
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
27
- keyPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"algolia" , "docsearch" , "apiKey" }) + `\b([a-zA-Z0-9]{32})\b` )
28
29
idPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"algolia" , "docsearch" , "appId" }) + `\b([A-Z0-9]{10})\b` )
30
+ keyPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"algolia" , "docsearch" , "apiKey" }) + `\b([a-zA-Z0-9]{32})\b` )
29
31
)
30
32
31
33
// Keywords are used for efficiently pre-filtering chunks.
@@ -38,105 +40,115 @@ func (s Scanner) Keywords() []string {
38
40
func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
39
41
dataStr := string (data )
40
42
41
- matches := keyPat . FindAllStringSubmatch ( dataStr , - 1 )
42
- idMatches := idPat . FindAllStringSubmatch ( dataStr , - 1 )
43
-
44
- for _ , match := range matches {
45
- if len ( match ) != 2 {
46
- continue
43
+ // Deduplicate matches.
44
+ idMatches := make ( map [ string ] struct {} )
45
+ for _ , match := range idPat . FindAllStringSubmatch ( dataStr , - 1 ) {
46
+ id := match [ 1 ]
47
+ if detectors . StringShannonEntropy ( id ) > 2 {
48
+ idMatches [ id ] = struct {}{}
47
49
}
48
- resMatch := strings .TrimSpace (match [1 ])
49
- for _ , idMatch := range idMatches {
50
- if len (idMatch ) != 2 {
51
- continue
52
- }
53
- resIdMatch := strings .TrimSpace (idMatch [1 ])
50
+ }
51
+ keyMatches := make (map [string ]struct {})
52
+ for _ , match := range keyPat .FindAllStringSubmatch (dataStr , - 1 ) {
53
+ key := match [1 ]
54
+ if detectors .StringShannonEntropy (key ) > 3 {
55
+ keyMatches [key ] = struct {}{}
56
+ }
57
+ }
54
58
55
- s1 := detectors.Result {
59
+ // Test matches.
60
+ for key := range keyMatches {
61
+ for id := range idMatches {
62
+ r := detectors.Result {
56
63
DetectorType : detectorspb .DetectorType_AlgoliaAdminKey ,
57
- Raw : []byte (resMatch ),
58
- RawV2 : []byte (resMatch + resIdMatch ),
64
+ Raw : []byte (key ),
65
+ RawV2 : []byte (id + ":" + key ),
59
66
}
60
67
61
68
if verify {
62
69
// Verify if the key is a valid Algolia Admin Key.
63
- isVerified , verificationErr := verifyAlgoliaKey (ctx , resIdMatch , resMatch )
64
-
65
- // Verify if the key has sensitive permissions, even if it's not an Admin Key.
66
- if ! isVerified {
67
- isVerified , verificationErr = verifyAlgoliaKeyACL (ctx , resIdMatch , resMatch )
68
- }
69
-
70
- s1 .SetVerificationError (verificationErr , resMatch )
71
- s1 .Verified = isVerified
70
+ isVerified , extraData , verificationErr := verifyMatch (ctx , id , key )
71
+ r .Verified = isVerified
72
+ r .ExtraData = extraData
73
+ r .SetVerificationError (verificationErr , key )
72
74
}
73
75
74
- results = append (results , s1 )
76
+ results = append (results , r )
77
+ if r .Verified {
78
+ break
79
+ }
75
80
}
76
81
}
77
82
return results , nil
78
83
}
79
84
80
- func verifyAlgoliaKey (ctx context.Context , appId , apiKey string ) (bool , error ) {
81
- req , err := http .NewRequestWithContext (ctx , http .MethodGet , "https://" + appId + "-dsn.algolia.net/1/keys" , nil )
82
- if err != nil {
83
- return false , err
84
- }
85
-
86
- req .Header .Add ("X-Algolia-Application-Id" , appId )
87
- req .Header .Add ("X-Algolia-API-Key" , apiKey )
88
-
89
- res , err := client .Do (req )
90
- if err != nil {
91
- return false , err
92
- }
93
- defer res .Body .Close ()
94
-
95
- if res .StatusCode == 403 {
96
- return false , nil
97
- } else if res .StatusCode < 200 || res .StatusCode > 299 {
98
- return false , fmt .Errorf ("unexpected HTTP response status %d" , res .StatusCode )
99
- }
100
-
101
- return true , nil
85
+ // https://www.algolia.com/doc/guides/security/api-keys/#access-control-list-acl
86
+ var nonSensitivePermissions = map [string ]struct {}{
87
+ "listIndexes" : {},
88
+ "search" : {},
89
+ "settings" : {},
102
90
}
103
91
104
- func verifyAlgoliaKeyACL (ctx context.Context , appId , apiKey string ) (bool , error ) {
92
+ func verifyMatch (ctx context.Context , appId , apiKey string ) (bool , map [string ]string , error ) {
93
+ // https://www.algolia.com/doc/rest-api/search/#section/Base-URLs
105
94
req , err := http .NewRequestWithContext (ctx , http .MethodGet , "https://" + appId + ".algolia.net/1/keys/" + apiKey , nil )
106
95
if err != nil {
107
- return false , err
96
+ return false , nil , err
108
97
}
109
98
110
- req .Header .Add ("X-Algolia-Application-Id" , appId )
111
- req .Header .Add ("X-Algolia-API-Key" , apiKey )
99
+ req .Header .Set ("X-Algolia-Application-Id" , appId )
100
+ req .Header .Set ("X-Algolia-API-Key" , apiKey )
112
101
113
102
res , err := client .Do (req )
114
103
if err != nil {
115
- return false , err
116
- }
117
- defer res .Body .Close ()
118
-
119
- if res .StatusCode == 403 {
120
- return false , nil
121
- } else if res .StatusCode < 200 || res .StatusCode > 299 {
122
- return false , fmt .Errorf ("unexpected HTTP response status %d" , res .StatusCode )
123
- }
124
-
125
- var jsonResponse struct {
126
- ACL []string `json:"acl"`
104
+ return false , nil , err
127
105
}
106
+ defer func () {
107
+ _ , _ = io .Copy (io .Discard , res .Body )
108
+ _ = res .Body .Close ()
109
+ }()
110
+
111
+ switch res .StatusCode {
112
+ case http .StatusOK :
113
+ var keyRes keyResponse
114
+ if err := json .NewDecoder (res .Body ).Decode (& keyRes ); err != nil {
115
+ return false , nil , err
116
+ }
128
117
129
- if err := json .NewDecoder (res .Body ).Decode (& jsonResponse ); err != nil {
130
- return false , err
131
- }
118
+ // Check if the key has sensitive permissions, even if it's not an Admin Key.
119
+ hasSensitivePerms := false
120
+ for _ , acl := range keyRes .ACL {
121
+ if _ , ok := nonSensitivePermissions [acl ]; ! ok {
122
+ hasSensitivePerms = true
123
+ break
124
+ }
125
+ }
126
+ if ! hasSensitivePerms {
127
+ return false , nil , nil
128
+ }
132
129
133
- for _ , acl := range jsonResponse . ACL {
134
- if acl != "search" && acl != "listIndexes" && acl != "settings" {
135
- return true , nil // Other permissions are sensitive.
130
+ slices . Sort ( keyRes . ACL )
131
+ extraData := map [ string ] string {
132
+ "acl" : strings . Join ( keyRes . ACL , "," ),
136
133
}
134
+ if keyRes .Description != "" && keyRes .Description != "<redacted>" {
135
+ extraData ["description" ] = keyRes .Description
136
+ }
137
+ return true , extraData , nil
138
+ case http .StatusUnauthorized :
139
+ return false , nil , nil
140
+ case http .StatusForbidden :
141
+ // Key is valid but lacks permissions.
142
+ return true , nil , nil
143
+ default :
144
+ return false , nil , fmt .Errorf ("unexpected HTTP response status %d" , res .StatusCode )
137
145
}
146
+ }
138
147
139
- return false , nil
148
+ // https://www.algolia.com/doc/rest-api/search/#tag/Api-Keys/operation/getApiKey
149
+ type keyResponse struct {
150
+ ACL []string `json:"acl"`
151
+ Description string `json:"description"`
140
152
}
141
153
142
154
func (s Scanner ) Type () detectorspb.DetectorType {
0 commit comments