github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/javascript/parse_package_lock.go (about)

     1  package javascript
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  
    10  	"github.com/anchore/syft/internal/log"
    11  	"github.com/anchore/syft/syft/artifact"
    12  	"github.com/anchore/syft/syft/file"
    13  	"github.com/anchore/syft/syft/pkg"
    14  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    15  )
    16  
    17  // integrity check
    18  var _ generic.Parser = parsePackageLock
    19  
    20  // packageLock represents a JavaScript package.lock json file
    21  type packageLock struct {
    22  	Name            string                            `json:"name"`
    23  	Version         string                            `json:"version"`
    24  	LockfileVersion int                               `json:"lockfileVersion"`
    25  	Dependencies    map[string]*packageLockDependency `json:"dependencies"`
    26  	Packages        map[string]*packageLockPackage    `json:"packages"`
    27  	Requires        bool                              `json:"requires"`
    28  }
    29  
    30  type packageLockPackage struct {
    31  	Name            string             `json:"name"`
    32  	Version         string             `json:"version"`
    33  	Integrity       string             `json:"integrity"`
    34  	Resolved        string             `json:"resolved"`
    35  	Dependencies    map[string]string  `json:"dependencies"`
    36  	DevDependencies map[string]string  `json:"devDependencies"`
    37  	License         packageLockLicense `json:"license"`
    38  	Dev             bool               `json:"dev"`
    39  	Requires        map[string]string  `json:"requires"`
    40  }
    41  
    42  type packageLockDependency struct {
    43  	name         string
    44  	Version      string                            `json:"version"`
    45  	Requires     map[string]string                 `json:"requires"`
    46  	Integrity    string                            `json:"integrity"`
    47  	Resolved     string                            `json:"resolved"`
    48  	Dev          bool                              `json:"dev"`
    49  	Dependencies map[string]*packageLockDependency `json:"dependencies"`
    50  }
    51  
    52  // packageLockLicense
    53  type packageLockLicense []string
    54  
    55  func parsePackageLockFile(reader file.LocationReadCloser) (packageLock, error) {
    56  	// in the case we find package-lock.json files in the node_modules directories, skip those
    57  	// as the whole purpose of the lock file is for the specific dependencies of the root project
    58  	if pathContainsNodeModulesDirectory(reader.AccessPath()) {
    59  		return packageLock{}, nil
    60  	}
    61  	dec := json.NewDecoder(reader)
    62  
    63  	var lock packageLock
    64  	for {
    65  		if err := dec.Decode(&lock); errors.Is(err, io.EOF) {
    66  			break
    67  		} else if err != nil {
    68  			return packageLock{}, fmt.Errorf("failed to parse package-lock.json file: %w", err)
    69  		}
    70  	}
    71  	return lock, nil
    72  }
    73  
    74  // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages.
    75  func parsePackageLock(resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    76  	lock, err := parsePackageLockFile(reader)
    77  	if err != nil {
    78  		return nil, nil, err
    79  	}
    80  	pkgs, rels := finalizePackageLockWithoutPackageJSON(resolver, &lock, reader.Location)
    81  	return pkgs, rels, nil
    82  }
    83  
    84  func finalizePackageLockWithoutPackageJSON(resolver file.Resolver, pkglock *packageLock, indexLocation file.Location) ([]pkg.Package, []artifact.Relationship) {
    85  	var root pkg.Package
    86  	if pkglock.Name == "" {
    87  		name := rootNameFromPath(indexLocation)
    88  		p := packageLockPackage{Name: name, Version: "0.0.0"}
    89  		root = newPackageLockV2Package(resolver, indexLocation, name, p)
    90  	} else {
    91  		p := packageLockPackage{Name: pkglock.Name, Version: pkglock.Version}
    92  		root = newPackageLockV2Package(resolver, indexLocation, pkglock.Name, p)
    93  	}
    94  
    95  	if pkglock.LockfileVersion == 1 {
    96  		return finalizePackageLockWithoutPackageJSONV1(resolver, pkglock, indexLocation, root)
    97  	}
    98  
    99  	if pkglock.LockfileVersion == 3 || pkglock.LockfileVersion == 2 {
   100  		return finalizePackageLockV2(resolver, pkglock, indexLocation, root)
   101  	}
   102  
   103  	return nil, nil
   104  }
   105  
   106  func finalizePackageLockWithPackageJSON(resolver file.Resolver, pkgjson *packageJSON, pkglock *packageLock, indexLocation file.Location) ([]pkg.Package, []artifact.Relationship) {
   107  	if pkgjson == nil {
   108  		return finalizePackageLockWithoutPackageJSON(resolver, pkglock, indexLocation)
   109  	}
   110  
   111  	if !pkgjson.hasNameAndVersionValues() {
   112  		log.Debugf("encountered package.json file without a name and/or version field, ignoring (path=%q)", indexLocation.AccessPath())
   113  		return nil, nil
   114  	}
   115  
   116  	p := packageLockPackage{Name: pkgjson.Name, Version: pkgjson.Version}
   117  	root := newPackageLockV2Package(resolver, indexLocation, pkglock.Name, p)
   118  
   119  	if pkglock.LockfileVersion == 1 {
   120  		return finalizePackageLockWithPackageJSONV1(resolver, pkgjson, pkglock, indexLocation, root)
   121  	}
   122  
   123  	if pkglock.LockfileVersion == 3 || pkglock.LockfileVersion == 2 {
   124  		return finalizePackageLockV2(resolver, pkglock, indexLocation, root)
   125  	}
   126  
   127  	return nil, nil
   128  }
   129  
   130  func finalizePackageLockWithoutPackageJSONV1(resolver file.Resolver, pkglock *packageLock, indexLocation file.Location, root pkg.Package) ([]pkg.Package, []artifact.Relationship) {
   131  	if pkglock.LockfileVersion != 1 {
   132  		return nil, nil
   133  	}
   134  	pkgs := []pkg.Package{}
   135  
   136  	pkgs = append(pkgs, root)
   137  	depnameMap := map[string]pkg.Package{}
   138  
   139  	// create packages
   140  	for name, lockDep := range pkglock.Dependencies {
   141  		lockDep.name = name
   142  		pkg := newPackageLockV1Package(resolver, indexLocation, name, *lockDep)
   143  		pkgs = append(pkgs, pkg)
   144  		depnameMap[name] = pkg
   145  	}
   146  	pkg.Sort(pkgs)
   147  	return pkgs, nil
   148  }
   149  
   150  func finalizePackageLockWithPackageJSONV1(resolver file.Resolver, pkgjson *packageJSON, pkglock *packageLock, indexLocation file.Location, root pkg.Package) ([]pkg.Package, []artifact.Relationship) {
   151  	if pkglock.LockfileVersion != 1 {
   152  		return nil, nil
   153  	}
   154  	pkgs := []pkg.Package{}
   155  	relationships := []artifact.Relationship{}
   156  
   157  	pkgs = append(pkgs, root)
   158  	depnameMap := map[string]pkg.Package{}
   159  
   160  	// create packages
   161  	for name, lockDep := range pkglock.Dependencies {
   162  		lockDep.name = name
   163  		pkg := newPackageLockV1Package(resolver, indexLocation, name, *lockDep)
   164  		pkgs = append(pkgs, pkg)
   165  		depnameMap[name] = pkg
   166  	}
   167  
   168  	// create relationships
   169  	for name, lockDep := range pkglock.Dependencies {
   170  		lockDep.name = name
   171  		for name, sub := range lockDep.Dependencies {
   172  			sub.name = name
   173  			if subPkg, ok := depnameMap[name]; ok {
   174  				rel := artifact.Relationship{
   175  					From: subPkg,
   176  					To:   depnameMap[name],
   177  					Type: artifact.DependencyOfRelationship,
   178  				}
   179  				relationships = append(relationships, rel)
   180  			}
   181  		}
   182  		for name := range lockDep.Requires {
   183  			if subPkg, ok := depnameMap[name]; ok {
   184  				rel := artifact.Relationship{
   185  					From: subPkg,
   186  					To:   depnameMap[name],
   187  					Type: artifact.DependencyOfRelationship,
   188  				}
   189  				relationships = append(relationships, rel)
   190  			}
   191  		}
   192  	}
   193  
   194  	for name := range pkgjson.Dependencies {
   195  		rel := artifact.Relationship{
   196  			From: depnameMap[name],
   197  			To:   root,
   198  			Type: artifact.DependencyOfRelationship,
   199  		}
   200  		relationships = append(relationships, rel)
   201  	}
   202  
   203  	for name := range pkgjson.DevDependencies {
   204  		rel := artifact.Relationship{
   205  			From: depnameMap[name],
   206  			To:   root,
   207  			Type: artifact.DependencyOfRelationship,
   208  		}
   209  		relationships = append(relationships, rel)
   210  	}
   211  
   212  	pkg.Sort(pkgs)
   213  	pkg.SortRelationships(relationships)
   214  	return pkgs, relationships
   215  }
   216  
   217  func finalizePackageLockV2(resolver file.Resolver, pkglock *packageLock, indexLocation file.Location, root pkg.Package) ([]pkg.Package, []artifact.Relationship) {
   218  	if pkglock.LockfileVersion != 3 && pkglock.LockfileVersion != 2 {
   219  		return nil, nil
   220  	}
   221  
   222  	pkgs := []pkg.Package{}
   223  	relationships := []artifact.Relationship{}
   224  	depnameMap := map[string]pkg.Package{}
   225  	root.Licenses = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pkglock.Packages[""].License...)...)
   226  
   227  	// create packages
   228  	for name, lockDep := range pkglock.Packages {
   229  		// root pkg always equals "" in lock v2/v3
   230  		if name == "" {
   231  			continue
   232  		}
   233  
   234  		n := getNameFromPath(name)
   235  		pkg := newPackageLockV2Package(resolver, indexLocation, n, *lockDep)
   236  
   237  		pkgs = append(pkgs, pkg)
   238  		// need to store both names
   239  		depnameMap[name] = pkg
   240  		depnameMap[n] = pkg
   241  	}
   242  
   243  	// create relationships
   244  	for name, lockDep := range pkglock.Packages {
   245  		// root pkg always equals "" in lock v2/v3
   246  		if name == "" {
   247  			continue
   248  		}
   249  
   250  		if dep, ok := depnameMap[name]; ok {
   251  			for childName := range lockDep.Dependencies {
   252  				if childDep, ok := depnameMap[childName]; ok {
   253  					rel := artifact.Relationship{
   254  						From: childDep,
   255  						To:   dep,
   256  						Type: artifact.DependencyOfRelationship,
   257  					}
   258  					relationships = append(relationships, rel)
   259  				}
   260  			}
   261  			rootRel := artifact.Relationship{
   262  				From: dep,
   263  				To:   root,
   264  				Type: artifact.DependencyOfRelationship,
   265  			}
   266  			relationships = append(relationships, rootRel)
   267  		}
   268  	}
   269  
   270  	pkgs = append(pkgs, root)
   271  	pkg.Sort(pkgs)
   272  	pkg.SortRelationships(relationships)
   273  	return pkgs, relationships
   274  }
   275  
   276  func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) {
   277  	// The license field could be either a string or an array.
   278  
   279  	// 1. An array
   280  	var arr []string
   281  	if err := json.Unmarshal(data, &arr); err == nil {
   282  		*licenses = arr
   283  		return nil
   284  	}
   285  
   286  	// 2. A string
   287  	var str string
   288  	if err = json.Unmarshal(data, &str); err == nil {
   289  		*licenses = make([]string, 1)
   290  		(*licenses)[0] = str
   291  		return nil
   292  	}
   293  
   294  	// debug the content we did not expect
   295  	if len(data) > 0 {
   296  		log.WithFields("license", string(data)).Debug("Unable to parse the following `license` value in package-lock.json")
   297  	}
   298  
   299  	// 3. Unexpected
   300  	// In case we are unable to parse the license field,
   301  	// i.e if we have not covered the full specification,
   302  	// we do not want to throw an error, instead assign nil.
   303  	return nil
   304  }
   305  
   306  func getNameFromPath(path string) string {
   307  	parts := strings.Split(path, "node_modules/")
   308  	return parts[len(parts)-1]
   309  }