diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 4766e762..b471a159 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -40,6 +40,14 @@ data: resource-type: resource: serviceaccounts version: v1 + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + resource: configmaps + version: v1 + label-selectors: + - conjur.org/name=conjur-connect-configmap - kind: k8s-dynamic name: ark/roles config: 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 89a88ed3..fa7fb2b0 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -28,6 +28,14 @@ custom-cluster-description: resource-type: resource: serviceaccounts version: v1 + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + resource: configmaps + version: v1 + label-selectors: + - conjur.org/name=conjur-connect-configmap - kind: k8s-dynamic name: ark/roles config: @@ -137,6 +145,14 @@ custom-cluster-name: resource-type: resource: serviceaccounts version: v1 + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + resource: configmaps + version: v1 + label-selectors: + - conjur.org/name=conjur-connect-configmap - kind: k8s-dynamic name: ark/roles config: @@ -246,6 +262,14 @@ custom-period: resource-type: resource: serviceaccounts version: v1 + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + resource: configmaps + version: v1 + label-selectors: + - conjur.org/name=conjur-connect-configmap - kind: k8s-dynamic name: ark/roles config: @@ -355,6 +379,14 @@ defaults: resource-type: resource: serviceaccounts version: v1 + - kind: k8s-dynamic + name: ark/configmaps + config: + resource-type: + resource: configmaps + version: v1 + label-selectors: + - conjur.org/name=conjur-connect-configmap - kind: k8s-dynamic name: ark/roles config: diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml index 8845c2c3..047b6627 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -41,6 +41,16 @@ data-gatherers: resource: serviceaccounts version: v1 +# Gather Kubernetes config maps with specific conjur.org label +- name: ark/configmaps + kind: k8s-dynamic + config: + resource-type: + resource: configmaps + version: v1 + label-selectors: + - conjur.org/name=conjur-connect-configmap + # Gather Kubernetes roles - name: ark/roles kind: k8s-dynamic diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json index 21564538..8067cef7 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -153,5 +153,11 @@ "data": { "items": [] } + }, + { + "data-gatherer": "ark/configmaps", + "data": { + "items": [] + } } ] diff --git a/hack/ark/conjur-connect-configmap.yaml b/hack/ark/conjur-connect-configmap.yaml new file mode 100644 index 00000000..0fff2ac5 --- /dev/null +++ b/hack/ark/conjur-connect-configmap.yaml @@ -0,0 +1,40 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: conjur-connect-configmap + namespace: default + labels: + conjur.org/name: conjur-connect-configmap + app.kubernetes.io/name: authn-k8s + app.kubernetes.io/component: conjur-conn-configmap + app.kubernetes.io/instance: pet-store-authn-k8s + app.kubernetes.io/part-of: app-namespace-config + app.kubernetes.io/managed-by: helm + helm.sh/chart: authn-k8s-namespace-prep-1.0.0 +data: + CONJUR_ACCOUNT: myConjurAccount + CONJUR_APPLIANCE_URL: https://conjur.conjur-ns.svc.cluster.local + CONJUR_AUTHN_URL: https://conjur.conjur-ns.svc.cluster.local/authn-k8s/my-authenticator-id + CONJUR_AUTHENTICATOR_ID: my-authenticator-id + CONJUR_SSL_CERTIFICATE: | + -----BEGIN CERTIFICATE----- + MIIDYTCCAkmgAwIBAgIUTXBJk7Fm+M9kVD5x66jPiwU2JfcwDQYJKoZIhvcNAQEL + BQAwQDErMCkGA1UEAwwiY29uanVyLmNvbmp1ci1ucy5zdmMuY2x1c3Rlci5sb2Nh + bDERMA8GA1UECgwIRTJFIFRlc3QwHhcNMjYwMTI4MTMwNzA5WhcNMzYwMTI2MTMw + NzA5WjBAMSswKQYDVQQDDCJjb25qdXIuY29uanVyLW5zLnN2Yy5jbHVzdGVyLmxv + Y2FsMREwDwYDVQQKDAhFMkUgVGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC + AQoCggEBALdJ9InvV4oOy5LzP/JfZ7iAuM7RIQzeD1fDjm1EEfQcLqSgobH2yZtA + YETlj/c2bfJ8Cc2dTJMoTefwofwjA6iR43SBf0e78raKsGSmR3ors9BqaulvgII5 + Tk3y5jdZxty7UNIGOJP9QoJ4kPQHu37HhSfaA517yQJNCOa4NSLkpHWK155o6Cvf + k03M6Szzs5uL7GTK/8IJnl0WSXJezC7lQ8Q+0VVCR6Cq4CzAKm2ZoVCPGkYDZb+Y + 2i0aGe8ideO0JgTOsHzXiv5x1DzaEdX0+DhV+aQKbRJYENa2w5LCG0b1Z6Hpyvm6 + uT0LobEgNLxJ8fOxa3LEq2IryzHFZjUCAwEAAaNTMFEwHQYDVR0OBBYEFHuXVFoC + IaF7T3Iic7fKxyKwVhpkMB8GA1UdIwQYMBaAFHuXVFoCIaF7T3Iic7fKxyKwVhpk + MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAF/7DwNERFTpucWi + roDVME2SH1kTKiemcKzguoeOkDBZd70GbLejy64gWF9nIbcQ9WYxRIuqSI2h0j8d + ED9SGQ66nic3uw16GN5IJk21ucFwAJstgQG3kvWPBbSrxMO9TB0pounRozZ5DkZe + ZI+vZ4BNOZDT9TAE08xXLrzVhzVDM8DGAydzXUlvscfhYpTe77Cm7yMxmItO7QTA + xTrBaamgxM1XYbx+DiS8nTm1U2G3UVACCv9zH6MXDe2DDREBuX1U3skqqbJlsypf + 68ckx8fzdxIU5OLx0LZ4QZOR66cHyambDtngoD3iKqDcR1L8EdXajq+IaPRZfcD6 + VLEtA4Y= + -----END CERTIFICATE----- diff --git a/hack/ark/test-e2e.sh b/hack/ark/test-e2e.sh index 2765716e..e24487d3 100755 --- a/hack/ark/test-e2e.sh +++ b/hack/ark/test-e2e.sh @@ -74,6 +74,12 @@ kubectl create secret generic e2e-sample-secret-$(date '+%s') \ --namespace default \ --from-literal=username=${RANDOM} +# Create a sample ConfigMap in the cluster that will be discovered by the agent +# +# This ConfigMap has the label that matches the default label-selector configured +# in the ark/configmaps data gatherer (conjur.org/name=conjur-connect-configmap). +kubectl apply -f "${root_dir}/hack/ark/conjur-connect-configmap.yaml" + # We use a non-existent tag and omit the `--version` flag, to work around a Helm # v4 bug. See: https://github.com/helm/helm/issues/31600 helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \ diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index 0221bf21..0d5bcc08 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -71,6 +71,8 @@ type Snapshot struct { Secrets []runtime.Object `json:"secrets"` // ServiceAccounts is a list of ServiceAccount resources in the cluster. ServiceAccounts []runtime.Object `json:"serviceaccounts"` + // ConfigMaps is a list of ConfigMap resources in the cluster. + ConfigMaps []runtime.Object `json:"configmaps"` // Roles is a list of Role resources in the cluster. Roles []runtime.Object `json:"roles"` // ClusterRoles is a list of ClusterRole resources in the cluster. diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index 22db7d5c..735313bd 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -218,6 +218,9 @@ var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Sn "ark/pods": func(r *api.DataReading, s *dataupload.Snapshot) error { return extractResourceListFromReading(r, &s.Pods) }, + "ark/configmaps": func(r *api.DataReading, s *dataupload.Snapshot) error { + return extractResourceListFromReading(r, &s.ConfigMaps) + }, } // convertDataReadings processes a list of DataReadings using the provided diff --git a/pkg/client/client_cyberark_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go index 675c5bb6..a0fc2c27 100644 --- a/pkg/client/client_cyberark_convertdatareadings_test.go +++ b/pkg/client/client_cyberark_convertdatareadings_test.go @@ -317,6 +317,363 @@ func TestExtractResourceListFromReading(t *testing.T) { } } +// TestConvertDataReadings_ConfigMaps tests that configmaps are correctly converted. +func TestConvertDataReadings_ConfigMaps(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/configmaps": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ConfigMaps) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "test-cluster-id", + ServerVersion: &version.Info{ + GitVersion: "v1.21.0", + }, + }, + }, + { + DataGatherer: "ark/configmaps", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "conjur-connect", + "namespace": "conjur", + "labels": map[string]any{ + "conjur.org/name": "conjur-connect-configmap", + }, + }, + "data": map[string]any{ + "config.yaml": "some-config-data", + }, + }, + }, + }, + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "another-configmap", + "namespace": "default", + "labels": map[string]any{ + "conjur.org/name": "conjur-connect-configmap", + }, + }, + "data": map[string]any{ + "setting": "value", + }, + }, + }, + }, + // Deleted configmap should be ignored + { + DeletedAt: api.Time{Time: time.Now()}, + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "deleted-configmap", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + // Verify the snapshot contains the expected data + assert.Equal(t, "test-cluster-id", snapshot.ClusterID) + assert.Equal(t, "v1.21.0", snapshot.K8SVersion) + require.Len(t, snapshot.ConfigMaps, 2, "should have 2 configmaps (deleted one should be excluded)") + + // Verify the first configmap + cm1, ok := snapshot.ConfigMaps[0].(*unstructured.Unstructured) + require.True(t, ok, "configmap should be unstructured") + assert.Equal(t, "ConfigMap", cm1.GetKind()) + assert.Equal(t, "conjur-connect", cm1.GetName()) + assert.Equal(t, "conjur", cm1.GetNamespace()) + + // Verify the second configmap + cm2, ok := snapshot.ConfigMaps[1].(*unstructured.Unstructured) + require.True(t, ok, "configmap should be unstructured") + assert.Equal(t, "ConfigMap", cm2.GetKind()) + assert.Equal(t, "another-configmap", cm2.GetName()) + assert.Equal(t, "default", cm2.GetNamespace()) +} + +// TestConvertDataReadings_ServiceAccounts tests that serviceaccounts are correctly converted. +func TestConvertDataReadings_ServiceAccounts(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/serviceaccounts": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ServiceAccounts) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "test-cluster-id", + ServerVersion: &version.Info{ + GitVersion: "v1.22.0", + }, + }, + }, + { + DataGatherer: "ark/serviceaccounts", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": map[string]any{ + "name": "default", + "namespace": "default", + }, + }, + }, + }, + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": map[string]any{ + "name": "app-sa", + "namespace": "production", + "labels": map[string]any{ + "app": "myapp", + }, + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + assert.Equal(t, "test-cluster-id", snapshot.ClusterID) + assert.Equal(t, "v1.22.0", snapshot.K8SVersion) + require.Len(t, snapshot.ServiceAccounts, 2) + + sa1, ok := snapshot.ServiceAccounts[0].(*unstructured.Unstructured) + require.True(t, ok) + assert.Equal(t, "ServiceAccount", sa1.GetKind()) + assert.Equal(t, "default", sa1.GetName()) +} + +// TestConvertDataReadings_Roles tests that roles are correctly converted. +func TestConvertDataReadings_Roles(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/roles": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.Roles) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "rbac-cluster", + ServerVersion: &version.Info{ + GitVersion: "v1.23.0", + }, + }, + }, + { + DataGatherer: "ark/roles", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": map[string]any{ + "name": "pod-reader", + "namespace": "default", + "labels": map[string]any{ + "rbac.authorization.k8s.io/aggregate-to-view": "true", + }, + }, + "rules": []any{ + map[string]any{ + "apiGroups": []any{""}, + "resources": []any{"pods"}, + "verbs": []any{"get", "list"}, + }, + }, + }, + }, + }, + // Deleted role should be excluded + { + DeletedAt: api.Time{Time: time.Now()}, + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": map[string]any{ + "name": "deleted-role", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + assert.Equal(t, "rbac-cluster", snapshot.ClusterID) + require.Len(t, snapshot.Roles, 1, "deleted role should be excluded") + + role, ok := snapshot.Roles[0].(*unstructured.Unstructured) + require.True(t, ok) + assert.Equal(t, "Role", role.GetKind()) + assert.Equal(t, "pod-reader", role.GetName()) +} + +// TestConvertDataReadings_MultipleResources tests conversion with multiple resource types. +func TestConvertDataReadings_MultipleResources(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/configmaps": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ConfigMaps) + }, + "ark/serviceaccounts": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ServiceAccounts) + }, + "ark/deployments": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.Deployments) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "multi-resource-cluster", + ServerVersion: &version.Info{ + GitVersion: "v1.24.0", + }, + }, + }, + { + DataGatherer: "ark/configmaps", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]any{ + "name": "app-config", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + { + DataGatherer: "ark/serviceaccounts", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": map[string]any{ + "name": "app-sa", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + { + DataGatherer: "ark/deployments", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]any{ + "name": "web-app", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + // Verify all resources are present + assert.Equal(t, "multi-resource-cluster", snapshot.ClusterID) + assert.Equal(t, "v1.24.0", snapshot.K8SVersion) + require.Len(t, snapshot.ConfigMaps, 1) + require.Len(t, snapshot.ServiceAccounts, 1) + require.Len(t, snapshot.Deployments, 1) + + // Verify each resource type + cm, ok := snapshot.ConfigMaps[0].(*unstructured.Unstructured) + require.True(t, ok) + assert.Equal(t, "app-config", cm.GetName()) + + sa, ok := snapshot.ServiceAccounts[0].(*unstructured.Unstructured) + require.True(t, ok) + assert.Equal(t, "app-sa", sa.GetName()) + + deploy, ok := snapshot.Deployments[0].(*unstructured.Unstructured) + require.True(t, ok) + assert.Equal(t, "web-app", deploy.GetName()) +} + // TestConvertDataReadings tests the convertDataReadings function. func TestConvertDataReadings(t *testing.T) { simpleExtractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index 1ed1caed..9a963300 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -79,6 +79,7 @@ func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) { var defaultDynamicDatagathererNames = []string{ "ark/secrets", "ark/serviceaccounts", + "ark/configmaps", "ark/roles", "ark/clusterroles", "ark/rolebindings", diff --git a/pkg/datagatherer/k8sdynamic/dynamic.go b/pkg/datagatherer/k8sdynamic/dynamic.go index a02a6733..55cb4cde 100644 --- a/pkg/datagatherer/k8sdynamic/dynamic.go +++ b/pkg/datagatherer/k8sdynamic/dynamic.go @@ -166,6 +166,9 @@ var kubernetesNativeResources = map[schema.GroupVersionResource]sharedInformerFu corev1.SchemeGroupVersion.WithResource("services"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer { return sharedFactory.Core().V1().Services().Informer() }, + corev1.SchemeGroupVersion.WithResource("configmaps"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer { + return sharedFactory.Core().V1().ConfigMaps().Informer() + }, appsv1.SchemeGroupVersion.WithResource("deployments"): func(sharedFactory informers.SharedInformerFactory) k8scache.SharedIndexInformer { return sharedFactory.Apps().V1().Deployments().Informer() },