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