github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/analyzer/analyzer_test.go (about) 1 package analyzer_test 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "sync" 8 "testing" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 "golang.org/x/sync/semaphore" 13 "golang.org/x/xerrors" 14 15 dio "github.com/aquasecurity/go-dep-parser/pkg/io" 16 "github.com/devseccon/trivy/pkg/fanal/analyzer" 17 "github.com/devseccon/trivy/pkg/fanal/types" 18 "github.com/devseccon/trivy/pkg/javadb" 19 "github.com/devseccon/trivy/pkg/mapfs" 20 21 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/imgconf/apk" 22 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/language/java/jar" 23 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/language/python/poetry" 24 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/language/ruby/bundler" 25 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/os/alpine" 26 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/os/ubuntu" 27 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/pkg/apk" 28 _ "github.com/devseccon/trivy/pkg/fanal/analyzer/repo/apk" 29 _ "github.com/devseccon/trivy/pkg/fanal/handler/all" 30 _ "modernc.org/sqlite" 31 ) 32 33 func TestAnalysisResult_Merge(t *testing.T) { 34 type fields struct { 35 m sync.Mutex 36 OS types.OS 37 PackageInfos []types.PackageInfo 38 Applications []types.Application 39 } 40 type args struct { 41 new *analyzer.AnalysisResult 42 } 43 tests := []struct { 44 name string 45 fields fields 46 args args 47 want analyzer.AnalysisResult 48 }{ 49 { 50 name: "happy path", 51 fields: fields{ 52 OS: types.OS{ 53 Family: types.Debian, 54 Name: "9.8", 55 }, 56 PackageInfos: []types.PackageInfo{ 57 { 58 FilePath: "var/lib/dpkg/status.d/libc", 59 Packages: types.Packages{ 60 { 61 Name: "libc", 62 Version: "1.2.3", 63 }, 64 }, 65 }, 66 }, 67 Applications: []types.Application{ 68 { 69 Type: "bundler", 70 FilePath: "app/Gemfile.lock", 71 Libraries: types.Packages{ 72 { 73 Name: "rails", 74 Version: "5.0.0", 75 }, 76 }, 77 }, 78 }, 79 }, 80 args: args{ 81 new: &analyzer.AnalysisResult{ 82 PackageInfos: []types.PackageInfo{ 83 { 84 FilePath: "var/lib/dpkg/status.d/openssl", 85 Packages: types.Packages{ 86 { 87 Name: "openssl", 88 Version: "1.1.1", 89 }, 90 }, 91 }, 92 }, 93 Applications: []types.Application{ 94 { 95 Type: "bundler", 96 FilePath: "app2/Gemfile.lock", 97 Libraries: types.Packages{ 98 { 99 Name: "nokogiri", 100 Version: "1.0.0", 101 }, 102 }, 103 }, 104 }, 105 }, 106 }, 107 want: analyzer.AnalysisResult{ 108 OS: types.OS{ 109 Family: types.Debian, 110 Name: "9.8", 111 }, 112 PackageInfos: []types.PackageInfo{ 113 { 114 FilePath: "var/lib/dpkg/status.d/libc", 115 Packages: types.Packages{ 116 { 117 Name: "libc", 118 Version: "1.2.3", 119 }, 120 }, 121 }, 122 { 123 FilePath: "var/lib/dpkg/status.d/openssl", 124 Packages: types.Packages{ 125 { 126 Name: "openssl", 127 Version: "1.1.1", 128 }, 129 }, 130 }, 131 }, 132 Applications: []types.Application{ 133 { 134 Type: "bundler", 135 FilePath: "app/Gemfile.lock", 136 Libraries: types.Packages{ 137 { 138 Name: "rails", 139 Version: "5.0.0", 140 }, 141 }, 142 }, 143 { 144 Type: "bundler", 145 FilePath: "app2/Gemfile.lock", 146 Libraries: types.Packages{ 147 { 148 Name: "nokogiri", 149 Version: "1.0.0", 150 }, 151 }, 152 }, 153 }, 154 }, 155 }, 156 { 157 name: "redhat must be replaced with oracle", 158 fields: fields{ 159 OS: types.OS{ 160 Family: types.RedHat, // this must be overwritten 161 Name: "8.0", 162 }, 163 }, 164 args: args{ 165 new: &analyzer.AnalysisResult{ 166 OS: types.OS{ 167 Family: types.Oracle, 168 Name: "8.0", 169 }, 170 }, 171 }, 172 want: analyzer.AnalysisResult{ 173 OS: types.OS{ 174 Family: types.Oracle, 175 Name: "8.0", 176 }, 177 }, 178 }, 179 { 180 name: "debian must be replaced with ubuntu", 181 fields: fields{ 182 OS: types.OS{ 183 Family: types.Debian, // this must be overwritten 184 Name: "9.0", 185 }, 186 }, 187 args: args{ 188 new: &analyzer.AnalysisResult{ 189 OS: types.OS{ 190 Family: types.Ubuntu, 191 Name: "18.04", 192 }, 193 }, 194 }, 195 want: analyzer.AnalysisResult{ 196 OS: types.OS{ 197 Family: types.Ubuntu, 198 Name: "18.04", 199 }, 200 }, 201 }, 202 { 203 name: "merge extended flag", 204 fields: fields{ 205 // This must be overwritten 206 OS: types.OS{ 207 Family: types.Ubuntu, 208 Name: "16.04", 209 }, 210 }, 211 args: args{ 212 new: &analyzer.AnalysisResult{ 213 OS: types.OS{ 214 Family: types.Ubuntu, 215 Extended: true, 216 }, 217 }, 218 }, 219 want: analyzer.AnalysisResult{ 220 OS: types.OS{ 221 Family: types.Ubuntu, 222 Name: "16.04", 223 Extended: true, 224 }, 225 }, 226 }, 227 { 228 name: "alpine OS needs to be extended with apk repositories", 229 fields: fields{ 230 OS: types.OS{ 231 Family: types.Alpine, 232 Name: "3.15.3", 233 }, 234 }, 235 args: args{ 236 new: &analyzer.AnalysisResult{ 237 Repository: &types.Repository{ 238 Family: types.Alpine, 239 Release: "edge", 240 }, 241 }, 242 }, 243 want: analyzer.AnalysisResult{ 244 OS: types.OS{ 245 Family: types.Alpine, 246 Name: "3.15.3", 247 }, 248 Repository: &types.Repository{ 249 Family: types.Alpine, 250 Release: "edge", 251 }, 252 }, 253 }, 254 { 255 name: "alpine must not be replaced with oracle", 256 fields: fields{ 257 OS: types.OS{ 258 Family: types.Alpine, // this must not be overwritten 259 Name: "3.11", 260 }, 261 }, 262 args: args{ 263 new: &analyzer.AnalysisResult{ 264 OS: types.OS{ 265 Family: types.Oracle, 266 Name: "8.0", 267 }, 268 }, 269 }, 270 want: analyzer.AnalysisResult{ 271 OS: types.OS{ 272 Family: types.Alpine, // this must not be overwritten 273 Name: "3.11", 274 }, 275 }, 276 }, 277 } 278 for _, tt := range tests { 279 t.Run(tt.name, func(t *testing.T) { 280 r := analyzer.AnalysisResult{ 281 OS: tt.fields.OS, 282 PackageInfos: tt.fields.PackageInfos, 283 Applications: tt.fields.Applications, 284 } 285 r.Merge(tt.args.new) 286 assert.Equal(t, tt.want, r) 287 }) 288 } 289 } 290 291 func TestAnalyzerGroup_AnalyzeFile(t *testing.T) { 292 type args struct { 293 filePath string 294 testFilePath string 295 disabledAnalyzers []analyzer.Type 296 filePatterns []string 297 } 298 tests := []struct { 299 name string 300 args args 301 want *analyzer.AnalysisResult 302 wantErr string 303 }{ 304 { 305 name: "happy path with os analyzer", 306 args: args{ 307 filePath: "/etc/alpine-release", 308 testFilePath: "testdata/etc/alpine-release", 309 }, 310 want: &analyzer.AnalysisResult{ 311 OS: types.OS{ 312 Family: "alpine", 313 Name: "3.11.6", 314 }, 315 }, 316 }, 317 { 318 name: "happy path with disabled os analyzer", 319 args: args{ 320 filePath: "/etc/alpine-release", 321 testFilePath: "testdata/etc/alpine-release", 322 disabledAnalyzers: []analyzer.Type{analyzer.TypeAlpine}, 323 }, 324 want: &analyzer.AnalysisResult{}, 325 }, 326 { 327 name: "happy path with package analyzer", 328 args: args{ 329 filePath: "/lib/apk/db/installed", 330 testFilePath: "testdata/lib/apk/db/installed", 331 }, 332 want: &analyzer.AnalysisResult{ 333 PackageInfos: []types.PackageInfo{ 334 { 335 FilePath: "/lib/apk/db/installed", 336 Packages: types.Packages{ 337 { 338 ID: "musl@1.1.24-r2", 339 Name: "musl", 340 Version: "1.1.24-r2", 341 SrcName: "musl", 342 SrcVersion: "1.1.24-r2", 343 Licenses: []string{"MIT"}, 344 Arch: "x86_64", 345 Digest: "sha1:cb2316a189ebee5282c4a9bd98794cc2477a74c6", 346 InstalledFiles: []string{"lib/libc.musl-x86_64.so.1", "lib/ld-musl-x86_64.so.1"}, 347 }, 348 }, 349 }, 350 }, 351 SystemInstalledFiles: []string{ 352 "lib/libc.musl-x86_64.so.1", 353 "lib/ld-musl-x86_64.so.1", 354 }, 355 }, 356 }, 357 { 358 name: "happy path with disabled package analyzer", 359 args: args{ 360 filePath: "/lib/apk/db/installed", 361 testFilePath: "testdata/lib/apk/db/installed", 362 disabledAnalyzers: []analyzer.Type{analyzer.TypeApk}, 363 }, 364 want: &analyzer.AnalysisResult{}, 365 }, 366 { 367 name: "happy path with library analyzer", 368 args: args{ 369 filePath: "/app/Gemfile.lock", 370 testFilePath: "testdata/app/Gemfile.lock", 371 }, 372 want: &analyzer.AnalysisResult{ 373 Applications: []types.Application{ 374 { 375 Type: "bundler", 376 FilePath: "/app/Gemfile.lock", 377 Libraries: types.Packages{ 378 { 379 ID: "actioncable@5.2.3", 380 Name: "actioncable", 381 Version: "5.2.3", 382 Indirect: false, 383 DependsOn: []string{ 384 "actionpack@5.2.3", 385 }, 386 Locations: []types.Location{ 387 { 388 StartLine: 4, 389 EndLine: 4, 390 }, 391 }, 392 }, 393 { 394 ID: "actionpack@5.2.3", 395 Name: "actionpack", 396 Version: "5.2.3", 397 Indirect: true, 398 Locations: []types.Location{ 399 { 400 StartLine: 6, 401 EndLine: 6, 402 }, 403 }, 404 }, 405 }, 406 }, 407 }, 408 }, 409 }, 410 { 411 name: "happy path with invalid os information", 412 args: args{ 413 filePath: "/etc/lsb-release", 414 testFilePath: "testdata/etc/hostname", 415 }, 416 want: &analyzer.AnalysisResult{}, 417 }, 418 { 419 name: "happy path with a directory", 420 args: args{ 421 filePath: "/etc/lsb-release", 422 testFilePath: "testdata/etc", 423 }, 424 want: &analyzer.AnalysisResult{}, 425 }, 426 { 427 name: "happy path with library analyzer file pattern regex", 428 args: args{ 429 filePath: "/app/Gemfile-dev.lock", 430 testFilePath: "testdata/app/Gemfile.lock", 431 filePatterns: []string{"bundler:Gemfile(-.*)?\\.lock"}, 432 }, 433 want: &analyzer.AnalysisResult{ 434 Applications: []types.Application{ 435 { 436 Type: "bundler", 437 FilePath: "/app/Gemfile-dev.lock", 438 Libraries: types.Packages{ 439 { 440 ID: "actioncable@5.2.3", 441 Name: "actioncable", 442 Version: "5.2.3", 443 Indirect: false, 444 DependsOn: []string{ 445 "actionpack@5.2.3", 446 }, 447 Locations: []types.Location{ 448 { 449 StartLine: 4, 450 EndLine: 4, 451 }, 452 }, 453 }, 454 { 455 ID: "actionpack@5.2.3", 456 Name: "actionpack", 457 Version: "5.2.3", 458 Indirect: true, 459 Locations: []types.Location{ 460 { 461 StartLine: 6, 462 EndLine: 6, 463 }, 464 }, 465 }, 466 }, 467 }, 468 }, 469 }, 470 }, 471 { 472 name: "ignore permission error", 473 args: args{ 474 filePath: "/etc/alpine-release", 475 testFilePath: "testdata/no-permission", 476 }, 477 want: &analyzer.AnalysisResult{}, 478 }, 479 { 480 name: "sad path with opener error", 481 args: args{ 482 filePath: "/lib/apk/db/installed", 483 testFilePath: "testdata/error", 484 }, 485 wantErr: "unable to open /lib/apk/db/installed", 486 }, 487 { 488 name: "sad path with broken file pattern regex", 489 args: args{ 490 filePath: "/app/Gemfile-dev.lock", 491 testFilePath: "testdata/app/Gemfile.lock", 492 filePatterns: []string{"bundler:Gemfile(-.*?\\.lock"}, 493 }, 494 wantErr: "error parsing regexp", 495 }, 496 { 497 name: "sad path with broken file pattern", 498 args: args{ 499 filePath: "/app/Gemfile-dev.lock", 500 testFilePath: "testdata/app/Gemfile.lock", 501 filePatterns: []string{"Gemfile(-.*)?\\.lock"}, 502 }, 503 wantErr: "invalid file pattern", 504 }, 505 } 506 for _, tt := range tests { 507 t.Run(tt.name, func(t *testing.T) { 508 var wg sync.WaitGroup 509 limit := semaphore.NewWeighted(3) 510 511 got := new(analyzer.AnalysisResult) 512 a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{ 513 FilePatterns: tt.args.filePatterns, 514 DisabledAnalyzers: tt.args.disabledAnalyzers, 515 }) 516 if err != nil && tt.wantErr != "" { 517 require.NotNil(t, err) 518 assert.Contains(t, err.Error(), tt.wantErr) 519 return 520 } 521 require.NoError(t, err) 522 523 info, err := os.Stat(tt.args.testFilePath) 524 require.NoError(t, err) 525 526 ctx := context.Background() 527 err = a.AnalyzeFile(ctx, &wg, limit, got, "", tt.args.filePath, info, 528 func() (dio.ReadSeekCloserAt, error) { 529 if tt.args.testFilePath == "testdata/error" { 530 return nil, xerrors.New("error") 531 } else if tt.args.testFilePath == "testdata/no-permission" { 532 os.Chmod(tt.args.testFilePath, 0000) 533 t.Cleanup(func() { 534 os.Chmod(tt.args.testFilePath, 0644) 535 }) 536 } 537 return os.Open(tt.args.testFilePath) 538 }, 539 nil, analyzer.AnalysisOptions{}, 540 ) 541 542 wg.Wait() 543 if tt.wantErr != "" { 544 require.NotNil(t, err) 545 assert.Contains(t, err.Error(), tt.wantErr) 546 return 547 } 548 549 require.NoError(t, err) 550 assert.Equal(t, tt.want, got) 551 }) 552 } 553 } 554 555 func TestAnalyzerGroup_PostAnalyze(t *testing.T) { 556 tests := []struct { 557 name string 558 dir string 559 analyzerType analyzer.Type 560 want *analyzer.AnalysisResult 561 }{ 562 { 563 name: "jars with invalid jar", 564 dir: "testdata/post-apps/jar/", 565 analyzerType: analyzer.TypeJar, 566 want: &analyzer.AnalysisResult{ 567 Applications: []types.Application{ 568 { 569 Type: types.Jar, 570 FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar", 571 Libraries: types.Packages{ 572 { 573 Name: "com.fasterxml.jackson.core:jackson-annotations", 574 Version: "2.15.0-rc2", 575 FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar", 576 }, 577 }, 578 }, 579 }, 580 }, 581 }, 582 { 583 name: "poetry files with invalid file", 584 dir: "testdata/post-apps/poetry/", 585 analyzerType: analyzer.TypePoetry, 586 want: &analyzer.AnalysisResult{ 587 Applications: []types.Application{ 588 { 589 Type: types.Poetry, 590 FilePath: "testdata/post-apps/poetry/happy/poetry.lock", 591 Libraries: types.Packages{ 592 { 593 ID: "certifi@2022.12.7", 594 Name: "certifi", 595 Version: "2022.12.7", 596 }, 597 }, 598 }, 599 }, 600 }, 601 }, 602 } 603 for _, tt := range tests { 604 t.Run(tt.name, func(t *testing.T) { 605 a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{}) 606 require.NoError(t, err) 607 608 // Create a virtual filesystem 609 composite, err := analyzer.NewCompositeFS(analyzer.AnalyzerGroup{}) 610 require.NoError(t, err) 611 612 mfs := mapfs.New() 613 require.NoError(t, mfs.CopyFilesUnder(tt.dir)) 614 composite.Set(tt.analyzerType, mfs) 615 616 if tt.analyzerType == analyzer.TypeJar { 617 // init java-trivy-db with skip update 618 javadb.Init("./language/java/jar/testdata", "ghcr.io/aquasecurity/trivy-java-db", true, false, types.RegistryOptions{Insecure: false}) 619 } 620 621 ctx := context.Background() 622 got := new(analyzer.AnalysisResult) 623 err = a.PostAnalyze(ctx, composite, got, analyzer.AnalysisOptions{}) 624 require.NoError(t, err) 625 assert.Equal(t, tt.want, got) 626 }) 627 } 628 } 629 630 func TestAnalyzerGroup_AnalyzerVersions(t *testing.T) { 631 tests := []struct { 632 name string 633 disabled []analyzer.Type 634 want analyzer.Versions 635 }{ 636 { 637 name: "happy path", 638 disabled: []analyzer.Type{}, 639 want: analyzer.Versions{ 640 Analyzers: map[string]int{ 641 "alpine": 1, 642 "apk-repo": 1, 643 "apk": 2, 644 "bundler": 1, 645 "ubuntu": 1, 646 "ubuntu-esm": 1, 647 }, 648 PostAnalyzers: map[string]int{ 649 "jar": 1, 650 "poetry": 1, 651 }, 652 }, 653 }, 654 { 655 name: "disable analyzers", 656 disabled: []analyzer.Type{ 657 analyzer.TypeAlpine, 658 analyzer.TypeApkRepo, 659 analyzer.TypeUbuntu, 660 analyzer.TypeUbuntuESM, 661 analyzer.TypeJar, 662 }, 663 want: analyzer.Versions{ 664 Analyzers: map[string]int{ 665 "apk": 2, 666 "bundler": 1, 667 }, 668 PostAnalyzers: map[string]int{ 669 "poetry": 1, 670 }, 671 }, 672 }, 673 } 674 for _, tt := range tests { 675 t.Run(tt.name, func(t *testing.T) { 676 a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{ 677 DisabledAnalyzers: tt.disabled, 678 }) 679 require.NoError(t, err) 680 got := a.AnalyzerVersions() 681 fmt.Printf("%v\n", got) 682 assert.Equal(t, tt.want, got) 683 }) 684 } 685 }