@@ -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,162 @@ 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. If an error occurs
216
+ // during analysis of a secret, the error is logged and the secret is kept in the
217
+ // snapshot (i.e., not excluded). Errors do not prevent the snapshot from being uploaded.
218
+ //
219
+ // It performs the following minimization steps:
220
+ //
221
+ // 1. Removal of non-clientauth TLS secrets: It filters out TLS secrets that do
222
+ // not contain a client certificate. This is done to avoid uploading large
223
+ // TLS secrets that are not relevant for the CyberArk Discovery and Context
224
+ // service.
225
+ //
226
+ // TODO(wallrj): Remove more from the snapshot as we learn more about what
227
+ // resources the Discovery and Context service require.
228
+ func minimizeSnapshot (log logr.Logger , snapshot * dataupload.Snapshot ) {
229
+ originalSecretCount := len (snapshot .Secrets )
230
+ filteredSecrets := make ([]runtime.Object , 0 , originalSecretCount )
231
+ for _ , secret := range snapshot .Secrets {
232
+ if isExcludableSecret (log , secret ) {
233
+ continue
234
+ }
235
+ filteredSecrets = append (filteredSecrets , secret )
236
+ }
237
+ snapshot .Secrets = filteredSecrets
238
+ log .Info ("Minimized snapshot" , "originalSecretCount" , originalSecretCount , "filteredSecretCount" , len (snapshot .Secrets ))
239
+ }
240
+
241
+ // isExcludableSecret filters out TLS secrets that are definitely of no interest
242
+ // to CyberArk's Discovery and Context service, specifically TLS secrets that do
243
+ // not contain a client certificate.
244
+ //
245
+ // The Secret is kept if there is any doubt or if there is a problem decoding
246
+ // its contents.
247
+ //
248
+ // Secrets are obtained by a DynamicClient, so they have type
249
+ // *unstructured.Unstructured.
250
+ func isExcludableSecret (log logr.Logger , obj runtime.Object ) bool {
251
+ // Fast path: type assertion and kind/type checks
252
+ unstructuredObj , ok := obj .(* unstructured.Unstructured )
253
+ if ! ok {
254
+ log .Info ("Object is not a Unstructured" , "type" , fmt .Sprintf ("%T" , obj ))
255
+ return false
256
+ }
257
+ if unstructuredObj .GetKind () != "Secret" || unstructuredObj .GetAPIVersion () != "v1" {
258
+ return false
259
+ }
260
+
261
+ log = log .WithValues ("namespace" , unstructuredObj .GetNamespace (), "name" , unstructuredObj .GetName ())
262
+ dataMap , found , err := unstructured .NestedMap (unstructuredObj .Object , "data" )
263
+ if err != nil || ! found {
264
+ log .Info ("Secret data missing or not a map" )
265
+ return false
266
+ }
267
+
268
+ secretType , found , err := unstructured .NestedString (unstructuredObj .Object , "type" )
269
+ if err != nil || ! found {
270
+ log .Info ("Secret object has no type" )
271
+ return false
272
+ }
273
+
274
+ if corev1 .SecretType (secretType ) != corev1 .SecretTypeTLS {
275
+ log .Info ("Secrets of this type are never excluded" , "type" , secretType )
276
+ return false
277
+ }
278
+
279
+ return isExcludableTLSSecret (log , dataMap )
280
+ }
281
+
282
+ // isExcludableTLSSecret checks if a TLS Secret contains a client certificate.
283
+ // It returns true if the Secret is a TLS Secret and its tls.crt does not
284
+ // contain a client certificate.
285
+ func isExcludableTLSSecret (log logr.Logger , dataMap map [string ]interface {}) bool {
286
+ tlsCrtRaw , found := dataMap [corev1 .TLSCertKey ]
287
+ if ! found {
288
+ log .Info ("TLS Secret does not contain tls.crt key" )
289
+ return true
290
+ }
291
+
292
+ // Decode base64 if necessary (K8s secrets store data as base64-encoded strings)
293
+ var tlsCrtBytes []byte
294
+ switch v := tlsCrtRaw .(type ) {
295
+ case string :
296
+ decoded , err := base64 .StdEncoding .DecodeString (v )
297
+ if err != nil {
298
+ log .Info ("Failed to decode tls.crt base64" , "error" , err .Error ())
299
+ return true
300
+ }
301
+ tlsCrtBytes = decoded
302
+ case []byte :
303
+ tlsCrtBytes = v
304
+ default :
305
+ log .Info ("tls.crt is not a string or byte slice" , "type" , fmt .Sprintf ("%T" , v ))
306
+ return true
307
+ }
308
+
309
+ // Parse PEM certificate chain
310
+ hasClientCert := searchPEM (tlsCrtBytes , func (block * pem.Block ) bool {
311
+ if block .Type != "CERTIFICATE" || len (block .Bytes ) == 0 {
312
+ return false
313
+ }
314
+ cert , err := x509 .ParseCertificate (block .Bytes )
315
+ if err != nil {
316
+ log .Info ("Failed to parse PEM block as X.509 certificate" , "error" , err .Error ())
317
+ return false
318
+ }
319
+ // Check if the certificate has the ClientAuth EKU
320
+ return isClientCertificate (cert )
321
+ })
322
+ return ! hasClientCert
323
+ }
324
+
325
+ // searchPEM parses the given PEM data and applies the visitor function to each
326
+ // PEM block found. If the visitor function returns true for any block, the search
327
+ // stops and searchPEM returns true. If no blocks cause the visitor to return true,
328
+ // searchPEM returns false.
329
+ func searchPEM (data []byte , visitor func (* pem.Block ) bool ) bool {
330
+ if visitor == nil {
331
+ return false
332
+ }
333
+ // Parse the PEM encoded certificate chain
334
+ var block * pem.Block
335
+ rest := data
336
+ for {
337
+ block , rest = pem .Decode (rest )
338
+ if block == nil {
339
+ break
340
+ }
341
+ if visitor (block ) {
342
+ return true
343
+ }
344
+ }
345
+ return false
346
+ }
347
+
348
+ // isClientCertificate checks if the given certificate is a client certificate
349
+ // by checking if it has the ClientAuth EKU.
350
+ func isClientCertificate (cert * x509.Certificate ) bool {
351
+ if cert == nil {
352
+ return false
353
+ }
354
+ // Skip CA certificates
355
+ if cert .IsCA {
356
+ return false
357
+ }
358
+ // Check if the certificate has the ClientAuth EKU
359
+ for _ , eku := range cert .ExtKeyUsage {
360
+ if eku == x509 .ExtKeyUsageClientAuth {
361
+ return true
362
+ }
363
+ }
364
+ return false
365
+ }
0 commit comments