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

     1  package javascript
     2  
     3  import (
     4  	"io"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"gopkg.in/yaml.v3"
    10  
    11  	"github.com/anchore/syft/internal/log"
    12  	"github.com/anchore/syft/syft/artifact"
    13  	"github.com/anchore/syft/syft/file"
    14  	"github.com/anchore/syft/syft/pkg"
    15  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    16  	"github.com/anchore/syft/syft/pkg/cataloger/javascript/key"
    17  )
    18  
    19  // integrity check
    20  var _ generic.Parser = parsePnpmLock
    21  
    22  type pnpmLockPackage struct {
    23  	Name         string
    24  	Version      string
    25  	Integrity    string
    26  	Resolved     string
    27  	Dependencies map[string]string
    28  }
    29  
    30  type pnpmLockYaml struct {
    31  	Version         string                      `yaml:"lockfileVersion"`
    32  	Specifiers      map[string]interface{}      `yaml:"specifiers"`
    33  	Dependencies    map[string]interface{}      `yaml:"dependencies"`
    34  	DevDependencies map[string]interface{}      `yaml:"devDependencies"`
    35  	Packages        map[string]*pnpmLockPackage `yaml:"packages"`
    36  }
    37  
    38  func newPnpmLockPackage(resolver file.Resolver, location file.Location, p *pnpmLockPackage) pkg.Package {
    39  	if p == nil {
    40  		return pkg.Package{}
    41  	}
    42  
    43  	return finalizeLockPkg(
    44  		resolver,
    45  		location,
    46  		pkg.Package{
    47  			Name:         p.Name,
    48  			Version:      p.Version,
    49  			Locations:    file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    50  			PURL:         packageURL(p.Name, p.Version),
    51  			MetadataType: pkg.NpmPackageLockJSONMetadataType,
    52  			Language:     pkg.JavaScript,
    53  			Type:         pkg.NpmPkg,
    54  			Metadata: pkg.NpmPackageLockJSONMetadata{
    55  				Resolved:  p.Resolved,
    56  				Integrity: p.Integrity,
    57  			},
    58  		},
    59  	)
    60  }
    61  
    62  func parsePnpmLock(resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    63  	pnpmMap, pnpmLock := parsePnpmLockFile(reader)
    64  	pkgs, _ := finalizePnpmLockWithoutPackageJSON(resolver, &pnpmLock, pnpmMap, reader.Location)
    65  	return pkgs, nil, nil
    66  }
    67  
    68  func finalizePnpmLockWithoutPackageJSON(resolver file.Resolver, _ *pnpmLockYaml, pnpmMap map[string]*pnpmLockPackage, indexLocation file.Location) ([]pkg.Package, []artifact.Relationship) {
    69  	seenPkgMap := make(map[string]bool)
    70  	var pkgs []pkg.Package
    71  	var relationships []artifact.Relationship
    72  
    73  	name := rootNameFromPath(indexLocation)
    74  	if name == "" {
    75  		return nil, nil
    76  	}
    77  	root := newPnpmLockPackage(resolver, indexLocation, &pnpmLockPackage{Name: name, Version: "0.0.0"})
    78  	pkgs = append(pkgs, root)
    79  
    80  	for _, lockPkg := range pnpmMap {
    81  		if seenPkgMap[key.NpmPackageKey(lockPkg.Name, lockPkg.Version)] {
    82  			continue
    83  		}
    84  
    85  		pkg := newPnpmLockPackage(resolver, indexLocation, lockPkg)
    86  		pkgs = append(pkgs, pkg)
    87  		seenPkgMap[key.NpmPackageKey(pkg.Name, pkg.Version)] = true
    88  	}
    89  
    90  	pkg.Sort(pkgs)
    91  	pkg.SortRelationships(relationships)
    92  	return pkgs, relationships
    93  }
    94  
    95  func finalizePnpmLockWithPackageJSON(resolver file.Resolver, pkgjson *packageJSON, pnpmLock *pnpmLockYaml, pnpmMap map[string]*pnpmLockPackage, indexLocation file.Location) ([]pkg.Package, []artifact.Relationship) {
    96  	seenPkgMap := make(map[string]bool)
    97  	var pkgs []pkg.Package
    98  	var relationships []artifact.Relationship
    99  
   100  	root := newPnpmLockPackage(resolver, indexLocation, &pnpmLockPackage{Name: pkgjson.Name, Version: pkgjson.Version})
   101  	pkgs = append(pkgs, root)
   102  
   103  	// create root relationships
   104  	for name, info := range pnpmLock.Dependencies {
   105  		version := parsePnpmDependencyInfo(info)
   106  		if version == "" {
   107  			continue
   108  		}
   109  
   110  		p := pnpmMap[key.NpmPackageKey(name, version)]
   111  		pkg := newPnpmLockPackage(resolver, indexLocation, p)
   112  		rel := artifact.Relationship{
   113  			From: pkg,
   114  			To:   root,
   115  			Type: artifact.DependencyOfRelationship,
   116  		}
   117  		relationships = append(relationships, rel)
   118  	}
   119  
   120  	// create packages
   121  	for _, lockPkg := range pnpmMap {
   122  		if seenPkgMap[key.NpmPackageKey(lockPkg.Name, lockPkg.Version)] {
   123  			continue
   124  		}
   125  
   126  		pkg := newPnpmLockPackage(resolver, indexLocation, lockPkg)
   127  		pkgs = append(pkgs, pkg)
   128  		seenPkgMap[key.NpmPackageKey(pkg.Name, pkg.Version)] = true
   129  	}
   130  
   131  	// create pkg relationships
   132  	for _, lockPkg := range pnpmMap {
   133  		p := pnpmMap[key.NpmPackageKey(lockPkg.Name, lockPkg.Version)]
   134  		pkg := newPnpmLockPackage(
   135  			resolver,
   136  			indexLocation,
   137  			p,
   138  		)
   139  
   140  		for name, version := range lockPkg.Dependencies {
   141  			dep := pnpmMap[key.NpmPackageKey(name, version)]
   142  			depPkg := newPnpmLockPackage(
   143  				resolver,
   144  				indexLocation,
   145  				dep,
   146  			)
   147  			rel := artifact.Relationship{
   148  				From: depPkg,
   149  				To:   pkg,
   150  				Type: artifact.DependencyOfRelationship,
   151  			}
   152  			relationships = append(relationships, rel)
   153  		}
   154  	}
   155  
   156  	pkg.Sort(pkgs)
   157  	pkg.SortRelationships(relationships)
   158  	return pkgs, relationships
   159  }
   160  
   161  func parsePnpmPackages(lockFile pnpmLockYaml, lockVersion float64, pnpmLock map[string]*pnpmLockPackage) {
   162  	packageNameRegex := regexp.MustCompile(`^/?([^(]*)(?:\(.*\))*$`)
   163  	splitChar := "/"
   164  	if lockVersion >= 6.0 {
   165  		splitChar = "@"
   166  	}
   167  
   168  	for nameVersion, packageDetails := range lockFile.Packages {
   169  		nameVersion = packageNameRegex.ReplaceAllString(nameVersion, "$1")
   170  		nameVersionSplit := strings.Split(strings.TrimPrefix(nameVersion, "/"), splitChar)
   171  
   172  		// last element in split array is version
   173  		version := nameVersionSplit[len(nameVersionSplit)-1]
   174  
   175  		// construct name from all array items other than last item (version)
   176  		name := strings.Join(nameVersionSplit[:len(nameVersionSplit)-1], splitChar)
   177  
   178  		if pnpmLock[key.NpmPackageKey(name, version)] != nil {
   179  			if pnpmLock[key.NpmPackageKey(name, version)].Version == version {
   180  				continue
   181  			}
   182  		}
   183  
   184  		packageDetails.Name = name
   185  		packageDetails.Version = version
   186  
   187  		pnpmLock[key.NpmPackageKey(name, version)] = packageDetails
   188  	}
   189  }
   190  
   191  func parsePnpmDependencyInfo(info interface{}) (version string) {
   192  	switch info := info.(type) {
   193  	case string:
   194  		version = info
   195  	case map[string]interface{}:
   196  		v, ok := info["version"]
   197  		if !ok {
   198  			break
   199  		}
   200  		ver, ok := v.(string)
   201  		if ok {
   202  			version = parseVersion(ver)
   203  		}
   204  	}
   205  	log.Tracef("unsupported pnpm dependency type: %+v", info)
   206  	return
   207  }
   208  
   209  func parsePnpmDependencies(lockFile pnpmLockYaml, pnpmLock map[string]*pnpmLockPackage) {
   210  	for name, info := range lockFile.Dependencies {
   211  		version := parsePnpmDependencyInfo(info)
   212  		if version == "" {
   213  			continue
   214  		}
   215  
   216  		if pnpmLock[key.NpmPackageKey(name, version)] != nil {
   217  			if pnpmLock[key.NpmPackageKey(name, version)].Version == version {
   218  				continue
   219  			}
   220  		}
   221  
   222  		pnpmLock[key.NpmPackageKey(name, version)] = &pnpmLockPackage{
   223  			Name:    name,
   224  			Version: version,
   225  		}
   226  	}
   227  }
   228  
   229  // parsePnpmLock parses a pnpm-lock.yaml file to get a list of packages
   230  func parsePnpmLockFile(file file.LocationReadCloser) (map[string]*pnpmLockPackage, pnpmLockYaml) {
   231  	pnpmLock := map[string]*pnpmLockPackage{}
   232  	bytes, err := io.ReadAll(file)
   233  	if err != nil {
   234  		return pnpmLock, pnpmLockYaml{}
   235  	}
   236  
   237  	var lockFile pnpmLockYaml
   238  	if err := yaml.Unmarshal(bytes, &lockFile); err != nil {
   239  		return pnpmLock, pnpmLockYaml{}
   240  	}
   241  
   242  	lockVersion, _ := strconv.ParseFloat(lockFile.Version, 64)
   243  
   244  	// parse packages from packages section of pnpm-lock.yaml
   245  	parsePnpmPackages(lockFile, lockVersion, pnpmLock)
   246  	parsePnpmDependencies(lockFile, pnpmLock)
   247  
   248  	return pnpmLock, lockFile
   249  }
   250  
   251  func parseVersion(version string) string {
   252  	return strings.SplitN(version, "(", 2)[0]
   253  }