From 5196fc91197dcaebfa3dfa00e06d56df782d854b Mon Sep 17 00:00:00 2001 From: Richard Wall Date: Fri, 8 Aug 2025 12:24:01 +0100 Subject: [PATCH] Add a snapshot JSON format for uploads and convert DataReadings to Snapshot format Signed-off-by: Richard Wall --- api/datareading.go | 37 ++++++- pkg/agent/run.go | 8 ++ pkg/client/client_cyberark.go | 9 -- pkg/datagatherer/k8s/discovery.go | 12 +-- pkg/datagatherer/k8s/dynamic.go | 8 +- pkg/datagatherer/k8s/dynamic_test.go | 16 ++- pkg/datagatherer/k8s/fieldfilter.go | 3 + .../cyberark/dataupload/dataupload.go | 9 +- .../cyberark/dataupload/dataupload_test.go | 71 +++++++----- pkg/internal/cyberark/dataupload/mock.go | 46 +++++--- pkg/internal/cyberark/dataupload/snapshot.go | 102 ++++++++++++++++++ .../cyberark/dataupload/snapshot_test.go | 29 +++++ .../dataupload/testdata/example-1/README.md | 27 +++++ .../dataupload/testdata/example-1/agent.yaml | 54 ++++++++++ .../testdata/example-1/datareadings.json.gz | Bin 0 -> 29312 bytes .../testdata/example-1/snapshot.json.gz | Bin 0 -> 26955 bytes pkg/testutil/datareadings.go | 90 ++++++++++++++++ 17 files changed, 441 insertions(+), 80 deletions(-) delete mode 100644 pkg/client/client_cyberark.go create mode 100644 pkg/internal/cyberark/dataupload/snapshot.go create mode 100644 pkg/internal/cyberark/dataupload/snapshot_test.go create mode 100644 pkg/internal/cyberark/dataupload/testdata/example-1/README.md create mode 100644 pkg/internal/cyberark/dataupload/testdata/example-1/agent.yaml create mode 100644 pkg/internal/cyberark/dataupload/testdata/example-1/datareadings.json.gz create mode 100644 pkg/internal/cyberark/dataupload/testdata/example-1/snapshot.json.gz create mode 100644 pkg/testutil/datareadings.go diff --git a/api/datareading.go b/api/datareading.go index 54637f3c..75e7dee3 100644 --- a/api/datareading.go +++ b/api/datareading.go @@ -1,8 +1,12 @@ package api import ( + "bytes" "encoding/json" "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/version" ) // DataReadingsPost is the payload in the upload request. @@ -28,8 +32,8 @@ type DataReading struct { type GatheredResource struct { // Resource is a reference to a k8s object that was found by the informer // should be of type unstructured.Unstructured, raw Object - Resource interface{} - DeletedAt Time + Resource interface{} `json:"resource"` + DeletedAt Time `json:"deleted_at,omitempty"` } func (v GatheredResource) MarshalJSON() ([]byte, error) { @@ -48,3 +52,32 @@ func (v GatheredResource) MarshalJSON() ([]byte, error) { return json.Marshal(data) } + +func (v *GatheredResource) UnmarshalJSON(data []byte) error { + var tmpResource struct { + Resource *unstructured.Unstructured `json:"resource"` + DeletedAt Time `json:"deleted_at,omitempty"` + } + + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + + if err := d.Decode(&tmpResource); err != nil { + return err + } + v.Resource = tmpResource.Resource + v.DeletedAt = tmpResource.DeletedAt + return nil +} + +// DynamicData is the DataReading.Data returned by the k8s.DataGathererDynamic +// gatherer +type DynamicData struct { + Items []*GatheredResource `json:"items"` +} + +// DiscoveryData is the DataReading.Data returned by the k8s.ConfigDiscovery +// gatherer +type DiscoveryData struct { + ServerVersion *version.Info `json:"server_version"` +} diff --git a/pkg/agent/run.go b/pkg/agent/run.go index 6995c548..13d4401f 100644 --- a/pkg/agent/run.go +++ b/pkg/agent/run.go @@ -321,6 +321,14 @@ func gatherAndOutputData(ctx context.Context, eventf Eventf, config CombinedConf var readings []*api.DataReading if config.InputPath != "" { + // TODO(wallrj): The datareadings read from disk can not yet be pushed + // to the CyberArk Discovery and Context API. Why? Because they have + // simple data types such as map[string]interface{}. In contrast, the + // data from data gatherers can be cast to rich types like DynamicData + // or DiscoveryData The CyberArk dataupload client requires the data to + // have rich types to convert it to the Discovery and Context snapshots + // format. Consider refactoring testutil.ParseDataReadings so that it + // can be used here. log.V(logs.Debug).Info("Reading data from local file", "inputPath", config.InputPath) data, err := os.ReadFile(config.InputPath) if err != nil { diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go deleted file mode 100644 index af26e96c..00000000 --- a/pkg/client/client_cyberark.go +++ /dev/null @@ -1,9 +0,0 @@ -package client - -import ( - "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" -) - -type CyberArkClient = dataupload.CyberArkClient - -var NewCyberArkClient = dataupload.NewCyberArkClient diff --git a/pkg/datagatherer/k8s/discovery.go b/pkg/datagatherer/k8s/discovery.go index 586622d6..340bcbda 100644 --- a/pkg/datagatherer/k8s/discovery.go +++ b/pkg/datagatherer/k8s/discovery.go @@ -6,6 +6,7 @@ import ( "k8s.io/client-go/discovery" + "github.com/jetstack/preflight/api" "github.com/jetstack/preflight/pkg/datagatherer" ) @@ -59,15 +60,12 @@ func (g *DataGathererDiscovery) WaitForCacheSync(ctx context.Context) error { // Fetch will fetch discovery data from the apiserver, or return an error func (g *DataGathererDiscovery) Fetch() (interface{}, int, error) { - data, err := g.cl.ServerVersion() + serverVersion, err := g.cl.ServerVersion() if err != nil { return nil, -1, fmt.Errorf("failed to get server version: %v", err) } - response := map[string]interface{}{ - // data has type Info: https://godoc.org/k8s.io/apimachinery/pkg/version#Info - "server_version": data, - } - - return response, len(response), nil + return &api.DiscoveryData{ + ServerVersion: serverVersion, + }, 1, nil } diff --git a/pkg/datagatherer/k8s/dynamic.go b/pkg/datagatherer/k8s/dynamic.go index b0e1dedf..9dbffe88 100644 --- a/pkg/datagatherer/k8s/dynamic.go +++ b/pkg/datagatherer/k8s/dynamic.go @@ -314,7 +314,6 @@ func (g *DataGathererDynamic) Fetch() (interface{}, int, error) { return nil, -1, fmt.Errorf("resource type must be specified") } - var list = map[string]interface{}{} var items = []*api.GatheredResource{} fetchNamespaces := g.namespaces @@ -344,10 +343,9 @@ func (g *DataGathererDynamic) Fetch() (interface{}, int, error) { return nil, -1, err } - // add gathered resources to items - list["items"] = items - - return list, len(items), nil + return &api.DynamicData{ + Items: items, + }, len(items), nil } func redactList(list []*api.GatheredResource, excludeAnnotKeys, excludeLabelKeys []*regexp.Regexp) error { diff --git a/pkg/datagatherer/k8s/dynamic_test.go b/pkg/datagatherer/k8s/dynamic_test.go index 072c4c1c..525c8892 100644 --- a/pkg/datagatherer/k8s/dynamic_test.go +++ b/pkg/datagatherer/k8s/dynamic_test.go @@ -730,15 +730,12 @@ func TestDynamicGatherer_Fetch(t *testing.T) { } if tc.expected != nil { - items, ok := res.(map[string]interface{}) + data, ok := res.(*api.DynamicData) if !ok { - t.Errorf("expected result be an map[string]interface{} but wasn't") + t.Errorf("expected result be *api.DynamicData but wasn't") } - list, ok := items["items"].([]*api.GatheredResource) - if !ok { - t.Errorf("expected result be an []*api.GatheredResource but wasn't") - } + list := data.Items // sorting list of results by name sortGatheredResources(list) // sorting list of expected results by name @@ -1045,10 +1042,9 @@ func TestDynamicGathererNativeResources_Fetch(t *testing.T) { } if tc.expected != nil { - res, ok := rawRes.(map[string]interface{}) - require.Truef(t, ok, "expected result be an map[string]interface{} but wasn't") - actual := res["items"].([]*api.GatheredResource) - require.Truef(t, ok, "expected result be an []*api.GatheredResource but wasn't") + res, ok := rawRes.(*api.DynamicData) + require.Truef(t, ok, "expected result be an *api.DynamicData but wasn't") + actual := res.Items // sorting list of results by name sortGatheredResources(actual) diff --git a/pkg/datagatherer/k8s/fieldfilter.go b/pkg/datagatherer/k8s/fieldfilter.go index ed39acb3..1bddd387 100644 --- a/pkg/datagatherer/k8s/fieldfilter.go +++ b/pkg/datagatherer/k8s/fieldfilter.go @@ -16,6 +16,9 @@ var SecretSelectedFields = []FieldPath{ {"metadata", "ownerReferences"}, {"metadata", "selfLink"}, {"metadata", "uid"}, + {"metadata", "creationTimestamp"}, + {"metadata", "deletionTimestamp"}, + {"metadata", "resourceVersion"}, {"type"}, {"data", "tls.crt"}, diff --git a/pkg/internal/cyberark/dataupload/dataupload.go b/pkg/internal/cyberark/dataupload/dataupload.go index d8835e3b..014aa85b 100644 --- a/pkg/internal/cyberark/dataupload/dataupload.go +++ b/pkg/internal/cyberark/dataupload/dataupload.go @@ -58,14 +58,19 @@ func NewCyberArkClient(trustedCAs *x509.CertPool, baseURL string, authenticateRe // PostDataReadingsWithOptions PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API. // // [AWS presigned URL]: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html -func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payload api.DataReadingsPost, opts Options) error { +func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, opts Options) error { if opts.ClusterName == "" { return fmt.Errorf("programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty") } + snapshot, err := convertDataReadingsToCyberarkSnapshot(readings) + if err != nil { + return fmt.Errorf("while converting datareadings to Cyberark snapshot format: %s", err) + } + encodedBody := &bytes.Buffer{} checksum := sha3.New256() - if err := json.NewEncoder(io.MultiWriter(encodedBody, checksum)).Encode(payload); err != nil { + if err := json.NewEncoder(io.MultiWriter(encodedBody, checksum)).Encode(snapshot); err != nil { return err } diff --git a/pkg/internal/cyberark/dataupload/dataupload_test.go b/pkg/internal/cyberark/dataupload/dataupload_test.go index cadb296a..4639029f 100644 --- a/pkg/internal/cyberark/dataupload/dataupload_test.go +++ b/pkg/internal/cyberark/dataupload/dataupload_test.go @@ -3,6 +3,7 @@ package dataupload_test import ( "crypto/x509" "encoding/pem" + "errors" "fmt" "net/http" "os" @@ -17,28 +18,23 @@ import ( "github.com/jetstack/preflight/pkg/internal/cyberark/dataupload" "github.com/jetstack/preflight/pkg/internal/cyberark/identity" "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery" + "github.com/jetstack/preflight/pkg/testutil" _ "k8s.io/klog/v2/ktesting/init" ) -func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { +func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) { fakeTime := time.Unix(123, 0) - defaultPayload := api.DataReadingsPost{ - AgentMetadata: &api.AgentMetadata{ - Version: "test-version", - ClusterID: "test", - }, - DataGatherTime: fakeTime, - DataReadings: []*api.DataReading{ - { - ClusterID: "success-cluster-id", - DataGatherer: "test-gatherer", - Timestamp: api.Time{Time: fakeTime}, - Data: map[string]interface{}{"test": "data"}, - SchemaVersion: "v1", - }, + defaultDataReadings := []*api.DataReading{ + { + ClusterID: "success-cluster-id", + DataGatherer: "test-gatherer", + Timestamp: api.Time{Time: fakeTime}, + Data: map[string]interface{}{"test": "data"}, + SchemaVersion: "v1", }, } + defaultOpts := dataupload.Options{ ClusterName: "success-cluster-id", } @@ -52,14 +48,14 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { tests := []struct { name string - payload api.DataReadingsPost + readings []*api.DataReading authenticate func(req *http.Request) error opts dataupload.Options requireFn func(t *testing.T, err error) }{ { name: "successful upload", - payload: defaultPayload, + readings: defaultDataReadings, opts: defaultOpts, authenticate: setToken("success-token"), requireFn: func(t *testing.T, err error) { @@ -68,7 +64,7 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { }, { name: "error when cluster name is empty", - payload: defaultPayload, + readings: defaultDataReadings, opts: dataupload.Options{ClusterName: ""}, authenticate: setToken("success-token"), requireFn: func(t *testing.T, err error) { @@ -77,16 +73,27 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { }, { name: "error when bearer token is incorrect", - payload: defaultPayload, + readings: defaultDataReadings, opts: defaultOpts, authenticate: setToken("fail-token"), requireFn: func(t *testing.T, err error) { require.ErrorContains(t, err, "while retrieving snapshot upload URL: received response with status code 500: should authenticate using the correct bearer token") }, }, + { + name: "error contains authenticate error", + readings: defaultDataReadings, + opts: defaultOpts, + authenticate: func(_ *http.Request) error { + return errors.New("simulated-authenticate-error") + }, + requireFn: func(t *testing.T, err error) { + require.ErrorContains(t, err, "while retrieving snapshot upload URL: failed to authenticate request: simulated-authenticate-error") + }, + }, { name: "invalid JSON from server (RetrievePresignedUploadURL step)", - payload: defaultPayload, + readings: defaultDataReadings, opts: dataupload.Options{ClusterName: "invalid-json-retrieve-presigned"}, authenticate: setToken("success-token"), requireFn: func(t *testing.T, err error) { @@ -95,7 +102,7 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { }, { name: "500 from server (RetrievePresignedUploadURL step)", - payload: defaultPayload, + readings: defaultDataReadings, opts: dataupload.Options{ClusterName: "invalid-response-post-data"}, authenticate: setToken("success-token"), requireFn: func(t *testing.T, err error) { @@ -106,6 +113,9 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + logger := ktesting.NewLogger(t, ktesting.DefaultConfig) + ctx := klog.NewContext(t.Context(), logger) + server := dataupload.MockDataUploadServer() defer server.Close() @@ -118,13 +128,13 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { cyberArkClient, err := dataupload.NewCyberArkClient(certPool, server.Server.URL, tc.authenticate) require.NoError(t, err) - err = cyberArkClient.PostDataReadingsWithOptions(t.Context(), tc.payload, tc.opts) + err = cyberArkClient.PostDataReadingsWithOptions(ctx, tc.readings, tc.opts) tc.requireFn(t, err) }) } } -// TestPostDataReadingsWithOptionsWithRealAPI demonstrates that the dataupload code works with the real inventory API. +// TestCyberArkClient_PostDataReadingsWithOptions_RealAPI demonstrates that the dataupload code works with the real inventory API. // An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment. // ARK_SUBDOMAIN should be your tenant subdomain. // ARK_PLATFORM_DOMAIN should be either integration-cyberark.cloud or cyberark.cloud @@ -132,8 +142,8 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) { // To enable verbose request logging: // // go test ./pkg/internal/cyberark/dataupload/... \ -// -v -count 1 -run TestPostDataReadingsWithOptionsWithRealAPI -args -testing.v 6 -func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) { +// -v -count 1 -run TestCyberArkClient_PostDataReadingsWithOptions_RealAPI -args -testing.v 6 +func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) { platformDomain := os.Getenv("ARK_PLATFORM_DOMAIN") subdomain := os.Getenv("ARK_SUBDOMAIN") username := os.Getenv("ARK_USERNAME") @@ -172,8 +182,13 @@ func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) { cyberArkClient, err := dataupload.NewCyberArkClient(nil, serviceURL, identityClient.AuthenticateRequest) require.NoError(t, err) - err = cyberArkClient.PostDataReadingsWithOptions(ctx, api.DataReadingsPost{}, dataupload.Options{ - ClusterName: "bb068932-c80d-460d-88df-34bc7f3f3297", - }) + dataReadings := testutil.ParseDataReadings(t, testutil.ReadGZIP(t, "testdata/example-1/datareadings.json.gz")) + err = cyberArkClient.PostDataReadingsWithOptions( + ctx, + dataReadings, + dataupload.Options{ + ClusterName: "bb068932-c80d-460d-88df-34bc7f3f3297", + }, + ) require.NoError(t, err) } diff --git a/pkg/internal/cyberark/dataupload/mock.go b/pkg/internal/cyberark/dataupload/mock.go index f8a2530b..5887253c 100644 --- a/pkg/internal/cyberark/dataupload/mock.go +++ b/pkg/internal/cyberark/dataupload/mock.go @@ -1,6 +1,7 @@ package dataupload import ( + "bytes" "crypto/sha3" "encoding/hex" "encoding/json" @@ -41,10 +42,7 @@ func (mds *mockDataUploadServer) ServeHTTP(w http.ResponseWriter, r *http.Reques mds.handlePresignedUpload(w, r) return case "/presigned-upload": - mds.handleUpload(w, r, false) - return - case "/presigned-upload-invalid-json": - mds.handleUpload(w, r, false) + mds.handleUpload(w, r) return default: w.WriteHeader(http.StatusNotFound) @@ -54,7 +52,10 @@ func (mds *mockDataUploadServer) ServeHTTP(w http.ResponseWriter, r *http.Reques func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) - _, _ = w.Write([]byte(`{"message":"method not allowed"}`)) + _, err := w.Write([]byte(`{"message":"method not allowed"}`)) + if err != nil { + panic(err) + } return } @@ -94,7 +95,10 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r if req.ClusterID == "invalid-json-retrieve-presigned" { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"url":`)) // invalid JSON + _, err := w.Write([]byte(`{"url":`)) // invalid JSON + if err != nil { + panic(err) + } return } @@ -118,10 +122,13 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r }{presignedURL}) } -func (mds *mockDataUploadServer) handleUpload(w http.ResponseWriter, r *http.Request, invalidJSON bool) { +func (mds *mockDataUploadServer) handleUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { w.WriteHeader(http.StatusMethodNotAllowed) - _, _ = w.Write([]byte(`{"message":"method not allowed"}`)) + _, err := w.Write([]byte(`{"message":"method not allowed"}`)) + if err != nil { + panic(err) + } return } @@ -130,21 +137,26 @@ func (mds *mockDataUploadServer) handleUpload(w http.ResponseWriter, r *http.Req return } - if invalidJSON { - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"url":`)) // invalid JSON - return + body, err := io.ReadAll(r.Body) + if err != nil { + panic(err) } checksum := sha3.New256() - _, _ = io.Copy(checksum, r.Body) - + _, err = checksum.Write(body) + if err != nil { + panic(err) + } if r.URL.Query().Get("checksum") != hex.EncodeToString(checksum.Sum(nil)) { http.Error(w, "checksum is invalid", http.StatusInternalServerError) } + var snapshot snapshot + d := json.NewDecoder(bytes.NewBuffer(body)) + d.DisallowUnknownFields() + if err := d.Decode(&snapshot); err != nil { + panic(err) + } + w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"success":true}`)) } diff --git a/pkg/internal/cyberark/dataupload/snapshot.go b/pkg/internal/cyberark/dataupload/snapshot.go new file mode 100644 index 00000000..41e2c9e5 --- /dev/null +++ b/pkg/internal/cyberark/dataupload/snapshot.go @@ -0,0 +1,102 @@ +package dataupload + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/jetstack/preflight/api" + "github.com/jetstack/preflight/pkg/version" +) + +type resourceData map[string][]*unstructured.Unstructured + +// snapshot is the JSON that the CyberArk Discovery and Context API expects to +// be uploaded to the AWS presigned URL. +type snapshot struct { + AgentVersion string `json:"agent_version"` + ClusterID string `json:"cluster_id"` + K8SVersion string `json:"k8s_version"` + Secrets []*unstructured.Unstructured `json:"secrets"` + ServiceAccounts []*unstructured.Unstructured `json:"service_accounts"` + Roles []*unstructured.Unstructured `json:"roles"` + RoleBindings []*unstructured.Unstructured `json:"role_bindings"` +} + +// The names of Datagatherers which have the data to populate the Cyberark +// Snapshot mapped to the key in the Cyberark snapshot. +var gathererNameToResourceDataKeyMap = map[string]string{ + "ark/secrets": "secrets", + "ark/serviceaccounts": "serviceaccounts", + "ark/roles": "roles", + "ark/clusterroles": "roles", + "ark/rolebindings": "rolebindings", + "ark/clusterrolebindings": "rolebindings", +} + +// extractResourceListFromReading converts the opaque data from a DynamicData +// data reading to Unstructured resources, to allow access to the metadata and +// other kubernetes API fields. +func extractResourceListFromReading(reading *api.DataReading) ([]*unstructured.Unstructured, error) { + data, ok := reading.Data.(*api.DynamicData) + if !ok { + return nil, fmt.Errorf("failed to convert data: %s", reading.DataGatherer) + } + items := data.Items + resources := make([]*unstructured.Unstructured, len(items)) + for i, item := range items { + if resource, ok := item.Resource.(*unstructured.Unstructured); ok { + resources[i] = resource + } else { + return nil, fmt.Errorf("failed to convert resource: %#v", item) + } + } + return resources, nil +} + +// extractServerVersionFromReading converts the opaque data from a DiscoveryData +// data reding to allow access to the Kubernetes version fields within. +func extractServerVersionFromReading(reading *api.DataReading) (string, error) { + data, ok := reading.Data.(*api.DiscoveryData) + if !ok { + return "", fmt.Errorf("failed to convert data: %s", reading.DataGatherer) + } + if data.ServerVersion == nil { + return "unknown", nil + } + return data.ServerVersion.GitVersion, nil +} + +// convertDataReadingsToCyberarkSnapshot converts DataReadings to the Cyberark +// Snapshot format. +func convertDataReadingsToCyberarkSnapshot( + readings []*api.DataReading, +) (*snapshot, error) { + k8sVersion := "" + resourceData := resourceData{} + for _, reading := range readings { + if reading.DataGatherer == "ark/discovery" { + var err error + k8sVersion, err = extractServerVersionFromReading(reading) + if err != nil { + return nil, fmt.Errorf("while extracting server version from data-reading: %s", err) + } + } + if key, found := gathererNameToResourceDataKeyMap[reading.DataGatherer]; found { + resources, err := extractResourceListFromReading(reading) + if err != nil { + return nil, fmt.Errorf("while extracting resource list from data-reading: %s", err) + } + resourceData[key] = append(resourceData[key], resources...) + } + } + + return &snapshot{ + AgentVersion: version.PreflightVersion, + K8SVersion: k8sVersion, + Secrets: resourceData["secrets"], + ServiceAccounts: resourceData["serviceaccounts"], + Roles: resourceData["roles"], + RoleBindings: resourceData["rolebindings"], + }, nil +} diff --git a/pkg/internal/cyberark/dataupload/snapshot_test.go b/pkg/internal/cyberark/dataupload/snapshot_test.go new file mode 100644 index 00000000..f9fae63e --- /dev/null +++ b/pkg/internal/cyberark/dataupload/snapshot_test.go @@ -0,0 +1,29 @@ +package dataupload + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jetstack/preflight/pkg/testutil" +) + +func TestConvertDataReadingsToCyberarkSnapshot(t *testing.T) { + dataReadings := testutil.ParseDataReadings(t, testutil.ReadGZIP(t, "testdata/example-1/datareadings.json.gz")) + snapshot, err := convertDataReadingsToCyberarkSnapshot(dataReadings) + require.NoError(t, err) + + actualSnapshotBytes, err := json.MarshalIndent(snapshot, "", " ") + require.NoError(t, err) + + goldenFilePath := "testdata/example-1/snapshot.json.gz" + if _, update := os.LookupEnv("UPDATE_GOLDEN_FILES"); update { + testutil.WriteGZIP(t, goldenFilePath, actualSnapshotBytes) + } else { + expectedSnapshotBytes := testutil.ReadGZIP(t, goldenFilePath) + assert.JSONEq(t, string(expectedSnapshotBytes), string(actualSnapshotBytes)) + } +} diff --git a/pkg/internal/cyberark/dataupload/testdata/example-1/README.md b/pkg/internal/cyberark/dataupload/testdata/example-1/README.md new file mode 100644 index 00000000..0ea38f86 --- /dev/null +++ b/pkg/internal/cyberark/dataupload/testdata/example-1/README.md @@ -0,0 +1,27 @@ +# README + +Data captured from a cert-manager E2E test cluster. + +```bash +cd cert-manager +make e2e-setup +``` + +```bash +cd jetstack-secure +go run . agent \ + --api-token not-used \ + --install-namespace venafi \ + --log-level 6 \ + --one-shot \ + --agent-config-file pkg/internal/cyberark/dataupload/testdata/example-1/agent.yaml \ + --output-path pkg/internal/cyberark/dataupload/testdata/example-1/datareadings.json +gzip pkg/internal/cyberark/dataupload/testdata/example-1/datareadings.json +``` + + +To recreate the golden output file: + +```bash +UPDATE_GOLDEN_FILES=true go test ./pkg/internal/cyberark/dataupload/... -run TestConvertDataReadingsToCyberarkSnapshot +``` diff --git a/pkg/internal/cyberark/dataupload/testdata/example-1/agent.yaml b/pkg/internal/cyberark/dataupload/testdata/example-1/agent.yaml new file mode 100644 index 00000000..27f4f38b --- /dev/null +++ b/pkg/internal/cyberark/dataupload/testdata/example-1/agent.yaml @@ -0,0 +1,54 @@ +cluster_id: example-cluster-id +organization_id: example-organization-id +data-gatherers: +# gather k8s apiserver version information +- kind: k8s-discovery + name: ark/discovery +- kind: k8s-dynamic + name: ark/serviceaccounts + config: + resource-type: + resource: serviceaccounts + version: v1 +- kind: k8s-dynamic + name: ark/secrets + config: + resource-type: + version: v1 + resource: secrets + field-selectors: + - type!=kubernetes.io/service-account-token + - type!=kubernetes.io/dockercfg + - type!=kubernetes.io/dockerconfigjson + - type!=kubernetes.io/basic-auth + - type!=kubernetes.io/ssh-auth + - type!=bootstrap.kubernetes.io/token + - type!=helm.sh/release.v1 +- kind: k8s-dynamic + name: ark/roles + config: + resource-type: + version: v1 + group: rbac.authorization.k8s.io + resource: roles +- kind: k8s-dynamic + name: ark/clusterroles + config: + resource-type: + version: v1 + group: rbac.authorization.k8s.io + resource: clusterroles +- kind: k8s-dynamic + name: ark/rolebindings + config: + resource-type: + version: v1 + group: rbac.authorization.k8s.io + resource: rolebindings +- kind: k8s-dynamic + name: ark/clusterrolebindings + config: + resource-type: + version: v1 + group: rbac.authorization.k8s.io + resource: clusterrolebindings diff --git a/pkg/internal/cyberark/dataupload/testdata/example-1/datareadings.json.gz b/pkg/internal/cyberark/dataupload/testdata/example-1/datareadings.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..27d1e4abfb6b6596bcf3c6bc5d774423d8e8ec42 GIT binary patch literal 29312 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
-T>&o`Y|XUe7m=|E*Vu z-{!L)zUf)HsP3<(hp&(ctxvy23ex3Md@>+ImmChu&?QMjm$bq{)Iyi2<~vvhOba43 zD9I2|Ty8;$je|gq5J442B0DMQlDDYwxqW?BXspX*nYAq{MFI%VvUX$w@6aydG)~-s8*y3eknlK(n2Vt zyuisjUI_sUPV9Qm+H_ zYq7u>w5OyhSd*ZHWV0Y0t5W!hT-!AoY!e?!&|SoE2k zDRWmA%P^>8x0hB{B~!MLa$NiPt4FPPM;l{!e)La%rRT8YlDs^;-Q0&!pb9}ORD_*J70 z)))m@C^_V%vd*cJZBmHK+K zTvPjUmZ)}w=4fzhrTY~b?(9cDy6c}bJ&&qUS1DhN^do@>(_eiPQO2{i2t)(ndMV` z1zlTN{BA{8A6K;DhtGPofn{j<`0hSJ?eF1?Eb6=jNt+AK2_eR@U;uMNfif3BlHeg& z=b07`lZc2ZUC|cy--4>x&Fk%e^aTai6gB6(4?m!NK;zCrG?kRNBnI+BsFyzGrgXJ@b%NI{PTQI z{QjO#n|x15=`=OSpb9Z8)?0ru>H@5945BiiT;s^CI?z76%%>r^9qnAa3(?KitC#2M z)SDOgFbNU%o3*T0zT?Qi4|>91Hjm}^?o~=$Z)vmq(=S%O0uM>f^{)$O#<~e#%paxG zrfoT9ov3(v$%)(bEc;{!qlN{g7`e#$+Hr0*`!RZ`o-qsXwUR}b8*#Q7Jsa$orlbonf)Nq zbwPm6_}tqcb(ZC*72AmH(mMqiPHGJ@l5j3Udj09B*wU&$SRN)*VaZcH=TK!W5X=iIk-dF8Jq_J-C*~21^*4r;} zz9>!wle+~!b_+V~pO%YqFK;gI7;I+!;hK7akSzsJflpJzK~qCPV#hdDtSG!3?s6C~ zU%3Xe7)eJ z8w;{$3MIPrnq|rj8rij(ylb=B%rEw7cyoUv4ORPi3SF#SosqOYj>oB#Xr?=SxcRI#{$J~ajaWJqOQ literal 0 HcmV?d00001 diff --git a/pkg/internal/cyberark/dataupload/testdata/example-1/snapshot.json.gz b/pkg/internal/cyberark/dataupload/testdata/example-1/snapshot.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..68ca44bb9dd708616ae4e91f6e926a8322135cad GIT binary patch literal 26955 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 literal 0 HcmV?d00001 diff --git a/pkg/testutil/datareadings.go b/pkg/testutil/datareadings.go new file mode 100644 index 00000000..9b281325 --- /dev/null +++ b/pkg/testutil/datareadings.go @@ -0,0 +1,90 @@ +package testutil + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/jetstack/preflight/api" +) + +// ParseDataReadings decodes JSON encoded datareadings. +// It attempts to decode the data of each reading into a concrete type. +// It tries to decode the data as DynamicData and DiscoveryData and then gives +// up with a test failure. +// This function is useful for reading sample datareadings from disk for use in +// CyberArk dataupload client tests, which require the datareadings data to have +// rich types +// TODO(wallrj): Refactor this so that it can be used with the `agent +// --input-path` feature, to enable datareadings to be read from disk and pushed +// to CyberArk. +func ParseDataReadings(t *testing.T, data []byte) []*api.DataReading { + var dataReadings []*api.DataReading + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err := decoder.Decode(&dataReadings) + require.NoError(t, err) + + for _, reading := range dataReadings { + dataBytes, err := json.Marshal(reading.Data) + require.NoError(t, err) + in := bytes.NewReader(dataBytes) + d := json.NewDecoder(in) + d.DisallowUnknownFields() + + var dynamicGatherData api.DynamicData + if err := d.Decode(&dynamicGatherData); err == nil { + reading.Data = &dynamicGatherData + continue + } + + _, err = in.Seek(0, 0) + require.NoError(t, err) + + var discoveryData api.DiscoveryData + if err = d.Decode(&discoveryData); err == nil { + reading.Data = &discoveryData + continue + } + + require.Failf(t, "failed to parse reading", "reading: %#v", reading) + } + return dataReadings +} + +// ReadGZIP Reads the gzip file at path, and returns the decompressed bytes +func ReadGZIP(t *testing.T, path string) []byte { + f, err := os.Open(path) + require.NoError(t, err) + defer func() { require.NoError(t, f.Close()) }() + gzr, err := gzip.NewReader(f) + require.NoError(t, err) + defer func() { require.NoError(t, gzr.Close()) }() + bytes, err := io.ReadAll(gzr) + require.NoError(t, err) + return bytes +} + +// WriteGZIP writes gzips the data and writes it to path. +func WriteGZIP(t *testing.T, path string, data []byte) { + tmp, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*") + require.NoError(t, err) + gzw := gzip.NewWriter(tmp) + _, err = gzw.Write(data) + require.NoError(t, errors.Join( + err, + gzw.Flush(), + gzw.Close(), + tmp.Close(), + )) + err = os.Rename(tmp.Name(), path) + require.NoError(t, err) +}