github.com/anchore/syft@v1.38.2/syft/source/stereoscopesource/image_source.go (about) 1 package stereoscopesource 2 3 import ( 4 "fmt" 5 6 "github.com/bmatcuk/doublestar/v4" 7 "github.com/distribution/reference" 8 "github.com/opencontainers/go-digest" 9 10 "github.com/anchore/stereoscope/pkg/image" 11 "github.com/anchore/syft/internal/log" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/internal/fileresolver" 15 "github.com/anchore/syft/syft/source" 16 "github.com/anchore/syft/syft/source/internal" 17 ) 18 19 var _ source.Source = (*stereoscopeImageSource)(nil) 20 21 type ImageConfig struct { 22 Reference string 23 Platform *image.Platform 24 RegistryOptions *image.RegistryOptions 25 Exclude source.ExcludeConfig 26 Alias source.Alias 27 } 28 29 type stereoscopeImageSource struct { 30 id artifact.ID 31 config ImageConfig 32 image *image.Image 33 metadata source.ImageMetadata 34 } 35 36 func New(img *image.Image, cfg ImageConfig) source.Source { 37 metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference) 38 return &stereoscopeImageSource{ 39 id: deriveIDFromStereoscopeImage(cfg.Alias, metadata), 40 config: cfg, 41 image: img, 42 metadata: metadata, 43 } 44 } 45 46 func (s stereoscopeImageSource) ID() artifact.ID { 47 return s.id 48 } 49 50 func (s stereoscopeImageSource) Describe() source.Description { 51 a := s.config.Alias 52 53 name := a.Name 54 supplier := a.Supplier 55 nameIfUnset := func(n string) { 56 if name != "" { 57 return 58 } 59 name = n 60 } 61 62 version := a.Version 63 versionIfUnset := func(v string) { 64 if version != "" && version != "latest" { 65 return 66 } 67 version = v 68 } 69 70 ref, err := reference.Parse(s.metadata.UserInput) 71 if err != nil { 72 log.Debugf("unable to parse image ref: %s", s.config.Reference) 73 } else { 74 if ref, ok := ref.(reference.Named); ok { 75 nameIfUnset(ref.Name()) 76 } 77 78 if ref, ok := ref.(reference.NamedTagged); ok { 79 versionIfUnset(ref.Tag()) 80 } 81 82 if ref, ok := ref.(reference.Digested); ok { 83 versionIfUnset(ref.Digest().String()) 84 } 85 } 86 87 nameIfUnset(s.metadata.UserInput) 88 versionIfUnset(s.metadata.ManifestDigest) 89 90 return source.Description{ 91 ID: string(s.id), 92 Name: name, 93 Version: version, 94 Supplier: supplier, 95 Metadata: s.metadata, 96 } 97 } 98 99 func (s stereoscopeImageSource) FileResolver(scope source.Scope) (file.Resolver, error) { 100 var res file.Resolver 101 var err error 102 103 switch scope { 104 case source.SquashedScope: 105 res, err = fileresolver.NewFromContainerImageSquash(s.image) 106 case source.AllLayersScope: 107 res, err = fileresolver.NewFromContainerImageAllLayers(s.image) 108 case source.DeepSquashedScope: 109 res, err = fileresolver.NewFromContainerImageDeepSquash(s.image) 110 default: 111 return nil, fmt.Errorf("bad image scope provided: %+v", scope) 112 } 113 114 if err != nil { 115 return nil, err 116 } 117 118 // image tree contains all paths, so we filter out the excluded entries afterward 119 if len(s.config.Exclude.Paths) > 0 { 120 res = fileresolver.NewExcludingDecorator(res, getImageExclusionFunction(s.config.Exclude.Paths)) 121 } 122 123 return res, nil 124 } 125 126 func (s stereoscopeImageSource) Close() error { 127 if s.image == nil { 128 return nil 129 } 130 return s.image.Cleanup() 131 } 132 133 func imageMetadataFromStereoscopeImage(img *image.Image, reference string) source.ImageMetadata { 134 tags := make([]string, len(img.Metadata.Tags)) 135 for idx, tag := range img.Metadata.Tags { 136 tags[idx] = tag.String() 137 } 138 139 layers := make([]source.LayerMetadata, len(img.Layers)) 140 for idx, l := range img.Layers { 141 layers[idx] = source.LayerMetadata{ 142 MediaType: string(l.Metadata.MediaType), 143 Digest: l.Metadata.Digest, 144 Size: l.Metadata.Size, 145 } 146 } 147 148 return source.ImageMetadata{ 149 ID: img.Metadata.ID, 150 UserInput: reference, 151 ManifestDigest: img.Metadata.ManifestDigest, 152 Size: img.Metadata.Size, 153 MediaType: string(img.Metadata.MediaType), 154 Tags: tags, 155 Layers: layers, 156 RawConfig: img.Metadata.RawConfig, 157 RawManifest: img.Metadata.RawManifest, 158 RepoDigests: img.Metadata.RepoDigests, 159 Architecture: img.Metadata.Architecture, 160 Variant: img.Metadata.Variant, 161 OS: img.Metadata.OS, 162 Labels: img.Metadata.Config.Config.Labels, 163 } 164 } 165 166 // deriveIDFromStereoscopeImage derives an artifact ID from the given image metadata. The order of data precedence is: 167 // 1. prefer a digest of the raw container image manifest 168 // 2. if no manifest digest is available, calculate a chain ID from the image layer metadata 169 // 3. if no layer metadata is available, use the user input string 170 // 171 // in all cases, if an alias is provided, it is additionally considered in the ID calculation. This allows for the 172 // same image to be scanned multiple times with different aliases and be considered logically different. 173 func deriveIDFromStereoscopeImage(alias source.Alias, metadata source.ImageMetadata) artifact.ID { 174 var input string 175 176 if len(metadata.RawManifest) > 0 { 177 input = digest.Canonical.FromBytes(metadata.RawManifest).String() 178 } else { 179 // calculate chain ID for image sources where manifestDigest is not available 180 // https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid 181 input = calculateChainID(metadata.Layers) 182 if input == "" { 183 // TODO what happens here if image has no layers? 184 // is this case possible? 185 input = digest.Canonical.FromString(metadata.UserInput).String() 186 } 187 } 188 189 if !alias.IsEmpty() { 190 // if the user provided an alias, we want to consider that in the artifact ID. This way if the user 191 // scans the same item but is considered to be logically different, then ID will express that. 192 aliasStr := fmt.Sprintf(":%s@%s", alias.Name, alias.Version) 193 input = digest.Canonical.FromString(input + aliasStr).String() 194 } 195 196 return internal.ArtifactIDFromDigest(input) 197 } 198 199 func calculateChainID(lm []source.LayerMetadata) string { 200 if len(lm) < 1 { 201 return "" 202 } 203 204 // DiffID(L0) = digest of layer 0 205 // https://github.com/anchore/stereoscope/blob/1b1b744a919964f38d14e1416fb3f25221b761ce/pkg/image/layer_metadata.go#L19-L32 206 chainID := lm[0].Digest 207 id := chain(chainID, lm[1:]) 208 209 return id 210 } 211 212 func chain(chainID string, layers []source.LayerMetadata) string { 213 if len(layers) < 1 { 214 return chainID 215 } 216 217 chainID = digest.Canonical.FromString(layers[0].Digest + " " + chainID).String() 218 return chain(chainID, layers[1:]) 219 } 220 221 func getImageExclusionFunction(exclusions []string) func(string) bool { 222 if len(exclusions) == 0 { 223 return nil 224 } 225 // add subpath exclusions 226 for _, exclusion := range exclusions { 227 exclusions = append(exclusions, exclusion+"/**") 228 } 229 return func(path string) bool { 230 for _, exclusion := range exclusions { 231 matches, err := doublestar.Match(exclusion, path) 232 if err != nil { 233 return false 234 } 235 if matches { 236 return true 237 } 238 } 239 return false 240 } 241 }