github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/binary/classifier_cataloger_test.go (about)

     1  package binary
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/google/go-cmp/cmp/cmpopts"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/anchore/packageurl-go"
    18  	"github.com/anchore/stereoscope/pkg/imagetest"
    19  	"github.com/anchore/syft/syft/cpe"
    20  	"github.com/anchore/syft/syft/file"
    21  	"github.com/anchore/syft/syft/pkg"
    22  	"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil"
    23  	"github.com/anchore/syft/syft/source"
    24  	"github.com/anchore/syft/syft/source/directorysource"
    25  	"github.com/anchore/syft/syft/source/stereoscopesource"
    26  )
    27  
    28  var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)")
    29  
    30  func Test_Cataloger_PositiveCases(t *testing.T) {
    31  	tests := []struct {
    32  		name string
    33  		// logicalFixture is the logical path to the full binary or snippet. This is relative to the test-fixtures/classifiers/snippets
    34  		// or test-fixtures/classifiers/bin directory . Snippets are searched for first, and if not found, then existing binaries are
    35  		// used. If no binary or snippet is found the test will fail. If '-must-use-original-binaries' is used the only
    36  		// full binaries are tested (no snippets), and if no binary is found the test will be skipped.
    37  		logicalFixture string
    38  		expected       pkg.Package
    39  	}{
    40  		{
    41  			logicalFixture: "arangodb/3.11.8/linux-amd64",
    42  			expected: pkg.Package{
    43  				Name:      "arangodb",
    44  				Version:   "3.11.8",
    45  				Type:      "binary",
    46  				PURL:      "pkg:generic/arangodb@3.11.8",
    47  				Locations: locations("arangosh"),
    48  				Metadata:  metadata("arangodb-binary"),
    49  			},
    50  		},
    51  		{
    52  			logicalFixture: "postgres/15beta4/linux-amd64",
    53  			expected: pkg.Package{
    54  				Name:      "postgresql",
    55  				Version:   "15beta4",
    56  				Type:      "binary",
    57  				PURL:      "pkg:generic/postgresql@15beta4",
    58  				Locations: locations("postgres"),
    59  				Metadata:  metadata("postgresql-binary"),
    60  			},
    61  		},
    62  		{
    63  			logicalFixture: "postgres/15.1/linux-amd64",
    64  			expected: pkg.Package{
    65  				Name:      "postgresql",
    66  				Version:   "15.1",
    67  				Type:      "binary",
    68  				PURL:      "pkg:generic/postgresql@15.1",
    69  				Locations: locations("postgres"),
    70  				Metadata:  metadata("postgresql-binary"),
    71  			},
    72  		},
    73  		{
    74  			logicalFixture: "postgres/9.6.24/linux-amd64",
    75  			expected: pkg.Package{
    76  				Name:      "postgresql",
    77  				Version:   "9.6.24",
    78  				Type:      "binary",
    79  				PURL:      "pkg:generic/postgresql@9.6.24",
    80  				Locations: locations("postgres"),
    81  				Metadata:  metadata("postgresql-binary"),
    82  			},
    83  		},
    84  		{
    85  			// TODO: find original binary...
    86  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
    87  			logicalFixture: "postgres/9.5alpha1/linux-amd64",
    88  			expected: pkg.Package{
    89  				Name:      "postgresql",
    90  				Version:   "9.5alpha1",
    91  				Type:      "binary",
    92  				PURL:      "pkg:generic/postgresql@9.5alpha1",
    93  				Locations: locations("postgres"),
    94  				Metadata:  metadata("postgresql-binary"),
    95  			},
    96  		},
    97  		{
    98  			logicalFixture: "mysql/8.0.34/linux-amd64",
    99  			expected: pkg.Package{
   100  				Name:      "mysql",
   101  				Version:   "8.0.34",
   102  				Type:      "binary",
   103  				PURL:      "pkg:generic/mysql@8.0.34",
   104  				Locations: locations("mysql"),
   105  				Metadata:  metadata("mysql-binary"),
   106  			},
   107  		},
   108  		{
   109  			logicalFixture: "percona-server/8.0.35/linux-amd64",
   110  			expected: pkg.Package{
   111  				Name:      "percona-server",
   112  				Version:   "8.0.35",
   113  				Type:      "binary",
   114  				PURL:      "pkg:generic/percona-server@8.0.35",
   115  				Locations: locations("mysql"),
   116  				Metadata:  metadata("mysql-binary"),
   117  			},
   118  		},
   119  		{
   120  			logicalFixture: "percona-xtradb-cluster/8.0.34/linux-amd64",
   121  			expected: pkg.Package{
   122  				Name:      "percona-xtradb-cluster",
   123  				Version:   "8.0.34",
   124  				Type:      "binary",
   125  				PURL:      "pkg:generic/percona-xtradb-cluster@8.0.34",
   126  				Locations: locations("mysql"),
   127  				Metadata:  metadata("mysql-binary"),
   128  			},
   129  		},
   130  		{
   131  			logicalFixture: "percona-xtrabackup/8.0.35/linux-amd64",
   132  			expected: pkg.Package{
   133  				Name:      "percona-xtrabackup",
   134  				Version:   "8.0.35",
   135  				Type:      "binary",
   136  				PURL:      "pkg:generic/percona-xtrabackup@8.0.35",
   137  				Locations: locations("xtrabackup"),
   138  				Metadata:  metadata("xtrabackup-binary"),
   139  			},
   140  		},
   141  		{
   142  			logicalFixture: "mysql/5.6.51/linux-amd64",
   143  			expected: pkg.Package{
   144  				Name:      "mysql",
   145  				Version:   "5.6.51",
   146  				Type:      "binary",
   147  				PURL:      "pkg:generic/mysql@5.6.51",
   148  				Locations: locations("mysql"),
   149  				Metadata:  metadata("mysql-binary"),
   150  			},
   151  		},
   152  		{
   153  			logicalFixture: "mariadb/10.6.15/linux-amd64",
   154  			expected: pkg.Package{
   155  				Name:      "mariadb",
   156  				Version:   "10.6.15",
   157  				Type:      "binary",
   158  				PURL:      "pkg:generic/mariadb@10.6.15",
   159  				Locations: locations("mariadb"),
   160  				Metadata:  metadata("mariadb-binary"),
   161  			},
   162  		},
   163  		{
   164  			logicalFixture: "traefik/1.7.34/linux-amd64",
   165  			expected: pkg.Package{
   166  				Name:      "traefik",
   167  				Version:   "1.7.34",
   168  				Type:      "binary",
   169  				PURL:      "pkg:generic/traefik@1.7.34",
   170  				Locations: locations("traefik"),
   171  				Metadata:  metadata("traefik-binary"),
   172  			},
   173  		},
   174  		{
   175  			logicalFixture: "traefik/2.9.6/linux-amd64",
   176  			expected: pkg.Package{
   177  				Name:      "traefik",
   178  				Version:   "2.9.6",
   179  				Type:      "binary",
   180  				PURL:      "pkg:generic/traefik@2.9.6",
   181  				Locations: locations("traefik"),
   182  				Metadata:  metadata("traefik-binary"),
   183  			},
   184  		},
   185  		{
   186  			logicalFixture: "traefik/2.10.7/linux-amd64",
   187  			expected: pkg.Package{
   188  				Name:      "traefik",
   189  				Version:   "2.10.7",
   190  				Type:      "binary",
   191  				PURL:      "pkg:generic/traefik@2.10.7",
   192  				Locations: locations("traefik"),
   193  				Metadata:  metadata("traefik-binary"),
   194  			},
   195  		},
   196  		{
   197  			logicalFixture: "memcached/1.6.18/linux-amd64",
   198  			expected: pkg.Package{
   199  				Name:      "memcached",
   200  				Version:   "1.6.18",
   201  				Type:      "binary",
   202  				PURL:      "pkg:generic/memcached@1.6.18",
   203  				Locations: locations("memcached"),
   204  				Metadata:  metadata("memcached-binary"),
   205  			},
   206  		},
   207  		{
   208  			logicalFixture: "httpd/2.4.54/linux-amd64",
   209  			expected: pkg.Package{
   210  				Name:      "httpd",
   211  				Version:   "2.4.54",
   212  				Type:      "binary",
   213  				PURL:      "pkg:generic/httpd@2.4.54",
   214  				Locations: locations("httpd"),
   215  				Metadata:  metadata("httpd-binary"),
   216  			},
   217  		},
   218  		{
   219  			// TODO: find original binary...
   220  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   221  			logicalFixture: "php-cli/8.2.1/linux-amd64",
   222  			expected: pkg.Package{
   223  				Name:      "php-cli",
   224  				Version:   "8.2.1",
   225  				Type:      "binary",
   226  				PURL:      "pkg:generic/php-cli@8.2.1",
   227  				Locations: locations("php"),
   228  				Metadata:  metadata("php-cli-binary"),
   229  			},
   230  		},
   231  		{
   232  			// TODO: find original binary...
   233  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   234  			logicalFixture: "php-fpm/8.2.1/linux-amd64",
   235  			expected: pkg.Package{
   236  				Name:      "php-fpm",
   237  				Version:   "8.2.1",
   238  				Type:      "binary",
   239  				PURL:      "pkg:generic/php-fpm@8.2.1",
   240  				Locations: locations("php-fpm"),
   241  				Metadata:  metadata("php-fpm-binary"),
   242  			},
   243  		},
   244  		{
   245  			// TODO: find original binary...
   246  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   247  			logicalFixture: "php-apache/8.2.1/linux-amd64",
   248  			expected: pkg.Package{
   249  				Name:      "libphp",
   250  				Version:   "8.2.1",
   251  				Type:      "binary",
   252  				PURL:      "pkg:generic/php@8.2.1",
   253  				Locations: locations("libphp.so"),
   254  				Metadata:  metadata("php-apache-binary"),
   255  			},
   256  		},
   257  		{
   258  			// TODO: original binary is different than whats in config.yaml
   259  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   260  			logicalFixture: "perl/5.12.5/linux-amd64",
   261  			expected: pkg.Package{
   262  				Name:      "perl",
   263  				Version:   "5.12.5",
   264  				Type:      "binary",
   265  				PURL:      "pkg:generic/perl@5.12.5",
   266  				Locations: locations("perl"),
   267  				Metadata:  metadata("perl-binary"),
   268  			},
   269  		},
   270  		{
   271  			// TODO: original binary is different than whats in config.yaml
   272  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   273  			logicalFixture: "perl/5.20.0/linux-amd64",
   274  			expected: pkg.Package{
   275  				Name:      "perl",
   276  				Version:   "5.20.0",
   277  				Type:      "binary",
   278  				PURL:      "pkg:generic/perl@5.20.0",
   279  				Locations: locations("perl"),
   280  				Metadata:  metadata("perl-binary"),
   281  			},
   282  		},
   283  		{
   284  			// TODO: original binary is different than whats in config.yaml
   285  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   286  			logicalFixture: "perl/5.37.8/linux-amd64",
   287  			expected: pkg.Package{
   288  				Name:      "perl",
   289  				Version:   "5.37.8",
   290  				Type:      "binary",
   291  				PURL:      "pkg:generic/perl@5.37.8",
   292  				Locations: locations("perl"),
   293  				Metadata:  metadata("perl-binary"),
   294  			},
   295  		},
   296  		{
   297  			logicalFixture: "haproxy/1.5.14/linux-amd64",
   298  			expected: pkg.Package{
   299  				Name:      "haproxy",
   300  				Version:   "1.5.14",
   301  				Type:      "binary",
   302  				PURL:      "pkg:generic/haproxy@1.5.14",
   303  				Locations: locations("haproxy"),
   304  				Metadata:  metadata("haproxy-binary"),
   305  			},
   306  		},
   307  		{
   308  			logicalFixture: "haproxy/1.8.22/linux-amd64",
   309  			expected: pkg.Package{
   310  				Name:      "haproxy",
   311  				Version:   "1.8.22",
   312  				Type:      "binary",
   313  				PURL:      "pkg:generic/haproxy@1.8.22",
   314  				Locations: locations("haproxy"),
   315  				Metadata:  metadata("haproxy-binary"),
   316  			},
   317  		},
   318  		{
   319  			logicalFixture: "haproxy/2.7.3/linux-amd64",
   320  			expected: pkg.Package{
   321  				Name:      "haproxy",
   322  				Version:   "2.7.3",
   323  				Type:      "binary",
   324  				PURL:      "pkg:generic/haproxy@2.7.3",
   325  				Locations: locations("haproxy"),
   326  				Metadata:  metadata("haproxy-binary"),
   327  			},
   328  		},
   329  
   330  		{
   331  			logicalFixture: "helm/3.11.1/linux-amd64",
   332  			expected: pkg.Package{
   333  				Name:      "helm",
   334  				Version:   "3.11.1",
   335  				Type:      "binary",
   336  				PURL:      "pkg:golang/helm.sh/helm@3.11.1",
   337  				Locations: locations("helm"),
   338  				Metadata:  metadata("helm"),
   339  			},
   340  		},
   341  		{
   342  			logicalFixture: "helm/3.10.3/linux-amd64",
   343  			expected: pkg.Package{
   344  				Name:      "helm",
   345  				Version:   "3.10.3",
   346  				Type:      "binary",
   347  				PURL:      "pkg:golang/helm.sh/helm@3.10.3",
   348  				Locations: locations("helm"),
   349  				Metadata:  metadata("helm"),
   350  			},
   351  		},
   352  
   353  		{
   354  			// note: dynamic (non-snippet) test case
   355  			logicalFixture: "redis-server/2.8.23/linux-amd64",
   356  			expected: pkg.Package{
   357  				Name:      "redis",
   358  				Version:   "2.8.23",
   359  				Type:      "binary",
   360  				PURL:      "pkg:generic/redis@2.8.23",
   361  				Locations: locations("redis-server"),
   362  				Metadata:  metadata("redis-binary"),
   363  			},
   364  		},
   365  		{
   366  			// note: dynamic (non-snippet) test case
   367  			logicalFixture: "redis-server/4.0.11/linux-amd64",
   368  			expected: pkg.Package{
   369  				Name:      "redis",
   370  				Version:   "4.0.11",
   371  				Type:      "binary",
   372  				PURL:      "pkg:generic/redis@4.0.11",
   373  				Locations: locations("redis-server"),
   374  				Metadata:  metadata("redis-binary"),
   375  			},
   376  		},
   377  		{
   378  			logicalFixture: "redis-server/5.0.0/linux-amd64",
   379  			expected: pkg.Package{
   380  				Name:      "redis",
   381  				Version:   "5.0.0",
   382  				Type:      "binary",
   383  				PURL:      "pkg:generic/redis@5.0.0",
   384  				Locations: locations("redis-server"),
   385  				Metadata:  metadata("redis-binary"),
   386  			},
   387  		},
   388  		{
   389  			logicalFixture: "redis-server/6.0.16/linux-amd64",
   390  			expected: pkg.Package{
   391  				Name:      "redis",
   392  				Version:   "6.0.16",
   393  				Type:      "binary",
   394  				PURL:      "pkg:generic/redis@6.0.16",
   395  				Locations: locations("redis-server"),
   396  				Metadata:  metadata("redis-binary"),
   397  			},
   398  		},
   399  		{
   400  			logicalFixture: "redis-server/7.0.0/linux-amd64",
   401  			expected: pkg.Package{
   402  				Name:      "redis",
   403  				Version:   "7.0.0",
   404  				Type:      "binary",
   405  				PURL:      "pkg:generic/redis@7.0.0",
   406  				Locations: locations("redis-server"),
   407  				Metadata:  metadata("redis-binary"),
   408  			},
   409  		},
   410  		{
   411  			logicalFixture: "redis-server/7.0.14/linux-amd64",
   412  			expected: pkg.Package{
   413  				Name:      "redis",
   414  				Version:   "7.0.14",
   415  				Type:      "binary",
   416  				PURL:      "pkg:generic/redis@7.0.14",
   417  				Locations: locations("redis-server"),
   418  				Metadata:  metadata("redis-binary"),
   419  			},
   420  		},
   421  		{
   422  			// note: dynamic (non-snippet) test case
   423  			logicalFixture: "redis-server/7.2.3/linux-amd64",
   424  			expected: pkg.Package{
   425  				Name:      "redis",
   426  				Version:   "7.2.3",
   427  				Type:      "binary",
   428  				PURL:      "pkg:generic/redis@7.2.3",
   429  				Locations: locations("redis-server"),
   430  				Metadata:  metadata("redis-binary"),
   431  			},
   432  		},
   433  		{
   434  			// note: dynamic (non-snippet) test case
   435  			logicalFixture: "redis-server/7.2.3/linux-arm64",
   436  			expected: pkg.Package{
   437  				Name:      "redis",
   438  				Version:   "7.2.3",
   439  				Type:      "binary",
   440  				PURL:      "pkg:generic/redis@7.2.3",
   441  				Locations: locations("redis-server"),
   442  				Metadata:  metadata("redis-binary"),
   443  			},
   444  		},
   445  		{
   446  			logicalFixture: "python-shared-lib/3.7.4/linux-amd64",
   447  			expected: pkg.Package{
   448  				Name:      "python",
   449  				Version:   "3.7.4",
   450  				PURL:      "pkg:generic/python@3.7.4",
   451  				Locations: locations("libpython3.7m.so.1.0"),
   452  				Metadata:  metadata("python-binary-lib"),
   453  			},
   454  		},
   455  
   456  		{
   457  			// note: dynamic (non-snippet) test case
   458  			logicalFixture: "python-slim-shared-libs/3.11/linux-amd64",
   459  			expected: pkg.Package{
   460  				Name:      "python",
   461  				Version:   "3.11.2",
   462  				PURL:      "pkg:generic/python@3.11.2",
   463  				Locations: locations("python3.11", "libpython3.11.so.1.0"),
   464  				Metadata: pkg.BinarySignature{
   465  					Matches: []pkg.ClassifierMatch{
   466  						match("python-binary", "python3.11"),
   467  						match("python-binary", "libpython3.11.so.1.0"),
   468  						match("python-binary-lib", "libpython3.11.so.1.0"),
   469  					},
   470  				},
   471  			},
   472  		},
   473  		{
   474  			// note: dynamic (non-snippet) test case
   475  			logicalFixture: "python-rhel-shared-libs/3.9/linux-amd64",
   476  			expected: pkg.Package{
   477  				Name:      "python",
   478  				Version:   "3.9.13",
   479  				PURL:      "pkg:generic/python@3.9.13",
   480  				Locations: locations("python3.9", "libpython3.9.so.1.0"),
   481  				Metadata: pkg.BinarySignature{
   482  					Matches: []pkg.ClassifierMatch{
   483  						match("python-binary", "python3.9"),
   484  						match("python-binary", "libpython3.9.so.1.0"),
   485  						match("python-binary-lib", "libpython3.9.so.1.0"),
   486  					},
   487  				},
   488  			},
   489  		},
   490  		{
   491  			// note: dynamic (non-snippet) test case
   492  			logicalFixture: "python3.9/3.9.16/linux-amd64",
   493  			expected: pkg.Package{
   494  				Name:      "python",
   495  				Version:   "3.9.2",
   496  				PURL:      "pkg:generic/python@3.9.2",
   497  				Locations: locations("python3.9"),
   498  				Metadata: pkg.BinarySignature{
   499  					Matches: []pkg.ClassifierMatch{
   500  						match("python-binary", "python3.9"),
   501  					},
   502  				},
   503  			},
   504  		},
   505  		{
   506  			// note: dynamic (non-snippet) test case
   507  			logicalFixture: "python-alpine-shared-libs/3.4/linux-amd64",
   508  			expected: pkg.Package{
   509  				Name:      "python",
   510  				Version:   "3.4.10",
   511  				PURL:      "pkg:generic/python@3.4.10",
   512  				Locations: locations("python3.4", "libpython3.4m.so.1.0"),
   513  				Metadata: pkg.BinarySignature{
   514  					Matches: []pkg.ClassifierMatch{
   515  						match("python-binary", "python3.4"),
   516  						match("python-binary", "libpython3.4m.so.1.0"),
   517  						match("python-binary-lib", "libpython3.4m.so.1.0"),
   518  					},
   519  				},
   520  			},
   521  		},
   522  		{
   523  			// TODO: find original binary...
   524  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   525  			logicalFixture: "python-with-incorrect-match/3.5.3/linux-amd64",
   526  			expected: pkg.Package{
   527  				Name:      "python",
   528  				Version:   "3.5.3",
   529  				PURL:      "pkg:generic/python@3.5.3",
   530  				Locations: locations("python3.5"),
   531  				Metadata:  metadata("python-binary"),
   532  			},
   533  		},
   534  		{
   535  			// TODO: find original binary...
   536  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   537  			logicalFixture: "python/3.6.3/linux-amd64",
   538  			expected: pkg.Package{
   539  				Name:      "python",
   540  				Version:   "3.6.3",
   541  				PURL:      "pkg:generic/python@3.6.3",
   542  				Locations: locations("python3.6"),
   543  				Metadata:  metadata("python-binary"),
   544  			},
   545  		},
   546  		{
   547  			// TODO: find original binary...
   548  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   549  			logicalFixture: "python-duplicates/3.8.16/linux-amd64",
   550  			expected: pkg.Package{
   551  				Name:      "python",
   552  				Version:   "3.8.16",
   553  				Type:      "binary",
   554  				PURL:      "pkg:generic/python@3.8.16",
   555  				Locations: locations("dir/python3.8", "python3.8", "libpython3.8.so"),
   556  				Metadata: pkg.BinarySignature{
   557  					Matches: []pkg.ClassifierMatch{
   558  						match("python-binary", "dir/python3.8"),
   559  						match("python-binary", "python3.8"),
   560  						match("python-binary-lib", "libpython3.8.so"),
   561  					},
   562  				},
   563  			},
   564  		},
   565  		{
   566  			logicalFixture: "pypy-shared-lib/7.3.14/linux-amd64",
   567  			expected: pkg.Package{
   568  				Name:      "pypy",
   569  				Version:   "7.3.14",
   570  				PURL:      "pkg:generic/pypy@7.3.14",
   571  				Locations: locations("libpypy3.9-c.so"),
   572  				Metadata:  metadata("pypy-binary-lib"),
   573  			},
   574  		},
   575  		{
   576  			logicalFixture: "go/1.21.3/linux-amd64",
   577  			expected: pkg.Package{
   578  				Name:      "go",
   579  				Version:   "1.21.3",
   580  				PURL:      "pkg:generic/go@1.21.3",
   581  				Locations: locations("go"),
   582  				Metadata:  metadata("go-binary"),
   583  			},
   584  		},
   585  		{
   586  			logicalFixture: "node/19.2.0/linux-amd64",
   587  			expected: pkg.Package{
   588  				Name:      "node",
   589  				Version:   "19.2.0",
   590  				PURL:      "pkg:generic/node@19.2.0",
   591  				Locations: locations("node"),
   592  				Metadata:  metadata("nodejs-binary"),
   593  			},
   594  		},
   595  		{
   596  			// TODO: find original binary...
   597  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   598  			logicalFixture: "go-version-hint/1.15/any",
   599  			expected: pkg.Package{
   600  				Name:      "go",
   601  				Version:   "1.15",
   602  				PURL:      "pkg:generic/go@1.15",
   603  				Locations: locations("VERSION"),
   604  				Metadata:  metadata("go-binary-hint"),
   605  			},
   606  		},
   607  		{
   608  			// note: this is testing BUSYBOX which is typically through a link to "[" (in this case a symlink but in
   609  			// practice this is often a hard link).
   610  			logicalFixture: `busybox/1.36.1/linux-amd64`,
   611  			expected: pkg.Package{
   612  				Name:      "busybox",
   613  				Version:   "1.36.1",
   614  				PURL:      "pkg:generic/busybox@1.36.1",
   615  				Locations: locations("["), // note: busybox is a link to [
   616  				Metadata:  metadata("busybox-binary", "[", "busybox"),
   617  			},
   618  		},
   619  		{
   620  			logicalFixture: "java-jre-openjdk/1.8.0_352-b08/linux-amd64",
   621  			expected: pkg.Package{
   622  				Name:      "java/jre",
   623  				Version:   "1.8.0_352-b08",
   624  				Type:      "binary",
   625  				PURL:      "pkg:generic/java/jre@1.8.0_352-b08",
   626  				Locations: locations("java"),
   627  				Metadata:  metadata("java-binary-openjdk", "java"),
   628  			},
   629  		},
   630  		{
   631  			logicalFixture: "java-jre-openjdk/11.0.17/linux-amd64",
   632  			expected: pkg.Package{
   633  				Name:      "java/jre",
   634  				Version:   "11.0.17+8-LTS",
   635  				Type:      "binary",
   636  				PURL:      "pkg:generic/java/jre@11.0.17%2B8-LTS",
   637  				Locations: locations("java"),
   638  				Metadata:  metadata("java-binary-openjdk", "java"),
   639  			},
   640  		},
   641  		{
   642  			logicalFixture: "java-jre-openjdk-eclipse/11.0.22/linux-amd64",
   643  			expected: pkg.Package{
   644  				Name:      "java/jre",
   645  				Version:   "11.0.22+7",
   646  				Type:      "binary",
   647  				PURL:      "pkg:generic/java/jre@11.0.22%2B7",
   648  				Locations: locations("java"),
   649  				Metadata:  metadata("java-binary-openjdk", "java"),
   650  			},
   651  		},
   652  		{
   653  			logicalFixture: "java-jre-openjdk-arm64-eclipse/11.0.22/linux-arm64",
   654  			expected: pkg.Package{
   655  				Name:      "java/jre",
   656  				Version:   "11.0.22+7",
   657  				Type:      "binary",
   658  				PURL:      "pkg:generic/java/jre@11.0.22%2B7",
   659  				Locations: locations("java"),
   660  				Metadata:  metadata("java-binary-openjdk", "java"),
   661  			},
   662  		},
   663  		{
   664  			logicalFixture: "java-graal-openjdk/17.0.3+7-jvmci-22.1-b06/linux-amd64",
   665  			expected: pkg.Package{
   666  				Name:      "java/graalvm",
   667  				Version:   "17.0.3+7-jvmci-22.1-b06",
   668  				Type:      "binary",
   669  				PURL:      "pkg:generic/java/graalvm@17.0.3%2B7-jvmci-22.1-b06",
   670  				Locations: locations("java"),
   671  				Metadata:  metadata("java-binary-graalvm", "java"),
   672  			},
   673  		},
   674  		{
   675  			// TODO: find original binary...
   676  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   677  			logicalFixture: "java-jre-oracle/19.0.1/linux-amd64",
   678  			expected: pkg.Package{
   679  				Name:      "java/jre",
   680  				Version:   "19.0.1+10-21",
   681  				Type:      "binary",
   682  				PURL:      "pkg:generic/java/jre@19.0.1%2B10-21",
   683  				Locations: locations("java"),
   684  				Metadata:  metadata("java-binary-oracle", "java"),
   685  			},
   686  		},
   687  		{
   688  			// TODO: find original binary...
   689  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   690  			logicalFixture: "java-jre-oracle/19.0.1/darwin",
   691  			expected: pkg.Package{
   692  				Name:      "java/jre",
   693  				Version:   "19.0.1+10-21",
   694  				Type:      "binary",
   695  				PURL:      "pkg:generic/java/jre@19.0.1%2B10-21",
   696  				Locations: locations("java"),
   697  				Metadata:  metadata("java-binary-oracle", "java"),
   698  			},
   699  		},
   700  		{
   701  			logicalFixture: "java-jre-ibm/1.8.0_391/linux-amd64",
   702  			expected: pkg.Package{
   703  				Name:      "java/jre",
   704  				Version:   "1.8.0-foreman_2023_10_12_13_27-b00",
   705  				Type:      "binary",
   706  				PURL:      "pkg:generic/java/jre@1.8.0-foreman_2023_10_12_13_27-b00",
   707  				Locations: locations("java"),
   708  				Metadata:  metadata("java-binary-ibm", "java"),
   709  			},
   710  		},
   711  		{
   712  			logicalFixture: "java-jdk-openjdk/21.0.2+13-LTS/linux-amd64",
   713  			expected: pkg.Package{
   714  				Name:      "java/jdk",
   715  				Version:   "21.0.2+13-LTS",
   716  				Type:      "binary",
   717  				PURL:      "pkg:generic/java/jdk@21.0.2%2B13-LTS",
   718  				Locations: locations("jdb"),
   719  				Metadata:  metadata("java-binary-jdk", "java"),
   720  			},
   721  		},
   722  		{
   723  			logicalFixture: "rust-libstd/1.50.0/linux-amd64",
   724  			expected: pkg.Package{
   725  				Name:      "rust",
   726  				Version:   "1.50.0",
   727  				Type:      "binary",
   728  				PURL:      "pkg:generic/rust@1.50.0",
   729  				Locations: locations("libstd-6f77337c1826707d.so"),
   730  				Metadata:  metadata("rust-standard-library-linux"),
   731  			},
   732  		},
   733  		{
   734  			// TODO: find original binary...
   735  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   736  			logicalFixture: "rust-libstd/1.50.0/darwin",
   737  			expected: pkg.Package{
   738  				Name:      "rust",
   739  				Version:   "1.50.0",
   740  				Type:      "binary",
   741  				PURL:      "pkg:generic/rust@1.50.0",
   742  				Locations: locations("libstd-f6f9eec1635e636a.dylib"),
   743  				Metadata:  metadata("rust-standard-library-macos"),
   744  			},
   745  		},
   746  		{
   747  			// TODO: find original binary...
   748  			// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
   749  			logicalFixture: "rust-libstd/1.67.1/darwin",
   750  			expected: pkg.Package{
   751  				Name:      "rust",
   752  				Version:   "1.67.1",
   753  				Type:      "binary",
   754  				PURL:      "pkg:generic/rust@1.67.1",
   755  				Locations: locations("libstd-16f2b65e77054c42.dylib"),
   756  				Metadata:  metadata("rust-standard-library-macos"),
   757  			},
   758  		},
   759  		{
   760  			logicalFixture: "rust-libstd-musl/1.67.1/linux-amd64",
   761  			expected: pkg.Package{
   762  				Name:      "rust",
   763  				Version:   "1.67.1",
   764  				Type:      "binary",
   765  				PURL:      "pkg:generic/rust@1.67.1",
   766  				Locations: locations("libstd-86aefecbddda356d.so"),
   767  				Metadata:  metadata("rust-standard-library-linux"),
   768  			},
   769  		},
   770  		{
   771  			logicalFixture: "rust-libstd/1.67.1/linux-amd64",
   772  			expected: pkg.Package{
   773  				Name:      "rust",
   774  				Version:   "1.67.1",
   775  				Type:      "binary",
   776  				PURL:      "pkg:generic/rust@1.67.1",
   777  				Locations: locations("libstd-c6192dd4c4d410ac.so"),
   778  				Metadata:  metadata("rust-standard-library-linux"),
   779  			},
   780  		},
   781  		{
   782  			// note: dynamic (non-snippet) test case
   783  
   784  			name:           "positive-ruby-3.2.1",
   785  			logicalFixture: "ruby-bullseye-shared-libs/3.2.1/linux-amd64",
   786  			expected: pkg.Package{
   787  				Name:      "ruby",
   788  				Version:   "3.2.1",
   789  				Type:      "binary",
   790  				PURL:      "pkg:generic/ruby@3.2.1",
   791  				Locations: locations("ruby", "libruby.so.3.2.1"),
   792  				Metadata: pkg.BinarySignature{
   793  					Matches: []pkg.ClassifierMatch{
   794  						match("ruby-binary", "ruby"),
   795  						match("ruby-binary", "libruby.so.3.2.1"),
   796  					},
   797  				},
   798  			},
   799  		},
   800  		{
   801  			// note: dynamic (non-snippet) test case
   802  			logicalFixture: "ruby-bullseye-shared-libs/2.7.7/linux-amd64",
   803  			expected: pkg.Package{
   804  				Name:      "ruby",
   805  				Version:   "2.7.7p221",
   806  				Type:      "binary",
   807  				PURL:      "pkg:generic/ruby@2.7.7p221",
   808  				Locations: locations("ruby", "libruby.so.2.7.7"),
   809  				Metadata: pkg.BinarySignature{
   810  					Matches: []pkg.ClassifierMatch{
   811  						match("ruby-binary", "ruby"),
   812  						match("ruby-binary", "libruby.so.2.7.7"),
   813  					},
   814  				},
   815  			},
   816  		},
   817  		{
   818  			// note: dynamic (non-snippet) test case
   819  			logicalFixture: "ruby-shared-libs/2.6.10/linux-amd64",
   820  			expected: pkg.Package{
   821  				Name:      "ruby",
   822  				Version:   "2.6.10p210",
   823  				Type:      "binary",
   824  				PURL:      "pkg:generic/ruby@2.6.10p210",
   825  				Locations: locations("ruby", "libruby.so.2.6.10"),
   826  				Metadata: pkg.BinarySignature{
   827  					Matches: []pkg.ClassifierMatch{
   828  						match("ruby-binary", "ruby"),
   829  						match("ruby-binary", "libruby.so.2.6.10"),
   830  					},
   831  				},
   832  			},
   833  		},
   834  		{
   835  			logicalFixture: "ruby/1.9.3p551/linux-amd64",
   836  			expected: pkg.Package{
   837  				Name:      "ruby",
   838  				Version:   "1.9.3p551",
   839  				Type:      "binary",
   840  				PURL:      "pkg:generic/ruby@1.9.3p551",
   841  				Locations: locations("ruby"),
   842  				Metadata:  metadata("ruby-binary"),
   843  			},
   844  		},
   845  		{
   846  			logicalFixture: "consul/1.15.2/linux-amd64",
   847  			expected: pkg.Package{
   848  				Name:      "consul",
   849  				Version:   "1.15.2",
   850  				Type:      "binary",
   851  				PURL:      "pkg:golang/github.com/hashicorp/consul@1.15.2",
   852  				Locations: locations("consul"),
   853  				Metadata:  metadata("consul-binary"),
   854  			},
   855  		},
   856  		{
   857  			logicalFixture: "erlang/25.3.2.6/linux-amd64",
   858  			expected: pkg.Package{
   859  				Name:      "erlang",
   860  				Version:   "25.3.2.6",
   861  				Type:      "binary",
   862  				PURL:      "pkg:generic/erlang@25.3.2.6",
   863  				Locations: locations("erlexec"),
   864  				Metadata:  metadata("erlang-binary"),
   865  			},
   866  		},
   867  		{
   868  			logicalFixture: "erlang/26.2.0.0/linux-amd64",
   869  			expected: pkg.Package{
   870  				Name:      "erlang",
   871  				Version:   "26.2",
   872  				Type:      "binary",
   873  				PURL:      "pkg:generic/erlang@26.2",
   874  				Locations: locations("erlexec"),
   875  				Metadata:  metadata("erlang-binary"),
   876  			},
   877  		},
   878  		{
   879  			logicalFixture: "erlang/26.2.4/linux-amd64",
   880  			expected: pkg.Package{
   881  				Name:      "erlang",
   882  				Version:   "26.2.4",
   883  				Type:      "binary",
   884  				PURL:      "pkg:generic/erlang@26.2.4",
   885  				Locations: locations("liberts_internal.a"),
   886  				Metadata:  metadata("erlang-library"),
   887  			},
   888  		},
   889  		{
   890  			logicalFixture: "nginx/1.25.1/linux-amd64",
   891  			expected: pkg.Package{
   892  				Name:      "nginx",
   893  				Version:   "1.25.1",
   894  				Type:      "binary",
   895  				PURL:      "pkg:generic/nginx@1.25.1",
   896  				Locations: locations("nginx"),
   897  				Metadata:  metadata("nginx-binary"),
   898  			},
   899  		},
   900  		{
   901  			logicalFixture: "nginx-openresty/1.21.4.3/linux-amd64",
   902  			expected: pkg.Package{
   903  				Name:      "nginx",
   904  				Version:   "1.21.4",
   905  				Type:      "binary",
   906  				PURL:      "pkg:generic/nginx@1.21.4",
   907  				Locations: locations("nginx"),
   908  				Metadata:  metadata("nginx-binary"),
   909  			},
   910  		},
   911  		{
   912  			logicalFixture: "bash/5.1.16/linux-amd64",
   913  			expected: pkg.Package{
   914  				Name:      "bash",
   915  				Version:   "5.1.16",
   916  				Type:      "binary",
   917  				PURL:      "pkg:generic/bash@5.1.16",
   918  				Locations: locations("bash"),
   919  				Metadata:  metadata("bash-binary"),
   920  			},
   921  		},
   922  		{
   923  			logicalFixture: "openssl/3.1.4/linux-amd64",
   924  			expected: pkg.Package{
   925  				Name:      "openssl",
   926  				Version:   "3.1.4",
   927  				Type:      "binary",
   928  				PURL:      "pkg:generic/openssl@3.1.4",
   929  				Locations: locations("openssl"),
   930  				Metadata:  metadata("openssl-binary"),
   931  			},
   932  		},
   933  		{
   934  			logicalFixture: "openssl/1.1.1w/linux-arm64",
   935  			expected: pkg.Package{
   936  				Name:      "openssl",
   937  				Version:   "1.1.1w",
   938  				Type:      "binary",
   939  				PURL:      "pkg:generic/openssl@1.1.1w",
   940  				Locations: locations("openssl"),
   941  				Metadata:  metadata("openssl-binary"),
   942  			},
   943  		},
   944  		{
   945  			logicalFixture: "gcc/12.3.0/linux-amd64",
   946  			expected: pkg.Package{
   947  				Name:      "gcc",
   948  				Version:   "12.3.0",
   949  				Type:      "binary",
   950  				PURL:      "pkg:generic/gcc@12.3.0",
   951  				Locations: locations("gcc"),
   952  				Metadata:  metadata("gcc-binary"),
   953  			},
   954  		},
   955  		{
   956  			logicalFixture: "wp/2.9.0/linux-amd64",
   957  			expected: pkg.Package{
   958  				Name:      "wp-cli",
   959  				Version:   "2.9.0",
   960  				Type:      "binary",
   961  				PURL:      "pkg:generic/wp-cli@2.9.0",
   962  				Locations: locations("wp"),
   963  				Metadata:  metadata("wordpress-cli-binary"),
   964  			},
   965  		},
   966  	}
   967  
   968  	for _, test := range tests {
   969  		t.Run(test.logicalFixture, func(t *testing.T) {
   970  			c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
   971  
   972  			// logicalFixture is the logical path to the full binary or snippet. This is relative to the test-fixtures/classifiers/snippets
   973  			// or test-fixtures/classifiers/bin directory . Snippets are searched for first, and if not found, then existing binaries are
   974  			// used. If no binary or snippet is found the test will fail. If '-must-use-original-binaries' is used the only
   975  			// full binaries are tested (no snippets), and if no binary is found the test will be skipped.
   976  			path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
   977  
   978  			src, err := directorysource.NewFromPath(path)
   979  			require.NoError(t, err)
   980  
   981  			resolver, err := src.FileResolver(source.SquashedScope)
   982  			require.NoError(t, err)
   983  
   984  			packages, _, err := c.Catalog(context.Background(), resolver)
   985  			require.NoError(t, err)
   986  
   987  			require.Len(t, packages, 1, "mismatched package count")
   988  
   989  			assertPackagesAreEqual(t, test.expected, packages[0])
   990  		})
   991  	}
   992  }
   993  
   994  func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
   995  	tests := []struct {
   996  		name         string
   997  		fixtureImage string
   998  		expected     pkg.Package
   999  	}{
  1000  		{
  1001  			name:         "busybox-regression",
  1002  			fixtureImage: "image-busybox",
  1003  			expected: pkg.Package{
  1004  				Name:      "busybox",
  1005  				Version:   "1.35.0",
  1006  				PURL:      "pkg:generic/busybox@1.35.0",
  1007  				Locations: locations("/bin/["),
  1008  				Metadata:  metadata("busybox-binary", "/bin/[", "/bin/busybox"),
  1009  			},
  1010  		},
  1011  	}
  1012  
  1013  	for _, test := range tests {
  1014  		t.Run(test.name, func(t *testing.T) {
  1015  			c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
  1016  
  1017  			img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
  1018  			src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
  1019  				Reference: test.fixtureImage,
  1020  			})
  1021  
  1022  			resolver, err := src.FileResolver(source.SquashedScope)
  1023  			require.NoError(t, err)
  1024  
  1025  			packages, _, err := c.Catalog(context.Background(), resolver)
  1026  			require.NoError(t, err)
  1027  
  1028  			for _, p := range packages {
  1029  				expectedLocations := test.expected.Locations.ToSlice()
  1030  				gotLocations := p.Locations.ToSlice()
  1031  				require.Len(t, gotLocations, len(expectedLocations))
  1032  
  1033  				for i, expectedLocation := range expectedLocations {
  1034  					gotLocation := gotLocations[i]
  1035  					if expectedLocation.RealPath != gotLocation.RealPath {
  1036  						t.Fatalf("locations do not match; expected: %v got: %v", expectedLocations, gotLocations)
  1037  					}
  1038  				}
  1039  
  1040  				assertPackagesAreEqual(t, test.expected, p)
  1041  			}
  1042  		})
  1043  	}
  1044  }
  1045  
  1046  func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
  1047  	c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
  1048  
  1049  	src, err := directorysource.NewFromPath("test-fixtures/classifiers/negative")
  1050  	assert.NoError(t, err)
  1051  
  1052  	resolver, err := src.FileResolver(source.SquashedScope)
  1053  	assert.NoError(t, err)
  1054  
  1055  	actualResults, _, err := c.Catalog(context.Background(), resolver)
  1056  	assert.NoError(t, err)
  1057  	assert.Equal(t, 0, len(actualResults))
  1058  }
  1059  
  1060  func Test_Cataloger_CustomClassifiers(t *testing.T) {
  1061  	defaultClassifers := DefaultClassifiers()
  1062  
  1063  	golangExpected := pkg.Package{
  1064  		Name:      "go",
  1065  		Version:   "1.14",
  1066  		PURL:      "pkg:generic/go@1.14",
  1067  		Locations: locations("go"),
  1068  		Metadata:  metadata("go-binary"),
  1069  	}
  1070  	customExpected := pkg.Package{
  1071  		Name:      "foo",
  1072  		Version:   "1.2.3",
  1073  		PURL:      "pkg:generic/foo@1.2.3",
  1074  		Locations: locations("foo"),
  1075  		Metadata:  metadata("foo-binary"),
  1076  	}
  1077  	fooClassifier := Classifier{
  1078  		Class:    "foo-binary",
  1079  		FileGlob: "**/foo",
  1080  		EvidenceMatcher: FileContentsVersionMatcher(
  1081  			`(?m)foobar\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
  1082  		Package: "foo",
  1083  		PURL:    mustPURL("pkg:generic/foo@version"),
  1084  		CPEs:    singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
  1085  	}
  1086  
  1087  	tests := []struct {
  1088  		name       string
  1089  		config     ClassifierCatalogerConfig
  1090  		fixtureDir string
  1091  		expected   *pkg.Package
  1092  	}{
  1093  		{
  1094  			name: "empty-negative",
  1095  			config: ClassifierCatalogerConfig{
  1096  				Classifiers: []Classifier{},
  1097  			},
  1098  			fixtureDir: "test-fixtures/custom/go-1.14",
  1099  			expected:   nil,
  1100  		},
  1101  		{
  1102  			name: "default-positive",
  1103  			config: ClassifierCatalogerConfig{
  1104  				Classifiers: defaultClassifers,
  1105  			},
  1106  			fixtureDir: "test-fixtures/custom/go-1.14",
  1107  			expected:   &golangExpected,
  1108  		},
  1109  		{
  1110  			name: "nodefault-negative",
  1111  			config: ClassifierCatalogerConfig{
  1112  				Classifiers: []Classifier{fooClassifier},
  1113  			},
  1114  			fixtureDir: "test-fixtures/custom/go-1.14",
  1115  			expected:   nil,
  1116  		},
  1117  		{
  1118  			name: "default-extended-positive",
  1119  			config: ClassifierCatalogerConfig{
  1120  				Classifiers: append(
  1121  					append([]Classifier{}, defaultClassifers...),
  1122  					fooClassifier,
  1123  				),
  1124  			},
  1125  			fixtureDir: "test-fixtures/custom/go-1.14",
  1126  			expected:   &golangExpected,
  1127  		},
  1128  		{
  1129  			name: "default-custom-negative",
  1130  			config: ClassifierCatalogerConfig{
  1131  
  1132  				Classifiers: append(
  1133  					append([]Classifier{}, defaultClassifers...),
  1134  					Classifier{
  1135  						Class:           "foo-binary",
  1136  						FileGlob:        "**/foo",
  1137  						EvidenceMatcher: FileContentsVersionMatcher(`(?m)not there`),
  1138  						Package:         "foo",
  1139  						PURL:            mustPURL("pkg:generic/foo@version"),
  1140  						CPEs:            singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
  1141  					},
  1142  				),
  1143  			},
  1144  			fixtureDir: "test-fixtures/custom/extra",
  1145  			expected:   nil,
  1146  		},
  1147  		{
  1148  			name: "default-cutsom-positive",
  1149  			config: ClassifierCatalogerConfig{
  1150  				Classifiers: append(
  1151  					append([]Classifier{}, defaultClassifers...),
  1152  					fooClassifier,
  1153  				),
  1154  			},
  1155  			fixtureDir: "test-fixtures/custom/extra",
  1156  			expected:   &customExpected,
  1157  		},
  1158  	}
  1159  	for _, test := range tests {
  1160  		t.Run(test.name, func(t *testing.T) {
  1161  			c := NewClassifierCataloger(test.config)
  1162  
  1163  			src, err := directorysource.NewFromPath(test.fixtureDir)
  1164  			require.NoError(t, err)
  1165  
  1166  			resolver, err := src.FileResolver(source.SquashedScope)
  1167  			require.NoError(t, err)
  1168  
  1169  			packages, _, err := c.Catalog(context.Background(), resolver)
  1170  			require.NoError(t, err)
  1171  
  1172  			if test.expected == nil {
  1173  				assert.Equal(t, 0, len(packages))
  1174  			} else {
  1175  				require.Len(t, packages, 1)
  1176  
  1177  				assertPackagesAreEqual(t, *test.expected, packages[0])
  1178  			}
  1179  		})
  1180  	}
  1181  }
  1182  
  1183  func locations(locations ...string) file.LocationSet {
  1184  	var locs []file.Location
  1185  	for _, s := range locations {
  1186  		locs = append(locs, file.NewLocation(s))
  1187  	}
  1188  	return file.NewLocationSet(locs...)
  1189  }
  1190  
  1191  // metadata paths are: realPath, virtualPath
  1192  func metadata(classifier string, paths ...string) pkg.BinarySignature {
  1193  	return pkg.BinarySignature{
  1194  		Matches: []pkg.ClassifierMatch{
  1195  			match(classifier, paths...),
  1196  		},
  1197  	}
  1198  }
  1199  
  1200  // match paths are: realPath, virtualPath
  1201  func match(classifier string, paths ...string) pkg.ClassifierMatch {
  1202  	realPath := ""
  1203  	if len(paths) > 0 {
  1204  		realPath = paths[0]
  1205  	}
  1206  	virtualPath := ""
  1207  	if len(paths) > 1 {
  1208  		virtualPath = paths[1]
  1209  	}
  1210  	return pkg.ClassifierMatch{
  1211  		Classifier: classifier,
  1212  		Location: file.NewVirtualLocationFromCoordinates(
  1213  			file.Coordinates{
  1214  				RealPath: realPath,
  1215  			},
  1216  			virtualPath,
  1217  		),
  1218  	}
  1219  }
  1220  
  1221  func assertPackagesAreEqual(t *testing.T, expected pkg.Package, p pkg.Package) {
  1222  	var failMessages []string
  1223  	expectedLocations := expected.Locations.ToSlice()
  1224  	gotLocations := p.Locations.ToSlice()
  1225  
  1226  	if len(expectedLocations) != len(gotLocations) {
  1227  		failMessages = append(failMessages, "locations are not equal length")
  1228  	} else {
  1229  		for i, expectedLocation := range expectedLocations {
  1230  			gotLocation := gotLocations[i]
  1231  			if expectedLocation.RealPath != gotLocation.RealPath {
  1232  				failMessages = append(failMessages, fmt.Sprintf("locations do not match; expected: %v got: %v", expectedLocation.RealPath, gotLocation.RealPath))
  1233  			}
  1234  		}
  1235  	}
  1236  
  1237  	m1 := expected.Metadata.(pkg.BinarySignature).Matches
  1238  	m2 := p.Metadata.(pkg.BinarySignature).Matches
  1239  	matches := true
  1240  	if len(m1) == len(m2) {
  1241  		for i, m1 := range m1 {
  1242  			m2 := m2[i]
  1243  			if m1.Classifier != m2.Classifier {
  1244  				matches = false
  1245  				break
  1246  			}
  1247  		}
  1248  	} else {
  1249  		matches = false
  1250  	}
  1251  
  1252  	if !matches {
  1253  		failMessages = append(failMessages, "classifier matches not equal")
  1254  	}
  1255  	if expected.Name != p.Name ||
  1256  		expected.Version != p.Version ||
  1257  		expected.PURL != p.PURL {
  1258  		failMessages = append(failMessages, "packages do not match")
  1259  	}
  1260  
  1261  	if len(failMessages) > 0 {
  1262  		assert.Failf(t, strings.Join(failMessages, "; "), "diff: %s",
  1263  			cmp.Diff(expected, p,
  1264  				cmp.Transformer("Locations", func(l file.LocationSet) []file.Location {
  1265  					return l.ToSlice()
  1266  				}),
  1267  				cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationData{}),
  1268  				cmpopts.IgnoreFields(pkg.Package{}, "CPEs", "FoundBy", "Type", "Locations", "Licenses"),
  1269  			),
  1270  		)
  1271  	}
  1272  }
  1273  
  1274  type panicyResolver struct {
  1275  	searchCalled bool
  1276  }
  1277  
  1278  func (p *panicyResolver) FilesByExtension(_ ...string) ([]file.Location, error) {
  1279  	p.searchCalled = true
  1280  	return nil, errors.New("not implemented")
  1281  }
  1282  
  1283  func (p *panicyResolver) FilesByBasename(_ ...string) ([]file.Location, error) {
  1284  	p.searchCalled = true
  1285  	return nil, errors.New("not implemented")
  1286  }
  1287  
  1288  func (p *panicyResolver) FilesByBasenameGlob(_ ...string) ([]file.Location, error) {
  1289  	p.searchCalled = true
  1290  	return nil, errors.New("not implemented")
  1291  }
  1292  
  1293  func (p *panicyResolver) FileContentsByLocation(_ file.Location) (io.ReadCloser, error) {
  1294  	p.searchCalled = true
  1295  	return nil, errors.New("not implemented")
  1296  }
  1297  
  1298  func (p *panicyResolver) HasPath(_ string) bool {
  1299  	return true
  1300  }
  1301  
  1302  func (p *panicyResolver) FilesByPath(_ ...string) ([]file.Location, error) {
  1303  	p.searchCalled = true
  1304  	return nil, errors.New("not implemented")
  1305  }
  1306  
  1307  func (p *panicyResolver) FilesByGlob(_ ...string) ([]file.Location, error) {
  1308  	p.searchCalled = true
  1309  	return nil, errors.New("not implemented")
  1310  }
  1311  
  1312  func (p *panicyResolver) FilesByMIMEType(_ ...string) ([]file.Location, error) {
  1313  	p.searchCalled = true
  1314  	return nil, errors.New("not implemented")
  1315  }
  1316  
  1317  func (p *panicyResolver) RelativeFileByPath(_ file.Location, _ string) *file.Location {
  1318  	return nil
  1319  }
  1320  
  1321  func (p *panicyResolver) AllLocations(_ context.Context) <-chan file.Location {
  1322  	return nil
  1323  }
  1324  
  1325  func (p *panicyResolver) FileMetadataByLocation(_ file.Location) (file.Metadata, error) {
  1326  	return file.Metadata{}, errors.New("not implemented")
  1327  }
  1328  
  1329  var _ file.Resolver = (*panicyResolver)(nil)
  1330  
  1331  func Test_Cataloger_ResilientToErrors(t *testing.T) {
  1332  	c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
  1333  
  1334  	resolver := &panicyResolver{}
  1335  	_, _, err := c.Catalog(context.Background(), resolver)
  1336  	assert.NoError(t, err)
  1337  	assert.True(t, resolver.searchCalled)
  1338  }
  1339  
  1340  func TestCatalogerConfig_MarshalJSON(t *testing.T) {
  1341  
  1342  	tests := []struct {
  1343  		name    string
  1344  		cfg     ClassifierCatalogerConfig
  1345  		want    string
  1346  		wantErr assert.ErrorAssertionFunc
  1347  	}{
  1348  		{
  1349  			name: "only show names of classes",
  1350  			cfg: ClassifierCatalogerConfig{
  1351  				Classifiers: []Classifier{
  1352  					{
  1353  						Class:           "class",
  1354  						FileGlob:        "glob",
  1355  						EvidenceMatcher: FileContentsVersionMatcher(".thing"),
  1356  						Package:         "pkg",
  1357  						PURL: packageurl.PackageURL{
  1358  							Type:       "type",
  1359  							Namespace:  "namespace",
  1360  							Name:       "name",
  1361  							Version:    "version",
  1362  							Qualifiers: nil,
  1363  							Subpath:    "subpath",
  1364  						},
  1365  						CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)},
  1366  					},
  1367  				},
  1368  			},
  1369  			want: `["class"]`,
  1370  		},
  1371  	}
  1372  	for _, tt := range tests {
  1373  		t.Run(tt.name, func(t *testing.T) {
  1374  			if tt.wantErr == nil {
  1375  				tt.wantErr = assert.NoError
  1376  			}
  1377  			got, err := tt.cfg.MarshalJSON()
  1378  			if !tt.wantErr(t, err) {
  1379  				return
  1380  			}
  1381  			assert.Equal(t, tt.want, string(got))
  1382  		})
  1383  	}
  1384  }