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

     1  package javascript
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"regexp"
     8  
     9  	"github.com/mitchellh/mapstructure"
    10  
    11  	"github.com/anchore/syft/internal"
    12  	"github.com/anchore/syft/internal/log"
    13  	"github.com/anchore/syft/syft/artifact"
    14  	"github.com/anchore/syft/syft/file"
    15  	"github.com/anchore/syft/syft/pkg"
    16  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    17  )
    18  
    19  // integrity check
    20  var _ generic.Parser = parsePackageJSON
    21  
    22  type packageJSON struct {
    23  	Name                 string            `json:"name"`
    24  	Version              string            `json:"version"`
    25  	Author               author            `json:"author"`
    26  	License              json.RawMessage   `json:"license"`
    27  	Licenses             json.RawMessage   `json:"licenses"`
    28  	Homepage             string            `json:"homepage"`
    29  	Private              bool              `json:"private"`
    30  	Description          string            `json:"description"`
    31  	Develop              bool              `json:"dev"` // lock v3
    32  	Repository           repository        `json:"repository"`
    33  	Dependencies         map[string]string `json:"dependencies"`
    34  	DevDependencies      map[string]string `json:"devDependencies"`
    35  	PeerDependencies     map[string]string `json:"peerDependencies"`
    36  	PeerDependenciesMeta map[string]struct {
    37  		Optional bool `json:"optional"`
    38  	} `json:"peerDependenciesMeta"`
    39  	File string `json:"-"`
    40  }
    41  
    42  type author struct {
    43  	Name  string `json:"name" mapstruct:"name"`
    44  	Email string `json:"email" mapstruct:"email"`
    45  	URL   string `json:"url" mapstruct:"url"`
    46  }
    47  
    48  type repository struct {
    49  	Type string `json:"type" mapstructure:"type"`
    50  	URL  string `json:"url" mapstructure:"url"`
    51  }
    52  
    53  // match example: "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)"
    54  // ---> name: "Isaac Z. Schlueter" email: "i@izs.me" url: "http://blog.izs.me"
    55  var authorPattern = regexp.MustCompile(`^\s*(?P<name>[^<(]*)(\s+<(?P<email>.*)>)?(\s\((?P<url>.*)\))?\s*$`)
    56  
    57  func parsePackageJSON(resolver file.Resolver, e *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    58  	pkgjson, err := parsePackageJSONFile(resolver, e, reader)
    59  	if err != nil {
    60  		return nil, nil, err
    61  	}
    62  
    63  	if !pkgjson.hasNameAndVersionValues() {
    64  		log.Debugf("encountered package.json file without a name and/or version field, ignoring (path=%q)", reader.Location.AccessPath())
    65  		return nil, nil, nil
    66  	}
    67  
    68  	rootPkg := newPackageJSONRootPackage(*pkgjson, reader.Location)
    69  	return []pkg.Package{rootPkg}, nil, nil
    70  }
    71  
    72  // parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
    73  func parsePackageJSONFile(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) (*packageJSON, error) {
    74  	var js *packageJSON
    75  	decoder := json.NewDecoder(reader)
    76  	err := decoder.Decode(&js)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	return js, nil
    82  }
    83  
    84  func (a *author) UnmarshalJSON(b []byte) error {
    85  	var authorStr string
    86  	var fields map[string]string
    87  	var auth author
    88  
    89  	if err := json.Unmarshal(b, &authorStr); err != nil {
    90  		// string parsing did not work, assume a map was given
    91  		// for more information: https://docs.npmjs.com/files/package.json#people-fields-author-contributors
    92  		if err := json.Unmarshal(b, &fields); err != nil {
    93  			return fmt.Errorf("unable to parse package.json author: %w", err)
    94  		}
    95  	} else {
    96  		// parse out "name <email> (url)" into an author struct
    97  		fields = internal.MatchNamedCaptureGroups(authorPattern, authorStr)
    98  	}
    99  
   100  	// translate the map into a structure
   101  	if err := mapstructure.Decode(fields, &auth); err != nil {
   102  		return fmt.Errorf("unable to decode package.json author: %w", err)
   103  	}
   104  
   105  	*a = auth
   106  
   107  	return nil
   108  }
   109  
   110  func (a *author) AuthorString() string {
   111  	result := a.Name
   112  	if a.Email != "" {
   113  		result += fmt.Sprintf(" <%s>", a.Email)
   114  	}
   115  	if a.URL != "" {
   116  		result += fmt.Sprintf(" (%s)", a.URL)
   117  	}
   118  	return result
   119  }
   120  
   121  func (r *repository) UnmarshalJSON(b []byte) error {
   122  	var repositoryStr string
   123  	var fields map[string]string
   124  	var repo repository
   125  
   126  	if err := json.Unmarshal(b, &repositoryStr); err != nil {
   127  		// string parsing did not work, assume a map was given
   128  		// for more information: https://docs.npmjs.com/files/package.json#people-fields-author-contributors
   129  		if err := json.Unmarshal(b, &fields); err != nil {
   130  			return fmt.Errorf("unable to parse package.json author: %w", err)
   131  		}
   132  		// translate the map into a structure
   133  		if err := mapstructure.Decode(fields, &repo); err != nil {
   134  			return fmt.Errorf("unable to decode package.json author: %w", err)
   135  		}
   136  
   137  		*r = repo
   138  	} else {
   139  		r.URL = repositoryStr
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  type npmPackageLicense struct {
   146  	Type string `json:"type"`
   147  	URL  string `json:"url"`
   148  }
   149  
   150  func licenseFromJSON(b []byte) (string, error) {
   151  	// first try as string
   152  	var licenseString string
   153  	err := json.Unmarshal(b, &licenseString)
   154  	if err == nil {
   155  		return licenseString, nil
   156  	}
   157  
   158  	// then try as object (this format is deprecated)
   159  	var licenseObject npmPackageLicense
   160  	err = json.Unmarshal(b, &licenseObject)
   161  	if err == nil {
   162  		return licenseObject.Type, nil
   163  	}
   164  
   165  	return "", errors.New("unable to unmarshal license field as either string or object")
   166  }
   167  
   168  func (p packageJSON) licensesFromJSON() ([]string, error) {
   169  	if p.License == nil && p.Licenses == nil {
   170  		// This package.json doesn't specify any licenses whatsoever
   171  		return []string{}, nil
   172  	}
   173  
   174  	singleLicense, err := licenseFromJSON(p.License)
   175  	if err == nil {
   176  		return []string{singleLicense}, nil
   177  	}
   178  
   179  	multiLicense, err := licensesFromJSON(p.Licenses)
   180  
   181  	// The "licenses" field is deprecated. It should be inspected as a last resort.
   182  	if multiLicense != nil && err == nil {
   183  		mapLicenses := func(licenses []npmPackageLicense) []string {
   184  			mappedLicenses := make([]string, len(licenses))
   185  			for i, l := range licenses {
   186  				mappedLicenses[i] = l.Type
   187  			}
   188  			return mappedLicenses
   189  		}
   190  
   191  		return mapLicenses(multiLicense), nil
   192  	}
   193  
   194  	return nil, err
   195  }
   196  
   197  func licensesFromJSON(b []byte) ([]npmPackageLicense, error) {
   198  	var licenseObject []npmPackageLicense
   199  	err := json.Unmarshal(b, &licenseObject)
   200  	if err == nil {
   201  		return licenseObject, nil
   202  	}
   203  
   204  	return nil, errors.New("unmarshal failed")
   205  }
   206  
   207  func (p packageJSON) hasNameAndVersionValues() bool {
   208  	return p.Name != "" && p.Version != ""
   209  }
   210  
   211  // this supports both windows and unix paths
   212  var filepathSeparator = regexp.MustCompile(`[\\/]`)
   213  
   214  func pathContainsNodeModulesDirectory(p string) bool {
   215  	for _, subPath := range filepathSeparator.Split(p, -1) {
   216  		if subPath == "node_modules" {
   217  			return true
   218  		}
   219  	}
   220  	return false
   221  }