github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/javascript/parse_package_json.go (about)

     1  package javascript
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/go-viper/mapstructure/v2"
    13  
    14  	"github.com/anchore/syft/internal"
    15  	"github.com/anchore/syft/syft/artifact"
    16  	"github.com/anchore/syft/syft/file"
    17  	"github.com/anchore/syft/syft/pkg"
    18  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    19  )
    20  
    21  // integrity check
    22  var _ generic.Parser = parsePackageJSON
    23  
    24  // packageJSON represents a JavaScript package.json file
    25  type packageJSON struct {
    26  	Version      string            `json:"version"`
    27  	Latest       []string          `json:"latest"`
    28  	Author       person            `json:"author"`
    29  	Authors      people            `json:"authors"`
    30  	Contributors people            `json:"contributors"`
    31  	Maintainers  people            `json:"maintainers"`
    32  	License      json.RawMessage   `json:"license"`
    33  	Licenses     json.RawMessage   `json:"licenses"`
    34  	Name         string            `json:"name"`
    35  	Homepage     string            `json:"homepage"`
    36  	Description  string            `json:"description"`
    37  	Dependencies map[string]string `json:"dependencies"`
    38  	Repository   repository        `json:"repository"`
    39  	Private      bool              `json:"private"`
    40  }
    41  
    42  type person struct {
    43  	Name  string `json:"name" mapstructure:"name"`
    44  	Email string `json:"email" mapstructure:"email"`
    45  	URL   string `json:"url" mapstructure:"url"`
    46  }
    47  
    48  type people []person
    49  
    50  type repository struct {
    51  	Type string `json:"type" mapstructure:"type"`
    52  	URL  string `json:"url" mapstructure:"url"`
    53  }
    54  
    55  // match example: "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)"
    56  // ---> name: "Isaac Z. Schlueter" email: "i@izs.me" url: "http://blog.izs.me"
    57  var authorPattern = regexp.MustCompile(`^\s*(?P<name>[^<(]*)(\s+<(?P<email>.*)>)?(\s\((?P<url>.*)\))?\s*$`)
    58  
    59  // parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
    60  func parsePackageJSON(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    61  	var pkgs []pkg.Package
    62  	dec := json.NewDecoder(reader)
    63  
    64  	for {
    65  		var p packageJSON
    66  		if err := dec.Decode(&p); errors.Is(err, io.EOF) {
    67  			break
    68  		} else if err != nil {
    69  			return nil, nil, fmt.Errorf("failed to parse package.json file: %w", err)
    70  		}
    71  
    72  		// always create a package, regardless of having a valid name and/or version,
    73  		// a compliance filter later will remove these packages based on compliance rules
    74  		pkgs = append(
    75  			pkgs,
    76  			newPackageJSONPackage(ctx, resolver, p, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    77  		)
    78  	}
    79  
    80  	pkg.Sort(pkgs)
    81  
    82  	return pkgs, nil, nil
    83  }
    84  
    85  func (p *person) UnmarshalJSON(b []byte) error {
    86  	var authorStr string
    87  	var auth person
    88  
    89  	if err := json.Unmarshal(b, &authorStr); err == nil {
    90  		// successfully parsed as a string, now parse that string into fields
    91  		fields := internal.MatchNamedCaptureGroups(authorPattern, authorStr)
    92  		if err := mapstructure.Decode(fields, &auth); err != nil {
    93  			return fmt.Errorf("unable to decode package.json author: %w", err)
    94  		}
    95  	} else {
    96  		// it's a map that may contain fields of various data types (not just strings)
    97  		var fields map[string]interface{}
    98  		if err := json.Unmarshal(b, &fields); err != nil {
    99  			return fmt.Errorf("unable to parse package.json author: %w", err)
   100  		}
   101  		if err := mapstructure.Decode(fields, &auth); err != nil {
   102  			return fmt.Errorf("unable to decode package.json author: %w", err)
   103  		}
   104  	}
   105  
   106  	*p = auth
   107  
   108  	return nil
   109  }
   110  
   111  func (p *person) AuthorString() string {
   112  	result := p.Name
   113  	if p.Email != "" {
   114  		result += fmt.Sprintf(" <%s>", p.Email)
   115  	}
   116  	if p.URL != "" {
   117  		result += fmt.Sprintf(" (%s)", p.URL)
   118  	}
   119  	return result
   120  }
   121  
   122  func (r *repository) UnmarshalJSON(b []byte) error {
   123  	var repositoryStr string
   124  	var fields map[string]string
   125  	var repo repository
   126  
   127  	if err := json.Unmarshal(b, &repositoryStr); err != nil {
   128  		// string parsing did not work, assume a map was given
   129  		// for more information: https://docs.npmjs.com/files/package.json#people-fields-author-contributors
   130  		if err := json.Unmarshal(b, &fields); err != nil {
   131  			return fmt.Errorf("unable to parse package.json author: %w", err)
   132  		}
   133  		// translate the map into a structure
   134  		if err := mapstructure.Decode(fields, &repo); err != nil {
   135  			return fmt.Errorf("unable to decode package.json author: %w", err)
   136  		}
   137  
   138  		*r = repo
   139  	} else {
   140  		r.URL = repositoryStr
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  type npmPackageLicense struct {
   147  	Type string `json:"type"`
   148  	URL  string `json:"url"`
   149  }
   150  
   151  func licenseFromJSON(b []byte) (string, error) {
   152  	// first try as string
   153  	var licenseString string
   154  	err := json.Unmarshal(b, &licenseString)
   155  	if err == nil {
   156  		return licenseString, nil
   157  	}
   158  
   159  	// then try as object (this format is deprecated)
   160  	var licenseObject npmPackageLicense
   161  	err = json.Unmarshal(b, &licenseObject)
   162  	if err == nil {
   163  		return licenseObject.Type, nil
   164  	}
   165  
   166  	return "", errors.New("unable to unmarshal license field as either string or object")
   167  }
   168  
   169  func (p packageJSON) licensesFromJSON() ([]string, error) {
   170  	if p.License == nil && p.Licenses == nil {
   171  		// This package.json doesn't specify any licenses whatsoever
   172  		return []string{}, nil
   173  	}
   174  
   175  	singleLicense, err := licenseFromJSON(p.License)
   176  	if err == nil {
   177  		return []string{singleLicense}, nil
   178  	}
   179  
   180  	multiLicense, err := licensesFromJSON(p.Licenses)
   181  
   182  	// The "licenses" field is deprecated. It should be inspected as a last resort.
   183  	if multiLicense != nil && err == nil {
   184  		mapLicenses := func(licenses []npmPackageLicense) []string {
   185  			mappedLicenses := make([]string, len(licenses))
   186  			for i, l := range licenses {
   187  				mappedLicenses[i] = l.Type
   188  			}
   189  			return mappedLicenses
   190  		}
   191  
   192  		return mapLicenses(multiLicense), nil
   193  	}
   194  
   195  	return nil, err
   196  }
   197  
   198  func licensesFromJSON(b []byte) ([]npmPackageLicense, error) {
   199  	var licenseObject []npmPackageLicense
   200  	err := json.Unmarshal(b, &licenseObject)
   201  	if err == nil {
   202  		return licenseObject, nil
   203  	}
   204  
   205  	return nil, errors.New("unmarshal failed")
   206  }
   207  
   208  // this supports both windows and unix paths
   209  var filepathSeparator = regexp.MustCompile(`[\\/]`)
   210  
   211  func pathContainsNodeModulesDirectory(p string) bool {
   212  	for _, subPath := range filepathSeparator.Split(p, -1) {
   213  		if subPath == "node_modules" {
   214  			return true
   215  		}
   216  	}
   217  	return false
   218  }
   219  
   220  func (p *people) UnmarshalJSON(b []byte) error {
   221  	// Try to unmarshal as an array of strings
   222  	var authorStrings []string
   223  	if err := json.Unmarshal(b, &authorStrings); err == nil {
   224  		// Successfully parsed as an array of strings
   225  		auths := make([]person, len(authorStrings))
   226  		for i, authorStr := range authorStrings {
   227  			// Parse each string into author fields
   228  			fields := internal.MatchNamedCaptureGroups(authorPattern, authorStr)
   229  			var auth person
   230  			if err := mapstructure.Decode(fields, &auth); err != nil {
   231  				return fmt.Errorf("unable to decode package.json author: %w", err)
   232  			}
   233  			// Trim whitespace from name if it was parsed
   234  			if auth.Name != "" {
   235  				auth.Name = strings.TrimSpace(auth.Name)
   236  			}
   237  			auths[i] = auth
   238  		}
   239  		*p = auths
   240  		return nil
   241  	}
   242  
   243  	// Try to unmarshal as an array of objects
   244  	var authorObjs []map[string]interface{}
   245  	if err := json.Unmarshal(b, &authorObjs); err == nil {
   246  		// Successfully parsed as an array of objects
   247  		auths := make([]person, len(authorObjs))
   248  		for i, fields := range authorObjs {
   249  			var auth person
   250  			if err := mapstructure.Decode(fields, &auth); err != nil {
   251  				return fmt.Errorf("unable to decode package.json author object: %w", err)
   252  			}
   253  			auths[i] = auth
   254  		}
   255  		*p = auths
   256  		return nil
   257  	}
   258  
   259  	// If we get here, it means neither format matched
   260  	return fmt.Errorf("unable to parse package.json authors field: expected array of strings or array of objects")
   261  }
   262  
   263  func (p people) String() string {
   264  	if len(p) == 0 {
   265  		return ""
   266  	}
   267  
   268  	authorStrings := make([]string, len(p))
   269  	for i, auth := range p {
   270  		authorStrings[i] = auth.AuthorString()
   271  	}
   272  	return strings.Join(authorStrings, ", ")
   273  }