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 }