@@ -2,15 +2,23 @@ package client
2
2
3
3
import (
4
4
"context"
5
+ "crypto/x509"
6
+ "encoding/base64"
7
+ "encoding/pem"
5
8
"fmt"
6
9
"net/http"
7
10
11
+ "github.com/go-logr/logr"
12
+ corev1 "k8s.io/api/core/v1"
13
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8
14
"k8s.io/apimachinery/pkg/runtime"
9
15
"k8s.io/apimachinery/pkg/util/sets"
16
+ "k8s.io/klog/v2"
10
17
11
18
"github.com/jetstack/preflight/api"
12
19
"github.com/jetstack/preflight/internal/cyberark"
13
20
"github.com/jetstack/preflight/internal/cyberark/dataupload"
21
+ "github.com/jetstack/preflight/pkg/logs"
14
22
"github.com/jetstack/preflight/pkg/version"
15
23
)
16
24
@@ -40,14 +48,20 @@ func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
40
48
41
49
// PostDataReadingsWithOptions uploads data readings to CyberArk.
42
50
// It converts the supplied data readings into a snapshot format expected by CyberArk.
51
+ // It then minimizes the snapshot to avoid uploading unnecessary data.
43
52
// It initializes a data upload client with the configured HTTP client and credentials,
44
53
// then uploads a snapshot.
45
54
// The supplied Options are not used by this publisher.
46
55
func (o * CyberArkClient ) PostDataReadingsWithOptions (ctx context.Context , readings []* api.DataReading , _ Options ) error {
56
+ log := klog .FromContext (ctx )
47
57
var snapshot dataupload.Snapshot
48
58
if err := convertDataReadings (defaultExtractorFunctions , readings , & snapshot ); err != nil {
49
59
return fmt .Errorf ("while converting data readings: %s" , err )
50
60
}
61
+
62
+ // Minimize the snapshot to reduce size and improve privacy
63
+ minimizeSnapshot (log .V (logs .Debug ), & snapshot )
64
+
51
65
snapshot .AgentVersion = version .PreflightVersion
52
66
53
67
cfg , err := o .configLoader ()
@@ -190,3 +204,158 @@ func convertDataReadings(
190
204
}
191
205
return nil
192
206
}
207
+
208
+ // minimizeSnapshot reduces the size of the snapshot by removing unnecessary data.
209
+ //
210
+ // This reduces the bandwidth used when uploading the snapshot to CyberArk,
211
+ // it reduces the storage used by CyberArk to store the snapshot, and
212
+ // it provides better privacy for the cluster being scanned; only the necessary
213
+ // data is included in the snapshot.
214
+ //
215
+ // This is a best-effort attempt to minimize the snapshot size. Errors during
216
+ // minimization are logged but do not prevent the snapshot from being uploaded.
217
+ //
218
+ // It performs the following minimization steps:
219
+ //
220
+ // 1. Removal of non-clientauth TLS secrets: It filters out TLS secrets that do
221
+ // not contain a client certificate. This is done to avoid uploading large
222
+ // TLS secrets that are not relevant for the CyberArk Discovery and Context
223
+ // service.
224
+ func minimizeSnapshot (log logr.Logger , snapshot * dataupload.Snapshot ) {
225
+ originalSecretCount := len (snapshot .Secrets )
226
+ filteredSecrets := make ([]runtime.Object , 0 , originalSecretCount )
227
+ for _ , secret := range snapshot .Secrets {
228
+ if isExcludableSecret (log , secret ) {
229
+ continue
230
+ }
231
+ filteredSecrets = append (filteredSecrets , secret )
232
+ }
233
+ snapshot .Secrets = filteredSecrets
234
+ log .Info ("Minimized snapshot" , "originalSecretCount" , originalSecretCount , "filteredSecretCount" , len (snapshot .Secrets ))
235
+ }
236
+
237
+ // isExcludableSecret filters out TLS secrets that are definitely of no interest
238
+ // to CyberArk's Discovery and Context service, specifically TLS secrets that do
239
+ // not contain a client certificate.
240
+ //
241
+ // The Secret is kept if there is any doubt or if there is a problem decoding
242
+ // its contents.
243
+ //
244
+ // Secrets are obtained by a DynamicClient, so they have type
245
+ // *unstructured.Unstructured.
246
+ func isExcludableSecret (log logr.Logger , obj runtime.Object ) bool {
247
+ // Fast path: type assertion and kind/type checks
248
+ unstructuredObj , ok := obj .(* unstructured.Unstructured )
249
+ if ! ok {
250
+ log .Info ("Object is not a Unstructured" , "type" , fmt .Sprintf ("%T" , obj ))
251
+ return false
252
+ }
253
+ if unstructuredObj .GetKind () != "Secret" || unstructuredObj .GetAPIVersion () != "v1" {
254
+ return false
255
+ }
256
+
257
+ log = log .WithValues ("namespace" , unstructuredObj .GetNamespace (), "name" , unstructuredObj .GetName ())
258
+ dataMap , found , err := unstructured .NestedMap (unstructuredObj .Object , "data" )
259
+ if err != nil || ! found {
260
+ log .Info ("Secret data missing or not a map" )
261
+ return false
262
+ }
263
+
264
+ secretType , found , err := unstructured .NestedString (unstructuredObj .Object , "type" )
265
+ if err != nil || ! found {
266
+ log .Info ("Secret object has no type" )
267
+ return false
268
+ }
269
+
270
+ if corev1 .SecretType (secretType ) != corev1 .SecretTypeTLS {
271
+ log .Info ("Secret of this type are never excluded" , "type" , secretType )
272
+ return false
273
+ }
274
+
275
+ return isExcludableTLSSecret (log , dataMap )
276
+ }
277
+
278
+ // isExcludableTLSSecret checks if a TLS Secret contains a client certificate.
279
+ // It returns true if the Secret is a TLS Secret and its tls.crt does not
280
+ // contain a client certificate.
281
+ func isExcludableTLSSecret (log logr.Logger , dataMap map [string ]interface {}) bool {
282
+ tlsCrtRaw , found := dataMap [corev1 .TLSCertKey ]
283
+ if ! found {
284
+ log .Info ("TLS Secret does not contain tls.crt key" )
285
+ return true
286
+ }
287
+
288
+ // Decode base64 if necessary (K8s secrets store data as base64-encoded strings)
289
+ var tlsCrtBytes []byte
290
+ switch v := tlsCrtRaw .(type ) {
291
+ case string :
292
+ decoded , err := base64 .StdEncoding .DecodeString (v )
293
+ if err != nil {
294
+ log .Info ("Failed to decode tls.crt base64" , "error" , err .Error ())
295
+ return true
296
+ }
297
+ tlsCrtBytes = decoded
298
+ case []byte :
299
+ tlsCrtBytes = v
300
+ default :
301
+ log .Info ("tls.crt is not a string or byte slice" , "type" , fmt .Sprintf ("%T" , v ))
302
+ return true
303
+ }
304
+
305
+ // Parse PEM certificate chain
306
+ hasClientCert := searchPEM (tlsCrtBytes , func (block * pem.Block ) bool {
307
+ if block .Type != "CERTIFICATE" || len (block .Bytes ) == 0 {
308
+ return false
309
+ }
310
+ cert , err := x509 .ParseCertificate (block .Bytes )
311
+ if err != nil {
312
+ log .Info ("Failed to parse PEM block as X.509 certificate" , "error" , err .Error ())
313
+ return false
314
+ }
315
+ // Check if the certificate has the ClientAuth EKU
316
+ return isClientCertificate (cert )
317
+ })
318
+ return ! hasClientCert
319
+ }
320
+
321
+ // searchPEM parses the given PEM data and applies the visitor function to each
322
+ // PEM block found. If the visitor function returns true for any block, the search
323
+ // stops and searchPEM returns true. If no blocks cause the visitor to return true,
324
+ // searchPEM returns false.
325
+ func searchPEM (data []byte , visitor func (* pem.Block ) bool ) bool {
326
+ if visitor == nil {
327
+ return false
328
+ }
329
+ // Parse the PEM encoded certificate chain
330
+ var block * pem.Block
331
+ rest := data
332
+ for {
333
+ block , rest = pem .Decode (rest )
334
+ if block == nil {
335
+ break
336
+ }
337
+ if visitor (block ) {
338
+ return true
339
+ }
340
+ }
341
+ return false
342
+ }
343
+
344
+ // isClientCertificate checks if the given certificate is a client certificate
345
+ // by checking if it has the ClientAuth EKU.
346
+ func isClientCertificate (cert * x509.Certificate ) bool {
347
+ if cert == nil {
348
+ return false
349
+ }
350
+ // Skip CA certificates
351
+ if cert .IsCA {
352
+ return false
353
+ }
354
+ // Check if the certificate has the ClientAuth EKU
355
+ for _ , eku := range cert .ExtKeyUsage {
356
+ if eku == x509 .ExtKeyUsageClientAuth {
357
+ return true
358
+ }
359
+ }
360
+ return false
361
+ }
0 commit comments