Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bab758d
[INS-241] New detector (datadogapikey) for datadog apikeys
MuneebUllahKhan222 Dec 29, 2025
a942fe2
Analyzer updated to cater endpoint
MuneebUllahKhan222 Dec 30, 2025
8d6a588
Added new tests for anlyzers
MuneebUllahKhan222 Dec 30, 2025
7951443
Removed print statement
MuneebUllahKhan222 Jan 2, 2026
c6a2b6f
resolved comments and fixed integration tests.
MuneebUllahKhan222 Jan 5, 2026
3152a40
resolved comments
MuneebUllahKhan222 Jan 5, 2026
3e3a7de
changed cli prompt
MuneebUllahKhan222 Jan 5, 2026
ffb8d69
Merge branch 'main' into datadogapikey-detector
MuneebUllahKhan222 Jan 7, 2026
5fa6481
Merge branch 'main' into datadogapikey-detector
MuneebUllahKhan222 Jan 8, 2026
30bc23c
Fixed the comments and added app key validation in analyzer
MuneebUllahKhan222 Jan 9, 2026
e2bc5d8
Merge branch 'datadogapikey-detector' of https://github.com/MuneebUll…
MuneebUllahKhan222 Jan 9, 2026
829c986
Merge branch 'main' into datadogapikey-detector
MuneebUllahKhan222 Jan 9, 2026
20f4f19
renamed regex variable
MuneebUllahKhan222 Jan 12, 2026
491341c
Added found verified endpoint to ExtraData
MuneebUllahKhan222 Jan 12, 2026
8dd5d50
Merge branch 'main' into datadogapikey-detector
MuneebUllahKhan222 Jan 19, 2026
a82c86d
Clean up Analyze function by removing comments
MuneebUllahKhan222 Jan 23, 2026
b41f6a3
[INS-286] Added support to analyze just the apikey in datadog's analyzer
MuneebUllahKhan222 Jan 26, 2026
52b1a19
Merge branch 'main' into datadogapikey-detector
MuneebUllahKhan222 Jan 26, 2026
a475fbf
fixed linter issue
MuneebUllahKhan222 Jan 26, 2026
19fea8a
Merge branch 'main' into datadogapikey-detector
MuneebUllahKhan222 Jan 27, 2026
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
73 changes: 56 additions & 17 deletions pkg/analyzer/analyzers/datadog/datadog.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,72 @@ func (a Analyzer) Type() analyzers.AnalyzerType {

// Analyze performs the analysis of the Datadog API key and returns the analyzer result.
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
apiKey, exist := credInfo["apiKey"]
if !exist {
return nil, errors.New("API key not found in credentials info")
}

// Get appKey if provided
apiKey := credInfo["apiKey"]
appKey := credInfo["appKey"]
endpoint := credInfo["endpoint"]

info, err := AnalyzePermissions(a.Cfg, apiKey, appKey)
info, err := AnalyzePermissions(a.Cfg, apiKey, appKey, endpoint)
if err != nil {
return nil, err
}

return secretInfoToAnalyzerResult(info), nil
}

func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey string, appKey string) {
info, err := AnalyzePermissions(cfg, apiKey, appKey)
func AnalyzeAndPrintPermissions(cfg *config.Config, apiKey, appKey, endpoint string) {
info, err := AnalyzePermissions(cfg, apiKey, appKey, endpoint)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}

color.Green("[i] Valid Datadog API Key\n")
if info == nil {
color.Red("[x] No information retrieved")
return
}

color.Green("[i] Valid Datadog API Key\n")
printUser(info.User)
printResources(info.Resources)
printPermissions(info.Permissions)
}

// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, apiKey string, appKey string) (*SecretInfo, error) {
func AnalyzePermissions(cfg *config.Config, apiKey, appKey, endpoint string) (*SecretInfo, error) {
if apiKey == "" {
return nil, errors.New("api key not found in credentials info")
}

// create the http client
client := analyzers.NewAnalyzeClient(cfg)

var secretInfo = &SecretInfo{}

// First detect which DataDog domain works with this API key
baseURL, err := DetectDomain(client, apiKey, appKey)
if err != nil {
return nil, fmt.Errorf("[x] %v", err)
var baseURL string
var err error

// If endpoint is provided, use it directly; otherwise detect domain
if endpoint != "" {
baseURL = endpoint + "/api"
} else {
baseURL, err = DetectDomain(client, apiKey, appKey)
if err != nil {
return nil, fmt.Errorf("[x] %v", err)
}
}

if appKey == "" {
// If no application key is provided, we can only validate the API key
isValidApiKey, err := ValidateApiKey(client, baseURL, apiKey)
if err != nil {
return nil, fmt.Errorf("failed to validate api key: %v", err)
}
if !isValidApiKey {
return nil, errors.New("invalid api key provided")
}
if err := CaptureApiKeyPermissions(client, baseURL, apiKey, appKey, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
}
return secretInfo, nil
}

// capture user information in secretInfo
Expand All @@ -85,6 +110,10 @@ func AnalyzePermissions(cfg *config.Config, apiKey string, appKey string) (*Secr
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
}

// Capture API key permissions
if err := CaptureApiKeyPermissions(client, baseURL, apiKey, appKey, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
}
return secretInfo, nil
}

Expand Down Expand Up @@ -114,7 +143,17 @@ func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
}

permissionBindings := secretInfoPermissionsToAnalyzerPermission(info.Permissions)
result.Bindings = analyzers.BindAllPermissions(*userResource, *permissionBindings...)
if userResource != nil && len(*permissionBindings) > 0 {
result.Bindings = analyzers.BindAllPermissions(*userResource, *permissionBindings...)
}
if userResource == nil && len(*permissionBindings) > 0 {
result.Bindings = analyzers.BindAllPermissions(analyzers.Resource{
FullyQualifiedName: "Unknown User",
Name: "Unknown User",
Type: "User",
Metadata: map[string]any{},
}, *permissionBindings...)
}

// Extract information from resources to create bindings
for _, resource := range info.Resources {
Expand Down
118 changes: 110 additions & 8 deletions pkg/analyzer/analyzers/datadog/datadog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import (
//go:embed expected_output.json
var expectedOutput []byte

//go:embed expected_output_apikey.json
var expectedOutputAPIKey []byte

func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
defer cancel()
Expand All @@ -38,11 +41,12 @@ func TestAnalyzer_Analyze(t *testing.T) {
}

tests := []struct {
name string
apiKey string
appKey string
want []byte // JSON string
wantErr bool
name string
apiKey string
appKey string
endpoint string
want []byte // JSON string
wantErr bool
}{
{
name: "valid datadog credentials",
Expand All @@ -51,6 +55,27 @@ func TestAnalyzer_Analyze(t *testing.T) {
want: expectedOutput,
wantErr: false,
},
{
name: "valid datadog credentials with endpoint",
apiKey: apiKey,
appKey: appKey,
endpoint: "https://api.us5.datadoghq.com",
want: expectedOutput,
wantErr: false,
},
{
name: "valid datadog credentials with invalid endpoint",
apiKey: apiKey,
appKey: appKey,
endpoint: "https://api.eu.datadoghq.com",
want: []byte(fmt.Sprintf(`{
"AnalyzerType": %s,
"Bindings": [],
"UnboundedResources": null,
"Metadata": {}
}`, analyzers.AnalyzerTypeDatadog)),
wantErr: true,
},
{
name: "invalid credentials",
apiKey: "invalid_api_key",
Expand All @@ -63,7 +88,7 @@ func TestAnalyzer_Analyze(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"apiKey": tt.apiKey, "appKey": tt.appKey})
got, err := a.Analyze(ctx, map[string]string{"apiKey": tt.apiKey, "appKey": tt.appKey, "endpoint": tt.endpoint})

if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
Expand Down Expand Up @@ -96,8 +121,6 @@ func TestAnalyzer_Analyze(t *testing.T) {
t.Fatalf("could not marshal got to JSON: %s", err)
}

fmt.Println(string(gotJSON))

// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(tt.want, &wantObj); err != nil {
Expand Down Expand Up @@ -131,6 +154,85 @@ func TestAnalyzer_Analyze(t *testing.T) {
}
}

func TestAnalyzer_Analyze_ApiKeyOnly(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
defer cancel()

// Get API keys from GCP
var apiKey string
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
if err != nil {
t.Fatalf("Could not get test secrets from GCP: %s", err)
}

// Get the required credentials
apiKey = testSecrets.MustGetField("DATADOG_API_KEY")

// Fail if credentials are not available
if apiKey == "" {
t.Fatalf("Datadog credentials are required for this test")
}
want := expectedOutputAPIKey

a := Analyzer{Cfg: &config.Config{}}
got, err := a.Analyze(ctx, map[string]string{"apiKey": apiKey, "endpoint": "https://api.us5.datadoghq.com"})
if err != nil {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, false)
return
}

// For valid cases, verify we got a result
if got == nil {
t.Errorf("Analyzer.Analyze() = nil, want non-nil")
return
}

// Verify type is correct
if got.AnalyzerType != analyzers.AnalyzerTypeDatadog {
t.Errorf("Analyzer.Analyze() returned wrong analyzer type, got %d want %d",
got.AnalyzerType, analyzers.AnalyzerTypeDatadog)
}

// Bindings need to be in the same order to be comparable
sortBindings(got.Bindings)

// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}

// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal(want, &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}

// Bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)

// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}

// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented, wantIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
if err != nil {
t.Fatalf("could not marshal want to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
}
}

// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
Expand Down
Loading
Loading