github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/python/poetrylock/poetrylock.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 poetrylock extracts poetry.lock files.
    16  package poetrylock
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"path/filepath"
    22  	"slices"
    23  
    24  	"github.com/BurntSushi/toml"
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/osv"
    28  	"github.com/google/osv-scalibr/inventory"
    29  	"github.com/google/osv-scalibr/plugin"
    30  	"github.com/google/osv-scalibr/purl"
    31  )
    32  
    33  const (
    34  	// Name is the unique name of this extractor.
    35  	Name = "python/poetrylock"
    36  )
    37  
    38  type poetryLockPackageSource struct {
    39  	Type   string `toml:"type"`
    40  	Commit string `toml:"resolved_reference"`
    41  }
    42  
    43  type poetryLockPackage struct {
    44  	Name     string                  `toml:"name"`
    45  	Version  string                  `toml:"version"`
    46  	Optional bool                    `toml:"optional"`
    47  	Groups   []string                `toml:"groups"`
    48  	Source   poetryLockPackageSource `toml:"source"`
    49  }
    50  
    51  type poetryLockFile struct {
    52  	Version  int                 `toml:"version"`
    53  	Packages []poetryLockPackage `toml:"package"`
    54  }
    55  
    56  // Extractor extracts python packages from poetry.lock files.
    57  type Extractor struct{}
    58  
    59  // New returns a new instance of the extractor.
    60  func New() filesystem.Extractor { return &Extractor{} }
    61  
    62  // Name of the extractor
    63  func (e Extractor) Name() string { return Name }
    64  
    65  // Version of the extractor
    66  func (e Extractor) Version() int { return 0 }
    67  
    68  // Requirements of the extractor
    69  func (e Extractor) Requirements() *plugin.Capabilities {
    70  	return &plugin.Capabilities{}
    71  }
    72  
    73  // FileRequired returns true if the specified file matches poetry lockfile patterns
    74  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
    75  	return filepath.Base(api.Path()) == "poetry.lock"
    76  }
    77  
    78  func resolveGroups(pkg poetryLockPackage) []string {
    79  	// by definition an optional package cannot be in any other group,
    80  	// otherwise that would make it a required package
    81  	if pkg.Optional {
    82  		return []string{"optional"}
    83  	}
    84  
    85  	if pkg.Groups == nil {
    86  		return []string{}
    87  	}
    88  
    89  	if slices.Contains(pkg.Groups, "main") {
    90  		return []string{}
    91  	}
    92  
    93  	return pkg.Groups
    94  }
    95  
    96  // Extract extracts packages from poetry.lock files passed through the scan input.
    97  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
    98  	var parsedLockfile *poetryLockFile
    99  
   100  	_, err := toml.NewDecoder(input.Reader).Decode(&parsedLockfile)
   101  
   102  	if err != nil {
   103  		return inventory.Inventory{}, fmt.Errorf("could not extract: %w", err)
   104  	}
   105  
   106  	packages := make([]*extractor.Package, 0, len(parsedLockfile.Packages))
   107  
   108  	for _, lockPackage := range parsedLockfile.Packages {
   109  		pkgDetails := &extractor.Package{
   110  			Name:      lockPackage.Name,
   111  			Version:   lockPackage.Version,
   112  			PURLType:  purl.TypePyPi,
   113  			Locations: []string{input.Path},
   114  			Metadata: osv.DepGroupMetadata{
   115  				DepGroupVals: resolveGroups(lockPackage),
   116  			},
   117  		}
   118  		if lockPackage.Source.Commit != "" {
   119  			pkgDetails.SourceCode = &extractor.SourceCodeIdentifier{
   120  				Commit: lockPackage.Source.Commit,
   121  			}
   122  		}
   123  		packages = append(packages, pkgDetails)
   124  	}
   125  
   126  	return inventory.Inventory{Packages: packages}, nil
   127  }
   128  
   129  var _ filesystem.Extractor = Extractor{}