From acceb73b6ac15c0b1c87fad46e82919986bea06d Mon Sep 17 00:00:00 2001 From: Richard Wall Date: Wed, 17 Sep 2025 20:43:51 +0100 Subject: [PATCH] feat(api): add telemetry header to outgoing requests for integration tracking - Introduce SetTelemetryRequestHeader to set X-Cybr-Telemetry header - Add telemetry header to dataupload, identity, and servicediscovery requests - Update mocks to validate presence or absence of telemetry header as appropriate - Add tests for telemetry header encoding and correctness Signed-off-by: Richard Wall --- internal/cyberark/api/telemetry.go | 43 +++++++++++++++++++ internal/cyberark/api/telemetry_test.go | 31 +++++++++++++ internal/cyberark/dataupload/dataupload.go | 4 ++ internal/cyberark/dataupload/mock.go | 13 ++++++ internal/cyberark/identity/identity.go | 3 ++ internal/cyberark/identity/mock.go | 9 +++- .../cyberark/servicediscovery/discovery.go | 4 +- internal/cyberark/servicediscovery/mock.go | 7 +++ 8 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 internal/cyberark/api/telemetry.go create mode 100644 internal/cyberark/api/telemetry_test.go diff --git a/internal/cyberark/api/telemetry.go b/internal/cyberark/api/telemetry.go new file mode 100644 index 00000000..d0c40a56 --- /dev/null +++ b/internal/cyberark/api/telemetry.go @@ -0,0 +1,43 @@ +package api + +import ( + "encoding/base64" + "net/http" + "net/url" + + "github.com/jetstack/preflight/pkg/version" +) + +// Integrations working with the Identity Security Platform, should add metadata +// in their API calls, to provide insights into how customers utilize each API. +// +// - IntegrationName (in): The vendor integration name (required) +// - IntegrationType (it): Integration Type (required) +// - IntegrationVersion (iv): The plugin version being used (required) +// - VendorName (vn): Vendor name (required) +// - VendorVersion (vv): Version of the vendor product in which the plugin is used (if applicable) + +const ( + // TelemetryHeaderKey is the name of the HTTP header to use for telemetry + TelemetryHeaderKey = "X-Cybr-Telemetry" +) + +var ( + telemetryValues url.Values + telemetryValueEncoded string +) + +func init() { + telemetryValues = url.Values{} + telemetryValues.Set("in", "cyberark-disco-agent") + telemetryValues.Set("vn", "CyberArk") + telemetryValues.Set("it", "KubernetesAgent") + telemetryValues.Set("iv", version.PreflightVersion) + telemetryValueEncoded = base64.URLEncoding.EncodeToString([]byte(telemetryValues.Encode())) +} + +// SetTelemetryRequestHeader adds the x-cybr-telemetry header to the given HTTP +// request, with information about this integration. +func SetTelemetryRequestHeader(req *http.Request) { + req.Header.Set(TelemetryHeaderKey, telemetryValueEncoded) +} diff --git a/internal/cyberark/api/telemetry_test.go b/internal/cyberark/api/telemetry_test.go new file mode 100644 index 00000000..f7d39c7a --- /dev/null +++ b/internal/cyberark/api/telemetry_test.go @@ -0,0 +1,31 @@ +package api + +import ( + "encoding/base64" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +// Test the SetTelemetryRequestHeader function +func TestSetTelemetryRequestHeader(t *testing.T) { + // Create a new HTTP request + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", nil) + require.NoError(t, err, "failed to create HTTP request") + + // Call the function to set the telemetry header + SetTelemetryRequestHeader(req) + + base64Value := req.Header.Get(TelemetryHeaderKey) + // Check that the header is set + require.NotEmpty(t, base64Value, "telemetry header should be set") + + queryString, err := base64.URLEncoding.DecodeString(base64Value) + require.NoError(t, err, "failed to decode telemetry header value") + + values, err := url.ParseQuery(string(queryString)) + require.NoError(t, err, "failed to parse telemetry header value") + require.Equal(t, telemetryValues, values, "telemetry header value should match expected values") +} diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index 0f6b95ef..f73a8c1c 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" + arkapi "github.com/jetstack/preflight/internal/cyberark/api" "github.com/jetstack/preflight/pkg/version" ) @@ -168,6 +169,9 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu } version.SetUserAgent(req) + // Add telemetry headers + arkapi.SetTelemetryRequestHeader(req) + res, err := c.httpClient.Do(req) if err != nil { return "", err diff --git a/internal/cyberark/dataupload/mock.go b/internal/cyberark/dataupload/mock.go index 992c8385..d84ea1d4 100644 --- a/internal/cyberark/dataupload/mock.go +++ b/internal/cyberark/dataupload/mock.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "k8s.io/client-go/transport" + arkapi "github.com/jetstack/preflight/internal/cyberark/api" "github.com/jetstack/preflight/pkg/version" ) @@ -82,6 +83,12 @@ func (mds *mockDataUploadServer) handleSnapshotLinks(w http.ResponseWriter, r *h return } + if r.Header.Get(arkapi.TelemetryHeaderKey) == "" { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("should set telemetry header on all requests")) + return + } + if r.Header.Get("Content-Type") != "application/json" { http.Error(w, "should send JSON on all requests", http.StatusInternalServerError) return @@ -159,6 +166,12 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r return } + if r.Header.Get(arkapi.TelemetryHeaderKey) != "" { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("should NOT set telemetry header on requests to presigned URL")) + return + } + amzChecksum := r.Header.Get("X-Amz-Checksum-Sha256") if amzChecksum == "" { http.Error(w, "should set x-amz-checksum-sha256 header on all requests", http.StatusInternalServerError) diff --git a/internal/cyberark/identity/identity.go b/internal/cyberark/identity/identity.go index 51b1620b..e88ba0c1 100644 --- a/internal/cyberark/identity/identity.go +++ b/internal/cyberark/identity/identity.go @@ -12,6 +12,7 @@ import ( "k8s.io/klog/v2" + arkapi "github.com/jetstack/preflight/internal/cyberark/api" "github.com/jetstack/preflight/pkg/logs" "github.com/jetstack/preflight/pkg/version" ) @@ -422,4 +423,6 @@ func setIdentityHeaders(r *http.Request) { r.Header.Set("Content-Type", "application/json") r.Header.Set("X-IDAP-NATIVE-CLIENT", "true") //nolint: canonicalheader version.SetUserAgent(r) + // Add telemetry headers + arkapi.SetTelemetryRequestHeader(r) } diff --git a/internal/cyberark/identity/mock.go b/internal/cyberark/identity/mock.go index 5f5d579d..2bad8b36 100644 --- a/internal/cyberark/identity/mock.go +++ b/internal/cyberark/identity/mock.go @@ -8,8 +8,10 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" "k8s.io/client-go/transport" + arkapi "github.com/jetstack/preflight/internal/cyberark/api" "github.com/jetstack/preflight/pkg/version" _ "embed" @@ -108,6 +110,10 @@ func checkRequestHeaders(r *http.Request) error { errs = append(errs, fmt.Errorf("should set X-IDAP-NATIVE-CLIENT header to true on all requests")) } + if r.Header.Get(arkapi.TelemetryHeaderKey) == "" { + errs = append(errs, fmt.Errorf("should set telemetry header on all requests")) + } + return errors.Join(errs...) } @@ -120,9 +126,8 @@ func (mis *mockIdentityServer) handleStartAuthentication(w http.ResponseWriter, return } - if err := checkRequestHeaders(r); err != nil { + if err := checkRequestHeaders(r); !assert.NoError(mis.t, err, "request headers are not correct") { w.WriteHeader(http.StatusForbidden) - fmt.Fprintf(w, `{"message":"issues with headers sent to mock server: %s"}`, err.Error()) return } diff --git a/internal/cyberark/servicediscovery/discovery.go b/internal/cyberark/servicediscovery/discovery.go index 250f6e47..5fc920f5 100644 --- a/internal/cyberark/servicediscovery/discovery.go +++ b/internal/cyberark/servicediscovery/discovery.go @@ -10,6 +10,7 @@ import ( "os" "path" + arkapi "github.com/jetstack/preflight/internal/cyberark/api" "github.com/jetstack/preflight/pkg/version" ) @@ -109,7 +110,8 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi request.Header.Set("Accept", "application/json") version.SetUserAgent(request) - + // Add telemetry headers + arkapi.SetTelemetryRequestHeader(request) resp, err := c.client.Do(request) if err != nil { return nil, fmt.Errorf("failed to perform HTTP request: %s", err) diff --git a/internal/cyberark/servicediscovery/mock.go b/internal/cyberark/servicediscovery/mock.go index f462a6c5..360c87a3 100644 --- a/internal/cyberark/servicediscovery/mock.go +++ b/internal/cyberark/servicediscovery/mock.go @@ -13,6 +13,7 @@ import ( "k8s.io/client-go/transport" + arkapi "github.com/jetstack/preflight/internal/cyberark/api" "github.com/jetstack/preflight/pkg/version" _ "embed" @@ -92,6 +93,12 @@ func (mds *mockDiscoveryServer) ServeHTTP(w http.ResponseWriter, r *http.Request return } + if r.Header.Get(arkapi.TelemetryHeaderKey) == "" { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("should set telemetry header on all requests")) + return + } + if r.Header.Get("Accept") != "application/json" { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("should request JSON on all requests"))