diff --git a/pkg/connector/datadog.go b/pkg/connector/datadog.go new file mode 100644 index 000000000..ff138ff7a --- /dev/null +++ b/pkg/connector/datadog.go @@ -0,0 +1,93 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package connector + +import ( + "fmt" + "net/url" +) + +const DatadogProvider = "DATADOG" + +// datadogSite describes one Datadog site's web (authorization) host and its +// API domain. The API/token host is always api.. +type datadogSite struct { + appHost string + apiDomain string +} + +// datadogSites is the fixed, exhaustive allow-list of Datadog sites. Every +// site/domain that reaches URL construction MUST be validated against this +// table — both `site` (operator-influenced, at initiate) and `domain` +// (provider-supplied on the callback) feed URL hosts, so an unvetted value +// would be an SSRF vector. Read-only after initialization (effectively +// constant). +var datadogSites = map[string]datadogSite{ + "US1": {appHost: "app.datadoghq.com", apiDomain: "datadoghq.com"}, + "US3": {appHost: "us3.datadoghq.com", apiDomain: "us3.datadoghq.com"}, + "US5": {appHost: "us5.datadoghq.com", apiDomain: "us5.datadoghq.com"}, + "EU1": {appHost: "app.datadoghq.eu", apiDomain: "datadoghq.eu"}, + "AP1": {appHost: "ap1.datadoghq.com", apiDomain: "ap1.datadoghq.com"}, + "AP2": {appHost: "ap2.datadoghq.com", apiDomain: "ap2.datadoghq.com"}, + "US1-FED": {appHost: "app.ddog-gov.com", apiDomain: "ddog-gov.com"}, +} + +// DatadogAuthorizeURL returns the OAuth2 authorize endpoint for a Datadog +// site key (e.g. "US3"). It errors on any site not in the allow-list. +func DatadogAuthorizeURL(site string) (string, error) { + s, ok := datadogSites[site] + if !ok { + return "", fmt.Errorf("cannot build authorize URL: unknown datadog site") + } + + u := url.URL{Scheme: "https", Host: s.appHost, Path: "/oauth2/v1/authorize"} + + return u.String(), nil +} + +// DatadogTokenURL returns the OAuth2 token endpoint for a Datadog API domain +// (e.g. "us3.datadoghq.com"). It errors on any domain not in the allow-list. +func DatadogTokenURL(domain string) (string, error) { + if !IsValidDatadogDomain(domain) { + return "", fmt.Errorf("cannot build token URL: unknown datadog domain") + } + + u := url.URL{Scheme: "https", Host: "api." + domain, Path: "/oauth2/v1/token"} + + return u.String(), nil +} + +// IsValidDatadogDomain reports whether domain is one of Datadog's known API +// domains. +func IsValidDatadogDomain(domain string) bool { + for _, s := range datadogSites { + if s.apiDomain == domain { + return true + } + } + + return false +} + +// DatadogSiteForDomain reverse-maps a Datadog API domain to its site key. +func DatadogSiteForDomain(domain string) (string, bool) { + for key, s := range datadogSites { + if s.apiDomain == domain { + return key, true + } + } + + return "", false +} diff --git a/pkg/connector/datadog_test.go b/pkg/connector/datadog_test.go new file mode 100644 index 000000000..b6994ab9c --- /dev/null +++ b/pkg/connector/datadog_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package connector_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.probo.inc/probo/pkg/connector" +) + +func TestDatadogAuthorizeURL(t *testing.T) { + t.Parallel() + + got, err := connector.DatadogAuthorizeURL("US3") + require.NoError(t, err) + assert.Equal(t, "https://us3.datadoghq.com/oauth2/v1/authorize", got) + + got, err = connector.DatadogAuthorizeURL("US1") + require.NoError(t, err) + assert.Equal(t, "https://app.datadoghq.com/oauth2/v1/authorize", got) + + _, err = connector.DatadogAuthorizeURL("BOGUS") + require.Error(t, err) +} + +func TestDatadogTokenURL(t *testing.T) { + t.Parallel() + + got, err := connector.DatadogTokenURL("us3.datadoghq.com") + require.NoError(t, err) + assert.Equal(t, "https://api.us3.datadoghq.com/oauth2/v1/token", got) + + got, err = connector.DatadogTokenURL("datadoghq.eu") + require.NoError(t, err) + assert.Equal(t, "https://api.datadoghq.eu/oauth2/v1/token", got) + + _, err = connector.DatadogTokenURL("evil.example.com") + require.Error(t, err) +} + +func TestIsValidDatadogDomain(t *testing.T) { + t.Parallel() + + assert.True(t, connector.IsValidDatadogDomain("datadoghq.com")) + assert.True(t, connector.IsValidDatadogDomain("ddog-gov.com")) + assert.False(t, connector.IsValidDatadogDomain("attacker.datadoghq.com.evil.com")) + assert.False(t, connector.IsValidDatadogDomain("")) +} + +func TestDatadogSiteForDomain(t *testing.T) { + t.Parallel() + + site, ok := connector.DatadogSiteForDomain("us5.datadoghq.com") + require.True(t, ok) + assert.Equal(t, "US5", site) + + _, ok = connector.DatadogSiteForDomain("nope.com") + assert.False(t, ok) +}