From 8708801319700a16c56abb86628803ba766d5563 Mon Sep 17 00:00:00 2001 From: Muneeb Ullah Khan <100969065+MuneebUllahKhan222@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:27:36 +0500 Subject: [PATCH] [INS-255] Updated datadog detector to set verificationError in case of a verification error (#4661) * [INS-233] Added support to verify token agains all datadog domains * Fixed cloud endpoint test * resolved bugbot comments --- pkg/detectors/datadogtoken/datadogtoken.go | 86 +++++++++++++------ .../datadogtoken_integration_test.go | 69 ++++++++++++++- 2 files changed, 125 insertions(+), 30 deletions(-) diff --git a/pkg/detectors/datadogtoken/datadogtoken.go b/pkg/detectors/datadogtoken/datadogtoken.go index c74446612..a3b07d726 100644 --- a/pkg/detectors/datadogtoken/datadogtoken.go +++ b/pkg/detectors/datadogtoken/datadogtoken.go @@ -3,6 +3,8 @@ package datadogtoken import ( "context" "encoding/json" + "fmt" + "io" "net/http" "strings" @@ -14,6 +16,7 @@ import ( ) type Scanner struct { + client *http.Client detectors.EndpointSetter detectors.DefaultMultiPartCredentialProvider } @@ -26,7 +29,7 @@ var _ detectors.CloudProvider = (*Scanner)(nil) func (Scanner) CloudEndpoint() string { return "https://api.datadoghq.com" } var ( - client = common.SaneHttpClient() + defaultClient = 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`) @@ -93,6 +96,13 @@ func setOrganizationInfo(opt []*options, s1 *detectors.Result) { } +func (s Scanner) getClient() *http.Client { + if s.client != nil { + return s.client + } + return defaultClient +} + // Keywords are used for efficiently pre-filtering chunks. // Use identifiers in the secret preferably, or the provider name. func (s Scanner) Keywords() []string { @@ -114,6 +124,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result for endpoint := range uniqueFoundUrls { endpoints = append(endpoints, endpoint) } + client := s.getClient() for _, apiMatch := range apiMatches { resApiMatch := strings.TrimSpace(apiMatch[1]) @@ -131,40 +142,24 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result if verify { for _, baseURL := range s.Endpoints(endpoints...) { - req, err := http.NewRequestWithContext(ctx, "GET", baseURL+"/api/v2/users", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("DD-API-KEY", resApiMatch) - req.Header.Add("DD-APPLICATION-KEY", resAppMatch) - res, err := client.Do(req) - if err == nil { - defer func() { _ = res.Body.Close() }() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - s1.SecretParts["endpoint"] = baseURL - var serviceResponse userServiceResponse - if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err == nil { - // setup emails - if len(serviceResponse.Data) > 0 { - setUserEmails(serviceResponse.Data, &s1) - } - // setup organizations - if len(serviceResponse.Included) > 0 { - setOrganizationInfo(serviceResponse.Included, &s1) - } - } - // break the loop once we've successfully validated the token against a baseURL - break + res, isVerified, verificationErr := verifyMatch(ctx, client, resApiMatch, resAppMatch, baseURL) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resApiMatch, resAppMatch) + s1.SecretParts["endpoint"] = baseURL + if isVerified && res != nil { + if len(res.Data) > 0 { + setUserEmails(res.Data, &s1) } + if len(res.Included) > 0 { + setOrganizationInfo(res.Included, &s1) + } + break } } } results = append(results, s1) } } - return results, nil } @@ -175,3 +170,38 @@ func (s Scanner) Type() detector_typepb.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 verifyMatch(ctx context.Context, client *http.Client, apiKey, appKey, baseUrl string) (*userServiceResponse, bool, error) { + // Reference: https://docs.datadoghq.com/api/latest/users/ + + req, err := http.NewRequestWithContext(ctx, "GET", baseUrl+"/api/v2/users", nil) + if err != nil { + return nil, false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("DD-API-KEY", apiKey) + req.Header.Add("DD-APPLICATION-KEY", appKey) + res, err := client.Do(req) + if err != nil { + return nil, false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + + switch res.StatusCode { + case http.StatusOK: + var serviceResponse userServiceResponse + if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err != nil { + return nil, true, nil + } + return &serviceResponse, true, nil + case http.StatusUnauthorized, http.StatusForbidden: + return nil, false, nil + default: + return nil, false, fmt.Errorf("unexpected status code: %d", res.StatusCode) + } +} diff --git a/pkg/detectors/datadogtoken/datadogtoken_integration_test.go b/pkg/detectors/datadogtoken/datadogtoken_integration_test.go index e33450919..ca829394d 100644 --- a/pkg/detectors/datadogtoken/datadogtoken_integration_test.go +++ b/pkg/detectors/datadogtoken/datadogtoken_integration_test.go @@ -9,7 +9,8 @@ import ( "testing" "time" - "github.com/kylelemons/godebug/pretty" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/common" @@ -79,6 +80,11 @@ func TestDatadogToken_FromChunk(t *testing.T) { ExtraData: map[string]string{ "Type": "Application+APIKey", }, + SecretParts: map[string]string{ + "api_key": apiKey, + "app_key": inactiveAppKey, + "endpoint": "https://api.datadoghq.com", // default endpoint + }, }, }, wantErr: false, @@ -117,13 +123,72 @@ func TestDatadogToken_FromChunk(t *testing.T) { got[i].RawV2 = nil delete(got[i].ExtraData, "user_emails") } - if diff := pretty.Compare(got, tt.want); diff != "" { + ignoreOpts := cmpopts.IgnoreFields( + detectors.Result{}, + "ExtraData", + "verificationError", + "primarySecret", + "chunkOffset", + "chunkOffsetSet", + ) + if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { t.Errorf("DatadogToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff) } }) } } +func TestDatadogToken_FromChunk_Unverified(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") + if err != nil { + t.Fatalf("could not get test secrets from GCP: %s", err) + } + + apiKey := testSecrets.MustGetField("DATADOGTOKEN_TOKEN") + inactiveAppKey := testSecrets.MustGetField("DATADOGTOKEN_INACTIVE") + + data := []byte(fmt.Sprintf( + "You can find a datadogtoken secret %s within but datadog %s not valid", + inactiveAppKey, + apiKey, + )) + + s := Scanner{} + s.UseCloudEndpoint(true) + s.SetCloudEndpoint(s.CloudEndpoint()) + s.UseFoundEndpoints(true) + + results, err := s.FromData(ctx, true, data) + if err != nil { + t.Fatalf("FromData returned error: %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + + r := results[0] + + if r.DetectorType != detector_typepb.DetectorType_DatadogToken { + t.Errorf("unexpected detector type: %v", r.DetectorType) + } + + if r.Verified { + t.Errorf("expected token to be unverified") + } + + if got := r.ExtraData["Type"]; got != "Application+APIKey" { + t.Errorf("unexpected ExtraData Type: %q", got) + } + + if len(r.Raw) == 0 { + t.Errorf("expected raw secret to be present") + } +} + func BenchmarkFromData(benchmark *testing.B) { ctx := context.Background() s := Scanner{}