github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/golang/licenses_test.go (about)

     1  package golang
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io/fs"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/anchore/syft/syft/file"
    20  	"github.com/anchore/syft/syft/internal/fileresolver"
    21  	"github.com/anchore/syft/syft/license"
    22  	"github.com/anchore/syft/syft/pkg"
    23  	"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
    24  )
    25  
    26  func Test_LicenseSearch(t *testing.T) {
    27  	ctx := pkgtest.Context()
    28  
    29  	loc1 := file.NewLocation("github.com/someorg/somename@v0.3.2/LICENSE")
    30  	loc2 := file.NewLocation("github.com/!cap!o!r!g/!cap!project@v4.111.5/LICENSE.txt")
    31  	loc3 := file.NewLocation("github.com/someorg/strangelicense@v1.2.3/LiCeNsE.tXt")
    32  
    33  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    34  		buf := &bytes.Buffer{}
    35  		uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/")
    36  
    37  		parts := strings.Split(uri, "/@v/")
    38  		modPath := parts[0]
    39  		modVersion := parts[1]
    40  
    41  		wd, err := os.Getwd()
    42  		require.NoError(t, err)
    43  		testDir := filepath.Join(wd, "test-fixtures", "licenses", "pkg", "mod", processCaps(modPath)+"@"+modVersion)
    44  
    45  		archive := zip.NewWriter(buf)
    46  
    47  		entries, err := os.ReadDir(testDir)
    48  		require.NoError(t, err)
    49  		for _, f := range entries {
    50  			// the zip files downloaded contain a path to the repo that somewhat matches where it ends up on disk,
    51  			// so prefix entries with something similar
    52  			writer, err := archive.Create(path.Join(moduleDir(modPath, modVersion), f.Name()))
    53  			require.NoError(t, err)
    54  			contents, err := os.ReadFile(filepath.Join(testDir, f.Name()))
    55  			require.NoError(t, err)
    56  			_, err = writer.Write(contents)
    57  			require.NoError(t, err)
    58  		}
    59  
    60  		err = archive.Close()
    61  		require.NoError(t, err)
    62  
    63  		w.Header().Add("Content-Length", fmt.Sprintf("%d", buf.Len()))
    64  
    65  		_, err = w.Write(buf.Bytes())
    66  		require.NoError(t, err)
    67  	}))
    68  	defer server.Close()
    69  
    70  	wd, err := os.Getwd()
    71  	require.NoError(t, err)
    72  
    73  	localVendorDir := filepath.Join(wd, "test-fixtures", "licenses-vendor")
    74  
    75  	tests := []struct {
    76  		name     string
    77  		version  string
    78  		config   CatalogerConfig
    79  		expected []pkg.License
    80  	}{
    81  		{
    82  			name:    "github.com/someorg/somename",
    83  			version: "v0.3.2",
    84  			config: CatalogerConfig{
    85  				SearchLocalModCacheLicenses: true,
    86  				LocalModCacheDir:            filepath.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
    87  			},
    88  			expected: []pkg.License{{
    89  				Value:          "Apache-2.0",
    90  				SPDXExpression: "Apache-2.0",
    91  				Type:           license.Concluded,
    92  				Contents:       mustContentsFromLocation(t, loc1),
    93  				URLs:           []string{"file://$GOPATH/pkg/mod/" + loc1.RealPath},
    94  				Locations:      file.NewLocationSet(),
    95  			}},
    96  		},
    97  		{
    98  			name:    "github.com/CapORG/CapProject",
    99  			version: "v4.111.5",
   100  			config: CatalogerConfig{
   101  				SearchLocalModCacheLicenses: true,
   102  				LocalModCacheDir:            filepath.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
   103  			},
   104  			expected: []pkg.License{{
   105  				Value:          "MIT",
   106  				SPDXExpression: "MIT",
   107  				Type:           license.Concluded,
   108  				Contents:       mustContentsFromLocation(t, loc2, 23, 1105),
   109  				URLs:           []string{"file://$GOPATH/pkg/mod/" + loc2.RealPath},
   110  				Locations:      file.NewLocationSet(),
   111  			}},
   112  		},
   113  		{
   114  			name:    "github.com/someorg/strangelicense",
   115  			version: "v1.2.3",
   116  			config: CatalogerConfig{
   117  				SearchLocalModCacheLicenses: true,
   118  				LocalModCacheDir:            filepath.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
   119  			},
   120  			expected: []pkg.License{{
   121  				Value:          "Apache-2.0",
   122  				SPDXExpression: "Apache-2.0",
   123  				Type:           license.Concluded,
   124  				Contents:       mustContentsFromLocation(t, loc3),
   125  				URLs:           []string{"file://$GOPATH/pkg/mod/" + loc3.RealPath},
   126  				Locations:      file.NewLocationSet(),
   127  			}},
   128  		},
   129  		{
   130  			name:    "github.com/someorg/somename",
   131  			version: "v0.3.2",
   132  			config: CatalogerConfig{
   133  				SearchRemoteLicenses: true,
   134  				Proxies:              []string{server.URL},
   135  			},
   136  			expected: []pkg.License{{
   137  				Value:          "Apache-2.0",
   138  				SPDXExpression: "Apache-2.0",
   139  				Contents:       mustContentsFromLocation(t, loc1),
   140  				Type:           license.Concluded,
   141  				URLs:           []string{server.URL + "/github.com/someorg/somename/@v/v0.3.2.zip#" + loc1.RealPath},
   142  				Locations:      file.NewLocationSet(),
   143  			}},
   144  		},
   145  		{
   146  			name:    "github.com/CapORG/CapProject",
   147  			version: "v4.111.5",
   148  			config: CatalogerConfig{
   149  				SearchRemoteLicenses: true,
   150  				Proxies:              []string{server.URL},
   151  			},
   152  			expected: []pkg.License{{
   153  				Value:          "MIT",
   154  				SPDXExpression: "MIT",
   155  				Contents:       mustContentsFromLocation(t, loc2, 23, 1105), // offset for correct scanner contents
   156  				Type:           license.Concluded,
   157  				URLs:           []string{server.URL + "/github.com/CapORG/CapProject/@v/v4.111.5.zip#" + loc2.RealPath},
   158  				Locations:      file.NewLocationSet(),
   159  			}},
   160  		},
   161  		{
   162  			name:    "github.com/CapORG/CapProject",
   163  			version: "v4.111.5",
   164  			config: CatalogerConfig{
   165  				SearchLocalModCacheLicenses: true,
   166  				LocalModCacheDir:            filepath.Join(wd, "test-fixtures"), // valid dir but does not find modules
   167  				SearchRemoteLicenses:        true,
   168  				Proxies:                     []string{server.URL},
   169  			},
   170  			expected: []pkg.License{{
   171  				Value:          "MIT",
   172  				SPDXExpression: "MIT",
   173  				Contents:       mustContentsFromLocation(t, loc2, 23, 1105), // offset for correct scanner contents
   174  				Type:           license.Concluded,
   175  				URLs:           []string{server.URL + "/github.com/CapORG/CapProject/@v/v4.111.5.zip#" + loc2.RealPath},
   176  				Locations:      file.NewLocationSet(),
   177  			}},
   178  		},
   179  		{
   180  			name:    "github.com/someorg/somename",
   181  			version: "v0.3.2",
   182  			config: CatalogerConfig{
   183  				SearchLocalVendorLicenses: true,
   184  				LocalVendorDir:            localVendorDir,
   185  			},
   186  			expected: []pkg.License{{
   187  				Value:          "Apache-2.0",
   188  				SPDXExpression: "Apache-2.0",
   189  				Type:           license.Concluded,
   190  				Contents:       mustContentsFromLocation(t, loc1),
   191  				URLs:           []string{"file://$GO_VENDOR/github.com/someorg/somename/LICENSE"},
   192  				Locations:      file.NewLocationSet(),
   193  			}},
   194  		},
   195  		{
   196  			name:    "github.com/CapORG/CapProject",
   197  			version: "v4.111.5",
   198  			config: CatalogerConfig{
   199  				SearchLocalVendorLicenses: true,
   200  				LocalVendorDir:            localVendorDir,
   201  			},
   202  			expected: []pkg.License{{
   203  				Value:          "MIT",
   204  				SPDXExpression: "MIT",
   205  				Contents:       mustContentsFromLocation(t, loc2, 23, 1105), // offset for correct scanner contents
   206  				Type:           license.Concluded,
   207  				URLs:           []string{"file://$GO_VENDOR/github.com/!cap!o!r!g/!cap!project/LICENSE.txt"},
   208  				Locations:      file.NewLocationSet(),
   209  			}},
   210  		},
   211  		{
   212  			name:    "github.com/someorg/strangelicense",
   213  			version: "v1.2.3",
   214  			config: CatalogerConfig{
   215  				SearchLocalVendorLicenses: true,
   216  				LocalVendorDir:            localVendorDir,
   217  			},
   218  			expected: []pkg.License{{
   219  				Value:          "Apache-2.0",
   220  				SPDXExpression: "Apache-2.0",
   221  				Contents:       mustContentsFromLocation(t, loc1),
   222  				Type:           license.Concluded,
   223  				URLs:           []string{"file://$GO_VENDOR/github.com/someorg/strangelicense/LiCeNsE.tXt"},
   224  				Locations:      file.NewLocationSet(),
   225  			}},
   226  		},
   227  	}
   228  
   229  	for _, test := range tests {
   230  		t.Run(test.name, func(t *testing.T) {
   231  			l := newGoLicenseResolver("", test.config)
   232  			lics := l.getLicenses(ctx, fileresolver.Empty{}, test.name, test.version)
   233  			require.EqualValues(t, test.expected, lics)
   234  		})
   235  	}
   236  }
   237  
   238  func Test_processCaps(t *testing.T) {
   239  	tests := []struct {
   240  		name     string
   241  		expected string
   242  	}{
   243  		{
   244  			name:     "CycloneDX",
   245  			expected: "!cyclone!d!x",
   246  		},
   247  		{
   248  			name:     "Azure",
   249  			expected: "!azure",
   250  		},
   251  		{
   252  			name:     "xkcd",
   253  			expected: "xkcd",
   254  		},
   255  	}
   256  
   257  	for _, test := range tests {
   258  		t.Run(test.name, func(t *testing.T) {
   259  			got := processCaps(test.name)
   260  
   261  			require.Equal(t, test.expected, got)
   262  		})
   263  	}
   264  }
   265  
   266  func Test_remotesForModule(t *testing.T) {
   267  	allProxies := []string{"https://somewhere.org", "direct"}
   268  	directProxy := []string{"direct"}
   269  
   270  	tests := []struct {
   271  		module   string
   272  		noProxy  string
   273  		expected []string
   274  	}{
   275  		{
   276  			module:   "github.com/anchore/syft",
   277  			expected: allProxies,
   278  		},
   279  		{
   280  			module:   "github.com/anchore/sbom-action",
   281  			noProxy:  "*/anchore/*",
   282  			expected: directProxy,
   283  		},
   284  		{
   285  			module:   "github.com/anchore/sbom-action",
   286  			noProxy:  "*/user/mod,*/anchore/sbom-action",
   287  			expected: directProxy,
   288  		},
   289  	}
   290  
   291  	for _, test := range tests {
   292  		t.Run(test.module, func(t *testing.T) {
   293  			got := remotesForModule(allProxies, strings.Split(test.noProxy, ","), test.module)
   294  			require.Equal(t, test.expected, got)
   295  		})
   296  	}
   297  }
   298  
   299  func Test_findVersionPath(t *testing.T) {
   300  	f := os.DirFS("test-fixtures/zip-fs")
   301  	vp := findVersionPath(f, ".")
   302  	require.Equal(t, "github.com/someorg/somepkg@version", vp)
   303  }
   304  
   305  func Test_walkDirErrors(t *testing.T) {
   306  	resolver := newGoLicenseResolver("", CatalogerConfig{})
   307  	_, err := resolver.findLicensesInFS(context.Background(), "somewhere", badFS{})
   308  	require.Error(t, err)
   309  }
   310  
   311  type badFS struct{}
   312  
   313  func (b badFS) Open(_ string) (fs.File, error) {
   314  	return nil, fmt.Errorf("error")
   315  }
   316  
   317  var _ fs.FS = (*badFS)(nil)
   318  
   319  func Test_noLocalGoModDir(t *testing.T) {
   320  	emptyTmp := t.TempDir()
   321  
   322  	validTmp := t.TempDir()
   323  	require.NoError(t, os.MkdirAll(filepath.Join(validTmp, "mod@ver"), 0700|os.ModeDir))
   324  	ctx := pkgtest.Context()
   325  	tests := []struct {
   326  		name    string
   327  		dir     string
   328  		wantErr require.ErrorAssertionFunc
   329  	}{
   330  		{
   331  			name:    "empty",
   332  			dir:     "",
   333  			wantErr: require.Error,
   334  		},
   335  		{
   336  			name:    "invalid dir",
   337  			dir:     filepath.Join(emptyTmp, "invalid-dir"),
   338  			wantErr: require.Error,
   339  		},
   340  		{
   341  			name:    "missing mod dir",
   342  			dir:     emptyTmp,
   343  			wantErr: require.Error,
   344  		},
   345  		{
   346  			name:    "valid mod dir",
   347  			dir:     validTmp,
   348  			wantErr: require.NoError,
   349  		},
   350  	}
   351  
   352  	for _, test := range tests {
   353  		t.Run(test.name, func(t *testing.T) {
   354  			resolver := newGoLicenseResolver("", CatalogerConfig{
   355  				SearchLocalModCacheLicenses: true,
   356  				LocalModCacheDir:            test.dir,
   357  			})
   358  			_, err := resolver.getLicensesFromLocal(ctx, "mod", "ver")
   359  			test.wantErr(t, err)
   360  		})
   361  	}
   362  }
   363  
   364  func mustContentsFromLocation(t *testing.T, loc file.Location, offset ...int) string {
   365  	t.Helper()
   366  
   367  	contentsPath := "test-fixtures/licenses/pkg/mod/" + loc.RealPath
   368  	contents, err := os.ReadFile(contentsPath)
   369  	require.NoErrorf(t, err, "could not open contents for fixture at %s", contentsPath)
   370  
   371  	if len(offset) == 0 {
   372  		return string(contents)
   373  	}
   374  
   375  	require.Equal(t, 2, len(offset), "invalid offset provided, expected two integers: start and end")
   376  
   377  	start, end := offset[0], offset[1]
   378  	require.GreaterOrEqual(t, start, 0, "offset start must be >= 0")
   379  	require.LessOrEqual(t, end, len(contents), "offset end must be <= content length")
   380  	require.LessOrEqual(t, start, end, "offset start must be <= end")
   381  
   382  	return string(contents[start:end])
   383  }