zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/search/cve/trivy/scanner.go (about) 1 package trivy 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path" 8 "sync" 9 10 "github.com/aquasecurity/trivy-db/pkg/metadata" 11 dbTypes "github.com/aquasecurity/trivy-db/pkg/types" 12 "github.com/aquasecurity/trivy/pkg/commands/artifact" 13 "github.com/aquasecurity/trivy/pkg/commands/operation" 14 fanalTypes "github.com/aquasecurity/trivy/pkg/fanal/types" 15 "github.com/aquasecurity/trivy/pkg/flag" 16 "github.com/aquasecurity/trivy/pkg/javadb" 17 "github.com/aquasecurity/trivy/pkg/types" 18 regTypes "github.com/google/go-containerregistry/pkg/v1/types" 19 godigest "github.com/opencontainers/go-digest" 20 ispec "github.com/opencontainers/image-spec/specs-go/v1" 21 _ "modernc.org/sqlite" 22 23 zerr "zotregistry.io/zot/errors" 24 zcommon "zotregistry.io/zot/pkg/common" 25 cvecache "zotregistry.io/zot/pkg/extensions/search/cve/cache" 26 cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" 27 "zotregistry.io/zot/pkg/log" 28 mTypes "zotregistry.io/zot/pkg/meta/types" 29 "zotregistry.io/zot/pkg/storage" 30 ) 31 32 const cacheSize = 1000000 33 34 // getNewScanOptions sets trivy configuration values for our scans and returns them as 35 // a trivy Options structure. 36 func getNewScanOptions(dir, dbRepository, javaDBRepository string) *flag.Options { 37 scanOptions := flag.Options{ 38 GlobalOptions: flag.GlobalOptions{ 39 CacheDir: dir, 40 }, 41 ScanOptions: flag.ScanOptions{ 42 Scanners: types.Scanners{types.VulnerabilityScanner}, 43 OfflineScan: true, 44 }, 45 VulnerabilityOptions: flag.VulnerabilityOptions{ 46 VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary}, 47 }, 48 DBOptions: flag.DBOptions{ 49 DBRepository: dbRepository, 50 JavaDBRepository: javaDBRepository, 51 SkipDBUpdate: true, 52 SkipJavaDBUpdate: true, 53 }, 54 ReportOptions: flag.ReportOptions{ 55 Format: "table", 56 Severities: []dbTypes.Severity{ 57 dbTypes.SeverityUnknown, 58 dbTypes.SeverityLow, 59 dbTypes.SeverityMedium, 60 dbTypes.SeverityHigh, 61 dbTypes.SeverityCritical, 62 }, 63 }, 64 } 65 66 return &scanOptions 67 } 68 69 type cveTrivyController struct { 70 DefaultCveConfig *flag.Options 71 SubCveConfig map[string]*flag.Options 72 } 73 74 type Scanner struct { 75 metaDB mTypes.MetaDB 76 cveController cveTrivyController 77 storeController storage.StoreController 78 log log.Logger 79 dbLock *sync.Mutex 80 cache *cvecache.CveCache 81 dbRepository string 82 javaDBRepository string 83 } 84 85 func NewScanner(storeController storage.StoreController, 86 metaDB mTypes.MetaDB, dbRepository, javaDBRepository string, log log.Logger, 87 ) *Scanner { 88 cveController := cveTrivyController{} 89 90 subCveConfig := make(map[string]*flag.Options) 91 92 if storeController.DefaultStore != nil { 93 imageStore := storeController.DefaultStore 94 95 rootDir := imageStore.RootDir() 96 97 cacheDir := path.Join(rootDir, "_trivy") 98 opts := getNewScanOptions(cacheDir, dbRepository, javaDBRepository) 99 100 cveController.DefaultCveConfig = opts 101 } 102 103 if storeController.SubStore != nil { 104 for route, storage := range storeController.SubStore { 105 rootDir := storage.RootDir() 106 107 cacheDir := path.Join(rootDir, "_trivy") 108 opts := getNewScanOptions(cacheDir, dbRepository, javaDBRepository) 109 110 subCveConfig[route] = opts 111 } 112 } 113 114 cveController.SubCveConfig = subCveConfig 115 116 return &Scanner{ 117 log: log, 118 metaDB: metaDB, 119 cveController: cveController, 120 storeController: storeController, 121 dbLock: &sync.Mutex{}, 122 cache: cvecache.NewCveCache(cacheSize, log), 123 dbRepository: dbRepository, 124 javaDBRepository: javaDBRepository, 125 } 126 } 127 128 func (scanner Scanner) getTrivyOptions(image string) flag.Options { 129 // Split image to get route prefix 130 prefixName := storage.GetRoutePrefix(image) 131 132 var opts flag.Options 133 134 var ok bool 135 136 var rootDir string 137 138 // Get corresponding CVE trivy config, if no sub cve config present that means its default 139 _, ok = scanner.cveController.SubCveConfig[prefixName] 140 if ok { 141 opts = *scanner.cveController.SubCveConfig[prefixName] 142 143 imgStore := scanner.storeController.SubStore[prefixName] 144 145 rootDir = imgStore.RootDir() 146 } else { 147 opts = *scanner.cveController.DefaultCveConfig 148 149 imgStore := scanner.storeController.DefaultStore 150 151 rootDir = imgStore.RootDir() 152 } 153 154 opts.ScanOptions.Target = path.Join(rootDir, image) 155 opts.ImageOptions.Input = path.Join(rootDir, image) 156 157 return opts 158 } 159 160 func (scanner Scanner) runTrivy(ctx context.Context, opts flag.Options) (types.Report, error) { 161 err := scanner.checkDBPresence() 162 if err != nil { 163 return types.Report{}, err 164 } 165 166 runner, err := artifact.NewRunner(ctx, opts) 167 if err != nil { 168 return types.Report{}, err 169 } 170 defer runner.Close(ctx) 171 172 report, err := runner.ScanImage(ctx, opts) 173 if err != nil { 174 return types.Report{}, err 175 } 176 177 report, err = runner.Filter(ctx, opts, report) 178 if err != nil { 179 return types.Report{}, err 180 } 181 182 return report, nil 183 } 184 185 func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) { 186 var ( 187 digestStr = ref 188 mediaType string 189 ) 190 191 if zcommon.IsTag(ref) { 192 imgDescriptor, err := getImageDescriptor(context.Background(), scanner.metaDB, repo, ref) 193 if err != nil { 194 return false, err 195 } 196 197 digestStr = imgDescriptor.Digest 198 mediaType = imgDescriptor.MediaType 199 } else { 200 var found bool 201 202 found, mediaType = findMediaTypeForDigest(scanner.metaDB, godigest.Digest(ref)) 203 if !found { 204 return false, zerr.ErrManifestNotFound 205 } 206 } 207 208 return scanner.IsImageMediaScannable(repo, digestStr, mediaType) 209 } 210 211 func (scanner Scanner) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) { 212 image := repo + "@" + digestStr 213 214 switch mediaType { 215 case ispec.MediaTypeImageManifest: 216 ok, err := scanner.isManifestScanable(digestStr) 217 if err != nil { 218 return ok, fmt.Errorf("image '%s' %w", image, err) 219 } 220 221 return ok, nil 222 case ispec.MediaTypeImageIndex: 223 ok, err := scanner.isIndexScannable(digestStr) 224 if err != nil { 225 return ok, fmt.Errorf("image '%s' %w", image, err) 226 } 227 228 return ok, nil 229 default: 230 return false, nil 231 } 232 } 233 234 func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) { 235 if scanner.cache.Get(digestStr) != nil { 236 return true, nil 237 } 238 239 manifestData, err := scanner.metaDB.GetImageMeta(godigest.Digest(digestStr)) 240 if err != nil { 241 return false, err 242 } 243 244 for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers { 245 switch imageLayer.MediaType { 246 case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): 247 continue 248 default: 249 return false, zerr.ErrScanNotSupported 250 } 251 } 252 253 return true, nil 254 } 255 256 func (scanner Scanner) isManifestDataScannable(manifestData mTypes.ManifestMeta) (bool, error) { 257 if scanner.cache.Get(manifestData.Digest.String()) != nil { 258 return true, nil 259 } 260 261 for _, imageLayer := range manifestData.Manifest.Layers { 262 switch imageLayer.MediaType { 263 case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): 264 continue 265 default: 266 return false, zerr.ErrScanNotSupported 267 } 268 } 269 270 return true, nil 271 } 272 273 func (scanner Scanner) isIndexScannable(digestStr string) (bool, error) { 274 if scanner.cache.Get(digestStr) != nil { 275 return true, nil 276 } 277 278 indexData, err := scanner.metaDB.GetImageMeta(godigest.Digest(digestStr)) 279 if err != nil { 280 return false, err 281 } 282 283 if indexData.Index == nil { 284 return false, zerr.ErrUnexpectedMediaType 285 } 286 287 indexContent := *indexData.Index 288 289 if len(indexContent.Manifests) == 0 { 290 return true, nil 291 } 292 293 for _, manifest := range indexData.Manifests { 294 isScannable, err := scanner.isManifestDataScannable(manifest) 295 if err != nil { 296 continue 297 } 298 299 // if at least 1 manifest is scannable, the whole index is scannable 300 if isScannable { 301 return true, nil 302 } 303 } 304 305 return false, nil 306 } 307 308 func (scanner Scanner) IsResultCached(digest string) bool { 309 // Check if the entry exists in cache without updating the recent-ness 310 return scanner.cache.Contains(digest) 311 } 312 313 func (scanner Scanner) GetCachedResult(digest string) map[string]cvemodel.CVE { 314 return scanner.cache.Get(digest) 315 } 316 317 func (scanner Scanner) ScanImage(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { 318 var ( 319 originalImageInput = image 320 digest string 321 mediaType string 322 ) 323 324 repo, ref, isTag := zcommon.GetImageDirAndReference(image) 325 326 digest = ref 327 328 if isTag { 329 imgDescriptor, err := getImageDescriptor(ctx, scanner.metaDB, repo, ref) 330 if err != nil { 331 return map[string]cvemodel.CVE{}, err 332 } 333 334 digest = imgDescriptor.Digest 335 mediaType = imgDescriptor.MediaType 336 } else { 337 var found bool 338 339 found, mediaType = findMediaTypeForDigest(scanner.metaDB, godigest.Digest(ref)) 340 if !found { 341 return map[string]cvemodel.CVE{}, zerr.ErrManifestNotFound 342 } 343 } 344 345 var ( 346 cveIDMap map[string]cvemodel.CVE 347 err error 348 ) 349 350 switch mediaType { 351 case ispec.MediaTypeImageIndex: 352 cveIDMap, err = scanner.scanIndex(ctx, repo, digest) 353 default: 354 cveIDMap, err = scanner.scanManifest(ctx, repo, digest) 355 } 356 357 if err != nil { 358 scanner.log.Error().Err(err).Str("image", originalImageInput).Msg("unable to scan image") 359 360 return map[string]cvemodel.CVE{}, err 361 } 362 363 return cveIDMap, nil 364 } 365 366 func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (map[string]cvemodel.CVE, error) { 367 if cachedMap := scanner.cache.Get(digest); cachedMap != nil { 368 return cachedMap, nil 369 } 370 371 cveidMap := map[string]cvemodel.CVE{} 372 image := repo + "@" + digest 373 374 scanner.dbLock.Lock() 375 opts := scanner.getTrivyOptions(image) 376 report, err := scanner.runTrivy(ctx, opts) 377 scanner.dbLock.Unlock() 378 379 if err != nil { //nolint: wsl 380 return cveidMap, err 381 } 382 383 for _, result := range report.Results { 384 for _, vulnerability := range result.Vulnerabilities { 385 pkgName := vulnerability.PkgName 386 387 installedVersion := vulnerability.InstalledVersion 388 389 var fixedVersion string 390 if vulnerability.FixedVersion != "" { 391 fixedVersion = vulnerability.FixedVersion 392 } else { 393 fixedVersion = "Not Specified" 394 } 395 396 _, ok := cveidMap[vulnerability.VulnerabilityID] 397 if ok { 398 cveDetailStruct := cveidMap[vulnerability.VulnerabilityID] 399 400 pkgList := cveDetailStruct.PackageList 401 402 pkgList = append( 403 pkgList, 404 cvemodel.Package{ 405 Name: pkgName, 406 InstalledVersion: installedVersion, 407 FixedVersion: fixedVersion, 408 }, 409 ) 410 411 cveDetailStruct.PackageList = pkgList 412 413 cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct 414 } else { 415 newPkgList := make([]cvemodel.Package, 0) 416 417 newPkgList = append( 418 newPkgList, 419 cvemodel.Package{ 420 Name: pkgName, 421 InstalledVersion: installedVersion, 422 FixedVersion: fixedVersion, 423 }, 424 ) 425 426 cveidMap[vulnerability.VulnerabilityID] = cvemodel.CVE{ 427 ID: vulnerability.VulnerabilityID, 428 Title: vulnerability.Title, 429 Description: vulnerability.Description, 430 Severity: convertSeverity(vulnerability.Severity), 431 PackageList: newPkgList, 432 } 433 } 434 } 435 } 436 437 scanner.cache.Add(digest, cveidMap) 438 439 return cveidMap, nil 440 } 441 442 func (scanner Scanner) scanIndex(ctx context.Context, repo, digest string) (map[string]cvemodel.CVE, error) { 443 if cachedMap := scanner.cache.Get(digest); cachedMap != nil { 444 return cachedMap, nil 445 } 446 447 indexData, err := scanner.metaDB.GetImageMeta(godigest.Digest(digest)) 448 if err != nil { 449 return map[string]cvemodel.CVE{}, err 450 } 451 452 if indexData.Index == nil { 453 return map[string]cvemodel.CVE{}, zerr.ErrUnexpectedMediaType 454 } 455 456 indexCveIDMap := map[string]cvemodel.CVE{} 457 458 for _, manifest := range indexData.Index.Manifests { 459 if isScannable, err := scanner.isManifestScanable(manifest.Digest.String()); isScannable && err == nil { 460 manifestCveIDMap, err := scanner.scanManifest(ctx, repo, manifest.Digest.String()) 461 if err != nil { 462 return nil, err 463 } 464 465 for vulnerabilityID, CVE := range manifestCveIDMap { 466 indexCveIDMap[vulnerabilityID] = CVE 467 } 468 } 469 } 470 471 scanner.cache.Add(digest, indexCveIDMap) 472 473 return indexCveIDMap, nil 474 } 475 476 // UpdateDB downloads the Trivy DB / Cache under the store root directory. 477 func (scanner Scanner) UpdateDB(ctx context.Context) error { 478 // We need a lock as using multiple substores each with its own DB 479 // can result in a DATARACE because some varibles in trivy-db are global 480 // https://github.com/project-zot/trivy-db/blob/main/pkg/db/db.go#L23 481 scanner.dbLock.Lock() 482 defer scanner.dbLock.Unlock() 483 484 if scanner.storeController.DefaultStore != nil { 485 dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy") 486 487 err := scanner.updateDB(ctx, dbDir) 488 if err != nil { 489 return err 490 } 491 } 492 493 if scanner.storeController.SubStore != nil { 494 for _, storage := range scanner.storeController.SubStore { 495 dbDir := path.Join(storage.RootDir(), "_trivy") 496 497 err := scanner.updateDB(ctx, dbDir) 498 if err != nil { 499 return err 500 } 501 } 502 } 503 504 scanner.cache.Purge() 505 506 return nil 507 } 508 509 func (scanner Scanner) updateDB(ctx context.Context, dbDir string) error { 510 scanner.log.Debug().Str("dbDir", dbDir).Msg("Download Trivy DB to destination dir") 511 512 registryOpts := fanalTypes.RegistryOptions{Insecure: false} 513 514 scanner.log.Debug().Str("dbDir", dbDir).Msg("Started downloading Trivy DB to destination dir") 515 516 err := operation.DownloadDB(ctx, "dev", dbDir, scanner.dbRepository, false, false, registryOpts) 517 if err != nil { 518 scanner.log.Error().Err(err).Str("dbDir", dbDir). 519 Str("dbRepository", scanner.dbRepository).Msg("Error downloading Trivy DB to destination dir") 520 521 return err 522 } 523 524 if scanner.javaDBRepository != "" { 525 javadb.Init(dbDir, scanner.javaDBRepository, false, false, registryOpts) 526 527 if err := javadb.Update(); err != nil { 528 scanner.log.Error().Err(err).Str("dbDir", dbDir). 529 Str("javaDBRepository", scanner.javaDBRepository).Msg("Error downloading Trivy Java DB to destination dir") 530 531 return err 532 } 533 } 534 535 scanner.log.Debug().Str("dbDir", dbDir).Msg("Finished downloading Trivy DB to destination dir") 536 537 return nil 538 } 539 540 // checkDBPresence errors if the DB metadata files cannot be accessed. 541 func (scanner Scanner) checkDBPresence() error { 542 result := true 543 544 if scanner.storeController.DefaultStore != nil { 545 dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy") 546 if _, err := os.Stat(metadata.Path(dbDir)); err != nil { 547 result = false 548 } 549 } 550 551 if scanner.storeController.SubStore != nil { 552 for _, storage := range scanner.storeController.SubStore { 553 dbDir := path.Join(storage.RootDir(), "_trivy") 554 555 if _, err := os.Stat(metadata.Path(dbDir)); err != nil { 556 result = false 557 } 558 } 559 } 560 561 if !result { 562 return zerr.ErrCVEDBNotFound 563 } 564 565 return nil 566 } 567 568 func getImageDescriptor(ctx context.Context, metaDB mTypes.MetaDB, repo, tag string) (mTypes.Descriptor, error) { 569 repoMeta, err := metaDB.GetRepoMeta(ctx, repo) 570 if err != nil { 571 return mTypes.Descriptor{}, err 572 } 573 574 imageDescriptor, ok := repoMeta.Tags[tag] 575 if !ok { 576 return mTypes.Descriptor{}, zerr.ErrTagMetaNotFound 577 } 578 579 return imageDescriptor, nil 580 } 581 582 // findMediaTypeForDigest will look into the buckets for a certain digest. Depending on which bucket that 583 // digest is found the corresponding mediatype is returned. 584 func findMediaTypeForDigest(metaDB mTypes.MetaDB, digest godigest.Digest) (bool, string) { 585 imageMeta, err := metaDB.GetImageMeta(digest) 586 if err == nil { 587 return true, imageMeta.MediaType 588 } 589 590 return false, "" 591 } 592 593 func convertSeverity(detectedSeverity string) string { 594 trivySeverity, _ := dbTypes.NewSeverity(detectedSeverity) 595 596 sevMap := map[dbTypes.Severity]string{ 597 dbTypes.SeverityUnknown: cvemodel.SeverityUnknown, 598 dbTypes.SeverityLow: cvemodel.SeverityLow, 599 dbTypes.SeverityMedium: cvemodel.SeverityMedium, 600 dbTypes.SeverityHigh: cvemodel.SeverityHigh, 601 dbTypes.SeverityCritical: cvemodel.SeverityCritical, 602 } 603 604 return sevMap[trivySeverity] 605 }