github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/golang/gomod/gomod_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 gomod_test 16 17 import ( 18 "testing" 19 20 "github.com/google/go-cmp/cmp" 21 "github.com/google/go-cmp/cmp/cmpopts" 22 "github.com/google/osv-scalibr/extractor" 23 "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gomod" 24 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 25 "github.com/google/osv-scalibr/inventory" 26 "github.com/google/osv-scalibr/purl" 27 "github.com/google/osv-scalibr/testing/extracttest" 28 ) 29 30 func TestExtractor_FileRequired(t *testing.T) { 31 tests := []struct { 32 name string 33 inputPath string 34 want bool 35 }{ 36 { 37 inputPath: "", 38 want: false, 39 }, 40 { 41 inputPath: "go.mod", 42 want: true, 43 }, 44 { 45 inputPath: "path/to/my/go.mod", 46 want: true, 47 }, 48 { 49 inputPath: "path/to/my/go.mod/file", 50 want: false, 51 }, 52 { 53 inputPath: "path/to/my/go.mod.file", 54 want: false, 55 }, 56 { 57 inputPath: "path.to.my.go.mod", 58 want: false, 59 }, 60 } 61 for _, tt := range tests { 62 t.Run(tt.inputPath, func(t *testing.T) { 63 e := gomod.Extractor{} 64 got := e.FileRequired(simplefileapi.New(tt.inputPath, nil)) 65 if got != tt.want { 66 t.Errorf("FileRequired(%s) got = %v, want %v", tt.inputPath, got, tt.want) 67 } 68 }) 69 } 70 } 71 72 func TestExtractor_Extract(t *testing.T) { 73 tests := []*extracttest.TestTableEntry{ 74 { 75 Name: "invalid", 76 InputConfig: extracttest.ScanInputMockConfig{ 77 Path: "testdata/not-go-mod.mod", 78 }, 79 WantErr: extracttest.ContainsErrStr{Str: "could not extract"}, 80 }, 81 { 82 Name: "no packages", 83 InputConfig: extracttest.ScanInputMockConfig{ 84 Path: "testdata/empty.mod", 85 }, 86 WantPackages: nil, 87 }, 88 { 89 Name: "one package", 90 InputConfig: extracttest.ScanInputMockConfig{ 91 Path: "testdata/one-package.mod", 92 }, 93 WantPackages: []*extractor.Package{ 94 { 95 Name: "github.com/BurntSushi/toml", 96 Version: "1.0.0", 97 PURLType: purl.TypeGolang, 98 Locations: []string{"testdata/one-package.mod"}, 99 }, 100 }, 101 }, 102 { 103 Name: "two packages", 104 InputConfig: extracttest.ScanInputMockConfig{ 105 Path: "testdata/two-packages.mod", 106 }, 107 WantPackages: []*extractor.Package{ 108 { 109 Name: "github.com/BurntSushi/toml", 110 Version: "1.0.0", 111 PURLType: purl.TypeGolang, 112 Locations: []string{"testdata/two-packages.mod"}, 113 }, 114 { 115 Name: "gopkg.in/yaml.v2", 116 Version: "2.4.0", 117 PURLType: purl.TypeGolang, 118 Locations: []string{"testdata/two-packages.mod"}, 119 }, 120 { 121 Name: "stdlib", 122 Version: "1.17", 123 PURLType: purl.TypeGolang, 124 Locations: []string{"testdata/two-packages.mod"}, 125 }, 126 }, 127 }, 128 { 129 Name: "toolchain", 130 InputConfig: extracttest.ScanInputMockConfig{ 131 Path: "testdata/toolchain.mod", 132 }, 133 WantPackages: []*extractor.Package{ 134 { 135 Name: "github.com/BurntSushi/toml", 136 Version: "1.0.0", 137 PURLType: purl.TypeGolang, 138 Locations: []string{"testdata/toolchain.mod"}, 139 }, 140 { 141 Name: "stdlib", 142 Version: "1.23.6", 143 PURLType: purl.TypeGolang, 144 Locations: []string{"testdata/toolchain.mod"}, 145 }, 146 }, 147 }, 148 { 149 Name: "toolchain with suffix", 150 InputConfig: extracttest.ScanInputMockConfig{ 151 Path: "testdata/toolchain-with-suffix.mod", 152 }, 153 WantPackages: []*extractor.Package{ 154 { 155 Name: "github.com/BurntSushi/toml", 156 Version: "1.0.0", 157 PURLType: purl.TypeGolang, 158 Locations: []string{"testdata/toolchain-with-suffix.mod"}, 159 }, 160 { 161 Name: "stdlib", 162 Version: "1.23.6", 163 PURLType: purl.TypeGolang, 164 Locations: []string{"testdata/toolchain-with-suffix.mod"}, 165 }, 166 }, 167 }, 168 { 169 Name: "indirect packages", 170 InputConfig: extracttest.ScanInputMockConfig{ 171 Path: "testdata/indirect-packages.mod", 172 }, 173 WantPackages: []*extractor.Package{ 174 { 175 Name: "github.com/BurntSushi/toml", 176 Version: "1.0.0", 177 PURLType: purl.TypeGolang, 178 Locations: []string{"testdata/indirect-packages.mod"}, 179 }, 180 { 181 Name: "gopkg.in/yaml.v2", 182 Version: "2.4.0", 183 PURLType: purl.TypeGolang, 184 Locations: []string{"testdata/indirect-packages.mod"}, 185 }, 186 { 187 Name: "github.com/mattn/go-colorable", 188 Version: "0.1.9", 189 PURLType: purl.TypeGolang, 190 Locations: []string{"testdata/indirect-packages.mod"}, 191 }, 192 { 193 Name: "github.com/mattn/go-isatty", 194 Version: "0.0.14", 195 PURLType: purl.TypeGolang, 196 Locations: []string{"testdata/indirect-packages.mod"}, 197 }, 198 { 199 Name: "golang.org/x/sys", 200 Version: "0.0.0-20210630005230-0f9fa26af87c", 201 PURLType: purl.TypeGolang, 202 Locations: []string{"testdata/indirect-packages.mod"}, 203 }, 204 { 205 Name: "stdlib", 206 Version: "1.17", 207 PURLType: purl.TypeGolang, 208 Locations: []string{"testdata/indirect-packages.mod"}, 209 }, 210 }, 211 }, 212 { 213 Name: "replacements_ one", 214 InputConfig: extracttest.ScanInputMockConfig{ 215 Path: "testdata/replace-one.mod", 216 }, 217 WantPackages: []*extractor.Package{ 218 { 219 Name: "example.com/fork/net", 220 Version: "1.4.5", 221 PURLType: purl.TypeGolang, 222 Locations: []string{"testdata/replace-one.mod"}, 223 }, 224 }, 225 }, 226 { 227 Name: "replacements_ mixed", 228 InputConfig: extracttest.ScanInputMockConfig{ 229 Path: "testdata/replace-mixed.mod", 230 }, 231 WantPackages: []*extractor.Package{ 232 { 233 Name: "example.com/fork/net", 234 Version: "1.4.5", 235 PURLType: purl.TypeGolang, 236 Locations: []string{"testdata/replace-mixed.mod"}, 237 }, 238 { 239 Name: "golang.org/x/net", 240 Version: "0.5.6", 241 PURLType: purl.TypeGolang, 242 Locations: []string{"testdata/replace-mixed.mod"}, 243 }, 244 }, 245 }, 246 { 247 Name: "replacements_ local", 248 InputConfig: extracttest.ScanInputMockConfig{ 249 Path: "testdata/replace-local.mod", 250 }, 251 WantPackages: []*extractor.Package{ 252 { 253 Name: "./fork/net", 254 Version: "", 255 PURLType: purl.TypeGolang, 256 Locations: []string{"testdata/replace-local.mod"}, 257 }, 258 { 259 Name: "github.com/BurntSushi/toml", 260 Version: "1.0.0", 261 PURLType: purl.TypeGolang, 262 Locations: []string{"testdata/replace-local.mod"}, 263 }, 264 }, 265 }, 266 { 267 Name: "replacements_ different", 268 InputConfig: extracttest.ScanInputMockConfig{ 269 Path: "testdata/replace-different.mod", 270 }, 271 WantPackages: []*extractor.Package{ 272 { 273 Name: "example.com/fork/foe", 274 Version: "1.4.5", 275 PURLType: purl.TypeGolang, 276 Locations: []string{"testdata/replace-different.mod"}, 277 }, 278 { 279 Name: "example.com/fork/foe", 280 Version: "1.4.2", 281 PURLType: purl.TypeGolang, 282 Locations: []string{"testdata/replace-different.mod"}, 283 }, 284 }, 285 }, 286 { 287 Name: "replacements_ not required", 288 InputConfig: extracttest.ScanInputMockConfig{ 289 Path: "testdata/replace-not-required.mod", 290 }, 291 WantPackages: []*extractor.Package{ 292 { 293 Name: "golang.org/x/net", 294 Version: "0.5.6", 295 PURLType: purl.TypeGolang, 296 Locations: []string{"testdata/replace-not-required.mod"}, 297 }, 298 { 299 Name: "github.com/BurntSushi/toml", 300 Version: "1.0.0", 301 PURLType: purl.TypeGolang, 302 Locations: []string{"testdata/replace-not-required.mod"}, 303 }, 304 }, 305 }, 306 { 307 Name: "replacements_ no version", 308 InputConfig: extracttest.ScanInputMockConfig{ 309 Path: "testdata/replace-no-version.mod", 310 }, 311 WantPackages: []*extractor.Package{ 312 { 313 Name: "example.com/fork/net", 314 Version: "1.4.5", 315 PURLType: purl.TypeGolang, 316 Locations: []string{"testdata/replace-no-version.mod"}, 317 }, 318 }, 319 }, 320 { 321 Name: "test extractor for go > 1.16", 322 InputConfig: extracttest.ScanInputMockConfig{ 323 Path: "testdata/indirect-1.23.mod", 324 }, 325 WantPackages: []*extractor.Package{ 326 { 327 Name: "github.com/sirupsen/logrus", 328 Version: "1.9.3", 329 PURLType: purl.TypeGolang, 330 Locations: []string{"testdata/indirect-1.23.mod"}, 331 }, 332 { 333 Name: "golang.org/x/sys", 334 Version: "0.0.0-20220715151400-c0bba94af5f8", 335 PURLType: purl.TypeGolang, 336 Locations: []string{"testdata/indirect-1.23.mod"}, 337 }, 338 { 339 Name: "stdlib", 340 Version: "1.23", 341 PURLType: purl.TypeGolang, 342 Locations: []string{"testdata/indirect-1.23.mod"}, 343 }, 344 }, 345 }, 346 { 347 Name: "test extractor for go <=1.16", 348 InputConfig: extracttest.ScanInputMockConfig{ 349 Path: "testdata/indirect-1.16.mod", 350 }, 351 WantPackages: []*extractor.Package{ 352 { 353 Name: "github.com/davecgh/go-spew", 354 Version: "1.1.1", 355 PURLType: purl.TypeGolang, 356 Locations: []string{"testdata/indirect-1.16.sum"}, 357 }, 358 { 359 Name: "github.com/pmezard/go-difflib", 360 Version: "1.0.0", 361 PURLType: purl.TypeGolang, 362 Locations: []string{"testdata/indirect-1.16.sum"}, 363 }, 364 { 365 Name: "github.com/sirupsen/logrus", 366 Version: "1.9.3", 367 PURLType: purl.TypeGolang, 368 Locations: []string{ 369 "testdata/indirect-1.16.mod", "testdata/indirect-1.16.sum", 370 }, 371 }, 372 { 373 Name: "github.com/stretchr/testify", 374 Version: "1.7.0", 375 PURLType: purl.TypeGolang, 376 Locations: []string{"testdata/indirect-1.16.sum"}, 377 }, 378 { 379 Name: "golang.org/x/sys", 380 Version: "0.0.0-20220715151400-c0bba94af5f8", 381 PURLType: purl.TypeGolang, 382 Locations: []string{"testdata/indirect-1.16.sum"}, 383 }, 384 { 385 Name: "gopkg.in/yaml.v3", 386 Version: "3.0.0-20200313102051-9f266ea9e77c", 387 PURLType: purl.TypeGolang, 388 Locations: []string{"testdata/indirect-1.16.sum"}, 389 }, 390 { 391 Name: "stdlib", 392 Version: "1.16", 393 PURLType: purl.TypeGolang, 394 Locations: []string{"testdata/indirect-1.16.mod"}, 395 }, 396 }, 397 }, 398 } 399 400 for _, tt := range tests { 401 t.Run(tt.Name, func(t *testing.T) { 402 extr := gomod.New() 403 404 scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) 405 defer extracttest.CloseTestScanInput(t, scanInput) 406 407 got, err := extr.Extract(t.Context(), &scanInput) 408 409 if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { 410 t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) 411 return 412 } 413 414 wantInv := inventory.Inventory{Packages: tt.WantPackages} 415 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" { 416 t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) 417 } 418 }) 419 } 420 } 421 422 func TestExtractor_Extract_WithExcludeIndirectConfig(t *testing.T) { 423 tests := []struct { 424 name string 425 config gomod.Config 426 inputPath string 427 wantPackages []*extractor.Package 428 wantNotPackages []*extractor.Package 429 wantErr error 430 }{ 431 { 432 name: "exclude indirect", 433 config: gomod.Config{ExcludeIndirect: true}, 434 inputPath: "testdata/indirect-packages.mod", 435 wantPackages: []*extractor.Package{ 436 { 437 Name: "github.com/BurntSushi/toml", 438 Version: "1.0.0", 439 PURLType: purl.TypeGolang, 440 Locations: []string{"testdata/indirect-packages.mod"}, 441 }, 442 { 443 Name: "gopkg.in/yaml.v2", 444 Version: "2.4.0", 445 PURLType: purl.TypeGolang, 446 Locations: []string{"testdata/indirect-packages.mod"}, 447 }, 448 { 449 Name: "stdlib", 450 Version: "1.17", 451 PURLType: purl.TypeGolang, 452 Locations: []string{"testdata/indirect-packages.mod"}, 453 }, 454 }, 455 wantNotPackages: []*extractor.Package{ 456 { 457 Name: "github.com/mattn/go-colorable", 458 Version: "0.1.9", 459 PURLType: purl.TypeGolang, 460 Locations: []string{"testdata/indirect-packages.mod"}, 461 }, 462 { 463 Name: "github.com/mattn/go-isatty", 464 Version: "0.0.14", 465 PURLType: purl.TypeGolang, 466 Locations: []string{"testdata/indirect-packages.mod"}, 467 }, 468 { 469 Name: "golang.org/x/sys", 470 Version: "0.0.0-20210630005230-0f9fa26af87c", 471 PURLType: purl.TypeGolang, 472 Locations: []string{"testdata/indirect-packages.mod"}, 473 }, 474 }, 475 }, 476 } 477 478 for _, tt := range tests { 479 t.Run(tt.name, func(t *testing.T) { 480 extr := gomod.NewWithConfig(tt.config) 481 482 scanInput := extracttest.GenerateScanInputMock(t, extracttest.ScanInputMockConfig{ 483 Path: tt.inputPath, 484 }) 485 defer extracttest.CloseTestScanInput(t, scanInput) 486 487 got, err := extr.Extract(t.Context(), &scanInput) 488 489 if tt.wantErr != nil { 490 if err == nil { 491 t.Errorf("want error %v, got nil", tt.wantErr) 492 } 493 494 if diff := cmp.Diff(tt.wantErr, err, cmpopts.EquateErrors()); diff != "" { 495 t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), scanInput.Path, diff) 496 } 497 498 return 499 } 500 501 wantInv := inventory.Inventory{Packages: tt.wantPackages} 502 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" { 503 t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), scanInput.Path, diff) 504 } 505 506 // Verify that packages that should not be included are actually excluded 507 for _, shouldNotHave := range tt.wantNotPackages { 508 for _, gotPkg := range got.Packages { 509 if gotPkg.Name == shouldNotHave.Name && gotPkg.Version == shouldNotHave.Version { 510 t.Errorf("Package %s@%s should not be included but was found in results", shouldNotHave.Name, shouldNotHave.Version) 511 } 512 } 513 } 514 }) 515 } 516 }