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 }