updated twilio detector (#3734)
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run

This commit is contained in:
Kashif Khan
2024-12-05 09:49:39 -06:00
committed by GitHub
parent 4cd055fe3f
commit 710d09ba85
+54 -41
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
regexp "github.com/wasilibs/go-re2" regexp "github.com/wasilibs/go-re2"
@@ -23,7 +24,6 @@ var _ detectors.Detector = (*Scanner)(nil)
var ( var (
defaultClient = common.SaneHttpClient() defaultClient = common.SaneHttpClient()
identifierPat = regexp.MustCompile(`(?i)sid.{0,20}AC[0-9a-f]{32}`) // Should we have this? Seems restrictive.
sidPat = regexp.MustCompile(`\bAC[0-9a-f]{32}\b`) sidPat = regexp.MustCompile(`\bAC[0-9a-f]{32}\b`)
keyPat = regexp.MustCompile(`\b[0-9a-f]{32}\b`) keyPat = regexp.MustCompile(`\b[0-9a-f]{32}\b`)
) )
@@ -38,22 +38,24 @@ type service struct {
AccountSID string `json:"account_sid"` // account sid AccountSID string `json:"account_sid"` // account sid
} }
func (s Scanner) getClient() *http.Client {
if s.client != nil {
return s.client
}
return defaultClient
}
// Keywords are used for efficiently pre-filtering chunks. // Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name. // Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string { func (s Scanner) Keywords() []string {
return []string{"sid"} return []string{"sid", "twilio"}
} }
// FromData will find and optionally verify Twilio secrets in a given set of bytes. // FromData will find and optionally verify Twilio secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data) dataStr := string(data)
identifierMatches := identifierPat.FindAllString(dataStr, -1)
if len(identifierMatches) == 0 {
return
}
keyMatches := keyPat.FindAllString(dataStr, -1) keyMatches := keyPat.FindAllString(dataStr, -1)
sidMatches := sidPat.FindAllString(dataStr, -1) sidMatches := sidPat.FindAllString(dataStr, -1)
@@ -71,48 +73,22 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
} }
if verify { if verify {
client := s.client extraData, isVerified, verificationErr := verifyTwilio(ctx, s.getClient(), key, sid)
if client == nil { s1.Verified = isVerified
client = defaultClient s1.SetVerificationError(verificationErr)
for key, value := range extraData {
s1.ExtraData[key] = value
} }
req, err := http.NewRequestWithContext( if s1.Verified {
ctx, "GET", "https://verify.twilio.com/v2/Services", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "*/*")
req.SetBasicAuth(sid, key)
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
s1.AnalysisInfo = map[string]string{"key": key, "sid": sid} s1.AnalysisInfo = map[string]string{"key": key, "sid": sid}
var serviceResponse serviceResponse
if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err == nil && len(serviceResponse.Services) > 0 { // no error in parsing and have at least one service
service := serviceResponse.Services[0]
s1.ExtraData["friendly_name"] = service.FriendlyName
s1.ExtraData["account_sid"] = service.AccountSID
}
} else if res.StatusCode == 401 || res.StatusCode == 403 {
// The secret is determinately not verified (nothing to do)
} else {
err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
s1.SetVerificationError(err, key)
}
} else {
s1.SetVerificationError(err, key)
} }
} }
if len(keyMatches) > 0 {
results = append(results, s1) results = append(results, s1)
} }
} }
}
return results, nil return results, nil
} }
@@ -124,3 +100,40 @@ func (s Scanner) Type() detectorspb.DetectorType {
func (s Scanner) Description() string { func (s Scanner) Description() string {
return "Twilio is a cloud communications platform that allows software developers to programmatically make and receive phone calls, send and receive text messages, and perform other communication functions using its web service APIs." return "Twilio is a cloud communications platform that allows software developers to programmatically make and receive phone calls, send and receive text messages, and perform other communication functions using its web service APIs."
} }
func verifyTwilio(ctx context.Context, client *http.Client, key, sid string) (map[string]string, bool, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "https://verify.twilio.com/v2/Services", nil)
if err != nil {
return nil, false, nil
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "*/*")
req.SetBasicAuth(sid, key)
resp, err := client.Do(req)
if err != nil {
return nil, false, nil
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
switch resp.StatusCode {
case http.StatusOK:
extraData := make(map[string]string)
var serviceResponse serviceResponse
if err := json.NewDecoder(resp.Body).Decode(&serviceResponse); err == nil && len(serviceResponse.Services) > 0 { // no error in parsing and have at least one service
service := serviceResponse.Services[0]
extraData["friendly_name"] = service.FriendlyName
extraData["account_sid"] = service.AccountSID
}
return extraData, true, nil
case http.StatusUnauthorized, http.StatusForbidden:
return nil, false, nil
default:
return nil, false, fmt.Errorf("unexpected HTTP response status %d", resp.StatusCode)
}
}