github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/pkg/cataloger/javascript/parse_package_json.go (about)

     1  package javascript
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"regexp"
     9  
    10  	"github.com/mitchellh/mapstructure"
    11  
    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/lineaje-labs/syft/internal"
    17  	"github.com/lineaje-labs/syft/internal/log"
    18  )
    19  
    20  // integrity check
    21  var _ generic.Parser = parsePackageJSON
    22  
    23  // packageJSON represents a JavaScript package.json file
    24  type packageJSON struct {
    25  	Version      string            `json:"version"`
    26  	Latest       []string          `json:"latest"`
    27  	Author       author            `json:"author"`
    28  	License      json.RawMessage   `json:"license"`
    29  	Licenses     json.RawMessage   `json:"licenses"`
    30  	Name         string            `json:"name"`
    31  	Homepage     string            `json:"homepage"`
    32  	Description  string            `json:"description"`
    33  	Dependencies map[string]string `json:"dependencies"`
    34  	Repository   repository        `json:"repository"`
    35  	Private      bool              `json:"private"`
    36  }
    37  
    38  type author struct {
    39  	Name  string `json:"name" mapstruct:"name"`
    40  	Email string `json:"email" mapstruct:"email"`
    41  	URL   string `json:"url" mapstruct:"url"`
    42  }
    43  
    44  type repository struct {
    45  	Type string `json:"type" mapstructure:"type"`
    46  	URL  string `json:"url" mapstructure:"url"`
    47  }
    48  
    49  // match example: "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)"
    50  // ---> name: "Isaac Z. Schlueter" email: "i@izs.me" url: "http://blog.izs.me"
    51  var authorPattern = regexp.MustCompile(`^\s*(?P<name>[^<(]*)(\s+<(?P<email>.*)>)?(\s\((?P<url>.*)\))?\s*$`)
    52  
    53  // parsePackageJSON parses a package.json and returns the discovered JavaScript packages.
    54  func parsePackageJSON(
    55  	_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser,
    56  ) ([]pkg.Package, []artifact.Relationship, error) {
    57  	var pkgs []pkg.Package
    58  	dec := json.NewDecoder(reader)
    59  
    60  	for {
    61  		var p packageJSON
    62  		if err := dec.Decode(&p); errors.Is(err, io.EOF) {
    63  			break
    64  		} else if err != nil {
    65  			return nil, nil, fmt.Errorf("failed to parse package.json file: %w", err)
    66  		}
    67  
    68  		if !p.hasNameAndVersionValues() {
    69  			log.Debugf("encountered package.json file without a name and/or version field, ignoring (path=%q)", reader.Path())
    70  			return nil, nil, nil
    71  		}
    72  
    73  		pkgs = append(
    74  			pkgs,
    75  			newPackageJSONPackage(p, reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    76  		)
    77  	}
    78  
    79  	pkg.Sort(pkgs)
    80  
    81  	return pkgs, nil, 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  }