@@ -2,15 +2,23 @@ package client
22
33import (
44 "context"
5+ "crypto/x509"
6+ "encoding/base64"
7+ "encoding/pem"
58 "fmt"
69 "net/http"
710
11+ "github.com/go-logr/logr"
12+ corev1 "k8s.io/api/core/v1"
13+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
814 "k8s.io/apimachinery/pkg/runtime"
915 "k8s.io/apimachinery/pkg/util/sets"
16+ "k8s.io/klog/v2"
1017
1118 "github.com/jetstack/preflight/api"
1219 "github.com/jetstack/preflight/internal/cyberark"
1320 "github.com/jetstack/preflight/internal/cyberark/dataupload"
21+ "github.com/jetstack/preflight/pkg/logs"
1422 "github.com/jetstack/preflight/pkg/version"
1523)
1624
@@ -44,10 +52,13 @@ func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
4452// then uploads a snapshot.
4553// The supplied Options are not used by this publisher.
4654func (o * CyberArkClient ) PostDataReadingsWithOptions (ctx context.Context , readings []* api.DataReading , _ Options ) error {
55+ log := klog .FromContext (ctx )
4756 var snapshot dataupload.Snapshot
4857 if err := convertDataReadings (defaultExtractorFunctions , readings , & snapshot ); err != nil {
4958 return fmt .Errorf ("while converting data readings: %s" , err )
5059 }
60+ minimizeSnapshot (log , & snapshot )
61+
5162 snapshot .AgentVersion = version .PreflightVersion
5263
5364 cfg , err := o .configLoader ()
@@ -66,6 +77,35 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
6677 return nil
6778}
6879
80+ // minimizeSnapshot reduces the size of the snapshot by removing unnecessary data.
81+ //
82+ // This reduces the bandwidth used when uploading the snapshot to CyberArk,
83+ // it reduces the storage used by CyberArk to store the snapshot, and
84+ // it provides better privacy for the cluster being scanned; only the necessary
85+ // data is included in the snapshot.
86+ //
87+ // This is a best-effort attempt to minimize the snapshot size. Errors during
88+ // minimization are logged but do not prevent the snapshot from being uploaded.
89+ //
90+ // It performs the following minimization steps:
91+ //
92+ // 1. Removal of non-clientauth TLS secrets: It filters out TLS secrets that do
93+ // not contain a client certificate. This is done to avoid uploading large
94+ // TLS secrets that are not relevant for the CyberArk Discovery and Context
95+ // service.
96+ func minimizeSnapshot (log logr.Logger , snapshot * dataupload.Snapshot ) {
97+ originalSecretCount := len (snapshot .Secrets )
98+ filteredSecrets := make ([]runtime.Object , 0 , originalSecretCount )
99+ for _ , secret := range snapshot .Secrets {
100+ if isTLSSecretWithoutClientCert (log , secret ) {
101+ continue
102+ }
103+ filteredSecrets = append (filteredSecrets , secret )
104+ }
105+ snapshot .Secrets = filteredSecrets
106+ log .V (logs .Debug ).Info ("Minimized snapshot" , "originalSecretCount" , originalSecretCount , "filteredSecretCount" , len (snapshot .Secrets ))
107+ }
108+
69109// extractClusterIDAndServerVersionFromReading converts the opaque data from a DiscoveryData
70110// data reading to allow access to the Kubernetes version fields within.
71111func extractClusterIDAndServerVersionFromReading (reading * api.DataReading , target * dataupload.Snapshot ) error {
@@ -190,3 +230,118 @@ func convertDataReadings(
190230 }
191231 return nil
192232}
233+
234+ // isTLSSecretWithoutClientCert filters out all TLS secrets that do not
235+ // contain a client certificate in the `tls.crt` key.
236+ // Secrets are obtained by a DynamicClient, so they have type
237+ // *unstructured.Unstructured.
238+ func isTLSSecretWithoutClientCert (log logr.Logger , obj runtime.Object ) bool {
239+ logTrace := log .V (logs .Trace )
240+ // Fast path: type assertion and kind/type checks
241+ unstructuredObj , ok := obj .(* unstructured.Unstructured )
242+ if ! ok {
243+ logTrace .Info ("Object is not a Unstructured" , "type" , fmt .Sprintf ("%T" , obj ))
244+ return false
245+ }
246+ if unstructuredObj .GetKind () != "Secret" || unstructuredObj .GetAPIVersion () != "v1" {
247+ return false
248+ }
249+
250+ logTrace = logTrace .WithValues ("namespace" , unstructuredObj .GetNamespace (), "name" , unstructuredObj .GetName ())
251+
252+ secretType , found , err := unstructured .NestedString (unstructuredObj .Object , "type" )
253+ if err != nil || ! found || secretType != string (corev1 .SecretTypeTLS ) {
254+ logTrace .Info ("Object is not a TLS Secret" , "type" , secretType )
255+ return false
256+ }
257+
258+ // Directly extract tls.crt from unstructured data (avoid conversion if possible)
259+ dataMap , found , err := unstructured .NestedMap (unstructuredObj .Object , "data" )
260+ if err != nil || ! found {
261+ logTrace .Info ("Secret data missing or not a map" )
262+ return true
263+ }
264+ tlsCrtRaw , found := dataMap [corev1 .TLSCertKey ]
265+ if ! found {
266+ logTrace .Info ("TLS Secret does not contain tls.crt key" )
267+ return true
268+ }
269+
270+ // Decode base64 if necessary (K8s secrets store data as base64-encoded strings)
271+ var tlsCrtBytes []byte
272+ switch v := tlsCrtRaw .(type ) {
273+ case string :
274+ decoded , err := base64 .StdEncoding .DecodeString (v )
275+ if err != nil {
276+ logTrace .Info ("Failed to decode tls.crt base64" , "error" , err .Error ())
277+ return true
278+ }
279+ tlsCrtBytes = decoded
280+ case []byte :
281+ tlsCrtBytes = v
282+ default :
283+ logTrace .Info ("tls.crt is not a string or byte slice" , "type" , fmt .Sprintf ("%T" , v ))
284+ return true
285+ }
286+
287+ // Parse PEM certificate chain
288+ certs , err := parsePEMCertificateChain (tlsCrtBytes )
289+ if err != nil || len (certs ) == 0 {
290+ logTrace .Info ("Failed to parse tls.crt as PEM encoded X.509 certificate chain" , "error" , err .Error ())
291+ return true
292+ }
293+
294+ // Check if the leaf certificate is a client certificate
295+ if isClientCertificate (certs [0 ]) {
296+ logTrace .Info ("TLS Secret contains a client certificate" )
297+ return false
298+ }
299+
300+ logTrace .Info ("TLS Secret does not contain a client certificate" )
301+ return true
302+ }
303+
304+ // isClientCertificate checks if the given certificate is a client certificate
305+ // by checking if it has the ClientAuth EKU.
306+ func isClientCertificate (cert * x509.Certificate ) bool {
307+ if cert == nil {
308+ return false
309+ }
310+ // Check if the certificate has the ClientAuth EKU
311+ for _ , eku := range cert .ExtKeyUsage {
312+ if eku == x509 .ExtKeyUsageClientAuth {
313+ return true
314+ }
315+ }
316+ return false
317+ }
318+
319+ // parsePEMCertificateChain parses a PEM encoded certificate chain and returns
320+ // a slice of x509.Certificate pointers. It returns an error if the data cannot
321+ // be parsed as a certificate chain.
322+ // The supplied data can contain multiple PEM blocks, the function will parse
323+ // all of them and return a slice of certificates.
324+ func parsePEMCertificateChain (data []byte ) ([]* x509.Certificate , error ) {
325+ // Parse the PEM encoded certificate chain
326+ var certs []* x509.Certificate
327+ var block * pem.Block
328+ rest := data
329+ for {
330+ block , rest = pem .Decode (rest )
331+ if block == nil {
332+ break
333+ }
334+ if block .Type != "CERTIFICATE" || len (block .Bytes ) == 0 {
335+ continue
336+ }
337+ cert , err := x509 .ParseCertificate (block .Bytes )
338+ if err != nil {
339+ return nil , fmt .Errorf ("failed to parse certificate: %w" , err )
340+ }
341+ certs = append (certs , cert )
342+ }
343+ if len (certs ) == 0 {
344+ return nil , fmt .Errorf ("no certificates found" )
345+ }
346+ return certs , nil
347+ }
0 commit comments