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{}