mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2026-05-16 13:20:35 +00:00
0fa069c12f
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues * skip lint on intentional reference of deprecated DetectorType values
457 lines
12 KiB
Go
457 lines
12 KiB
Go
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
|
|
}
|