github.com/google/osv-scalibr@v0.4.1/binary/cli/cli_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cli_test 16 17 import ( 18 "io/fs" 19 "os" 20 "path/filepath" 21 "runtime" 22 "strings" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 scalibr "github.com/google/osv-scalibr" 28 "github.com/google/osv-scalibr/binary/cli" 29 cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto" 30 "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gobinary" 31 "github.com/google/osv-scalibr/plugin" 32 "google.golang.org/protobuf/testing/protocmp" 33 ) 34 35 func TestValidateFlags(t *testing.T) { 36 for _, tc := range []struct { 37 desc string 38 flags *cli.Flags 39 wantErr error 40 }{ 41 { 42 desc: "Valid config", 43 flags: &cli.Flags{ 44 Root: "/", 45 ResultFile: "result.textproto", 46 Output: []string{"textproto=result2.textproto", "spdx23-yaml=result.spdx.yaml"}, 47 ExtractorsToRun: []string{"java,python", "javascript"}, 48 DetectorsToRun: []string{"weakcredentials,cis"}, 49 PluginsToRun: []string{"vex"}, 50 DirsToSkip: []string{"path1,path2", "path3"}, 51 SPDXCreators: "Tool:SCALIBR,Organization:Google", 52 }, 53 wantErr: nil, 54 }, 55 { 56 desc: "Only --version set", 57 flags: &cli.Flags{PrintVersion: true}, 58 wantErr: nil, 59 }, 60 { 61 desc: "Either output flag missing", 62 flags: &cli.Flags{Root: "/"}, 63 wantErr: cmpopts.AnyError, 64 }, { 65 desc: "Result flag present", 66 flags: &cli.Flags{ 67 Root: "/", 68 ResultFile: "result.textproto", 69 }, 70 wantErr: nil, 71 }, { 72 desc: "Output flag present", 73 flags: &cli.Flags{ 74 Root: "/", 75 Output: []string{"textproto=result.textproto"}, 76 }, 77 wantErr: nil, 78 }, { 79 desc: "Wrong result extension", 80 flags: &cli.Flags{ 81 Root: "/", 82 ResultFile: "result.png", 83 }, 84 wantErr: cmpopts.AnyError, 85 }, { 86 desc: "Invalid output format", 87 flags: &cli.Flags{ 88 Root: "/", 89 Output: []string{"invalid"}, 90 }, 91 wantErr: cmpopts.AnyError, 92 }, { 93 desc: "Unknown output format", 94 flags: &cli.Flags{ 95 Root: "/", 96 Output: []string{"unknown=foo.bar"}, 97 }, 98 wantErr: cmpopts.AnyError, 99 }, { 100 desc: "Wrong output extension", 101 flags: &cli.Flags{ 102 Root: "/", 103 Output: []string{"proto=result.png"}, 104 }, 105 wantErr: cmpopts.AnyError, 106 }, { 107 desc: "Invalid extractors", 108 flags: &cli.Flags{ 109 Root: "/", 110 ResultFile: "result.textproto", 111 ExtractorsToRun: []string{",python"}, 112 }, 113 wantErr: cmpopts.AnyError, 114 }, 115 { 116 desc: "Nonexistent extractors", 117 flags: &cli.Flags{ 118 Root: "/", 119 ResultFile: "result.textproto", 120 ExtractorsToRun: []string{"asdf"}, 121 }, 122 wantErr: cmpopts.AnyError, 123 }, 124 { 125 desc: "Invalid detectors", 126 flags: &cli.Flags{ 127 Root: "/", 128 ResultFile: "result.textproto", 129 DetectorsToRun: []string{"cve,"}, 130 }, 131 wantErr: cmpopts.AnyError, 132 }, 133 { 134 desc: "Nonexistent detectors", 135 flags: &cli.Flags{ 136 Root: "/", 137 ResultFile: "result.textproto", 138 DetectorsToRun: []string{"asdf"}, 139 }, 140 wantErr: cmpopts.AnyError, 141 }, 142 { 143 desc: "Detector with missing extractor dependency (enabled automatically)", 144 flags: &cli.Flags{ 145 Root: "/", 146 ResultFile: "result.textproto", 147 ExtractorsToRun: []string{"python,javascript"}, 148 DetectorsToRun: []string{"govulncheck"}, // Needs the Go binary extractor. 149 }, 150 wantErr: nil, 151 }, 152 { 153 desc: "Invalid paths to skip", 154 flags: &cli.Flags{ 155 Root: "/", 156 ResultFile: "result.textproto", 157 DirsToSkip: []string{"path1,,path3"}, 158 }, 159 wantErr: cmpopts.AnyError, 160 }, 161 { 162 desc: "Invalid glob for skipping directories", 163 flags: &cli.Flags{ 164 Root: "/", 165 ResultFile: "result.textproto", 166 SkipDirGlob: "[", 167 }, 168 wantErr: cmpopts.AnyError, 169 }, 170 { 171 desc: "Invalid SPDX creator format", 172 flags: &cli.Flags{ 173 Root: "/", 174 SPDXCreators: "invalid:creator:format", 175 }, 176 wantErr: cmpopts.AnyError, 177 }, 178 { 179 desc: "Image Platform without Remote Image", 180 flags: &cli.Flags{ 181 ImagePlatform: "linux/amd64", 182 }, 183 wantErr: cmpopts.AnyError, 184 }, 185 { 186 desc: "Image Platform with Remote Image", 187 flags: &cli.Flags{ 188 RemoteImage: "docker", 189 ImagePlatform: "linux/amd64", 190 ResultFile: "result.textproto", 191 }, 192 wantErr: nil, 193 }, 194 { 195 desc: "Remote Image with Image Tarball", 196 flags: &cli.Flags{ 197 RemoteImage: "docker", 198 ImageTarball: "image.tar", 199 ResultFile: "result.textproto", 200 }, 201 wantErr: cmpopts.AnyError, 202 }, 203 { 204 desc: "Local Docker Image", 205 flags: &cli.Flags{ 206 ImageLocal: "nginx:latest", 207 ResultFile: "result.textproto", 208 }, 209 wantErr: nil, 210 }, 211 { 212 desc: "Local Image with Image Tarball", 213 flags: &cli.Flags{ 214 ImageLocal: "nginx:latest", 215 ImageTarball: "image.tar", 216 ResultFile: "result.textproto", 217 }, 218 wantErr: cmpopts.AnyError, 219 }, 220 { 221 desc: "valid extractor override", 222 flags: &cli.Flags{ 223 Root: "/", 224 ResultFile: "result.textproto", 225 ExtractorOverride: []string{"python/wheelegg:*.py"}, 226 }, 227 wantErr: nil, 228 }, 229 { 230 desc: "extractor override invalid format", 231 flags: &cli.Flags{ 232 Root: "/", 233 ResultFile: "result.textproto", 234 ExtractorOverride: []string{"python/wheelegg"}, 235 }, 236 wantErr: cmpopts.AnyError, 237 }, 238 { 239 desc: "extractor override invalid glob", 240 flags: &cli.Flags{ 241 Root: "/", 242 ResultFile: "result.textproto", 243 ExtractorOverride: []string{"python/wheelegg:["}, 244 }, 245 wantErr: cmpopts.AnyError, 246 }, 247 } { 248 t.Run(tc.desc, func(t *testing.T) { 249 err := cli.ValidateFlags(tc.flags) 250 if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" { 251 t.Errorf("cli.ValidateFlags(%v) error got diff (-want +got):\n%s", tc.flags, diff) 252 } 253 }) 254 } 255 } 256 257 func TestGetScanConfig_ScanRoots(t *testing.T) { 258 for _, tc := range []struct { 259 desc string 260 flags map[string]*cli.Flags 261 wantScanRoots map[string][]string 262 }{ 263 { 264 desc: "Default scan roots", 265 flags: map[string]*cli.Flags{ 266 "darwin": {}, 267 "linux": {}, 268 "windows": {}, 269 }, 270 wantScanRoots: map[string][]string{ 271 "darwin": {"/"}, 272 "linux": {"/"}, 273 "windows": {"C:\\"}, 274 }, 275 }, 276 { 277 desc: "Scan root are provided and used", 278 flags: map[string]*cli.Flags{ 279 "darwin": {Root: "/root"}, 280 "linux": {Root: "/root"}, 281 "windows": {Root: "C:\\myroot"}, 282 }, 283 wantScanRoots: map[string][]string{ 284 "darwin": {"/root"}, 285 "linux": {"/root"}, 286 "windows": {"C:\\myroot"}, 287 }, 288 }, 289 { 290 desc: "Scan root is null if image tarball is provided", 291 flags: map[string]*cli.Flags{ 292 "darwin": {ImageTarball: "image.tar"}, 293 "linux": {ImageTarball: "image.tar"}, 294 "windows": {ImageTarball: "image.tar"}, 295 }, 296 wantScanRoots: map[string][]string{ 297 "darwin": nil, 298 "linux": nil, 299 "windows": nil, 300 }, 301 }, 302 { 303 desc: "Scan root is null if local image is provided", 304 flags: map[string]*cli.Flags{ 305 "darwin": {ImageLocal: "nginx:latest"}, 306 "linux": {ImageLocal: "nginx:latest"}, 307 "windows": {ImageLocal: "nginx:latest"}, 308 }, 309 wantScanRoots: map[string][]string{ 310 "darwin": nil, 311 "linux": nil, 312 "windows": nil, 313 }, 314 }, 315 } { 316 t.Run(tc.desc, func(t *testing.T) { 317 wantScanRoots, ok := tc.wantScanRoots[runtime.GOOS] 318 if !ok { 319 t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS) 320 } 321 322 flags, ok := tc.flags[runtime.GOOS] 323 if !ok { 324 t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS) 325 } 326 327 cfg, err := flags.GetScanConfig() 328 if err != nil { 329 t.Errorf("%v.GetScanConfig(): %v", flags, err) 330 } 331 var gotScanRoots []string 332 for _, r := range cfg.ScanRoots { 333 gotScanRoots = append(gotScanRoots, r.Path) 334 } 335 if diff := cmp.Diff(wantScanRoots, gotScanRoots); diff != "" { 336 t.Errorf("%v.GetScanConfig() ScanRoots got diff (-want +got):\n%s", flags, diff) 337 } 338 }) 339 } 340 } 341 342 func TestGetScanConfig_NetworkCapabilities(t *testing.T) { 343 for _, tc := range []struct { 344 desc string 345 flags cli.Flags 346 wantNetwork plugin.Network 347 }{ 348 { 349 desc: "online_if_nothing_set", 350 flags: cli.Flags{}, 351 wantNetwork: plugin.NetworkOnline, 352 }, 353 { 354 desc: "offline_if_offline_flag_set", 355 flags: cli.Flags{Offline: true}, 356 wantNetwork: plugin.NetworkOffline, 357 }, 358 } { 359 t.Run(tc.desc, func(t *testing.T) { 360 cfg, err := tc.flags.GetScanConfig() 361 if err != nil { 362 t.Errorf("%v.GetScanConfig(): %v", tc.flags, err) 363 } 364 if tc.wantNetwork != cfg.Capabilities.Network { 365 t.Errorf("%v.GetScanConfig(): want %v, got %v", tc.flags, tc.wantNetwork, cfg.Capabilities.Network) 366 } 367 }) 368 } 369 } 370 371 func TestGetScanConfig_DirsToSkip(t *testing.T) { 372 for _, tc := range []struct { 373 desc string 374 flags map[string]*cli.Flags 375 wantDirsToSkip map[string][]string 376 }{ 377 { 378 desc: "Skip default dirs", 379 flags: map[string]*cli.Flags{ 380 "darwin": {Root: "/"}, 381 "linux": {Root: "/"}, 382 "windows": {Root: "C:\\"}, 383 }, 384 wantDirsToSkip: map[string][]string{ 385 "darwin": {"/dev", "/proc", "/sys"}, 386 "linux": {"/dev", "/proc", "/sys"}, 387 "windows": {"C:\\Windows"}, 388 }, 389 }, 390 { 391 desc: "Skip additional dirs", 392 flags: map[string]*cli.Flags{ 393 "darwin": { 394 Root: "/", 395 DirsToSkip: []string{"/boot,/mnt,C:\\boot", "C:\\mnt"}, 396 }, 397 "linux": { 398 Root: "/", 399 DirsToSkip: []string{"/boot,/mnt", "C:\\boot,C:\\mnt"}, 400 }, 401 "windows": { 402 Root: "C:\\", 403 DirsToSkip: []string{"C:\\boot,C:\\mnt"}, 404 }, 405 }, 406 wantDirsToSkip: map[string][]string{ 407 "darwin": {"/dev", "/proc", "/sys", "/boot", "/mnt"}, 408 "linux": {"/dev", "/proc", "/sys", "/boot", "/mnt"}, 409 "windows": {"C:\\Windows", "C:\\boot", "C:\\mnt"}, 410 }, 411 }, 412 { 413 desc: "Ignore paths outside root", 414 flags: map[string]*cli.Flags{ 415 "darwin": { 416 Root: "/root", 417 DirsToSkip: []string{"/root/dir1,/dir2"}, 418 }, 419 "linux": { 420 Root: "/root", 421 DirsToSkip: []string{"/root/dir1,/dir2"}, 422 }, 423 "windows": { 424 Root: "C:\\root", 425 DirsToSkip: []string{"C:\\root\\dir1,c:\\dir2"}, 426 }, 427 }, 428 wantDirsToSkip: map[string][]string{ 429 "darwin": {"/root/dir1"}, 430 "linux": {"/root/dir1"}, 431 "windows": {"C:\\root\\dir1"}, 432 }, 433 }, 434 } { 435 t.Run(tc.desc, func(t *testing.T) { 436 wantDirsToSkip, ok := tc.wantDirsToSkip[runtime.GOOS] 437 if !ok { 438 t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS) 439 } 440 441 flags, ok := tc.flags[runtime.GOOS] 442 if !ok { 443 t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS) 444 } 445 446 cfg, err := flags.GetScanConfig() 447 if err != nil { 448 t.Errorf("%v.GetScanConfig(): %v", flags, err) 449 } 450 if diff := cmp.Diff(wantDirsToSkip, cfg.DirsToSkip); diff != "" { 451 t.Errorf("%v.GetScanConfig() dirsToSkip got diff (-want +got):\n%s", flags, diff) 452 } 453 }) 454 } 455 } 456 457 func TestGetScanConfig_SkipDirRegex(t *testing.T) { 458 for _, tc := range []struct { 459 desc string 460 flags *cli.Flags 461 wantSkipDirRegex string 462 wantNil bool 463 }{ 464 { 465 desc: "simple regex", 466 flags: &cli.Flags{ 467 Root: "/", 468 SkipDirRegex: "asdf.*foo", 469 }, 470 wantSkipDirRegex: "asdf.*foo", 471 }, 472 { 473 desc: "no regex", 474 flags: &cli.Flags{ 475 Root: "/", 476 }, 477 wantNil: true, 478 }, 479 } { 480 t.Run(tc.desc, func(t *testing.T) { 481 cfg, err := tc.flags.GetScanConfig() 482 if err != nil { 483 t.Errorf("%v.GetScanConfig(): %v", tc.flags, err) 484 } 485 if tc.wantNil && cfg.SkipDirRegex != nil { 486 t.Errorf("%v.GetScanConfig() SkipDirRegex got %q, want nil", tc.flags, cfg.SkipDirRegex) 487 } 488 if !tc.wantNil && tc.wantSkipDirRegex != cfg.SkipDirRegex.String() { 489 t.Errorf("%v.GetScanConfig() SkipDirRegex got %q, want %q", tc.flags, cfg.SkipDirRegex.String(), tc.wantSkipDirRegex) 490 } 491 }) 492 } 493 } 494 495 func TestGetScanConfig_CreatePlugins(t *testing.T) { 496 for _, tc := range []struct { 497 desc string 498 flags *cli.Flags 499 wantPluginCount int 500 }{ 501 { 502 desc: "Create an extractor", 503 flags: &cli.Flags{ 504 PluginsToRun: []string{"python/wheelegg"}, 505 }, 506 wantPluginCount: 1, 507 }, 508 { 509 desc: "Create an extractor - legacy field", 510 flags: &cli.Flags{ 511 ExtractorsToRun: []string{"python/wheelegg"}, 512 }, 513 wantPluginCount: 1, 514 }, 515 { 516 desc: "Create a detector - legacy field", 517 flags: &cli.Flags{ 518 PluginsToRun: []string{"cis"}, 519 }, 520 wantPluginCount: 1, 521 }, 522 { 523 desc: "Create a detector - legacy field", 524 flags: &cli.Flags{ 525 DetectorsToRun: []string{"cis"}, 526 }, 527 wantPluginCount: 1, 528 }, 529 { 530 desc: "Create an annotator", 531 flags: &cli.Flags{ 532 PluginsToRun: []string{"vex/cachedir"}, 533 }, 534 wantPluginCount: 1, 535 }, 536 } { 537 t.Run(tc.desc, func(t *testing.T) { 538 cfg, err := tc.flags.GetScanConfig() 539 if err != nil { 540 t.Errorf("%v.GetScanConfig(): %v", tc.flags, err) 541 } 542 if len(cfg.Plugins) != tc.wantPluginCount { 543 t.Errorf("%v.GetScanConfig() want plugin count %d got %d", tc.flags, tc.wantPluginCount, len(cfg.Plugins)) 544 } 545 }) 546 } 547 } 548 549 func TestGetScanConfig_PluginConfig(t *testing.T) { 550 for _, tc := range []struct { 551 desc string 552 cfgFlags []string 553 wantCFG *cpb.PluginConfig 554 wantMaxFileSizeBytes int64 555 wantVersionFromContent bool 556 }{ 557 { 558 desc: "single_setting_in_one_flag", 559 cfgFlags: []string{"max_file_size_bytes:1234"}, 560 wantCFG: &cpb.PluginConfig{ 561 MaxFileSizeBytes: 1234, 562 }, 563 wantMaxFileSizeBytes: 1234, 564 }, 565 { 566 desc: "multiple_settings_in_one_flag", 567 cfgFlags: []string{"max_file_size_bytes:1234 plugin_specific:{go_binary:{version_from_content:true}}"}, 568 wantCFG: &cpb.PluginConfig{ 569 MaxFileSizeBytes: 1234, 570 PluginSpecific: []*cpb.PluginSpecificConfig{ 571 {Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}}, 572 }, 573 }, 574 wantMaxFileSizeBytes: 1234, 575 wantVersionFromContent: true, 576 }, 577 { 578 desc: "multiple_settings_in_multiple_flags", 579 cfgFlags: []string{ 580 "max_file_size_bytes:1234", 581 "plugin_specific:{go_binary:{version_from_content:true}}", 582 }, 583 wantCFG: &cpb.PluginConfig{ 584 MaxFileSizeBytes: 1234, 585 PluginSpecific: []*cpb.PluginSpecificConfig{ 586 {Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}}, 587 }, 588 }, 589 wantMaxFileSizeBytes: 1234, 590 wantVersionFromContent: true, 591 }, 592 { 593 desc: "plugin_specific_config_short_version", 594 cfgFlags: []string{"go_binary:{version_from_content:true}"}, 595 wantCFG: &cpb.PluginConfig{ 596 PluginSpecific: []*cpb.PluginSpecificConfig{ 597 {Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}}, 598 }, 599 }, 600 wantVersionFromContent: true, 601 }, 602 } { 603 t.Run(tc.desc, func(t *testing.T) { 604 flags := &cli.Flags{ 605 ExtractorsToRun: []string{gobinary.Name}, 606 PluginCFG: tc.cfgFlags, 607 } 608 609 scanConfig, err := flags.GetScanConfig() 610 if err != nil { 611 t.Errorf("%v.GetScanConfig(): %v", flags, err) 612 } 613 614 if diff := cmp.Diff(tc.wantCFG, scanConfig.RequiredPluginConfig, protocmp.Transform()); diff != "" { 615 t.Errorf("%v.GetScanConfig() ScanRoots got diff (-want +got):\n%s", flags, diff) 616 } 617 if len(scanConfig.Plugins) != 1 { 618 t.Fatalf("%v.GetScanConfig(): Got %d plugins, want 1", flags, len(scanConfig.Plugins)) 619 } 620 621 ext, ok := scanConfig.Plugins[0].(*gobinary.Extractor) 622 if !ok { 623 t.Fatalf("%v.GetScanConfig(): Got wrong plugin type", flags) 624 } 625 626 maxFileSizeBytes := ext.MaxFileSizeBytes() 627 if tc.wantMaxFileSizeBytes != maxFileSizeBytes { 628 t.Errorf("%v.GetScanConfig(): Want maxFileSizeBytes %d, got %d", flags, tc.wantMaxFileSizeBytes, maxFileSizeBytes) 629 } 630 631 versionFromContent := ext.VersionFromContent() 632 if tc.wantVersionFromContent != versionFromContent { 633 t.Errorf("%v.GetScanConfig(): Want versionFromContent %t, got %t", flags, tc.wantVersionFromContent, versionFromContent) 634 } 635 }) 636 } 637 } 638 639 func TestGetScanConfig_MaxFileSize(t *testing.T) { 640 for _, tc := range []struct { 641 desc string 642 flags *cli.Flags 643 wantMaxFileSize int 644 }{ 645 { 646 desc: "max file size unset", 647 flags: &cli.Flags{ 648 MaxFileSize: 0, 649 }, 650 wantMaxFileSize: 0, 651 }, 652 { 653 desc: "max file size set", 654 flags: &cli.Flags{ 655 MaxFileSize: 100, 656 }, 657 wantMaxFileSize: 100, 658 }, 659 } { 660 t.Run(tc.desc, func(t *testing.T) { 661 cfg, err := tc.flags.GetScanConfig() 662 if err != nil { 663 t.Errorf("%+v.GetScanConfig(): %v", tc.flags, err) 664 } 665 if cfg.MaxFileSize != tc.wantMaxFileSize { 666 t.Errorf("%+v.GetScanConfig() got max file size %d, want %d", tc.flags, cfg.MaxFileSize, tc.wantMaxFileSize) 667 } 668 }) 669 } 670 } 671 672 func TestGetScanConfig_PluginGroups(t *testing.T) { 673 for _, tc := range []struct { 674 desc string 675 flags *cli.Flags 676 wantPlugins []string 677 dontWantPlugins []string 678 }{ 679 { 680 desc: "default_plugins_if_nothing_is_specified", 681 flags: &cli.Flags{}, 682 wantPlugins: []string{ 683 "python/wheelegg", 684 "windows/dismpatch", 685 "vex/cachedir", 686 }, 687 dontWantPlugins: []string{ 688 // Not default plugins 689 "govulncheck/binary", 690 "vscode/extensions", 691 "baseimage", 692 }, 693 }, 694 { 695 desc: "default_extractors_legacy", 696 flags: &cli.Flags{ 697 ExtractorsToRun: []string{"default"}, 698 }, 699 wantPlugins: []string{ 700 // Filesystem Extractor 701 "python/wheelegg", 702 // Standalone Extractor 703 "windows/dismpatch", 704 }, 705 dontWantPlugins: []string{ 706 // Not a default Extractor 707 "vscode/extensions", 708 // Not an Extractor 709 "govulncheck/binary", 710 }, 711 }, 712 { 713 desc: "all_extractors_legacy", 714 flags: &cli.Flags{ 715 ExtractorsToRun: []string{"all"}, 716 }, 717 wantPlugins: []string{ 718 // Filesystem Extractor 719 "vscode/extensions", 720 // Standalone Extractor 721 "windows/dismpatch", 722 }, 723 dontWantPlugins: []string{ 724 // Not an Extractor 725 "govulncheck/binary", 726 }, 727 }, 728 { 729 desc: "default_detectors_legacy", 730 flags: &cli.Flags{ 731 DetectorsToRun: []string{"default"}, 732 }, 733 // There are no default Detectors at the moment. 734 dontWantPlugins: []string{ 735 // Not a default Detector 736 "govulncheck/binary", 737 // Not a Detector 738 "python/wheelegg", 739 }, 740 }, 741 { 742 desc: "all_detectors_legacy", 743 flags: &cli.Flags{ 744 DetectorsToRun: []string{"all"}, 745 }, 746 wantPlugins: []string{ 747 "govulncheck/binary", 748 }, 749 dontWantPlugins: []string{ 750 // Not Detectors 751 "python/wheelegg", 752 "vex/cachedir", 753 }, 754 }, 755 { 756 desc: "all_extractors", 757 flags: &cli.Flags{ 758 PluginsToRun: []string{"extractors/all"}, 759 }, 760 wantPlugins: []string{ 761 // Filesystem Extractor 762 "vscode/extensions", 763 // Standalone Extractor 764 "windows/dismpatch", 765 }, 766 dontWantPlugins: []string{ 767 // Not an Extractor 768 "govulncheck/binary", 769 }, 770 }, 771 { 772 desc: "all_detectors", 773 flags: &cli.Flags{ 774 PluginsToRun: []string{"detectors/all"}, 775 }, 776 wantPlugins: []string{ 777 "govulncheck/binary", 778 }, 779 dontWantPlugins: []string{ 780 // Not Detectors 781 "python/wheelegg", 782 "vex/cachedir", 783 }, 784 }, 785 { 786 desc: "all_annotators", 787 flags: &cli.Flags{ 788 PluginsToRun: []string{"annotators/all"}, 789 }, 790 wantPlugins: []string{ 791 "vex/cachedir", 792 }, 793 dontWantPlugins: []string{ 794 // Not Annotators 795 "python/wheelegg", 796 "govulncheck/binary", 797 }, 798 }, 799 { 800 desc: "all_enrichers", 801 flags: &cli.Flags{ 802 PluginsToRun: []string{"enrichers/all"}, 803 }, 804 wantPlugins: []string{ 805 "baseimage", 806 }, 807 dontWantPlugins: []string{ 808 // Not Enrichers 809 "python/wheelegg", 810 "govulncheck/binary", 811 "vex/cachedir", 812 }, 813 }, 814 { 815 desc: "default_plugins", 816 flags: &cli.Flags{ 817 PluginsToRun: []string{"default"}, 818 }, 819 wantPlugins: []string{ 820 "python/wheelegg", 821 "windows/dismpatch", 822 "vex/cachedir", 823 }, 824 dontWantPlugins: []string{ 825 // Not default plugins 826 "govulncheck/binary", 827 "vscode/extensions", 828 "baseimage", 829 }, 830 }, 831 { 832 desc: "all_plugins", 833 flags: &cli.Flags{ 834 PluginsToRun: []string{"all"}, 835 }, 836 wantPlugins: []string{ 837 "python/wheelegg", 838 "windows/dismpatch", 839 "govulncheck/binary", 840 "vex/cachedir", 841 "baseimage", 842 }, 843 }, 844 } { 845 t.Run(tc.desc, func(t *testing.T) { 846 cfg, err := tc.flags.GetScanConfig() 847 if err != nil { 848 t.Errorf("%+v.GetScanConfig(): %v", tc.flags, err) 849 } 850 for _, name := range tc.wantPlugins { 851 found := false 852 for _, p := range cfg.Plugins { 853 if p.Name() == name { 854 found = true 855 break 856 } 857 } 858 if !found { 859 t.Errorf("%+v.GetScanConfig() didn't find wanted plugin %q in config", tc.flags, name) 860 } 861 } 862 for _, name := range tc.dontWantPlugins { 863 for _, p := range cfg.Plugins { 864 if p.Name() == name { 865 t.Errorf("%+v.GetScanConfig() found unwanted plugin %q in config", tc.flags, name) 866 break 867 } 868 } 869 } 870 }) 871 } 872 } 873 874 func TestWriteScanResults(t *testing.T) { 875 testDirPath := t.TempDir() 876 result := &scalibr.ScanResult{ 877 Version: "1.2.3", 878 Status: &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded}, 879 } 880 for _, tc := range []struct { 881 desc string 882 flags *cli.Flags 883 wantFilename string 884 wantContentPrefix string 885 }{ 886 { 887 desc: "Create proto using --result flag", 888 flags: &cli.Flags{ 889 ResultFile: filepath.Join(testDirPath, "result.textproto"), 890 }, 891 wantFilename: "result.textproto", 892 wantContentPrefix: "version:", 893 }, 894 { 895 desc: "Create proto using --output flag", 896 flags: &cli.Flags{ 897 Output: []string{"textproto=" + filepath.Join(testDirPath, "result2.textproto")}, 898 }, 899 wantFilename: "result2.textproto", 900 wantContentPrefix: "version:", 901 }, 902 { 903 desc: "Create SPDX 2.3", 904 flags: &cli.Flags{ 905 Output: []string{"spdx23-tag-value=" + filepath.Join(testDirPath, "result.spdx")}, 906 }, 907 wantFilename: "result.spdx", 908 wantContentPrefix: "SPDXVersion: SPDX-2.3", 909 }, 910 { 911 desc: "Create CDX", 912 flags: &cli.Flags{ 913 Output: []string{"cdx-json=" + filepath.Join(testDirPath, "result.cyclonedx.json")}, 914 }, 915 wantFilename: "result.cyclonedx.json", 916 wantContentPrefix: "{\n \"$schema\": \"http://cyclonedx.org/schema/bom-1.6.schema.json\"", 917 }, 918 } { 919 t.Run(tc.desc, func(t *testing.T) { 920 if err := tc.flags.WriteScanResults(result); err != nil { 921 t.Fatalf("%v.WriteScanResults(%v): %v", tc.flags, result, err) 922 } 923 924 fullPath := filepath.Join(testDirPath, tc.wantFilename) 925 got, err := os.ReadFile(fullPath) 926 if err != nil { 927 t.Fatalf("error while reading %s: %v", fullPath, err) 928 } 929 gotStr := string(got) 930 931 if !strings.HasPrefix(gotStr, tc.wantContentPrefix) { 932 t.Errorf("%v.WriteScanResults(%v) want file with content prefix %q, got %q", tc.flags, result, tc.wantContentPrefix, gotStr) 933 } 934 }) 935 } 936 } 937 938 type fakeFileAPI struct { 939 path string 940 } 941 942 func (f *fakeFileAPI) Path() string { 943 return f.path 944 } 945 946 func (f *fakeFileAPI) Stat() (fs.FileInfo, error) { 947 return nil, nil 948 } 949 950 func TestGetScanConfig_ExtractorOverride(t *testing.T) { 951 tests := []struct { 952 name string 953 flags *cli.Flags 954 fileAPI *fakeFileAPI 955 wantExtractorName string 956 wantNumExtractors int 957 wantErr error 958 }{ 959 { 960 name: "no_override", 961 flags: &cli.Flags{ 962 Root: "/", 963 ResultFile: "result.textproto", 964 }, 965 fileAPI: &fakeFileAPI{path: "foo.py"}, 966 wantNumExtractors: 0, 967 wantErr: nil, 968 }, 969 { 970 name: "extractor_override_plugin_not_found", 971 flags: &cli.Flags{ 972 Root: "/", 973 ResultFile: "result.textproto", 974 ExtractorOverride: []string{"nonexistent/plugin:*.py"}, 975 PluginsToRun: []string{"python/wheelegg"}, 976 }, 977 fileAPI: &fakeFileAPI{path: "foo.py"}, 978 wantNumExtractors: 0, 979 wantErr: cmpopts.AnyError, 980 }, 981 { 982 name: "override_matches", 983 flags: &cli.Flags{ 984 Root: "/", 985 ResultFile: "result.textproto", 986 ExtractorOverride: []string{"python/wheelegg:*.py"}, 987 PluginsToRun: []string{"python/wheelegg"}, 988 }, 989 fileAPI: &fakeFileAPI{path: "foo.py"}, 990 wantExtractorName: "python/wheelegg", 991 wantNumExtractors: 1, 992 wantErr: nil, 993 }, 994 { 995 name: "override_does_not_match", 996 flags: &cli.Flags{ 997 Root: "/", 998 ResultFile: "result.textproto", 999 ExtractorOverride: []string{"python/wheelegg:*.py"}, 1000 PluginsToRun: []string{"python/wheelegg"}, 1001 }, 1002 fileAPI: &fakeFileAPI{path: "abc/efg/foo.go"}, 1003 wantNumExtractors: 0, 1004 wantErr: nil, 1005 }, 1006 } 1007 1008 for _, tt := range tests { 1009 t.Run(tt.name, func(t *testing.T) { 1010 cfg, err := tt.flags.GetScanConfig() 1011 if diff := cmp.Diff(tt.wantErr, err, cmpopts.EquateErrors()); diff != "" { 1012 t.Fatalf("GetScanConfig() error got diff (-want +got):\n%s", diff) 1013 } 1014 1015 // If an error was expected, the rest of the checks are not necessary. 1016 if tt.wantErr != nil { 1017 return 1018 } 1019 1020 if cfg.ExtractorOverride == nil && tt.wantNumExtractors > 0 { 1021 t.Fatalf("ExtractorOverride is nil, want non-nil") 1022 } 1023 if cfg.ExtractorOverride != nil { 1024 extractors := cfg.ExtractorOverride(tt.fileAPI) 1025 if len(extractors) != tt.wantNumExtractors { 1026 t.Fatalf("ExtractorOverride() returned %d extractors, want %d", len(extractors), tt.wantNumExtractors) 1027 } 1028 if tt.wantNumExtractors == 1 && extractors[0].Name() != tt.wantExtractorName { 1029 t.Errorf("ExtractorOverride() returned extractor %q, want %q", extractors[0].Name(), tt.wantExtractorName) 1030 } 1031 } 1032 }) 1033 } 1034 }