github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/python/pipfilelock/pipfilelock.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package pipfilelock extracts Pipfile.lock files. 16 package pipfilelock 17 18 import ( 19 "context" 20 "encoding/json" 21 "fmt" 22 "maps" 23 "path/filepath" 24 "slices" 25 "strings" 26 27 "github.com/google/osv-scalibr/extractor" 28 "github.com/google/osv-scalibr/extractor/filesystem" 29 "github.com/google/osv-scalibr/extractor/filesystem/osv" 30 "github.com/google/osv-scalibr/inventory" 31 "github.com/google/osv-scalibr/plugin" 32 "github.com/google/osv-scalibr/purl" 33 ) 34 35 const ( 36 // Name is the unique name of this extractor. 37 Name = "python/pipfilelock" 38 ) 39 40 type pipenvPackage struct { 41 Version string `json:"version"` 42 } 43 44 type pipenvLockFile struct { 45 Packages map[string]pipenvPackage `json:"default"` 46 PackagesDev map[string]pipenvPackage `json:"develop"` 47 } 48 49 // Extractor extracts python packages from Pipfile.lock files. 50 type Extractor struct{} 51 52 // New returns a new instance of the extractor. 53 func New() filesystem.Extractor { return &Extractor{} } 54 55 // Name of the extractor 56 func (e Extractor) Name() string { return Name } 57 58 // Version of the extractor 59 func (e Extractor) Version() int { return 0 } 60 61 // Requirements of the extractor 62 func (e Extractor) Requirements() *plugin.Capabilities { 63 return &plugin.Capabilities{} 64 } 65 66 // FileRequired returns true if the specified file matches Pipenv lockfile patterns. 67 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 68 return filepath.Base(api.Path()) == "Pipfile.lock" 69 } 70 71 // Extract extracts packages from Pipfile.lock files passed through the scan input. 72 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 73 var parsedLockfile *pipenvLockFile 74 75 err := json.NewDecoder(input.Reader).Decode(&parsedLockfile) 76 77 if err != nil { 78 return inventory.Inventory{}, fmt.Errorf("could not extract: %w", err) 79 } 80 81 details := make(map[string]*extractor.Package) 82 83 addPkgDetails(details, parsedLockfile.Packages, "") 84 addPkgDetails(details, parsedLockfile.PackagesDev, "dev") 85 86 for key := range details { 87 details[key].Locations = []string{input.Path} 88 } 89 90 return inventory.Inventory{Packages: slices.Collect(maps.Values(details))}, nil 91 } 92 93 func addPkgDetails(details map[string]*extractor.Package, packages map[string]pipenvPackage, group string) { 94 for name, pipenvPackage := range packages { 95 if pipenvPackage.Version == "" { 96 continue 97 } 98 99 // All pipenv package versions should be pinned with a == 100 // If it is not, this lockfile is not in the format we expect. 101 if !strings.HasPrefix(pipenvPackage.Version, "==") || len(pipenvPackage.Version) < 3 { 102 // Potentially log a warning here 103 continue 104 } 105 106 version := pipenvPackage.Version[2:] 107 108 // Because in the caller, prod packages are added first, 109 // if it also exists in dev we don't want to add it to dev group 110 if _, ok := details[name+"@"+version]; !ok { 111 groupSlice := []string{} 112 if group != "" { 113 groupSlice = []string{group} 114 } 115 116 pkg := &extractor.Package{ 117 Name: name, 118 Version: version, 119 PURLType: purl.TypePyPi, 120 Metadata: osv.DepGroupMetadata{ 121 DepGroupVals: groupSlice, 122 }, 123 } 124 125 details[name+"@"+version] = pkg 126 } 127 } 128 } 129 130 var _ filesystem.Extractor = Extractor{}