Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 39 additions & 40 deletions pkg/detectors/datadogtoken/datadogtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package datadogtoken
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"

Expand All @@ -29,8 +30,9 @@ var (
client = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
appPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z-0-9]{40})\b`)
apiPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z-0-9]{32})\b`)
appPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z-0-9]{40})\b`)
apiPat = regexp.MustCompile(detectors.PrefixRegex([]string{"datadog", "dd"}) + `\b([a-zA-Z-0-9]{32})\b`)
datadogURLPat = regexp.MustCompile(`\b(api(?:\.[a-z0-9-]+)?\.(?:datadoghq|ddog-gov)\.[a-z]{2,3}/api)\b`)
)

type userServiceResponse struct {
Expand Down Expand Up @@ -95,7 +97,7 @@ func setOrganizationInfo(opt []*options, s1 *detectors.Result) {
// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string {
return []string{"datadog"}
return []string{"datadog", "ddog-gov"}
}

// FromData will find and optionally verify DatadogToken secrets in a given set of bytes.
Expand All @@ -105,12 +107,26 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
appMatches := appPat.FindAllStringSubmatch(dataStr, -1)
apiMatches := apiPat.FindAllStringSubmatch(dataStr, -1)

var uniqueFoundUrls = make(map[string]struct{})
for _, matches := range datadogURLPat.FindAllStringSubmatch(dataStr, -1) {
uniqueFoundUrls[matches[1]] = struct{}{}
}
var endpoints []string
if len(uniqueFoundUrls) == 0 {
// In case no endpoints were found in data, use the default cloud endpoint
s.UseCloudEndpoint(true)
endpoints = s.Endpoints()
} else {
// In case endpoints were found in data, use them
// also add cloud endpoint at the end so if not validated against found endpoints, try cloud endpoint as well
endpoints = s.configureEndpoints(uniqueFoundUrls)
endpoints = append(endpoints, s.CloudEndpoint())
}

for _, apiMatch := range apiMatches {
resApiMatch := strings.TrimSpace(apiMatch[1])
appIncluded := false
for _, appMatch := range appMatches {
resAppMatch := strings.TrimSpace(appMatch[1])

s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DatadogToken,
Raw: []byte(resAppMatch),
Expand All @@ -121,8 +137,8 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
}

if verify {
for _, baseURL := range s.Endpoints() {
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/v2/users", nil)
for _, baseURL := range endpoints {
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/v2/users", nil)
if err != nil {
continue
}
Expand All @@ -134,7 +150,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
s1.AnalysisInfo = map[string]string{"apiKey": resApiMatch, "appKey": resAppMatch}
s1.AnalysisInfo = map[string]string{"apiKey": resApiMatch, "appKey": resAppMatch, "endpoint": baseURL}
var serviceResponse userServiceResponse
if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err == nil {
// setup emails
Expand All @@ -146,38 +162,8 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
setOrganizationInfo(serviceResponse.Included, &s1)
}
}
}
}
}
}
appIncluded = true
results = append(results, s1)
}

if !appIncluded {
s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_DatadogToken,
Raw: []byte(resApiMatch),
RawV2: []byte(resApiMatch),
ExtraData: map[string]string{
"Type": "APIKeyOnly",
},
}

if verify {
for _, baseURL := range s.Endpoints() {
req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/v1/validate", nil)
if err != nil {
continue
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("DD-API-KEY", resApiMatch)
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{"apiKey": resApiMatch}
// break the loop once we've successfully validated the token against a baseURL
break
}
}
}
Expand All @@ -196,3 +182,16 @@ func (s Scanner) Type() detectorspb.DetectorType {
func (s Scanner) Description() string {
return "Datadog is a monitoring and security platform for cloud applications. Datadog API and Application keys can be used to access and manage data and configurations within Datadog."
}

func (s Scanner) configureEndpoints(uniqueFoundUrls map[string]struct{}) []string {
formattedEndpoints := make([]string, 0, len(uniqueFoundUrls))
for url := range uniqueFoundUrls {
if url == "" {
continue
}
url = strings.TrimSuffix(url, "/api")
formattedEndpoints = append(formattedEndpoints, fmt.Sprintf("https://%s", url))
}
return s.Endpoints(formattedEndpoints...)

}
25 changes: 6 additions & 19 deletions pkg/detectors/datadogtoken/datadogtoken_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func TestDatadogToken_FromChunk(t *testing.T) {
ExtraData: map[string]string{
"Type": "Application+APIKey",
},
AnalysisInfo: map[string]string{
"apiKey": apiKey,
"appKey": appKey,
"endpoint": "https://api.datadoghq.com",
},
},
},
wantErr: false,
Expand All @@ -77,25 +82,6 @@ func TestDatadogToken_FromChunk(t *testing.T) {
},
wantErr: false,
},
{
name: "api key found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a datadogtoken secret %s", apiKey)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_DatadogToken,
Verified: true,
ExtraData: map[string]string{
"Type": "APIKeyOnly",
},
},
},
wantErr: false,
},
{
name: "not found",
s: Scanner{},
Expand All @@ -115,6 +101,7 @@ func TestDatadogToken_FromChunk(t *testing.T) {
// use default cloud endpoint
s.UseCloudEndpoint(true)
s.SetCloudEndpoint(s.CloudEndpoint())
s.UseFoundEndpoints(true)

got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
Expand Down
Loading
Loading