github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/imagescan/collector/collector.go (about)

     1  package collector
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  
    12  	fanalyzer "github.com/aquasecurity/trivy/pkg/fanal/analyzer"
    13  	"github.com/aquasecurity/trivy/pkg/fanal/types"
    14  	"github.com/cenkalti/backoff/v4"
    15  	"github.com/google/go-containerregistry/pkg/name"
    16  	v1 "github.com/google/go-containerregistry/pkg/v1"
    17  	"github.com/samber/lo"
    18  	"github.com/sirupsen/logrus"
    19  	"google.golang.org/grpc"
    20  	"google.golang.org/protobuf/types/known/timestamppb"
    21  	"gopkg.in/yaml.v3"
    22  
    23  	analyzer "github.com/castai/image-analyzer"
    24  	"github.com/castai/image-analyzer/image"
    25  	"github.com/castai/image-analyzer/image/hostfs"
    26  	castaipb "github.com/castai/kvisor/api/v1/runtime"
    27  	"github.com/castai/kvisor/cmd/imagescan/config"
    28  )
    29  
    30  type ingestClient interface {
    31  	ImageMetadataIngest(ctx context.Context, in *castaipb.ImageMetadata, opts ...grpc.CallOption) (*castaipb.ImageMetadataIngestResponse, error)
    32  }
    33  
    34  func New(
    35  	log logrus.FieldLogger,
    36  	cfg config.Config,
    37  	ingestClient ingestClient,
    38  	cache analyzer.CacheClient,
    39  	hostfsConfig *hostfs.ContainerdHostFSConfig,
    40  ) *Collector {
    41  	return &Collector{
    42  		log:          log,
    43  		cfg:          cfg,
    44  		ingestClient: ingestClient,
    45  		cache:        cache,
    46  		hostFsConfig: hostfsConfig,
    47  	}
    48  }
    49  
    50  type Collector struct {
    51  	log          logrus.FieldLogger
    52  	cfg          config.Config
    53  	ingestClient ingestClient
    54  	cache        analyzer.CacheClient
    55  	hostFsConfig *hostfs.ContainerdHostFSConfig
    56  }
    57  
    58  type ImageInfo struct {
    59  	ID   string
    60  	Name string
    61  }
    62  
    63  func (c *Collector) Collect(ctx context.Context) error {
    64  	img, cleanup, err := c.getImage(ctx)
    65  	if err != nil {
    66  		return fmt.Errorf("getting image: %w", err)
    67  	}
    68  	defer cleanup()
    69  
    70  	artifact, err := analyzer.NewArtifact(img, c.log, c.cache, analyzer.ArtifactOption{
    71  		Offline:  true,
    72  		Parallel: c.cfg.Parallel,
    73  		DisabledAnalyzers: []fanalyzer.Type{
    74  			fanalyzer.TypeLicenseFile,
    75  			fanalyzer.TypeDpkgLicense,
    76  			fanalyzer.TypeHelm,
    77  		},
    78  	})
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	arRef, err := artifact.Inspect(ctx)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	manifest, err := img.Manifest()
    89  	if err != nil {
    90  		return fmt.Errorf("extract manifest: %w", err)
    91  	}
    92  
    93  	digest, err := img.Digest()
    94  	if err != nil {
    95  		return fmt.Errorf("extract manifest digest: %w", err)
    96  	}
    97  
    98  	metadata := &castaipb.ImageMetadata{
    99  		ImageName:    c.cfg.ImageName,
   100  		ImageId:      c.cfg.ImageID,
   101  		ImageDigest:  digest.String(),
   102  		Architecture: c.cfg.ImageArchitecture,
   103  		ResourceIds:  strings.Split(c.cfg.ResourceIDs, ","),
   104  	}
   105  	if arRef.OsInfo != nil {
   106  		metadata.OsName = arRef.OsInfo.Name
   107  	}
   108  	if arRef.ArtifactInfo != nil {
   109  		metadata.CreatedAt = timestamppb.New(arRef.ArtifactInfo.Created)
   110  	}
   111  	packagesBytes, err := json.Marshal(arRef.BlobsInfo)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	metadata.Packages = packagesBytes
   116  
   117  	manifestBytes, err := json.Marshal(manifest)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	metadata.Manifest = manifestBytes
   122  
   123  	configFileBytes, err := json.Marshal(arRef.ConfigFile)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	metadata.ConfigFile = configFileBytes
   128  
   129  	if index := img.Index(); index != nil {
   130  		indexBytes, err := json.Marshal(index)
   131  		if err != nil {
   132  			return err
   133  		}
   134  		metadata.Index = indexBytes
   135  	}
   136  
   137  	if err := backoff.RetryNotify(func() error {
   138  		return c.sendResult(ctx, metadata)
   139  	}, backoff.WithMaxRetries(backoff.NewConstantBackOff(time.Second), 3), func(err error, duration time.Duration) {
   140  		if err != nil {
   141  			c.log.Errorf("sending result: %v", err)
   142  		}
   143  	}); err != nil {
   144  		return err
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  func (c *Collector) getImage(ctx context.Context) (image.ImageWithIndex, func(), error) {
   151  	imgRef, err := name.ParseReference(c.cfg.ImageName)
   152  	if err != nil {
   153  		return nil, nil, err
   154  	}
   155  	if c.cfg.Mode == config.ModeRemote {
   156  		opts := types.ImageOptions{}
   157  		if c.cfg.ImagePullSecret != "" {
   158  			configData, err := config.ReadImagePullSecret(os.DirFS(config.SecretMountPath))
   159  			if err != nil {
   160  				return nil, nil, fmt.Errorf("reading image pull secret: %w", err)
   161  			}
   162  			cfg := image.DockerConfig{}
   163  			if err := json.Unmarshal(configData, &cfg); err != nil {
   164  				return nil, nil, fmt.Errorf("parsing image pull secret: %w", err)
   165  			}
   166  
   167  			if authKey, auth, ok := findRegistryAuth(cfg, imgRef); ok {
   168  				c.log.Infof("using registry auth, key=%s", authKey)
   169  				opts.RegistryOptions.Credentials = append(opts.RegistryOptions.Credentials, types.Credential{
   170  					Username: auth.Username,
   171  					Password: auth.Password,
   172  				})
   173  				opts.RegistryOptions.RegistryToken = auth.Token
   174  			}
   175  		} else if c.cfg.DockerOptionPath != "" {
   176  			optsData, err := os.ReadFile(c.cfg.DockerOptionPath)
   177  			if err != nil {
   178  				return nil, nil, fmt.Errorf("reading docker options file: %w", err)
   179  			}
   180  			if err := yaml.Unmarshal(optsData, &opts); err != nil {
   181  				return nil, nil, fmt.Errorf("unmarshaling docker options file: %w", err)
   182  			}
   183  		}
   184  		if c.cfg.ImageArchitecture != "" && c.cfg.ImageOS != "" {
   185  			opts.RegistryOptions.Platform = types.Platform{
   186  				Platform: &v1.Platform{
   187  					Architecture: c.cfg.ImageArchitecture,
   188  					OS:           c.cfg.ImageOS,
   189  				},
   190  			}
   191  		}
   192  		img, err := image.NewFromRemote(ctx, c.cfg.ImageName, opts)
   193  		return img, func() {}, err
   194  	}
   195  
   196  	if c.cfg.Runtime == config.RuntimeContainerd {
   197  		if c.cfg.Mode == config.ModeDaemon {
   198  			return image.NewFromContainerdDaemon(ctx, c.cfg.ImageName)
   199  		}
   200  		if c.cfg.Mode == config.ModeHostFS {
   201  			return image.NewFromContainerdHostFS(c.cfg.ImageID, *c.hostFsConfig)
   202  		}
   203  	}
   204  
   205  	if c.cfg.Runtime == config.RuntimeDocker {
   206  		if c.cfg.Mode == config.ModeTarArchive {
   207  			return image.NewFromDockerDaemonTarFile(c.cfg.ImageName, c.cfg.ImageLocalTarPath, imgRef)
   208  		}
   209  		if c.cfg.Mode == config.ModeDaemon {
   210  			return image.NewFromDockerDaemon(c.cfg.ImageName, imgRef)
   211  		}
   212  	}
   213  
   214  	return nil, nil, fmt.Errorf("unknown mode %q", c.cfg.Mode)
   215  }
   216  
   217  func (c *Collector) sendResult(ctx context.Context, imageMetadata *castaipb.ImageMetadata) error {
   218  	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
   219  	defer cancel()
   220  	if _, err := c.ingestClient.ImageMetadataIngest(ctx, imageMetadata); err != nil {
   221  		return err
   222  	}
   223  	return nil
   224  }
   225  
   226  func findRegistryAuth(cfg image.DockerConfig, imgRef name.Reference) (string, image.RegistryAuth, bool) {
   227  	imageRepo := fmt.Sprintf("%s/%s", imgRef.Context().RegistryStr(), imgRef.Context().RepositoryStr())
   228  
   229  	authKeys := lo.Keys(cfg.Auths)
   230  	sort.Strings(authKeys)
   231  
   232  	for _, key := range authKeys {
   233  		// User can provide registries with protocol which we don't care about while comparing with image name.
   234  		prefix := strings.TrimPrefix(key, "http://")
   235  		prefix = strings.TrimPrefix(prefix, "https://")
   236  		if strings.HasPrefix(imageRepo, prefix) {
   237  			return key, cfg.Auths[key], true
   238  		}
   239  	}
   240  	return "", image.RegistryAuth{}, false
   241  }