From 91d8f6aa9d9f0f39822b88b8296afae203b48a24 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:17:07 +0100 Subject: [PATCH 1/3] upload OIDC discovery data to disco backend Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- api/datareading.go | 1 + internal/cyberark/dataupload/dataupload.go | 9 +++++++++ pkg/client/client_cyberark.go | 20 ++++++++++++++++++++ pkg/datagatherer/oidc/oidc.go | 2 +- pkg/datagatherer/oidc/oidc_test.go | 4 ++-- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/api/datareading.go b/api/datareading.go index 3412c847..3ea95b3f 100644 --- a/api/datareading.go +++ b/api/datareading.go @@ -64,6 +64,7 @@ func (o *DataReading) UnmarshalJSON(data []byte) error { target any assign func(any) }{ + {&OIDCDiscoveryData{}, func(v any) { o.Data = v.(*OIDCDiscoveryData) }}, {&DiscoveryData{}, func(v any) { o.Data = v.(*DiscoveryData) }}, {&DynamicData{}, func(v any) { o.Data = v.(*DynamicData) }}, } diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index b9ccb5f5..0221bf21 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -57,6 +57,15 @@ type Snapshot struct { ClusterDescription string `json:"cluster_description,omitempty"` // K8SVersion is the version of Kubernetes which the cluster is running. K8SVersion string `json:"k8s_version"` + // OIDCConfig contains OIDC configuration data from the API server's + // `/.well-known/openid-configuration` endpoint + OIDCConfig map[string]any `json:"openid_configuration,omitempty"` + // OIDCConfigError contains any error encountered while fetching the OIDC configuration + OIDCConfigError string `json:"openid_configuration_error,omitempty"` + // JWKS contains JWKS data from the API server's `/openid/v1/jwks` endpoint + JWKS map[string]any `json:"jwks,omitempty"` + // JWKSError contains any error encountered while fetching the JWKS + JWKSError string `json:"jwks_error,omitempty"` // Secrets is a list of Secret resources in the cluster. Not all Secret // types are included and only a subset of the Secret data is included. Secrets []runtime.Object `json:"secrets"` diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index c9310265..f6256932 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -92,6 +92,25 @@ func baseSnapshotFromOptions(opts Options) dataupload.Snapshot { } } +// extractOIDCFromReading converts the opaque data from a OIDCDiscoveryData +// data reading to allow access to the OIDC fields within. +func extractOIDCFromReading(reading *api.DataReading, target *dataupload.Snapshot) error { + if reading == nil { + return fmt.Errorf("programmer mistake: the DataReading must not be nil") + } + data, ok := reading.Data.(*api.OIDCDiscoveryData) + if !ok { + return fmt.Errorf( + "programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. "+ + "This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data) + } + target.OIDCConfig = data.OIDCConfig + target.OIDCConfigError = data.OIDCConfigError + target.JWKS = data.JWKS + target.JWKSError = data.JWKSError + return nil +} + // extractClusterIDAndServerVersionFromReading converts the opaque data from a DiscoveryData // data reading to allow access to the Kubernetes version fields within. func extractClusterIDAndServerVersionFromReading(reading *api.DataReading, target *dataupload.Snapshot) error { @@ -149,6 +168,7 @@ func extractResourceListFromReading(reading *api.DataReading, target *[]runtime. // and populates the relevant field(s) of the Snapshot based on the DataReading's data. // Deleted resources are excluded from the snapshot because they are not needed by CyberArk. var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/oidc": extractOIDCFromReading, "ark/discovery": extractClusterIDAndServerVersionFromReading, "ark/secrets": func(r *api.DataReading, s *dataupload.Snapshot) error { return extractResourceListFromReading(r, &s.Secrets) diff --git a/pkg/datagatherer/oidc/oidc.go b/pkg/datagatherer/oidc/oidc.go index 9df3c370..5014194c 100644 --- a/pkg/datagatherer/oidc/oidc.go +++ b/pkg/datagatherer/oidc/oidc.go @@ -74,7 +74,7 @@ func (g *DataGathererOIDC) Fetch() (any, int, error) { return "" } - return api.OIDCDiscoveryData{ + return &api.OIDCDiscoveryData{ OIDCConfig: oidcResponse, OIDCConfigError: errToString(oidcErr), JWKS: jwksResponse, diff --git a/pkg/datagatherer/oidc/oidc_test.go b/pkg/datagatherer/oidc/oidc_test.go index 3c3f61f6..eda579a1 100644 --- a/pkg/datagatherer/oidc/oidc_test.go +++ b/pkg/datagatherer/oidc/oidc_test.go @@ -57,7 +57,7 @@ func TestFetch_Success(t *testing.T) { t.Fatalf("expected count 1, got %d", count) } - res, ok := anyRes.(api.OIDCDiscoveryData) + res, ok := anyRes.(*api.OIDCDiscoveryData) if !ok { t.Fatalf("unexpected result type: %T", anyRes) } @@ -101,7 +101,7 @@ func TestFetch_Errors(t *testing.T) { t.Fatalf("Fetch returned error: %v", err) } - res, ok := anyRes.(api.OIDCDiscoveryData) + res, ok := anyRes.(*api.OIDCDiscoveryData) if !ok { t.Fatalf("unexpected result type: %T", anyRes) } From c74a3bdaec014aa4f6eaae0f1ba21ecd28ee0b69 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:01:30 +0100 Subject: [PATCH 2/3] configure ark/oidc in chart and tests Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- .../disco-agent/templates/configmap.yaml | 2 ++ .../__snapshot__/configmap_test.yaml.snap | 8 +++++ examples/machinehub.yaml | 4 +++ examples/machinehub/input.json | 30 +++++++++++++++++++ pkg/client/client_cyberark_test.go | 7 +++++ 5 files changed, 51 insertions(+) diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 231a26cd..4766e762 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -19,6 +19,8 @@ data: {{- . | toYaml | nindent 6 }} {{- end }} data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap index 2c70df00..89a88ed3 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -7,6 +7,8 @@ custom-cluster-description: cluster_description: "A cloud hosted Kubernetes cluster hosting production workloads.\n\nteam: team-1\nemail: team-1@example.com\npurpose: Production workloads\n" period: "12h0m0s" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic @@ -114,6 +116,8 @@ custom-cluster-name: cluster_description: "" period: "12h0m0s" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic @@ -221,6 +225,8 @@ custom-period: cluster_description: "" period: "1m" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic @@ -328,6 +334,8 @@ defaults: cluster_description: "" period: "12h0m0s" data-gatherers: + - kind: oidc + name: ark/oidc - kind: k8s-discovery name: ark/discovery - kind: k8s-dynamic diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml index ea0b28e5..8845c2c3 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -12,6 +12,10 @@ # go run . agent --one-shot --machine-hub -v 6 --agent-config-file ./examples/machinehub.yaml data-gatherers: +# Gather Kubernetes OIDC information +- name: ark/oidc + kind: oidc + # Gather Kubernetes API server version information - name: ark/discovery kind: k8s-discovery diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json index 2cdba65c..21564538 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -1,4 +1,34 @@ [ + { + "data-gatherer": "ark/oidc", + "data": { + "openid_configuration": { + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "issuer": "https://kubernetes.default.svc.cluster.local", + "jwks_uri": "https://10.10.1.2:6443/openid/v1/jwks", + "response_types_supported": [ + "id_token" + ], + "subject_types_supported": [ + "public" + ] + }, + "jwks": { + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "kid": "C-2916LkMJqepqULK2nqhq6uzVB6So_yyGnqyuor71Q", + "kty": "RSA", + "n": "sYh6rDpl5DyzBk8qlnYXo6Sf9WbplnXJv3tPxWTvhCFsVu9G5oWjknkafVDq5UOJrlybJJNjBmUyiEi1wbdnuhceJS7rZ3sRnNp3aNoS0omCR6iHJCOuoboSlcaPuRmYw4oWXlVUXlKyw8PYPVbNCcTLuq9nqf8y33mIqe7XJsf5-Z5P05WbK9Rzj-SJvlZLQ4dSFtIiwqLkm_2fpRLj0d8Af1F6vuztnhhUE2_PDsfIWdl_kJKkrK3B5x7k5tgTyFrNQPzlRBgK9jmK0HskwAFIDaLKb7FUWuUiQjn94rjKCED4iy201YPAoZBKIHFDlFVkQ_S3quwPcRyOS18r7w", + "use": "sig" + } + ] + } + } + }, { "data-gatherer": "ark/discovery", "data": { diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index 61c33764..1ed1caed 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -104,6 +104,13 @@ func fakeReadings() []*api.DataReading { } return append([]*api.DataReading{ + { + DataGatherer: "ark/oidc", + Data: &api.OIDCDiscoveryData{ + OIDCConfigError: "Failed to fetch /.well-known/openid-configuration: 404 Not Found", + JWKSError: "Failed to fetch /openid/v1/jwks: 404 Not Found", + }, + }, { DataGatherer: "ark/discovery", Data: &api.DiscoveryData{ From 97d9b34132aac98dc1c2ef76f532a375a7f060e7 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:38:35 +0100 Subject: [PATCH 3/3] add unit tests Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- api/datareading_test.go | 14 ++++ ...lient_cyberark_convertdatareadings_test.go | 64 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/api/datareading_test.go b/api/datareading_test.go index 9fa90b01..087877d8 100644 --- a/api/datareading_test.go +++ b/api/datareading_test.go @@ -75,6 +75,20 @@ func TestDataReading_UnmarshalJSON(t *testing.T) { }`, wantDataType: &DynamicData{}, }, + { + name: "OIDCDiscoveryData type", + input: `{ + "cluster_id": "11111111-2222-3333-4444-555555555555", + "data-gatherer": "oidc", + "timestamp": "2024-06-01T12:00:00Z", + "data": { + "openid_configuration": {"issuer": "https://example.com"}, + "jwks": {"keys": []} + }, + "schema_version": "v1" + }`, + wantDataType: &OIDCDiscoveryData{}, + }, { name: "Invalid JSON", input: `not a json`, diff --git a/pkg/client/client_cyberark_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go index 4fc33198..675c5bb6 100644 --- a/pkg/client/client_cyberark_convertdatareadings_test.go +++ b/pkg/client/client_cyberark_convertdatareadings_test.go @@ -126,6 +126,70 @@ func TestExtractServerVersionFromReading(t *testing.T) { } } +// TestExtractOIDCFromReading tests the extractOIDCFromReading function. +func TestExtractOIDCFromReading(t *testing.T) { + type testCase struct { + name string + reading *api.DataReading + expectedSnapshot dataupload.Snapshot + expectError string + } + tests := []testCase{ + { + name: "nil reading", + expectError: `programmer mistake: the DataReading must not be nil`, + }, + { + name: "nil data", + reading: &api.DataReading{ + DataGatherer: "ark/oidc", + Data: nil, + }, + expectError: `programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. This DataReading (ark/oidc) has data type `, + }, + { + name: "wrong data type", + reading: &api.DataReading{ + DataGatherer: "ark/oidc", + Data: &api.DiscoveryData{}, + }, + expectError: `programmer mistake: the DataReading must have data type *api.OIDCDiscoveryData. This DataReading (ark/oidc) has data type *api.DiscoveryData`, + }, + { + name: "happy path", + reading: &api.DataReading{ + DataGatherer: "ark/oidc", + Data: &api.OIDCDiscoveryData{ + OIDCConfig: map[string]any{"issuer": "https://example.com"}, + OIDCConfigError: "oidc-err", + JWKS: map[string]any{"keys": []any{}}, + JWKSError: "jwks-err", + }, + }, + expectedSnapshot: dataupload.Snapshot{ + OIDCConfig: map[string]any{"issuer": "https://example.com"}, + OIDCConfigError: "oidc-err", + JWKS: map[string]any{"keys": []any{}}, + JWKSError: "jwks-err", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var snapshot dataupload.Snapshot + err := extractOIDCFromReading(test.reading, &snapshot) + if test.expectError != "" { + assert.EqualError(t, err, test.expectError) + assert.Equal(t, dataupload.Snapshot{}, snapshot) + return + } + require.NoError(t, err) + assert.Equal(t, test.expectedSnapshot, snapshot) + }) + } +} + // TestExtractResourceListFromReading tests the extractResourceListFromReading function. func TestExtractResourceListFromReading(t *testing.T) { type testCase struct {