github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/cataloger/javascript/parse_package_lock.go (about) 1 package javascript 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "strings" 10 11 "github.com/anchore/syft/internal/log" 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 ) 17 18 // packageLock represents a JavaScript package.lock json file 19 type packageLock struct { 20 Requires bool `json:"requires"` 21 LockfileVersion int `json:"lockfileVersion"` 22 Dependencies map[string]lockDependency 23 Packages map[string]lockPackage 24 } 25 26 // lockDependency represents a single package dependency listed in the package.lock json file 27 type lockDependency struct { 28 Version string `json:"version"` 29 Resolved string `json:"resolved"` 30 Integrity string `json:"integrity"` 31 } 32 33 type lockPackage struct { 34 Name string `json:"name"` // only present in the root package entry (named "") 35 Version string `json:"version"` 36 Resolved string `json:"resolved"` 37 Integrity string `json:"integrity"` 38 License packageLockLicense `json:"license"` 39 } 40 41 // packageLockLicense 42 type packageLockLicense []string 43 44 type genericPackageLockAdapter struct { 45 cfg CatalogerConfig 46 } 47 48 func newGenericPackageLockAdapter(cfg CatalogerConfig) genericPackageLockAdapter { 49 return genericPackageLockAdapter{ 50 cfg: cfg, 51 } 52 } 53 54 // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages. 55 func (a genericPackageLockAdapter) parsePackageLock(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 56 // in the case we find package-lock.json files in the node_modules directories, skip those 57 // as the whole purpose of the lock file is for the specific dependencies of the root project 58 if pathContainsNodeModulesDirectory(reader.Path()) { 59 return nil, nil, nil 60 } 61 62 var pkgs []pkg.Package 63 dec := json.NewDecoder(reader) 64 65 var lock packageLock 66 for { 67 if err := dec.Decode(&lock); errors.Is(err, io.EOF) { 68 break 69 } else if err != nil { 70 return nil, nil, fmt.Errorf("failed to parse package-lock.json file: %w", err) 71 } 72 } 73 74 if lock.LockfileVersion == 1 { 75 for name, pkgMeta := range lock.Dependencies { 76 pkgs = append(pkgs, newPackageLockV1Package(a.cfg, resolver, reader.Location, name, pkgMeta)) 77 } 78 } 79 80 if lock.LockfileVersion == 2 || lock.LockfileVersion == 3 { 81 for name, pkgMeta := range lock.Packages { 82 if name == "" { 83 if pkgMeta.Name == "" { 84 continue 85 } 86 name = pkgMeta.Name 87 } 88 89 // handles alias names 90 if pkgMeta.Name != "" { 91 name = pkgMeta.Name 92 } 93 94 pkgs = append( 95 pkgs, 96 newPackageLockV2Package(a.cfg, resolver, reader.Location, getNameFromPath(name), pkgMeta), 97 ) 98 } 99 } 100 101 pkg.Sort(pkgs) 102 103 return pkgs, nil, nil 104 } 105 106 func (licenses *packageLockLicense) UnmarshalJSON(data []byte) (err error) { 107 // The license field could be either a string or an array. 108 109 // 1. An array 110 var arr []string 111 if err := json.Unmarshal(data, &arr); err == nil { 112 *licenses = arr 113 return nil 114 } 115 116 // 2. A string 117 var str string 118 if err = json.Unmarshal(data, &str); err == nil { 119 *licenses = make([]string, 1) 120 (*licenses)[0] = str 121 return nil 122 } 123 124 // debug the content we did not expect 125 if len(data) > 0 { 126 log.WithFields("license", string(data)).Debug("Unable to parse the following `license` value in package-lock.json") 127 } 128 129 // 3. Unexpected 130 // In case we are unable to parse the license field, 131 // i.e if we have not covered the full specification, 132 // we do not want to throw an error, instead assign nil. 133 return nil 134 } 135 136 func getNameFromPath(path string) string { 137 parts := strings.Split(path, "node_modules/") 138 return parts[len(parts)-1] 139 }