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