github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/javascript/package.go (about) 1 package javascript 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "path" 10 "strings" 11 "time" 12 13 "github.com/anchore/packageurl-go" 14 "github.com/anchore/syft/internal" 15 "github.com/anchore/syft/internal/log" 16 "github.com/anchore/syft/syft/file" 17 "github.com/anchore/syft/syft/pkg" 18 ) 19 20 func newPackageJSONPackage(u packageJSON, indexLocation file.Location) pkg.Package { 21 licenseCandidates, err := u.licensesFromJSON() 22 if err != nil { 23 log.Warnf("unable to extract licenses from javascript package.json: %+v", err) 24 } 25 26 license := pkg.NewLicensesFromLocation(indexLocation, licenseCandidates...) 27 p := pkg.Package{ 28 Name: u.Name, 29 Version: u.Version, 30 PURL: packageURL(u.Name, u.Version), 31 Locations: file.NewLocationSet(indexLocation), 32 Language: pkg.JavaScript, 33 Licenses: pkg.NewLicenseSet(license...), 34 Type: pkg.NpmPkg, 35 Metadata: pkg.NpmPackage{ 36 Name: u.Name, 37 Version: u.Version, 38 Description: u.Description, 39 Author: u.Author.AuthorString(), 40 Homepage: u.Homepage, 41 URL: u.Repository.URL, 42 Private: u.Private, 43 }, 44 } 45 46 p.SetID() 47 48 return p 49 } 50 51 func newPackageLockV1Package(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockDependency) pkg.Package { 52 version := u.Version 53 54 const aliasPrefixPackageLockV1 = "npm:" 55 56 // Handles type aliases https://github.com/npm/rfcs/blob/main/implemented/0001-package-aliases.md 57 if strings.HasPrefix(version, aliasPrefixPackageLockV1) { 58 // this is an alias. 59 // `"version": "npm:canonical-name@X.Y.Z"` 60 canonicalPackageAndVersion := version[len(aliasPrefixPackageLockV1):] 61 versionSeparator := strings.LastIndex(canonicalPackageAndVersion, "@") 62 63 name = canonicalPackageAndVersion[:versionSeparator] 64 version = canonicalPackageAndVersion[versionSeparator+1:] 65 } 66 67 var licenseSet pkg.LicenseSet 68 69 if cfg.SearchRemoteLicenses { 70 license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version) 71 if err == nil && license != "" { 72 licenses := pkg.NewLicensesFromValues(license) 73 licenseSet = pkg.NewLicenseSet(licenses...) 74 } 75 if err != nil { 76 log.Warnf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, version, err) 77 } 78 } 79 80 return finalizeLockPkg( 81 resolver, 82 location, 83 pkg.Package{ 84 Name: name, 85 Version: version, 86 Licenses: licenseSet, 87 Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), 88 PURL: packageURL(name, version), 89 Language: pkg.JavaScript, 90 Type: pkg.NpmPkg, 91 Metadata: pkg.NpmPackageLockEntry{Resolved: u.Resolved, Integrity: u.Integrity}, 92 }, 93 ) 94 } 95 96 func newPackageLockV2Package(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name string, u lockPackage) pkg.Package { 97 var licenseSet pkg.LicenseSet 98 99 if u.License != nil { 100 licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(location, u.License...)...) 101 } else if cfg.SearchRemoteLicenses { 102 license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, u.Version) 103 if err == nil && license != "" { 104 licenses := pkg.NewLicensesFromValues(license) 105 licenseSet = pkg.NewLicenseSet(licenses...) 106 } 107 if err != nil { 108 log.Warnf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, u.Version, err) 109 } 110 } 111 112 return finalizeLockPkg( 113 resolver, 114 location, 115 pkg.Package{ 116 Name: name, 117 Version: u.Version, 118 Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), 119 Licenses: licenseSet, 120 PURL: packageURL(name, u.Version), 121 Language: pkg.JavaScript, 122 Type: pkg.NpmPkg, 123 Metadata: pkg.NpmPackageLockEntry{Resolved: u.Resolved, Integrity: u.Integrity}, 124 }, 125 ) 126 } 127 128 func newPnpmPackage(resolver file.Resolver, location file.Location, name, version string) pkg.Package { 129 return finalizeLockPkg( 130 resolver, 131 location, 132 pkg.Package{ 133 Name: name, 134 Version: version, 135 Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), 136 PURL: packageURL(name, version), 137 Language: pkg.JavaScript, 138 Type: pkg.NpmPkg, 139 }, 140 ) 141 } 142 143 func newYarnLockPackage(cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string, resolved string, integrity string) pkg.Package { 144 var licenseSet pkg.LicenseSet 145 146 if cfg.SearchRemoteLicenses { 147 license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version) 148 if err == nil && license != "" { 149 licenses := pkg.NewLicensesFromValues(license) 150 licenseSet = pkg.NewLicenseSet(licenses...) 151 } 152 if err != nil { 153 log.Warnf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, version, err) 154 } 155 } 156 return finalizeLockPkg( 157 resolver, 158 location, 159 pkg.Package{ 160 Name: name, 161 Version: version, 162 Licenses: licenseSet, 163 Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), 164 PURL: packageURL(name, version), 165 Language: pkg.JavaScript, 166 Type: pkg.NpmPkg, 167 Metadata: pkg.YarnLockEntry{Resolved: resolved, Integrity: integrity}, 168 }, 169 ) 170 } 171 172 func formatNpmRegistryURL(baseURL, packageName, version string) (requestURL string, err error) { 173 urlPath := []string{packageName, version} 174 requestURL, err = url.JoinPath(baseURL, urlPath...) 175 if err != nil { 176 return requestURL, fmt.Errorf("unable to format npm request for pkg:version %s%s; %w", packageName, version, err) 177 } 178 return requestURL, nil 179 } 180 181 func getLicenseFromNpmRegistry(basURL, packageName, version string) (string, error) { 182 // "https://registry.npmjs.org/%s/%s", packageName, version 183 requestURL, err := formatNpmRegistryURL(basURL, packageName, version) 184 if err != nil { 185 return "", fmt.Errorf("unable to format npm request for pkg:version %s%s; %w", packageName, version, err) 186 } 187 log.Tracef("trying to fetch remote package %s", requestURL) 188 189 npmRequest, err := http.NewRequest(http.MethodGet, requestURL, nil) 190 if err != nil { 191 return "", fmt.Errorf("unable to format remote request: %w", err) 192 } 193 194 httpClient := &http.Client{ 195 Timeout: time.Second * 10, 196 } 197 198 resp, err := httpClient.Do(npmRequest) 199 if err != nil { 200 return "", fmt.Errorf("unable to get package from npm registry: %w", err) 201 } 202 defer func() { 203 if err := resp.Body.Close(); err != nil { 204 log.Errorf("unable to close body: %+v", err) 205 } 206 }() 207 208 bytes, err := io.ReadAll(resp.Body) 209 if err != nil { 210 return "", fmt.Errorf("unable to parse package from npm registry: %w", err) 211 } 212 213 dec := json.NewDecoder(strings.NewReader(string(bytes))) 214 215 // Read "license" from the response 216 var license struct { 217 License string `json:"license"` 218 } 219 220 if err := dec.Decode(&license); err != nil { 221 return "", fmt.Errorf("unable to parse license from npm registry: %w", err) 222 } 223 224 log.Tracef("Retrieved License: %s", license.License) 225 226 return license.License, nil 227 } 228 229 func finalizeLockPkg(resolver file.Resolver, location file.Location, p pkg.Package) pkg.Package { 230 licenseCandidate := addLicenses(p.Name, resolver, location) 231 p.Licenses.Add(pkg.NewLicensesFromLocation(location, licenseCandidate...)...) 232 p.SetID() 233 return p 234 } 235 236 func addLicenses(name string, resolver file.Resolver, location file.Location) (allLicenses []string) { 237 if resolver == nil { 238 return allLicenses 239 } 240 241 dir := path.Dir(location.RealPath) 242 pkgPath := []string{dir, "node_modules"} 243 pkgPath = append(pkgPath, strings.Split(name, "/")...) 244 pkgPath = append(pkgPath, "package.json") 245 pkgFile := path.Join(pkgPath...) 246 locations, err := resolver.FilesByPath(pkgFile) 247 if err != nil { 248 log.Debugf("an error occurred attempting to read: %s - %+v", pkgFile, err) 249 return allLicenses 250 } 251 252 if len(locations) == 0 { 253 return allLicenses 254 } 255 256 for _, l := range locations { 257 licenses, err := parseLicensesFromLocation(l, resolver, pkgFile) 258 if err != nil { 259 return allLicenses 260 } 261 allLicenses = append(allLicenses, licenses...) 262 } 263 264 return allLicenses 265 } 266 267 func parseLicensesFromLocation(l file.Location, resolver file.Resolver, pkgFile string) ([]string, error) { 268 contentReader, err := resolver.FileContentsByLocation(l) 269 if err != nil { 270 log.Debugf("error getting file content reader for %s: %v", pkgFile, err) 271 return nil, err 272 } 273 defer internal.CloseAndLogError(contentReader, l.RealPath) 274 275 contents, err := io.ReadAll(contentReader) 276 if err != nil { 277 log.Debugf("error reading file contents for %s: %v", pkgFile, err) 278 return nil, err 279 } 280 281 var pkgJSON packageJSON 282 err = json.Unmarshal(contents, &pkgJSON) 283 if err != nil { 284 log.Debugf("error parsing %s: %v", pkgFile, err) 285 return nil, err 286 } 287 288 licenses, err := pkgJSON.licensesFromJSON() 289 if err != nil { 290 log.Debugf("error getting licenses from %s: %v", pkgFile, err) 291 return nil, err 292 } 293 return licenses, nil 294 } 295 296 // packageURL returns the PURL for the specific NPM package (see https://github.com/package-url/purl-spec) 297 func packageURL(name, version string) string { 298 var namespace string 299 300 fields := strings.SplitN(name, "/", 2) 301 if len(fields) > 1 { 302 namespace = fields[0] 303 name = fields[1] 304 } 305 306 return packageurl.NewPackageURL( 307 packageurl.TypeNPM, 308 namespace, 309 name, 310 version, 311 nil, 312 "", 313 ).ToString() 314 }