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

     1  package javascript
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"path"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/anchore/packageurl-go"
    14  	"github.com/anchore/syft/internal"
    15  	"github.com/anchore/syft/internal/log"
    16  	"github.com/anchore/syft/syft/file"
    17  	"github.com/anchore/syft/syft/pkg"
    18  )
    19  
    20  func newPackageJSONPackage(u packageJSON, indexLocation file.Location) pkg.Package {
    21  	licenseCandidates, err := u.licensesFromJSON()
    22  	if err != nil {
    23  		log.Warnf("unable to extract licenses from javascript package.json: %+v", err)
    24  	}
    25  
    26  	license := pkg.NewLicensesFromLocation(indexLocation, licenseCandidates...)
    27  	p := pkg.Package{
    28  		Name:      u.Name,
    29  		Version:   u.Version,
    30  		PURL:      packageURL(u.Name, u.Version),
    31  		Locations: file.NewLocationSet(indexLocation),
    32  		Language:  pkg.JavaScript,
    33  		Licenses:  pkg.NewLicenseSet(license...),
    34  		Type:      pkg.NpmPkg,
    35  		Metadata: pkg.NpmPackage{
    36  			Name:        u.Name,
    37  			Version:     u.Version,
    38  			Description: u.Description,
    39  			Author:      u.Author.AuthorString(),
    40  			Homepage:    u.Homepage,
    41  			URL:         u.Repository.URL,
    42  			Private:     u.Private,
    43  		},
    44  	}
    45  
    46  	p.SetID()
    47  
    48  	return p
    49  }
    50  
    51  func newPackageLockV1Package(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockDependency) pkg.Package {
    52  	version := u.Version
    53  
    54  	const aliasPrefixPackageLockV1 = "npm:"
    55  
    56  	// Handles type aliases https://github.com/npm/rfcs/blob/main/implemented/0001-package-aliases.md
    57  	if strings.HasPrefix(version, aliasPrefixPackageLockV1) {
    58  		// this is an alias.
    59  		// `"version": "npm:canonical-name@X.Y.Z"`
    60  		canonicalPackageAndVersion := version[len(aliasPrefixPackageLockV1):]
    61  		versionSeparator := strings.LastIndex(canonicalPackageAndVersion, "@")
    62  
    63  		name = canonicalPackageAndVersion[:versionSeparator]
    64  		version = canonicalPackageAndVersion[versionSeparator+1:]
    65  	}
    66  
    67  	var licenseSet pkg.LicenseSet
    68  
    69  	if cfg.SearchRemoteLicenses {
    70  		license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version)
    71  		if err == nil && license != "" {
    72  			licenses := pkg.NewLicensesFromValues(license)
    73  			licenseSet = pkg.NewLicenseSet(licenses...)
    74  		}
    75  		if err != nil {
    76  			log.Warnf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, version, err)
    77  		}
    78  	}
    79  
    80  	return finalizeLockPkg(
    81  		resolver,
    82  		location,
    83  		pkg.Package{
    84  			Name:      name,
    85  			Version:   version,
    86  			Licenses:  licenseSet,
    87  			Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    88  			PURL:      packageURL(name, version),
    89  			Language:  pkg.JavaScript,
    90  			Type:      pkg.NpmPkg,
    91  			Metadata:  pkg.NpmPackageLockEntry{Resolved: u.Resolved, Integrity: u.Integrity},
    92  		},
    93  	)
    94  }
    95  
    96  func newPackageLockV2Package(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockPackage) pkg.Package {
    97  	var licenseSet pkg.LicenseSet
    98  
    99  	if u.License != nil {
   100  		licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(location, u.License...)...)
   101  	} else if cfg.SearchRemoteLicenses {
   102  		license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, u.Version)
   103  		if err == nil && license != "" {
   104  			licenses := pkg.NewLicensesFromValues(license)
   105  			licenseSet = pkg.NewLicenseSet(licenses...)
   106  		}
   107  		if err != nil {
   108  			log.Warnf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, u.Version, err)
   109  		}
   110  	}
   111  
   112  	return finalizeLockPkg(
   113  		resolver,
   114  		location,
   115  		pkg.Package{
   116  			Name:      name,
   117  			Version:   u.Version,
   118  			Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
   119  			Licenses:  licenseSet,
   120  			PURL:      packageURL(name, u.Version),
   121  			Language:  pkg.JavaScript,
   122  			Type:      pkg.NpmPkg,
   123  			Metadata:  pkg.NpmPackageLockEntry{Resolved: u.Resolved, Integrity: u.Integrity},
   124  		},
   125  	)
   126  }
   127  
   128  func newPnpmPackage(resolver file.Resolver, location file.Location, name, version string) pkg.Package {
   129  	return finalizeLockPkg(
   130  		resolver,
   131  		location,
   132  		pkg.Package{
   133  			Name:      name,
   134  			Version:   version,
   135  			Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
   136  			PURL:      packageURL(name, version),
   137  			Language:  pkg.JavaScript,
   138  			Type:      pkg.NpmPkg,
   139  		},
   140  	)
   141  }
   142  
   143  func newYarnLockPackage(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, resolved string, integrity string) pkg.Package {
   144  	var licenseSet pkg.LicenseSet
   145  
   146  	if cfg.SearchRemoteLicenses {
   147  		license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version)
   148  		if err == nil && license != "" {
   149  			licenses := pkg.NewLicensesFromValues(license)
   150  			licenseSet = pkg.NewLicenseSet(licenses...)
   151  		}
   152  		if err != nil {
   153  			log.Warnf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, version, err)
   154  		}
   155  	}
   156  	return finalizeLockPkg(
   157  		resolver,
   158  		location,
   159  		pkg.Package{
   160  			Name:      name,
   161  			Version:   version,
   162  			Licenses:  licenseSet,
   163  			Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
   164  			PURL:      packageURL(name, version),
   165  			Language:  pkg.JavaScript,
   166  			Type:      pkg.NpmPkg,
   167  			Metadata:  pkg.YarnLockEntry{Resolved: resolved, Integrity: integrity},
   168  		},
   169  	)
   170  }
   171  
   172  func formatNpmRegistryURL(baseURL, packageName, version string) (requestURL string, err error) {
   173  	urlPath := []string{packageName, version}
   174  	requestURL, err = url.JoinPath(baseURL, urlPath...)
   175  	if err != nil {
   176  		return requestURL, fmt.Errorf("unable to format npm request for pkg:version %s%s; %w", packageName, version, err)
   177  	}
   178  	return requestURL, nil
   179  }
   180  
   181  func getLicenseFromNpmRegistry(basURL, packageName, version string) (string, error) {
   182  	// "https://registry.npmjs.org/%s/%s", packageName, version
   183  	requestURL, err := formatNpmRegistryURL(basURL, packageName, version)
   184  	if err != nil {
   185  		return "", fmt.Errorf("unable to format npm request for pkg:version %s%s; %w", packageName, version, err)
   186  	}
   187  	log.Tracef("trying to fetch remote package %s", requestURL)
   188  
   189  	npmRequest, err := http.NewRequest(http.MethodGet, requestURL, nil)
   190  	if err != nil {
   191  		return "", fmt.Errorf("unable to format remote request: %w", err)
   192  	}
   193  
   194  	httpClient := &http.Client{
   195  		Timeout: time.Second * 10,
   196  	}
   197  
   198  	resp, err := httpClient.Do(npmRequest)
   199  	if err != nil {
   200  		return "", fmt.Errorf("unable to get package from npm registry: %w", err)
   201  	}
   202  	defer func() {
   203  		if err := resp.Body.Close(); err != nil {
   204  			log.Errorf("unable to close body: %+v", err)
   205  		}
   206  	}()
   207  
   208  	bytes, err := io.ReadAll(resp.Body)
   209  	if err != nil {
   210  		return "", fmt.Errorf("unable to parse package from npm registry: %w", err)
   211  	}
   212  
   213  	dec := json.NewDecoder(strings.NewReader(string(bytes)))
   214  
   215  	// Read "license" from the response
   216  	var license struct {
   217  		License string `json:"license"`
   218  	}
   219  
   220  	if err := dec.Decode(&license); err != nil {
   221  		return "", fmt.Errorf("unable to parse license from npm registry: %w", err)
   222  	}
   223  
   224  	log.Tracef("Retrieved License: %s", license.License)
   225  
   226  	return license.License, nil
   227  }
   228  
   229  func finalizeLockPkg(resolver file.Resolver, location file.Location, p pkg.Package) pkg.Package {
   230  	licenseCandidate := addLicenses(p.Name, resolver, location)
   231  	p.Licenses.Add(pkg.NewLicensesFromLocation(location, licenseCandidate...)...)
   232  	p.SetID()
   233  	return p
   234  }
   235  
   236  func addLicenses(name string, resolver file.Resolver, location file.Location) (allLicenses []string) {
   237  	if resolver == nil {
   238  		return allLicenses
   239  	}
   240  
   241  	dir := path.Dir(location.RealPath)
   242  	pkgPath := []string{dir, "node_modules"}
   243  	pkgPath = append(pkgPath, strings.Split(name, "/")...)
   244  	pkgPath = append(pkgPath, "package.json")
   245  	pkgFile := path.Join(pkgPath...)
   246  	locations, err := resolver.FilesByPath(pkgFile)
   247  	if err != nil {
   248  		log.Debugf("an error occurred attempting to read: %s - %+v", pkgFile, err)
   249  		return allLicenses
   250  	}
   251  
   252  	if len(locations) == 0 {
   253  		return allLicenses
   254  	}
   255  
   256  	for _, l := range locations {
   257  		licenses, err := parseLicensesFromLocation(l, resolver, pkgFile)
   258  		if err != nil {
   259  			return allLicenses
   260  		}
   261  		allLicenses = append(allLicenses, licenses...)
   262  	}
   263  
   264  	return allLicenses
   265  }
   266  
   267  func parseLicensesFromLocation(l file.Location, resolver file.Resolver, pkgFile string) ([]string, error) {
   268  	contentReader, err := resolver.FileContentsByLocation(l)
   269  	if err != nil {
   270  		log.Debugf("error getting file content reader for %s: %v", pkgFile, err)
   271  		return nil, err
   272  	}
   273  	defer internal.CloseAndLogError(contentReader, l.RealPath)
   274  
   275  	contents, err := io.ReadAll(contentReader)
   276  	if err != nil {
   277  		log.Debugf("error reading file contents for %s: %v", pkgFile, err)
   278  		return nil, err
   279  	}
   280  
   281  	var pkgJSON packageJSON
   282  	err = json.Unmarshal(contents, &pkgJSON)
   283  	if err != nil {
   284  		log.Debugf("error parsing %s: %v", pkgFile, err)
   285  		return nil, err
   286  	}
   287  
   288  	licenses, err := pkgJSON.licensesFromJSON()
   289  	if err != nil {
   290  		log.Debugf("error getting licenses from %s: %v", pkgFile, err)
   291  		return nil, err
   292  	}
   293  	return licenses, nil
   294  }
   295  
   296  // packageURL returns the PURL for the specific NPM package (see https://github.com/package-url/purl-spec)
   297  func packageURL(name, version string) string {
   298  	var namespace string
   299  
   300  	fields := strings.SplitN(name, "/", 2)
   301  	if len(fields) > 1 {
   302  		namespace = fields[0]
   303  		name = fields[1]
   304  	}
   305  
   306  	return packageurl.NewPackageURL(
   307  		packageurl.TypeNPM,
   308  		namespace,
   309  		name,
   310  		version,
   311  		nil,
   312  		"",
   313  	).ToString()
   314  }