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