package datadog import ( "context" _ "embed" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "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 url, err := url.JoinPath(baseURL, endpoint) if err != nil { return nil, 0, fmt.Errorf("failed to build URL: %w", err) } req, err := http.NewRequestWithContext(ctx, method, url, 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) } } // -------------------------------- // Validate ApiKey // -------------------------------- func ValidateApiKey(client *http.Client, baseURL, apiKey string) (bool, error) { // Use a simple endpoint to test if the domain works endpoint, err := url.JoinPath(baseURL, endpoints[ResourceTypeValidate]) if err != nil { return false, fmt.Errorf("failed to build endpoint: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() // Create request req, err := http.NewRequestWithContext(ctx, "GET", endpoint, http.NoBody) if err != nil { return false, err } // Add required keys in the header req.Header.Set(apiKeyHeader, apiKey) resp, err := client.Do(req) if err != nil { return false, err } 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 switch resp.StatusCode { case http.StatusOK: return true, nil case http.StatusForbidden: return false, nil default: return false, fmt.Errorf("unable to validate api key with status code: %d", resp.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 { if scope.HttpTest.Endpoint != "" { 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 } // API key is not finely grained, so we assign some default permissions func CaptureApiKeyPermissions(secretInfo *SecretInfo) error { scopes, err := readInScopes() if err != nil { return fmt.Errorf("reading in scopes: %w", err) } permissions := make([]Permission, 0) for _, scope := range scopes { metadata := map[string]string{ "Resource": scope.Resource, } if scope.HttpTest.Endpoint == "" { permission := Permission{ Name: scope.Name, Title: scope.Title, Description: scope.Description, MetaData: metadata, } permissions = append(permissions, permission) } } secretInfo.Permissions = append(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 }