Analyzer/datadog (#4132)
* task: Datadog analyzer init commit with limited resources and permissions No cases covered * task: added more permissions * task: added more permissions | refactoring * task: added domain handling * refactor: incorporated PR feedback * task: Analyzer Result implementation for enterprise * task: added tests | Analysis Info configuration changes | permissions.yaml * revert: reverted datadog detector base URL * refactor: streamline Datadog API key analysis and permission handling - Simplified API key retrieval logic by removing redundant checks for "key". - Enhanced permission binding extraction by introducing a new function for permissions. - Cleaned up resource binding logic for better clarity and maintainability. - Removed unnecessary ID fields from scopes.json to reduce clutter.
This commit is contained in:
@@ -97,6 +97,7 @@ const (
|
||||
AnalyzerTypeNetlify
|
||||
AnalyzerTypeFastly
|
||||
AnalyzerTypeMonday
|
||||
AnalyzerTypeDatadog
|
||||
AnalyzerTypeNgrok
|
||||
AnalyzerTypeMux
|
||||
AnalyzerTypePosthog
|
||||
@@ -145,6 +146,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
||||
AnalyzerTypeNetlify: "Netlify",
|
||||
AnalyzerTypeFastly: "Fastly",
|
||||
AnalyzerTypeMonday: "Monday",
|
||||
AnalyzerTypeDatadog: "Datadog",
|
||||
AnalyzerTypeNgrok: "Ngrok",
|
||||
AnalyzerTypeMux: "Mux",
|
||||
AnalyzerTypePosthog: "Posthog",
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go datadog
|
||||
package datadog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
||||
|
||||
type Analyzer struct {
|
||||
Cfg *config.Config
|
||||
}
|
||||
|
||||
func (a Analyzer) Type() analyzers.AnalyzerType {
|
||||
return analyzers.AnalyzerTypeDatadog
|
||||
}
|
||||
|
||||
// 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
|
||||
appKey := credInfo["appKey"]
|
||||
|
||||
info, err := AnalyzePermissions(a.Cfg, apiKey, appKey)
|
||||
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)
|
||||
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")
|
||||
|
||||
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) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// capture user information in secretInfo
|
||||
// If the application key is scoped, user information cannot be retrieved even if all the permissions are granted
|
||||
// This is a non-documented Endpoint and can lead to unexpected behavior in future updates
|
||||
// If user information is not retrieved, we will move ahead with the rest of the analysis and print the error
|
||||
_ = CaptureUserInformation(client, baseURL, apiKey, appKey, secretInfo)
|
||||
|
||||
// capture resources in secretInfo
|
||||
if err := CaptureResources(client, baseURL, apiKey, appKey, secretInfo); err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch resources: %v", err)
|
||||
}
|
||||
|
||||
// capture permissions in secretInfo
|
||||
if err := CapturePermissions(client, baseURL, apiKey, appKey, secretInfo); err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch permissions: %v", err)
|
||||
}
|
||||
|
||||
return secretInfo, nil
|
||||
}
|
||||
|
||||
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
|
||||
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerTypeDatadog,
|
||||
Metadata: map[string]any{},
|
||||
Bindings: make([]analyzers.Binding, 0),
|
||||
}
|
||||
|
||||
// Create user resource to use as parent
|
||||
var userResource *analyzers.Resource
|
||||
if info.User.Id != "" {
|
||||
userResource = &analyzers.Resource{
|
||||
FullyQualifiedName: info.User.Id,
|
||||
Name: info.User.Name,
|
||||
Type: "User",
|
||||
Metadata: map[string]any{
|
||||
"email": info.User.Email,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
permissionBindings := secretInfoPermissionsToAnalyzerPermission(info.Permissions)
|
||||
result.Bindings = analyzers.BindAllPermissions(*userResource, *permissionBindings...)
|
||||
|
||||
// Extract information from resources to create bindings
|
||||
for _, resource := range info.Resources {
|
||||
resource := secretInfoResourceToAnalyzerResource(resource)
|
||||
|
||||
// Set the user resource as parent if available
|
||||
if userResource != nil {
|
||||
resource.Parent = userResource
|
||||
}
|
||||
|
||||
binding := analyzers.Binding{
|
||||
Resource: *resource,
|
||||
}
|
||||
|
||||
result.Bindings = append(result.Bindings, binding)
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// secretInfoPermissionsToAnalyzerPermission translate secret info Permission to analyzer resource for binding
|
||||
func secretInfoPermissionsToAnalyzerPermission(perms []Permission) *[]analyzers.Permission {
|
||||
permissions := make([]analyzers.Permission, 0, len(perms))
|
||||
for _, perm := range perms {
|
||||
permissions = append(permissions, analyzers.Permission{
|
||||
Value: perm.Title,
|
||||
})
|
||||
}
|
||||
return &permissions
|
||||
}
|
||||
|
||||
// secretInfoResourceToAnalyzerResource translate secret info Resource to analyzer resource for binding
|
||||
func secretInfoResourceToAnalyzerResource(resource Resource) *analyzers.Resource {
|
||||
analyzerRes := analyzers.Resource{
|
||||
FullyQualifiedName: resource.ID,
|
||||
Name: resource.Name,
|
||||
Type: resource.Type,
|
||||
Metadata: map[string]any{},
|
||||
}
|
||||
|
||||
for key, value := range resource.MetaData {
|
||||
analyzerRes.Metadata[key] = value
|
||||
}
|
||||
|
||||
return &analyzerRes
|
||||
}
|
||||
|
||||
func printUser(user User) {
|
||||
if user.Id == "" {
|
||||
color.Red("\n[x] User information not available")
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("\n[i] User Information:")
|
||||
userTable := table.NewWriter()
|
||||
userTable.SetOutputMirror(os.Stdout)
|
||||
userTable.AppendHeader(table.Row{"User Id", "Name", "Email"})
|
||||
userTable.AppendRow(table.Row{color.GreenString(user.Id), color.GreenString(user.Name), color.GreenString(user.Email)})
|
||||
userTable.Render()
|
||||
}
|
||||
|
||||
func printResources(resources []Resource) {
|
||||
if len(resources) == 0 {
|
||||
color.Red("[x] No resources found")
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("\n[i] Resources:")
|
||||
resourceTable := table.NewWriter()
|
||||
resourceTable.SetOutputMirror(os.Stdout)
|
||||
resourceTable.AppendHeader(table.Row{"Name", "Type"})
|
||||
for _, resource := range resources {
|
||||
resourceTable.AppendRow(table.Row{
|
||||
color.GreenString(resource.Name),
|
||||
color.GreenString(resource.Type),
|
||||
})
|
||||
}
|
||||
resourceTable.Render()
|
||||
}
|
||||
|
||||
func printPermissions(permissions []Permission) {
|
||||
if len(permissions) == 0 {
|
||||
color.Red("[x] No permissions found")
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("\n[i] Permissions:")
|
||||
permissionTable := table.NewWriter()
|
||||
permissionTable.SetOutputMirror(os.Stdout)
|
||||
permissionTable.AppendHeader(table.Row{"Title", "Name", "Description"})
|
||||
|
||||
// Set wrapping for long descriptions
|
||||
permissionTable.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 3, WidthMax: 50},
|
||||
})
|
||||
|
||||
for _, permission := range permissions {
|
||||
permissionTable.AppendRow(table.Row{
|
||||
color.GreenString(permission.Title),
|
||||
color.GreenString(permission.Name),
|
||||
color.GreenString(permission.Description),
|
||||
})
|
||||
}
|
||||
permissionTable.Render()
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package datadog
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
//go:embed expected_output.json
|
||||
var expectedOutput []byte
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2)
|
||||
defer cancel()
|
||||
|
||||
// Get API keys from GCP
|
||||
var apiKey, appKey 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")
|
||||
appKey = testSecrets.MustGetField("DATADOG_APP_KEY")
|
||||
|
||||
// Fail if credentials are not available
|
||||
if apiKey == "" || appKey == "" {
|
||||
t.Fatalf("Datadog credentials are required for this test")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
apiKey string
|
||||
appKey string
|
||||
want []byte // JSON string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid datadog credentials",
|
||||
apiKey: apiKey,
|
||||
appKey: appKey,
|
||||
want: expectedOutput,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid credentials",
|
||||
apiKey: "invalid_api_key",
|
||||
appKey: "invalid_app_key",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
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})
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip verification for error cases
|
||||
if tt.wantErr {
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Println(string(gotJSON))
|
||||
|
||||
// Parse the expected JSON string
|
||||
var wantObj analyzers.AnalyzerResult
|
||||
if err := json.Unmarshal(tt.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 {
|
||||
if bindings[i].Resource.Name == bindings[j].Resource.Name {
|
||||
return bindings[i].Permission.Value < bindings[j].Permission.Value
|
||||
}
|
||||
return bindings[i].Resource.Name < bindings[j].Resource.Name
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
package datadog
|
||||
|
||||
import "sync"
|
||||
|
||||
// Resource type constants for consistent usage
|
||||
const (
|
||||
ResourceTypeValidate = "Validate"
|
||||
ResourceTypeCurrentUser = "Current User"
|
||||
ResourceTypeDashboard = "Dashboard"
|
||||
ResourceTypeMonitor = "Monitor"
|
||||
)
|
||||
|
||||
// Permission represents a permission granted to an API key
|
||||
type Permission struct {
|
||||
Name string
|
||||
Title string
|
||||
Description string
|
||||
MetaData map[string]string
|
||||
}
|
||||
|
||||
// SecretInfo holds all information gathered about a Datadog API key
|
||||
type SecretInfo struct {
|
||||
User User
|
||||
Permissions []Permission
|
||||
|
||||
mu sync.RWMutex
|
||||
Resources []Resource
|
||||
}
|
||||
|
||||
// User is the information about the user to whom the token belongs
|
||||
type User struct {
|
||||
Id string
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
// Resource represents a Datadog resource
|
||||
type Resource struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
MetaData map[string]string
|
||||
}
|
||||
|
||||
// API response structures
|
||||
type currentUserResponse struct {
|
||||
Data struct {
|
||||
Id string `json:"id"`
|
||||
Attributes struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type dashboardResponse struct {
|
||||
Dashboards []DashboardItem `json:"dashboards"`
|
||||
}
|
||||
|
||||
type DashboardItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
IsReadOnly bool `json:"is_read_only"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ModifiedAt string `json:"modified_at"`
|
||||
AuthorHandle string `json:"author_handle"`
|
||||
Description *string `json:"description"`
|
||||
LayoutType string `json:"layout_type"`
|
||||
DeletedAt *string `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type monitorResponse []struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// appendResource adds a resource to secret info resources list
|
||||
func (s *SecretInfo) appendResource(resource Resource) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.Resources = append(s.Resources, resource)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
permissions:
|
||||
- dashboards_read
|
||||
- dashboards_write
|
||||
- dashboards_public_share
|
||||
- monitors_read
|
||||
- monitors_write
|
||||
- logs_modify_indexes
|
||||
- logs_write_pipelines
|
||||
- logs_write_archives
|
||||
- logs_generate_metrics
|
||||
- monitors_downtime
|
||||
- logs_read_data
|
||||
- logs_read_archives
|
||||
- security_monitoring_rules_read
|
||||
- security_monitoring_rules_write
|
||||
- security_monitoring_signals_read
|
||||
- security_monitoring_signals_write
|
||||
- user_access_invite
|
||||
- user_app_keys
|
||||
- org_app_keys_read
|
||||
- org_app_keys_write
|
||||
- user_access_manage
|
||||
- synthetics_private_location_read
|
||||
- synthetics_private_location_write
|
||||
- usage_read
|
||||
- metric_tags_write
|
||||
- audit_logs_read
|
||||
- api_keys_read
|
||||
- api_keys_write
|
||||
- synthetics_global_variable_read
|
||||
- synthetics_global_variable_write
|
||||
- synthetics_read
|
||||
- synthetics_write
|
||||
- synthetics_default_settings_read
|
||||
- service_account_write
|
||||
- apm_read
|
||||
- apm_retention_filter_read
|
||||
- apm_retention_filter_write
|
||||
- rum_apps_write
|
||||
- data_scanner_read
|
||||
- data_scanner_write
|
||||
- org_management
|
||||
- security_monitoring_filters_read
|
||||
- security_monitoring_filters_write
|
||||
- incident_read
|
||||
- incident_write
|
||||
- incident_settings_write
|
||||
- rum_apps_read
|
||||
- security_monitoring_notification_profiles_read
|
||||
- security_monitoring_notification_profiles_write
|
||||
- apm_generate_metrics
|
||||
- apm_pipelines_write
|
||||
- apm_pipelines_read
|
||||
- observability_pipelines_read
|
||||
- workflows_read
|
||||
- workflows_write
|
||||
- workflows_run
|
||||
- connections_read
|
||||
- connections_write
|
||||
- notebooks_read
|
||||
- notebooks_write
|
||||
- aws_configurations_manage
|
||||
- azure_configurations_manage
|
||||
- gcp_configurations_manage
|
||||
- manage_integrations
|
||||
- slos_read
|
||||
- slos_write
|
||||
- slos_corrections
|
||||
- monitor_config_policy_write
|
||||
@@ -0,0 +1,381 @@
|
||||
package datadog
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Constants and configuration
|
||||
const (
|
||||
defaultTimeout = 12 * time.Second
|
||||
apiKeyHeader = "DD-API-KEY"
|
||||
appKeyHeader = "DD-APPLICATION-KEY"
|
||||
)
|
||||
|
||||
// List of all DataDog domains to try
|
||||
var datadogDomains = []string{
|
||||
"https://api.us5.datadoghq.com/api", // Default domain
|
||||
"https://api.app.datadoghq.com/api",
|
||||
"https://api.us3.datadoghq.com/api",
|
||||
"https://api.app.datadoghq.eu/api",
|
||||
"https://api.app.ddog-gov.com/api",
|
||||
"https://api.ap1.datadoghq.com/api",
|
||||
}
|
||||
|
||||
// Endpoints map for API paths
|
||||
var endpoints = map[string]string{
|
||||
ResourceTypeCurrentUser: "/v2/current_user",
|
||||
ResourceTypeDashboard: "/v1/dashboard",
|
||||
ResourceTypeMonitor: "/v1/monitor",
|
||||
ResourceTypeValidate: "/v1/validate",
|
||||
}
|
||||
|
||||
//go:embed scopes.json
|
||||
var scopesConfig []byte
|
||||
|
||||
// --------------------------------
|
||||
// Data models
|
||||
// --------------------------------
|
||||
|
||||
// HttpStatusTest defines a test for checking HTTP endpoint permissions
|
||||
type HttpStatusTest struct {
|
||||
Method string `json:"method"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
ValidStatuses []int `json:"valid_statuses"`
|
||||
InvalidStatuses []int `json:"invalid_statuses"`
|
||||
}
|
||||
|
||||
// Scope represents a permission scope with a test
|
||||
type Scope struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Resource string `json:"resource"`
|
||||
HttpTest HttpStatusTest `json:"test"`
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Domain detection
|
||||
// --------------------------------
|
||||
|
||||
// DetectDomain tries each DataDog domain to find a working one
|
||||
func DetectDomain(client *http.Client, apiKey string, appKey string) (string, error) {
|
||||
for _, domain := range datadogDomains {
|
||||
// Use a simple endpoint to test if the domain works
|
||||
endpoint := domain + endpoints[ResourceTypeValidate]
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, http.NoBody)
|
||||
if err != nil {
|
||||
continue // Skip to next domain if request creation fails
|
||||
}
|
||||
|
||||
// Add required keys in the header
|
||||
req.Header.Set(apiKeyHeader, apiKey)
|
||||
|
||||
if appKey != "" {
|
||||
req.Header.Set(appKeyHeader, appKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
continue // Skip to next domain if request fails
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// If we get a response that's not a connection error, this domain works
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return domain, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("unable to validate any DataDog domain with the provided API key")
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// HTTP request utilities
|
||||
// --------------------------------
|
||||
|
||||
// makeDataDogRequest sends an HTTP GET API request to the specified endpoint with auth tokens
|
||||
func makeDataDogRequest(client *http.Client, baseURL, endpoint, method, apiKey string, appKey string) ([]byte, int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// create request
|
||||
req, err := http.NewRequestWithContext(ctx, method, baseURL+endpoint, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// add required keys in the header
|
||||
req.Header.Set(apiKeyHeader, apiKey)
|
||||
|
||||
if appKey != "" {
|
||||
req.Header.Set(appKeyHeader, appKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
responseBodyByte, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return responseBodyByte, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// RunTest executes an HTTP test against an API endpoint with provided headers
|
||||
func (h *HttpStatusTest) RunTest(client *http.Client, baseURL string, headers map[string]string) (bool, error) {
|
||||
apiKey := headers[apiKeyHeader]
|
||||
appKey := headers[appKeyHeader]
|
||||
|
||||
_, statusCode, err := makeDataDogRequest(client, baseURL, h.Endpoint, h.Method, apiKey, appKey)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error making request: %v\n", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check response status code
|
||||
switch {
|
||||
case slices.Contains(h.ValidStatuses, statusCode):
|
||||
return true, nil
|
||||
case slices.Contains(h.InvalidStatuses, statusCode):
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Data capture functions
|
||||
// --------------------------------
|
||||
|
||||
// CaptureUserInformation retrieves and stores user information
|
||||
func CaptureUserInformation(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
|
||||
caller, err := getCurrentUserInfo(client, baseURL, apiKey, appKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addUserToSecretInfo(caller, secretInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaptureResources retrieves and stores dashboard and monitor resources
|
||||
func CaptureResources(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
|
||||
var wg sync.WaitGroup
|
||||
errChan := make(chan error, 2) // Buffer size matches the number of tasks
|
||||
|
||||
// helper to launch tasks concurrently
|
||||
launchTask := func(task func() error) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := task(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
launchTask(func() error { return captureDashboard(client, baseURL, apiKey, appKey, secretInfo) })
|
||||
launchTask(func() error { return captureMonitor(client, baseURL, apiKey, appKey, secretInfo) })
|
||||
|
||||
// Wait for all tasks to complete
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Collect any errors
|
||||
var errs []error
|
||||
for err := range errChan {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CapturePermissions tests and records available permissions
|
||||
func CapturePermissions(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
|
||||
scopes, err := readInScopes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading in scopes: %w", err)
|
||||
}
|
||||
|
||||
permissions := make([]Permission, 0)
|
||||
headers := map[string]string{
|
||||
apiKeyHeader: apiKey,
|
||||
appKeyHeader: appKey,
|
||||
}
|
||||
|
||||
for _, scope := range scopes {
|
||||
status, err := scope.HttpTest.RunTest(client, baseURL, headers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("running test for scope %s: %w", scope.Name, err)
|
||||
}
|
||||
|
||||
metadata := map[string]string{
|
||||
"Resource": scope.Resource,
|
||||
}
|
||||
|
||||
if status {
|
||||
permission := Permission{
|
||||
Name: scope.Name,
|
||||
Title: scope.Title,
|
||||
Description: scope.Description,
|
||||
MetaData: metadata,
|
||||
}
|
||||
permissions = append(permissions, permission)
|
||||
}
|
||||
}
|
||||
|
||||
secretInfo.Permissions = permissions
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Resource capture helper functions
|
||||
// --------------------------------
|
||||
|
||||
// getCurrentUserInfo retrieves information about the current user
|
||||
func getCurrentUserInfo(client *http.Client, baseURL, apiKey, appKey string) (*currentUserResponse, error) {
|
||||
response, statusCode, err := makeDataDogRequest(client, baseURL, endpoints[ResourceTypeCurrentUser], http.MethodGet, apiKey, appKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var caller = ¤tUserResponse{}
|
||||
if err := json.Unmarshal(response, caller); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling user response: %w", err)
|
||||
}
|
||||
return caller, nil
|
||||
case http.StatusUnauthorized:
|
||||
return nil, errors.New("invalid API key or application key")
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// addUserToSecretInfo adds user information to the secret info object
|
||||
func addUserToSecretInfo(caller *currentUserResponse, secretInfo *SecretInfo) {
|
||||
user := User{
|
||||
Id: caller.Data.Id,
|
||||
Name: caller.Data.Attributes.Name,
|
||||
Email: caller.Data.Attributes.Email,
|
||||
}
|
||||
|
||||
secretInfo.User = user
|
||||
}
|
||||
|
||||
// captureDashboard retrieves dashboard information
|
||||
func captureDashboard(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
|
||||
response, statusCode, err := makeDataDogRequest(client, baseURL, endpoints[ResourceTypeDashboard], http.MethodGet, apiKey, appKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var dashboardResponse = &dashboardResponse{}
|
||||
if err := json.Unmarshal(response, dashboardResponse); err != nil {
|
||||
return fmt.Errorf("unmarshalling dashboard response: %w", err)
|
||||
}
|
||||
|
||||
for _, dashboard := range dashboardResponse.Dashboards {
|
||||
metadata := map[string]string{
|
||||
"Layout Type": dashboard.LayoutType,
|
||||
"URL": dashboard.URL,
|
||||
"Author Handle": dashboard.AuthorHandle,
|
||||
}
|
||||
|
||||
resource := Resource{
|
||||
ID: dashboard.ID,
|
||||
Name: dashboard.Title,
|
||||
Type: ResourceTypeDashboard,
|
||||
MetaData: metadata,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
return nil
|
||||
case http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code for dashboard API: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// captureMonitor retrieves monitor information
|
||||
func captureMonitor(client *http.Client, baseURL, apiKey, appKey string, secretInfo *SecretInfo) error {
|
||||
response, statusCode, err := makeDataDogRequest(client, baseURL, endpoints[ResourceTypeMonitor], http.MethodGet, apiKey, appKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
var monitorResponse = &monitorResponse{}
|
||||
if err := json.Unmarshal(response, monitorResponse); err != nil {
|
||||
return fmt.Errorf("unmarshalling monitor response: %w", err)
|
||||
}
|
||||
|
||||
for _, monitor := range *monitorResponse {
|
||||
resource := Resource{
|
||||
ID: strconv.Itoa(monitor.ID),
|
||||
Name: monitor.Name,
|
||||
Type: ResourceTypeMonitor,
|
||||
}
|
||||
|
||||
secretInfo.appendResource(resource)
|
||||
}
|
||||
return nil
|
||||
case http.StatusForbidden:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code for monitor API: %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// Utility functions
|
||||
// --------------------------------
|
||||
|
||||
// readInScopes loads permission scopes from the embedded configuration
|
||||
func readInScopes() ([]Scope, error) {
|
||||
var scopes []Scope
|
||||
if err := json.Unmarshal(scopesConfig, &scopes); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling scopes config: %w", err)
|
||||
}
|
||||
return scopes, nil
|
||||
}
|
||||
@@ -0,0 +1,818 @@
|
||||
[
|
||||
{
|
||||
"name": "dashboards_read",
|
||||
"title": "Dashboards Read",
|
||||
"description": "View dashboards.",
|
||||
"resource": "Dashboards",
|
||||
"test": {
|
||||
"endpoint": "/v1/dashboard",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dashboards_write",
|
||||
"title": "Dashboards Write",
|
||||
"description": "Create and change dashboards.",
|
||||
"resource": "Dashboards",
|
||||
"test": {
|
||||
"endpoint": "/v1/dashboard",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dashboards_public_share",
|
||||
"title": "Dashboards Public Share",
|
||||
"description": "Create, modify and delete shared dashboards with share type 'Public'. These dashboards can be accessed by anyone on the internet.",
|
||||
"resource": "Dashboards",
|
||||
"test": {
|
||||
"endpoint": "/v1/dashboard/public",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "monitors_read",
|
||||
"title": "Monitors Read",
|
||||
"description": "View monitors.",
|
||||
"resource": "Monitors",
|
||||
"test": {
|
||||
"endpoint": "/v1/monitor",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "monitors_write",
|
||||
"title": "Monitors Write",
|
||||
"description": "Edit and delete individual monitors.",
|
||||
"resource": "Monitors",
|
||||
"test": {
|
||||
"endpoint": "/v1/monitor",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs_modify_indexes",
|
||||
"title": "Logs Modify Indexes",
|
||||
"description": "Read and modify all indexes in your account.",
|
||||
"resource": "Logs",
|
||||
"test": {
|
||||
"endpoint": "/v1/logs/config/indexes/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs_write_pipelines",
|
||||
"title": "Logs Write Pipelines",
|
||||
"description": "Add and change log pipeline configurations.",
|
||||
"resource": "Logs",
|
||||
"test": {
|
||||
"endpoint": "/v1/logs/config/pipelines/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs_write_archives",
|
||||
"title": "Logs Write Archives",
|
||||
"description": "Add and edit Log Archives.",
|
||||
"resource": "Logs",
|
||||
"test": {
|
||||
"endpoint": "/v2/logs/config/archives/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs_generate_metrics",
|
||||
"title": "Logs Generate Metrics",
|
||||
"description": "Create custom metrics from logs.",
|
||||
"resource": "Logs",
|
||||
"test": {
|
||||
"endpoint": "/v2/logs/config/metrics",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "monitors_downtime",
|
||||
"title": "Manage Downtimes",
|
||||
"description": "Set downtimes to suppress alerts from any monitor in an organization.",
|
||||
"resource": "Monitors",
|
||||
"test": {
|
||||
"endpoint": "/v1/downtime",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs_read_data",
|
||||
"title": "Logs Read Data",
|
||||
"description": "Read log data. In order to read log data, a user must have both this permission and Logs Read Index Data.",
|
||||
"resource": "Logs",
|
||||
"test": {
|
||||
"endpoint": "/v2/logs/events",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logs_read_archives",
|
||||
"title": "Logs Read Archives",
|
||||
"description": "Read Log Archives location and use it for rehydration.",
|
||||
"resource": "Logs",
|
||||
"test": {
|
||||
"endpoint": "/v2/logs/config/archives",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_rules_read",
|
||||
"title": "Security Rules Read",
|
||||
"description": "Read Detection Rules.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/cloud_security_management/custom_frameworks/must/not-exist",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_rules_write",
|
||||
"title": "Security Rules Write",
|
||||
"description": "Create and edit Detection Rules.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/security_monitoring/rules",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_signals_read",
|
||||
"title": "Security Signals Read",
|
||||
"description": "View Security Signals.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/security_monitoring/signals",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_signals_write",
|
||||
"title": "Security Signals Write",
|
||||
"description": "Modify Security Signals.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v1/security_analytics/signals/must-not-exist/add_to_incident",
|
||||
"method": "PATCH",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_access_invite",
|
||||
"title": "User Access Invite",
|
||||
"description": "Invite other users to your organization.",
|
||||
"resource": "Users",
|
||||
"test": {
|
||||
"endpoint": "/v2/user_invitations/does-not-exist",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "User App Keys",
|
||||
"name": "user_app_keys",
|
||||
"description": "View and manage Application Keys owned by the user.",
|
||||
"resource": "Key Management",
|
||||
"test": {
|
||||
"endpoint": "/v2/current_user/application_keys/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org_app_keys_read",
|
||||
"title": "Org App Keys Read",
|
||||
"description": "View Application Keys owned by all users in the organization.",
|
||||
"resource": "Key Management",
|
||||
"test": {
|
||||
"endpoint": "/v2/application_keys",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org_app_keys_write",
|
||||
"title": "Org App Keys Write",
|
||||
"description": "Manage Application Keys owned by all users in the organization.",
|
||||
"resource": "Key Management",
|
||||
"test": {
|
||||
"endpoint": "/v2/application_keys/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user_access_manage",
|
||||
"title": "User Access Manage",
|
||||
"description": "Disable users, manage user roles, manage SAML-to-role mappings, and configure logs restriction queries.",
|
||||
"resource": "Users",
|
||||
"test": {
|
||||
"endpoint": "/v2/users/does-not-exist",
|
||||
"method": "PATCH",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_private_location_read",
|
||||
"title": "Synthetics Private Locations Read",
|
||||
"description": "View, search, and use Synthetics private locations.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/private-locations/does-not-exit",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_private_location_write",
|
||||
"title": "Synthetics Private Locations Write",
|
||||
"description": "Create and delete private locations in addition to having access to the associated installation guidelines.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/private-locations/does-not-exit",
|
||||
"method": "PUT",
|
||||
"valid_statuses": [200, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "usage_read",
|
||||
"title": "Usage Read",
|
||||
"description": "View your organization's usage and usage attribution.",
|
||||
"resource": "Usage Metering",
|
||||
"test": {
|
||||
"endpoint": "/v2/usage/hourly_usage",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "metric_tags_write",
|
||||
"title": "Metric Tags Write",
|
||||
"description": "Edit and save tag configurations for custom metrics.",
|
||||
"resource": "Metrics",
|
||||
"test": {
|
||||
"endpoint": "/v2/metrics/does-not-exit/tags",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "audit_logs_read",
|
||||
"title": "Audit Trail Read",
|
||||
"description": "View Audit Trail in your organization.",
|
||||
"resource": "Audit",
|
||||
"test": {
|
||||
"endpoint": "/v2/audit/events",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "api_keys_read",
|
||||
"title": "API Keys Read",
|
||||
"description": "List and retrieve the key values of all API Keys in your organization.",
|
||||
"resource": "Key Management",
|
||||
"test": {
|
||||
"endpoint": "/v2/api_keys",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "api_keys_write",
|
||||
"title": "API Keys Write",
|
||||
"description": "Create and rename API Keys for your organization.",
|
||||
"resource": "Key Management",
|
||||
"test": {
|
||||
"endpoint": "/v2/api_keys/does-not-exist",
|
||||
"method": "PATCH",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_global_variable_read",
|
||||
"title": "Synthetics Global Variable Read",
|
||||
"description": "View, search, and use Synthetics global variables.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/variables",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_global_variable_write",
|
||||
"title": "Synthetics Global Variable Write",
|
||||
"description": "Create, edit, and delete global variables for Synthetics.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/variables",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_read",
|
||||
"title": "Synthetics Read",
|
||||
"description": "List and view configured Synthetic tests and test results.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/tests",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_write",
|
||||
"title": "Synthetics Write",
|
||||
"description": "Create, edit, and delete Synthetic tests.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/tests/mobile/does-not-exit",
|
||||
"method": "PUT",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "synthetics_default_settings_read",
|
||||
"title": "Synthetics Default Settings Read",
|
||||
"description": "View the default settings for Synthetic Monitoring.",
|
||||
"resource": "Synthetics",
|
||||
"test": {
|
||||
"endpoint": "/v1/synthetics/settings/default_locations",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "service_account_write",
|
||||
"title": "Service Account Write",
|
||||
"description": "Create, disable, and use Service Accounts in your organization.",
|
||||
"resource": "Service Accounts",
|
||||
"test": {
|
||||
"endpoint": "/v2/service_accounts/does-not-exist/application_keys",
|
||||
"method": "POST",
|
||||
"valid_statuses": [200, 400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apm_read",
|
||||
"title": "APM Read",
|
||||
"description": "Read and query APM and Trace Analytics.",
|
||||
"resource": "APM",
|
||||
"test": {
|
||||
"endpoint": "/v2/apm/config/metrics",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apm_retention_filter_read",
|
||||
"title": "APM Retention Filters Read",
|
||||
"description": "Read trace retention filters. A user with this permission can view the retention filters page, list of filters, their statistics, and creation info.",
|
||||
"resource": "APM",
|
||||
"test": {
|
||||
"endpoint": "/v2/apm/config/retention-filters/should-not-exist",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apm_retention_filter_write",
|
||||
"title": "APM Retention Filters Write",
|
||||
"description": "Create, edit, and delete trace retention filters. A user with this permission can create new retention filters, and update or delete to existing retention filters.",
|
||||
"resource": "APM",
|
||||
"test": {
|
||||
"endpoint": "/v2/apm/config/retention-filters/should-not-exit",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rum_apps_write",
|
||||
"title": "RUM Apps Write",
|
||||
"description": "Create, edit, and delete RUM applications. Creating a RUM application automatically generates a Client Token. In order to create Client Tokens directly, a user needs the Client Tokens Write permission.",
|
||||
"resource": "RUM",
|
||||
"test": {
|
||||
"endpoint": "/v2/rum/applications/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "data_scanner_read",
|
||||
"title": "Data Scanner Read",
|
||||
"description": "View Sensitive Data Scanner configurations and scanning results.",
|
||||
"resource": "Sensitive Data Scanner",
|
||||
"test": {
|
||||
"endpoint": "/v2/sensitive-data-scanner/config/standard-patterns",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "data_scanner_write",
|
||||
"title": "Data Scanner Write",
|
||||
"description": "Edit Sensitive Data Scanner configurations.",
|
||||
"resource": "Sensitive Data Scanner",
|
||||
"test": {
|
||||
"endpoint": "/v2/sensitive-data-scanner/config/groups/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "org_management",
|
||||
"title": "Org Management",
|
||||
"description": "Edit org configurations, including authentication and certain security preferences such as configuring SAML, renaming an org, configuring allowed login methods, creating child orgs, subscribing & unsubscribing from apps in the marketplace, and enabling & disabling Remote Configuration for the entire organization.",
|
||||
"resource": "Organizations",
|
||||
"test": {
|
||||
"endpoint": "/v1/org",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_filters_read",
|
||||
"title": "Security Filters Read",
|
||||
"description": "Read Security Filters.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/security_monitoring/configuration/security_filters",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_filters_write",
|
||||
"title": "Security Filters Write",
|
||||
"description": "Create, edit, and delete Security Filters.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/security_monitoring/configuration/security_filters/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "incident_read",
|
||||
"title": "Incidents Read",
|
||||
"description": "View incidents in Datadog.",
|
||||
"resource": "Incidents",
|
||||
"test": {
|
||||
"endpoint": "/v2/incidents",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "incident_write",
|
||||
"title": "Incidents Write",
|
||||
"description": "Create, view, and manage incidents in Datadog.",
|
||||
"resource": "Incidents",
|
||||
"test": {
|
||||
"endpoint": "/v2/incidents/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "incident_settings_write",
|
||||
"title": "Incident Settings Write",
|
||||
"description": "Configure Incident Settings.",
|
||||
"resource": "Incidents",
|
||||
"test": {
|
||||
"endpoint": "/v2/incidents/config/types/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rum_apps_read",
|
||||
"title": "RUM Apps Read",
|
||||
"description": "View RUM Applications data.",
|
||||
"resource": "RUM",
|
||||
"test": {
|
||||
"endpoint": "/v2/rum/applications",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_notification_profiles_read",
|
||||
"title": "Security Notification Rules Read",
|
||||
"description": "Read Notification Rules.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/security/signals/notification_rules",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "security_monitoring_notification_profiles_write",
|
||||
"title": "Security Notification Rules Write",
|
||||
"description": "Create, edit, and delete Notification Rules.",
|
||||
"resource": "Security Monitoring",
|
||||
"test": {
|
||||
"endpoint": "/v2/security/signals/notification_rules/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apm_generate_metrics",
|
||||
"title": "APM Generate Metrics",
|
||||
"description": "Create custom metrics from spans.",
|
||||
"resource": "APM",
|
||||
"test": {
|
||||
"endpoint": "/v2/apm/config/metrics/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apm_pipelines_write",
|
||||
"title": "APM Pipelines Write",
|
||||
"description": "Add and change APM pipeline configurations.",
|
||||
"resource": "APM",
|
||||
"test": {
|
||||
"endpoint": "/v2/apm/config/retention-filters/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "apm_pipelines_read",
|
||||
"title": "APM Pipelines Read",
|
||||
"description": "View APM pipeline configurations.",
|
||||
"resource": "APM",
|
||||
"test": {
|
||||
"endpoint": "/v2/apm/config/retention-filters",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "observability_pipelines_read",
|
||||
"title": "Observability Pipelines Read",
|
||||
"description": "View pipelines in your organization.",
|
||||
"resource": "Observability Pipelines",
|
||||
"test": {
|
||||
"endpoint": "/v2/remote_config/products/obs_pipelines/pipelines",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflows_read",
|
||||
"title": "Workflows Read",
|
||||
"description": "View workflows.",
|
||||
"resource": "Workflows",
|
||||
"test": {
|
||||
"endpoint": "/v2/workflows/does-not-exist",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflows_write",
|
||||
"title": "Workflows Write",
|
||||
"description": "Create, edit, and delete workflows.",
|
||||
"resource": "Workflows",
|
||||
"test": {
|
||||
"endpoint": "/v2/workflows/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workflows_run",
|
||||
"title": "Workflows Run",
|
||||
"description": "Run workflows.",
|
||||
"resource": "Workflows",
|
||||
"test": {
|
||||
"endpoint": "/v2/workflows/should-not-exist/instances",
|
||||
"method": "POST",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "connections_read",
|
||||
"title": "Connections Read",
|
||||
"description": "List and view available connections. Connections contain secrets that cannot be revealed.",
|
||||
"resource": "Connections",
|
||||
"test": {
|
||||
"endpoint": "/v2/actions/connections/does-not-exist",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "connections_write",
|
||||
"title": "Connections Write",
|
||||
"description": "Create and delete connections.",
|
||||
"resource": "Connections",
|
||||
"test": {
|
||||
"endpoint": "/v2/actions/connections/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "notebooks_read",
|
||||
"title": "Notebooks Read",
|
||||
"description": "View notebooks.",
|
||||
"resource": "Notebooks",
|
||||
"test": {
|
||||
"endpoint": "/v1/notebooks",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "notebooks_write",
|
||||
"title": "Notebooks Write",
|
||||
"description": "Create and change notebooks.",
|
||||
"resource": "Notebooks",
|
||||
"test": {
|
||||
"endpoint": "/v1/notebooks/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aws_configurations_manage",
|
||||
"title": "AWS Configurations Manage",
|
||||
"description": "Add or remove but not edit AWS integration configurations.",
|
||||
"resource": "Integrations",
|
||||
"test": {
|
||||
"endpoint": "/v2/integration/aws/accounts/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "azure_configurations_manage",
|
||||
"title": "Azure Configurations Manage",
|
||||
"description": "Add or remove but not edit Azure integration configurations.",
|
||||
"resource": "Integrations",
|
||||
"test": {
|
||||
"endpoint": "/v1/integration/azure",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "gcp_configurations_manage",
|
||||
"title": "GCP Configurations Manage",
|
||||
"description": "Add or remove but not edit GCP integration configurations.",
|
||||
"resource": "Integrations",
|
||||
"test": {
|
||||
"endpoint": "/v2/integration/gcp/accounts/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "manage_integrations",
|
||||
"title": "Integrations Manage",
|
||||
"description": "Install, uninstall, and configure integrations.",
|
||||
"resource": "Integrations",
|
||||
"test": {
|
||||
"endpoint": "/v2/integrations/cloudflare/accounts/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slos_read",
|
||||
"title": "SLOs Read",
|
||||
"description": "View SLOs and status corrections.",
|
||||
"resource": "SLOs",
|
||||
"test": {
|
||||
"endpoint": "/v1/slo",
|
||||
"method": "GET",
|
||||
"valid_statuses": [200, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slos_write",
|
||||
"title": "SLOs Write",
|
||||
"description": "Create, edit, and delete SLOs.",
|
||||
"resource": "SLOs",
|
||||
"test": {
|
||||
"endpoint": "/v1/slo/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slos_corrections",
|
||||
"title": "SLOs Status Corrections",
|
||||
"description": "Apply, edit, and delete SLO status corrections. A user with this permission can make status corrections, even if they do not have permission to edit those SLOs.",
|
||||
"resource": "SLOs",
|
||||
"test": {
|
||||
"endpoint": "/v1/slo/correction",
|
||||
"method": "POST",
|
||||
"valid_statuses": [400, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "monitor_config_policy_write",
|
||||
"title": "Monitor Configuration Policy Write",
|
||||
"description": "Create, update, and delete monitor configuration policies.",
|
||||
"resource": "Monitors",
|
||||
"test": {
|
||||
"endpoint": "/v2/monitor/policy/does-not-exist",
|
||||
"method": "DELETE",
|
||||
"valid_statuses": [400, 404, 429],
|
||||
"invalid_statuses": [403]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/asana"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/databricks"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/datadog"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/digitalocean"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dropbox"
|
||||
@@ -136,6 +137,8 @@ func Run(keyType string, secretInfo SecretInfo) {
|
||||
fastly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "monday":
|
||||
monday.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "datadog":
|
||||
datadog.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["apiKey"], secretInfo.Parts["appKey"])
|
||||
case "ngrok":
|
||||
ngrok.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "mux":
|
||||
|
||||
@@ -134,6 +134,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}
|
||||
var serviceResponse userServiceResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err == nil {
|
||||
// setup emails
|
||||
@@ -176,6 +177,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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,18 @@ func New(c common.Common, keyType string) *AnalyzeForm {
|
||||
Required: true,
|
||||
RedactInput: true,
|
||||
}}
|
||||
case "datadog":
|
||||
inputs = []textinputs.InputConfig{{
|
||||
Label: "API Key",
|
||||
Key: "apiKey",
|
||||
Required: true,
|
||||
RedactInput: true,
|
||||
}, {
|
||||
Label: "Application Key",
|
||||
Key: "appKey",
|
||||
Required: true,
|
||||
RedactInput: true,
|
||||
}}
|
||||
case "mux":
|
||||
inputs = []textinputs.InputConfig{{
|
||||
Label: "Secret",
|
||||
|
||||
Reference in New Issue
Block a user