github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/golang/gobinary/gobinary_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 gobinary_test 16 17 import ( 18 "errors" 19 "io/fs" 20 "os" 21 "path/filepath" 22 "strings" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto" 28 "github.com/google/osv-scalibr/extractor" 29 "github.com/google/osv-scalibr/extractor/filesystem" 30 "github.com/google/osv-scalibr/extractor/filesystem/language/golang/gobinary" 31 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 32 scalibrfs "github.com/google/osv-scalibr/fs" 33 "github.com/google/osv-scalibr/inventory" 34 "github.com/google/osv-scalibr/purl" 35 "github.com/google/osv-scalibr/stats" 36 "github.com/google/osv-scalibr/testing/fakefs" 37 "github.com/google/osv-scalibr/testing/testcollector" 38 ) 39 40 func TestFileRequired(t *testing.T) { 41 tests := []struct { 42 name string 43 path string 44 mode fs.FileMode 45 fileSizeBytes int64 46 maxFileSizeBytes int64 47 wantRequired bool 48 wantResultMetric stats.FileRequiredResult 49 }{ 50 { 51 name: "user executable", 52 path: "some/path/a", 53 mode: 0766, 54 wantRequired: true, 55 wantResultMetric: stats.FileRequiredResultOK, 56 }, 57 { 58 name: "group executable", 59 path: "some/path/a", 60 mode: 0676, 61 wantRequired: true, 62 wantResultMetric: stats.FileRequiredResultOK, 63 }, 64 { 65 name: "other executable", 66 path: "some/path/a", 67 mode: 0667, 68 wantRequired: true, 69 wantResultMetric: stats.FileRequiredResultOK, 70 }, 71 { 72 name: "windows exe", 73 path: "some/path/a.exe", 74 mode: 0666, 75 wantRequired: true, 76 wantResultMetric: stats.FileRequiredResultOK, 77 }, 78 { 79 name: "executable required if size less than maxFileSizeBytes", 80 path: "some/path/a", 81 mode: 0766, 82 fileSizeBytes: 100, 83 maxFileSizeBytes: 1000, 84 wantRequired: true, 85 wantResultMetric: stats.FileRequiredResultOK, 86 }, 87 { 88 name: "executable required if size equal to maxFileSizeBytes", 89 path: "some/path/a", 90 mode: 0766, 91 fileSizeBytes: 1000, 92 maxFileSizeBytes: 1000, 93 wantRequired: true, 94 wantResultMetric: stats.FileRequiredResultOK, 95 }, 96 { 97 name: "executable not required if size greater than maxFileSizeBytes", 98 path: "some/path/a", 99 mode: 0766, 100 fileSizeBytes: 1000, 101 maxFileSizeBytes: 100, 102 wantRequired: false, 103 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 104 }, 105 { 106 name: "executable required if maxFileSizeBytes explicitly set to 0", 107 path: "some/path/a", 108 mode: 0766, 109 fileSizeBytes: 1000, 110 maxFileSizeBytes: 0, 111 wantRequired: true, 112 wantResultMetric: stats.FileRequiredResultOK, 113 }, 114 } 115 116 for _, tt := range tests { 117 t.Run(tt.name, func(t *testing.T) { 118 collector := testcollector.New() 119 e := gobinary.New(&cpb.PluginConfig{ 120 MaxFileSizeBytes: tt.maxFileSizeBytes, 121 }) 122 e.(*gobinary.Extractor).Stats = collector 123 124 // Set a default file size if not specified. 125 fileSizeBytes := tt.fileSizeBytes 126 if fileSizeBytes == 0 { 127 fileSizeBytes = 1000 128 } 129 130 if got := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 131 FileName: filepath.Base(tt.path), 132 FileMode: tt.mode, 133 FileSize: fileSizeBytes, 134 })); got != tt.wantRequired { 135 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, got, tt.wantRequired) 136 } 137 138 gotResultMetric := collector.FileRequiredResult(tt.path) 139 if gotResultMetric != tt.wantResultMetric { 140 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 141 } 142 }) 143 } 144 } 145 146 func TestExtract(t *testing.T) { 147 tests := []struct { 148 name string 149 cfg *cpb.PluginConfig 150 path string 151 wantPackages []*extractor.Package 152 wantErr error 153 wantResultMetric stats.FileExtractedResult 154 }{ 155 { 156 name: "binary_with_module_replacement-darwin-amd64", 157 path: "testdata/binary_with_module_replacement-darwin-amd64", 158 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-darwin-amd64"), 159 }, 160 { 161 name: "binary_with_module_replacement-darwin-arm64", 162 path: "testdata/binary_with_module_replacement-darwin-arm64", 163 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-darwin-arm64"), 164 }, 165 { 166 name: "binary_with_module_replacement-linux-386", 167 path: "testdata/binary_with_module_replacement-linux-386", 168 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-linux-386"), 169 }, 170 { 171 name: "binary_with_module_replacement-linux-amd64", 172 path: "testdata/binary_with_module_replacement-linux-amd64", 173 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-linux-amd64"), 174 }, 175 { 176 name: "binary_with_module_replacement-linux-arm64", 177 path: "testdata/binary_with_module_replacement-linux-arm64", 178 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-linux-arm64"), 179 }, 180 { 181 name: "binary_with_module_replacement-windows-386", 182 path: "testdata/binary_with_module_replacement-windows-386", 183 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-windows-386"), 184 }, 185 { 186 name: "binary_with_module_replacement-windows-amd64", 187 path: "testdata/binary_with_module_replacement-windows-amd64", 188 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-windows-amd64"), 189 }, 190 { 191 name: "binary_with_module_replacement-windows-arm64", 192 path: "testdata/binary_with_module_replacement-windows-arm64", 193 wantPackages: createPackagesWithMain(append(BinaryWithModuleReplacementPackages, Toolchain), "testdata/binary_with_module_replacement-windows-arm64"), 194 }, 195 { 196 name: "binary_with_modules-darwin-amd64", 197 path: "testdata/binary_with_modules-darwin-amd64", 198 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackages, Toolchain), "testdata/binary_with_modules-darwin-amd64"), 199 }, 200 { 201 name: "binary_with_modules-darwin-arm64", 202 path: "testdata/binary_with_modules-darwin-arm64", 203 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackages, Toolchain), "testdata/binary_with_modules-darwin-arm64"), 204 }, 205 { 206 name: "binary_with_modules-linux-386", 207 path: "testdata/binary_with_modules-linux-386", 208 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackages, Toolchain), "testdata/binary_with_modules-linux-386"), 209 }, 210 { 211 name: "binary_with_modules-linux-amd64", 212 path: "testdata/binary_with_modules-linux-amd64", 213 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackages, Toolchain), "testdata/binary_with_modules-linux-amd64"), 214 }, 215 { 216 name: "binary_with_modules-linux-arm64", 217 path: "testdata/binary_with_modules-linux-arm64", 218 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackages, Toolchain), "testdata/binary_with_modules-linux-arm64"), 219 }, 220 { 221 name: "binary_with_modules-windows-386", 222 path: "testdata/binary_with_modules-windows-386", 223 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackagesWindows, Toolchain), "testdata/binary_with_modules-windows-386"), 224 }, 225 { 226 name: "binary_with_modules-windows-amd64", 227 path: "testdata/binary_with_modules-windows-amd64", 228 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackagesWindows, Toolchain), "testdata/binary_with_modules-windows-amd64"), 229 }, 230 { 231 name: "binary_with_modules-windows-arm64", 232 path: "testdata/binary_with_modules-windows-arm64", 233 wantPackages: createPackagesWithMain(append(BinaryWithModulesPackagesWindows, Toolchain), "testdata/binary_with_modules-windows-arm64"), 234 }, 235 { 236 name: "nginx-ingress-controller with version from content off", 237 path: "testdata/nginx-ingress-controller", 238 wantPackages: createPackages(append(BinaryWithModulesPackagesNginx, goPackage("k8s.io/ingress-nginx", "(devel)")), "testdata/nginx-ingress-controller"), 239 }, 240 { 241 name: "nginx-ingress-controller_with_version_from_content_on", 242 path: "testdata/nginx-ingress-controller", 243 cfg: &cpb.PluginConfig{ 244 PluginSpecific: []*cpb.PluginSpecificConfig{ 245 {Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}}, 246 }, 247 }, 248 wantPackages: createPackages(append(BinaryWithModulesPackagesNginx, goPackage("k8s.io/ingress-nginx", "1.11.4")), "testdata/nginx-ingress-controller"), 249 }, 250 { 251 name: "dummy file that fails to parse will log an error, but won't fail extraction", 252 path: "testdata/dummy", 253 wantPackages: nil, 254 wantResultMetric: stats.FileExtractedResultErrorUnknown, 255 }, 256 } 257 258 for _, tt := range tests { 259 t.Run(tt.name, func(t *testing.T) { 260 f, err := os.Open(tt.path) 261 if err != nil { 262 t.Fatalf("os.Open(%s) unexpected error: %v", tt.path, err) 263 } 264 defer f.Close() 265 266 info, err := f.Stat() 267 if err != nil { 268 t.Fatalf("f.Stat() for %q unexpected error: %v", tt.path, err) 269 } 270 271 collector := testcollector.New() 272 273 input := &filesystem.ScanInput{FS: scalibrfs.DirFS("."), Path: tt.path, Info: info, Reader: f} 274 275 if tt.cfg == nil { 276 tt.cfg = &cpb.PluginConfig{} 277 } 278 279 e := gobinary.New(tt.cfg) 280 e.(*gobinary.Extractor).Stats = collector 281 got, err := e.Extract(t.Context(), input) 282 if !errors.Is(err, tt.wantErr) { 283 t.Fatalf("Extract(%s) got error: %v, want error: %v", tt.path, err, tt.wantErr) 284 } 285 sort := func(a, b *extractor.Package) bool { return a.Name < b.Name } 286 wantInv := inventory.Inventory{Packages: tt.wantPackages} 287 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(sort)); diff != "" { 288 t.Fatalf("Extract(%s) (-want +got):\n%s", tt.path, diff) 289 } 290 291 wantResultMetric := tt.wantResultMetric 292 if wantResultMetric == "" && tt.wantErr == nil { 293 wantResultMetric = stats.FileExtractedResultSuccess 294 } 295 gotResultMetric := collector.FileExtractedResult(tt.path) 296 if gotResultMetric != wantResultMetric { 297 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, wantResultMetric) 298 } 299 300 gotFileSizeMetric := collector.FileExtractedFileSize(tt.path) 301 if gotFileSizeMetric != info.Size() { 302 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size()) 303 } 304 }) 305 } 306 } 307 308 var ( 309 // BinaryWithModulesPackagesWindows is a list of packages built into the 310 // binary_with_modules-* testdata binaries, but only on Windows, where there 311 // is an indirect dependency that is not built-in. 312 BinaryWithModulesPackagesWindows = []*extractor.Package{ 313 // direct dependencies 314 goPackage("github.com/ulikunitz/xz", "0.5.11"), 315 goPackage("github.com/gin-gonic/gin", "1.8.1"), 316 317 // indirect dependencies 318 goPackage("github.com/gin-contrib/sse", "0.1.0"), 319 goPackage("github.com/go-playground/locales", "0.14.0"), 320 goPackage("github.com/go-playground/universal-translator", "0.18.0"), 321 goPackage("github.com/go-playground/validator/v10", "10.11.1"), 322 goPackage("github.com/leodido/go-urn", "1.2.1"), 323 goPackage("github.com/mattn/go-isatty", "0.0.16"), 324 goPackage("github.com/pelletier/go-toml/v2", "2.0.6"), 325 goPackage("github.com/ugorji/go/codec", "1.2.7"), 326 goPackage("golang.org/x/crypto", "0.4.0"), 327 goPackage("golang.org/x/net", "0.4.0"), 328 goPackage("golang.org/x/text", "0.5.0"), 329 goPackage("google.golang.org/protobuf", "1.28.1"), 330 goPackage("gopkg.in/yaml.v2", "2.4.0"), 331 } 332 333 // BinaryWithModulesPackages is a list of packages built into the 334 // binary_with_modules-* testdata binaries. 335 BinaryWithModulesPackages = append( 336 BinaryWithModulesPackagesWindows, 337 goPackage("golang.org/x/sys", "0.3.0"), 338 ) 339 340 // BinaryWithModuleReplacementPackages is a list of packages built into the 341 // binary_with_module_replacement-* testdata binaries. 342 BinaryWithModuleReplacementPackages = []*extractor.Package{ 343 // this binary replaces golang.org/x/xerrors => github.com/golang/xerrors 344 goPackage("github.com/golang/xerrors", "0.0.0-20220907171357-04be3eba64a2"), 345 } 346 347 BinaryWithModulesPackagesNginx = []*extractor.Package{ 348 goPackage("dario.cat/mergo", "1.0.1"), 349 goPackage("github.com/armon/go-proxyproto", "0.1.0"), 350 goPackage("github.com/beorn7/perks", "1.0.1"), 351 goPackage("github.com/blang/semver/v4", "4.0.0"), 352 goPackage("github.com/cespare/xxhash/v2", "2.3.0"), 353 goPackage("github.com/coreos/go-systemd/v22", "22.5.0"), 354 goPackage("github.com/cyphar/filepath-securejoin", "0.3.5"), 355 goPackage("github.com/davecgh/go-spew", "1.1.2-0.20180830191138-d8f796af33cc"), 356 goPackage("github.com/eapache/channels", "1.1.0"), 357 goPackage("github.com/eapache/queue", "1.1.0"), 358 goPackage("github.com/emicklei/go-restful/v3", "3.12.0"), 359 goPackage("github.com/fsnotify/fsnotify", "1.8.0"), 360 goPackage("github.com/fullsailor/pkcs7", "0.0.0-20190404230743-d7302db945fa"), 361 goPackage("github.com/fxamacker/cbor/v2", "2.7.0"), 362 goPackage("github.com/go-logr/logr", "1.4.2"), 363 goPackage("github.com/go-openapi/jsonpointer", "0.21.0"), 364 goPackage("github.com/go-openapi/jsonreference", "0.21.0"), 365 goPackage("github.com/go-openapi/swag", "0.23.0"), 366 goPackage("github.com/godbus/dbus/v5", "5.1.0"), 367 goPackage("github.com/gogo/protobuf", "1.3.2"), 368 goPackage("github.com/golang/protobuf", "1.5.4"), 369 goPackage("github.com/google/gnostic-models", "0.6.8"), 370 goPackage("github.com/google/go-cmp", "0.6.0"), 371 goPackage("github.com/google/gofuzz", "1.2.0"), 372 goPackage("github.com/google/uuid", "1.6.0"), 373 goPackage("github.com/josharian/intern", "1.0.0"), 374 goPackage("github.com/json-iterator/go", "1.1.12"), 375 goPackage("github.com/klauspost/compress", "1.17.9"), 376 goPackage("github.com/mailru/easyjson", "0.7.7"), 377 goPackage("github.com/mitchellh/go-ps", "1.0.0"), 378 goPackage("github.com/mitchellh/hashstructure/v2", "2.0.2"), 379 goPackage("github.com/mitchellh/mapstructure", "1.5.0"), 380 goPackage("github.com/moby/sys/mountinfo", "0.7.1"), 381 goPackage("github.com/moby/sys/userns", "0.1.0"), 382 goPackage("github.com/modern-go/concurrent", "0.0.0-20180306012644-bacd9c7ef1dd"), 383 goPackage("github.com/modern-go/reflect2", "1.0.2"), 384 goPackage("github.com/munnerz/goautoneg", "0.0.0-20191010083416-a7dc8b61c822"), 385 goPackage("github.com/ncabatoff/go-seq", "0.0.0-20180805175032-b08ef85ed833"), 386 goPackage("github.com/ncabatoff/process-exporter", "0.8.4"), 387 goPackage("github.com/opencontainers/runc", "1.2.3"), 388 goPackage("github.com/opencontainers/runtime-spec", "1.2.0"), 389 goPackage("github.com/pkg/errors", "0.9.1"), 390 goPackage("github.com/prometheus/client_golang", "1.20.5"), 391 goPackage("github.com/prometheus/client_model", "0.6.1"), 392 goPackage("github.com/prometheus/common", "0.61.0"), 393 goPackage("github.com/prometheus/procfs", "0.15.1"), 394 goPackage("github.com/sirupsen/logrus", "1.9.3"), 395 goPackage("github.com/spf13/cobra", "1.8.1"), 396 goPackage("github.com/spf13/pflag", "1.0.5"), 397 goPackage("github.com/x448/float16", "0.8.4"), 398 goPackage("github.com/zakjan/cert-chain-resolver", "0.0.0-20221221105603-fcedb00c5b30"), 399 goPackage("go", "1.23.4"), 400 goPackage("go.opentelemetry.io/otel", "1.31.0"), 401 goPackage("go.opentelemetry.io/otel/trace", "1.31.0"), 402 goPackage("golang.org/x/exp", "0.0.0-20240719175910-8a7402abbf56"), 403 goPackage("golang.org/x/net", "0.33.0"), 404 goPackage("golang.org/x/oauth2", "0.24.0"), 405 goPackage("golang.org/x/sys", "0.28.0"), 406 goPackage("golang.org/x/term", "0.27.0"), 407 goPackage("golang.org/x/text", "0.21.0"), 408 goPackage("golang.org/x/time", "0.7.0"), 409 goPackage("google.golang.org/protobuf", "1.35.2"), 410 goPackage("gopkg.in/evanphx/json-patch.v4", "4.12.0"), 411 goPackage("gopkg.in/go-playground/pool.v3", "3.1.1"), 412 goPackage("gopkg.in/inf.v0", "0.9.1"), 413 goPackage("gopkg.in/mcuadros/go-syslog.v2", "2.3.0"), 414 goPackage("gopkg.in/yaml.v3", "3.0.1"), 415 goPackage("k8s.io/api", "0.32.0"), 416 goPackage("k8s.io/apimachinery", "0.32.0"), 417 goPackage("k8s.io/apiserver", "0.32.0"), 418 goPackage("k8s.io/client-go", "0.32.0"), 419 goPackage("k8s.io/component-base", "0.32.0"), 420 goPackage("k8s.io/klog/v2", "2.130.1"), 421 goPackage("k8s.io/kube-openapi", "0.0.0-20241105132330-32ad38e42d3f"), 422 goPackage("k8s.io/utils", "0.0.0-20241104100929-3ea5e8cea738"), 423 goPackage("pault.ag/go/sniff", "0.0.0-20200207005214-cf7e4d167732"), 424 goPackage("sigs.k8s.io/json", "0.0.0-20241010143419-9aa6b5e7a4b3"), 425 goPackage("sigs.k8s.io/structured-merge-diff/v4", "4.4.2"), 426 goPackage("sigs.k8s.io/yaml", "1.4.0"), 427 } 428 429 Toolchain = goPackage("go", "1.22.0") 430 ) 431 432 func goPackage(name, version string) *extractor.Package { 433 return &extractor.Package{Name: name, Version: version, PURLType: purl.TypeGolang} 434 } 435 436 func createPackagesWithMain(pkgs []*extractor.Package, location string) []*extractor.Package { 437 res := createPackages(pkgs, location) 438 // Main package 439 mainName := strings.Split(strings.TrimPrefix(location, "testdata/"), "-")[0] 440 res = append(res, &extractor.Package{ 441 Name: mainName, Version: "(devel)", Locations: []string{location}, 442 PURLType: purl.TypeGolang, 443 }) 444 return res 445 } 446 447 func createPackages(pkgs []*extractor.Package, location string) []*extractor.Package { 448 res := []*extractor.Package{} 449 for _, p := range pkgs { 450 res = append(res, &extractor.Package{ 451 Name: p.Name, 452 Version: p.Version, 453 Locations: []string{location}, 454 PURLType: purl.TypeGolang, 455 }) 456 } 457 return res 458 }