zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/search/cve/cve.go (about) 1 package cveinfo 2 3 import ( 4 "context" 5 "sort" 6 "strings" 7 "time" 8 9 godigest "github.com/opencontainers/go-digest" 10 ispec "github.com/opencontainers/image-spec/specs-go/v1" 11 12 zerr "zotregistry.io/zot/errors" 13 zcommon "zotregistry.io/zot/pkg/common" 14 cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" 15 "zotregistry.io/zot/pkg/extensions/search/cve/trivy" 16 "zotregistry.io/zot/pkg/log" 17 mTypes "zotregistry.io/zot/pkg/meta/types" 18 "zotregistry.io/zot/pkg/storage" 19 ) 20 21 type CveInfo interface { 22 GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) 23 GetImageListWithCVEFixed(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) 24 GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE string, pageinput cvemodel.PageInput, 25 ) ([]cvemodel.CVE, zcommon.PageInfo, error) 26 GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) 27 } 28 29 type Scanner interface { 30 ScanImage(ctx context.Context, image string) (map[string]cvemodel.CVE, error) 31 IsImageFormatScannable(repo, ref string) (bool, error) 32 IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) 33 IsResultCached(digestStr string) bool 34 GetCachedResult(digestStr string) map[string]cvemodel.CVE 35 UpdateDB(ctx context.Context) error 36 } 37 38 type BaseCveInfo struct { 39 Log log.Logger 40 Scanner Scanner 41 MetaDB mTypes.MetaDB 42 } 43 44 func NewScanner(storeController storage.StoreController, metaDB mTypes.MetaDB, 45 dbRepository, javaDBRepository string, log log.Logger, 46 ) Scanner { 47 return trivy.NewScanner(storeController, metaDB, dbRepository, javaDBRepository, log) 48 } 49 50 func NewCVEInfo(scanner Scanner, metaDB mTypes.MetaDB, log log.Logger) *BaseCveInfo { 51 return &BaseCveInfo{ 52 Log: log, 53 Scanner: scanner, 54 MetaDB: metaDB, 55 } 56 } 57 58 func (cveinfo BaseCveInfo) GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) { 59 imgList := make([]cvemodel.TagInfo, 0) 60 61 repoMeta, err := cveinfo.MetaDB.GetRepoMeta(ctx, repo) 62 if err != nil { 63 cveinfo.Log.Error().Err(err).Str("repository", repo).Str("cve-id", cveID). 64 Msg("unable to get list of tags from repo") 65 66 return imgList, err 67 } 68 69 for tag, descriptor := range repoMeta.Tags { 70 switch descriptor.MediaType { 71 case ispec.MediaTypeImageManifest, ispec.MediaTypeImageIndex: 72 manifestDigestStr := descriptor.Digest 73 74 manifestDigest := godigest.Digest(manifestDigestStr) 75 76 isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr) 77 if !isScanableImage || err != nil { 78 cveinfo.Log.Debug().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable") 79 80 continue 81 } 82 83 cveMap, err := cveinfo.Scanner.ScanImage(ctx, zcommon.GetFullImageName(repo, tag)) 84 if err != nil { 85 if zcommon.IsContextDone(ctx) { 86 return imgList, err 87 } 88 89 cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed") 90 91 continue 92 } 93 94 if _, hasCVE := cveMap[cveID]; hasCVE { 95 imgList = append(imgList, cvemodel.TagInfo{ 96 Tag: tag, 97 Descriptor: cvemodel.Descriptor{ 98 Digest: manifestDigest, 99 MediaType: descriptor.MediaType, 100 }, 101 }) 102 } 103 default: 104 cveinfo.Log.Debug().Str("image", repo+":"+tag).Str("mediaType", descriptor.MediaType). 105 Msg("image media type not supported for scanning") 106 } 107 } 108 109 return imgList, nil 110 } 111 112 func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(ctx context.Context, repo, cveID string, 113 ) ([]cvemodel.TagInfo, error) { 114 repoMeta, err := cveinfo.MetaDB.GetRepoMeta(ctx, repo) 115 if err != nil { 116 cveinfo.Log.Error().Err(err).Str("repository", repo).Str("cve-id", cveID). 117 Msg("unable to get list of tags from repo") 118 119 return []cvemodel.TagInfo{}, err 120 } 121 122 vulnerableTags := make([]cvemodel.TagInfo, 0) 123 allTags := make([]cvemodel.TagInfo, 0) 124 125 for tag, descriptor := range repoMeta.Tags { 126 if zcommon.IsContextDone(ctx) { 127 return []cvemodel.TagInfo{}, ctx.Err() 128 } 129 130 switch descriptor.MediaType { 131 case ispec.MediaTypeImageManifest: 132 manifestDigestStr := descriptor.Digest 133 134 tagInfo, err := getTagInfoForManifest(tag, manifestDigestStr, cveinfo.MetaDB) 135 if err != nil { 136 cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag). 137 Str("cve-id", cveID).Msg("unable to retrieve manifest and config") 138 139 continue 140 } 141 142 allTags = append(allTags, tagInfo) 143 144 if cveinfo.isManifestVulnerable(ctx, repo, tag, manifestDigestStr, cveID) { 145 vulnerableTags = append(vulnerableTags, tagInfo) 146 } 147 case ispec.MediaTypeImageIndex: 148 indexDigestStr := descriptor.Digest 149 150 indexContent, err := getIndexContent(cveinfo.MetaDB, indexDigestStr) 151 if err != nil { 152 continue 153 } 154 155 vulnerableManifests := []cvemodel.DescriptorInfo{} 156 allManifests := []cvemodel.DescriptorInfo{} 157 158 for _, manifest := range indexContent.Manifests { 159 tagInfo, err := getTagInfoForManifest(tag, manifest.Digest.String(), cveinfo.MetaDB) 160 if err != nil { 161 cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag). 162 Str("cve-id", cveID).Msg("unable to retrieve manifest and config") 163 164 continue 165 } 166 167 manifestDescriptorInfo := cvemodel.DescriptorInfo{ 168 Descriptor: tagInfo.Descriptor, 169 Timestamp: tagInfo.Timestamp, 170 } 171 172 allManifests = append(allManifests, manifestDescriptorInfo) 173 174 if cveinfo.isManifestVulnerable(ctx, repo, tag, manifest.Digest.String(), cveID) { 175 vulnerableManifests = append(vulnerableManifests, manifestDescriptorInfo) 176 } 177 } 178 179 if len(allManifests) > 0 { 180 allTags = append(allTags, cvemodel.TagInfo{ 181 Tag: tag, 182 Descriptor: cvemodel.Descriptor{ 183 Digest: godigest.Digest(indexDigestStr), 184 MediaType: ispec.MediaTypeImageIndex, 185 }, 186 Manifests: allManifests, 187 Timestamp: mostRecentUpdate(allManifests), 188 }) 189 } 190 191 if len(vulnerableManifests) > 0 { 192 vulnerableTags = append(vulnerableTags, cvemodel.TagInfo{ 193 Tag: tag, 194 Descriptor: cvemodel.Descriptor{ 195 Digest: godigest.Digest(indexDigestStr), 196 MediaType: ispec.MediaTypeImageIndex, 197 }, 198 Manifests: vulnerableManifests, 199 Timestamp: mostRecentUpdate(vulnerableManifests), 200 }) 201 } 202 default: 203 cveinfo.Log.Debug().Str("mediaType", descriptor.MediaType). 204 Msg("image media type not supported for scanning") 205 } 206 } 207 208 var fixedTags []cvemodel.TagInfo 209 210 if len(vulnerableTags) != 0 { 211 cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID). 212 Interface("vulnerableTags", vulnerableTags).Msg("Vulnerable tags") 213 fixedTags = GetFixedTags(allTags, vulnerableTags) 214 cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID). 215 Interface("fixedTags", fixedTags).Msg("Fixed tags") 216 } else { 217 cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID). 218 Msg("image does not contain any tag that have given cve") 219 fixedTags = allTags 220 } 221 222 return fixedTags, nil 223 } 224 225 func mostRecentUpdate(allManifests []cvemodel.DescriptorInfo) time.Time { 226 if len(allManifests) == 0 { 227 return time.Time{} 228 } 229 230 timeStamp := allManifests[0].Timestamp 231 232 for i := range allManifests { 233 if timeStamp.Before(allManifests[i].Timestamp) { 234 timeStamp = allManifests[i].Timestamp 235 } 236 } 237 238 return timeStamp 239 } 240 241 func getTagInfoForManifest(tag, manifestDigestStr string, metaDB mTypes.MetaDB) (cvemodel.TagInfo, error) { 242 configContent, manifestDigest, err := getConfigAndDigest(metaDB, manifestDigestStr) 243 if err != nil { 244 return cvemodel.TagInfo{}, err 245 } 246 247 lastUpdated := zcommon.GetImageLastUpdated(configContent) 248 249 return cvemodel.TagInfo{ 250 Tag: tag, 251 Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest}, 252 Manifests: []cvemodel.DescriptorInfo{ 253 { 254 Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest}, 255 Timestamp: lastUpdated, 256 }, 257 }, 258 Timestamp: lastUpdated, 259 }, nil 260 } 261 262 func (cveinfo *BaseCveInfo) isManifestVulnerable(ctx context.Context, repo, tag, manifestDigestStr, cveID string, 263 ) bool { 264 image := zcommon.GetFullImageName(repo, tag) 265 266 isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, manifestDigestStr, ispec.MediaTypeImageManifest) 267 if !isValidImage || err != nil { 268 cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).Err(err). 269 Msg("image media type not supported for scanning, adding as a vulnerable image") 270 271 return true 272 } 273 274 cveMap, err := cveinfo.Scanner.ScanImage(ctx, zcommon.GetFullImageName(repo, manifestDigestStr)) 275 if err != nil { 276 cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). 277 Msg("scanning failed, adding as a vulnerable image") 278 279 return true 280 } 281 282 hasCVE := false 283 284 for id := range cveMap { 285 if id == cveID { 286 hasCVE = true 287 288 break 289 } 290 } 291 292 return hasCVE 293 } 294 295 func getIndexContent(metaDB mTypes.MetaDB, indexDigestStr string) (ispec.Index, error) { 296 indexDigest, err := godigest.Parse(indexDigestStr) 297 if err != nil { 298 return ispec.Index{}, err 299 } 300 301 indexData, err := metaDB.GetImageMeta(indexDigest) 302 if err != nil { 303 return ispec.Index{}, err 304 } 305 306 if indexData.Index == nil { 307 return ispec.Index{}, zerr.ErrUnexpectedMediaType 308 } 309 310 return *indexData.Index, nil 311 } 312 313 func getConfigAndDigest(metaDB mTypes.MetaDB, manifestDigestStr string) (ispec.Image, godigest.Digest, error) { 314 manifestDigest, err := godigest.Parse(manifestDigestStr) 315 if err != nil { 316 return ispec.Image{}, "", err 317 } 318 319 manifestData, err := metaDB.GetImageMeta(manifestDigest) 320 if err != nil { 321 return ispec.Image{}, "", err 322 } 323 324 // we'll fail the execution if the config is not compatible with ispec.Image because we can't scan this type of images. 325 if manifestData.Manifests[0].Manifest.Config.MediaType != ispec.MediaTypeImageConfig { 326 return ispec.Image{}, "", zerr.ErrUnexpectedMediaType 327 } 328 329 return manifestData.Manifests[0].Config, manifestDigest, err 330 } 331 332 func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) { 333 searchedCVE = strings.ToUpper(searchedCVE) 334 335 for _, cve := range cveMap { 336 if strings.Contains(strings.ToUpper(cve.Title), searchedCVE) || 337 strings.Contains(strings.ToUpper(cve.ID), searchedCVE) { 338 pageFinder.Add(cve) 339 } 340 } 341 } 342 343 func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref string, searchedCVE string, 344 pageInput cvemodel.PageInput, 345 ) ( 346 []cvemodel.CVE, zcommon.PageInfo, error, 347 ) { 348 isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref) 349 if !isValidImage { 350 cveinfo.Log.Debug().Str("image", repo+":"+ref).Err(err).Msg("image is not scanable") 351 352 return []cvemodel.CVE{}, zcommon.PageInfo{}, err 353 } 354 355 image := zcommon.GetFullImageName(repo, ref) 356 357 cveMap, err := cveinfo.Scanner.ScanImage(ctx, image) 358 if err != nil { 359 return []cvemodel.CVE{}, zcommon.PageInfo{}, err 360 } 361 362 pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy) 363 if err != nil { 364 return []cvemodel.CVE{}, zcommon.PageInfo{}, err 365 } 366 367 filterCVEList(cveMap, searchedCVE, pageFinder) 368 369 cveList, pageInfo := pageFinder.Page() 370 371 return cveList, pageInfo, nil 372 } 373 374 func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string, 375 ) (cvemodel.ImageCVESummary, error) { 376 // There are several cases, expected returned values below: 377 // not scanned yet - max severity "" - cve count 0 - no Errors 378 // not scannable - max severity "" - cve count 0 - has Errors 379 // scannable no issues found - max severity "NONE" - cve count 0 - no Errors 380 // scannable issues found - max severity from Scanner - cve count >0 - no Errors 381 imageCVESummary := cvemodel.ImageCVESummary{ 382 Count: 0, 383 MaxSeverity: cvemodel.SeverityNotScanned, 384 } 385 386 // For this call we only look at the scanner cache, we skip the actual scanning to save time 387 if !cveinfo.Scanner.IsResultCached(digest) { 388 isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType) 389 if !isValidImage { 390 cveinfo.Log.Debug().Str("digest", digest).Str("mediaType", mediaType). 391 Err(err).Msg("image is not scannable") 392 } 393 394 return imageCVESummary, err 395 } 396 397 // We will make due with cached results 398 cveMap := cveinfo.Scanner.GetCachedResult(digest) 399 400 imageCVESummary.Count = len(cveMap) 401 if imageCVESummary.Count == 0 { 402 imageCVESummary.MaxSeverity = cvemodel.SeverityNone 403 404 return imageCVESummary, nil 405 } 406 407 imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown 408 for _, cve := range cveMap { 409 if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 { 410 imageCVESummary.MaxSeverity = cve.Severity 411 } 412 } 413 414 return imageCVESummary, nil 415 } 416 417 func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo { 418 sort.Slice(allTags, func(i, j int) bool { 419 return allTags[i].Timestamp.Before(allTags[j].Timestamp) 420 }) 421 422 earliestVulnerable := vulnerableTags[0] 423 vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags)) 424 425 for _, tag := range vulnerableTags { 426 vulnerableTagMap[tag.Tag] = tag 427 428 switch tag.Descriptor.MediaType { 429 case ispec.MediaTypeImageManifest: 430 if tag.Timestamp.Before(earliestVulnerable.Timestamp) { 431 earliestVulnerable = tag 432 } 433 case ispec.MediaTypeImageIndex: 434 for _, manifestDesc := range tag.Manifests { 435 if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) { 436 earliestVulnerable = tag 437 } 438 } 439 default: 440 continue 441 } 442 } 443 444 var fixedTags []cvemodel.TagInfo 445 446 // There are some downsides to this logic 447 // We assume there can't be multiple "branches" of the same 448 // image built at different times containing different fixes 449 // There may be older images which have a fix or 450 // newer images which don't 451 for _, tag := range allTags { 452 switch tag.Descriptor.MediaType { 453 case ispec.MediaTypeImageManifest: 454 if tag.Timestamp.Before(earliestVulnerable.Timestamp) { 455 // The vulnerability did not exist at the time this 456 // image was built 457 continue 458 } 459 // If the image is old enough for the vulnerability to 460 // exist, but it was not detected, it means it contains 461 // the fix 462 if _, ok := vulnerableTagMap[tag.Tag]; !ok { 463 fixedTags = append(fixedTags, tag) 464 } 465 case ispec.MediaTypeImageIndex: 466 fixedManifests := []cvemodel.DescriptorInfo{} 467 468 // If the latest update inside the index is before the earliest vulnerability found then 469 // the index can't contain a fix 470 if tag.Timestamp.Before(earliestVulnerable.Timestamp) { 471 continue 472 } 473 474 vulnTagInfo, indexHasVulnerableManifest := vulnerableTagMap[tag.Tag] 475 476 for _, manifestDesc := range tag.Manifests { 477 if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) { 478 // The vulnerability did not exist at the time this image was built 479 continue 480 } 481 482 // check if the current manifest doesn't have the vulnerability 483 if !indexHasVulnerableManifest || !containsDescriptorInfo(vulnTagInfo.Manifests, manifestDesc) { 484 fixedManifests = append(fixedManifests, manifestDesc) 485 } 486 } 487 488 if len(fixedManifests) > 0 { 489 fixedTag := tag 490 fixedTag.Manifests = fixedManifests 491 492 fixedTags = append(fixedTags, fixedTag) 493 } 494 default: 495 continue 496 } 497 } 498 499 return fixedTags 500 } 501 502 func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvemodel.DescriptorInfo) bool { 503 for _, di := range slice { 504 if di.Digest == descriptorInfo.Digest { 505 return true 506 } 507 } 508 509 return false 510 }