github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/python/parse_poetry_lock.go (about)

     1  package python
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  
     8  	"github.com/BurntSushi/toml"
     9  
    10  	"github.com/anchore/syft/internal/log"
    11  	"github.com/anchore/syft/internal/unknown"
    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  	"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
    17  )
    18  
    19  type poetryPackageSource struct {
    20  	URL       string `toml:"url"`
    21  	Type      string `toml:"type"`
    22  	Reference string `toml:"reference"`
    23  }
    24  
    25  type poetryPackages struct {
    26  	Packages []poetryPackage `toml:"package"`
    27  }
    28  
    29  type poetryPackage struct {
    30  	Name                  string                    `toml:"name"`
    31  	Version               string                    `toml:"version"`
    32  	Category              string                    `toml:"category"`
    33  	Description           string                    `toml:"description"`
    34  	Optional              bool                      `toml:"optional"`
    35  	Source                poetryPackageSource       `toml:"source"`
    36  	DependenciesUnmarshal map[string]toml.Primitive `toml:"dependencies"`
    37  	Extras                map[string][]string       `toml:"extras"`
    38  	Dependencies          map[string][]poetryPackageDependency
    39  }
    40  
    41  type poetryPackageDependency struct {
    42  	Version  string   `toml:"version"`
    43  	Markers  string   `toml:"markers"`
    44  	Optional bool     `toml:"optional"`
    45  	Extras   []string `toml:"extras"`
    46  }
    47  
    48  type poetryLockParser struct {
    49  	cfg             CatalogerConfig
    50  	licenseResolver pythonLicenseResolver
    51  }
    52  
    53  func newPoetryLockParser(cfg CatalogerConfig) poetryLockParser {
    54  	return poetryLockParser{
    55  		cfg:             cfg,
    56  		licenseResolver: newPythonLicenseResolver(cfg),
    57  	}
    58  }
    59  
    60  // parsePoetryLock is a parser function for poetry.lock contents, returning all python packages discovered.
    61  func (plp poetryLockParser) parsePoetryLock(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    62  	pkgs, err := plp.poetryLockPackages(ctx, reader)
    63  	if err != nil {
    64  		return nil, nil, err
    65  	}
    66  
    67  	// since we would never expect to create relationships for packages across multiple poetry.lock files
    68  	// we should do this on a file parser level (each poetry.lock) instead of a cataloger level (across all
    69  	// poetry.lock files)
    70  	return pkgs, dependency.Resolve(poetryLockDependencySpecifier, pkgs), unknown.IfEmptyf(pkgs, "unable to determine packages")
    71  }
    72  
    73  func (plp poetryLockParser) poetryLockPackages(ctx context.Context, reader file.LocationReadCloser) ([]pkg.Package, error) {
    74  	metadata := poetryPackages{}
    75  	md, err := toml.NewDecoder(reader).Decode(&metadata)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("failed to read poetry lock package: %w", err)
    78  	}
    79  
    80  	for i, p := range metadata.Packages {
    81  		dependencies := make(map[string][]poetryPackageDependency)
    82  		for pkgName, du := range p.DependenciesUnmarshal {
    83  			var (
    84  				single    string
    85  				singleObj poetryPackageDependency
    86  				multiObj  []poetryPackageDependency
    87  			)
    88  
    89  			switch {
    90  			case md.PrimitiveDecode(du, &single) == nil:
    91  				dependencies[pkgName] = append(dependencies[pkgName], poetryPackageDependency{Version: single})
    92  			case md.PrimitiveDecode(du, &singleObj) == nil:
    93  				dependencies[pkgName] = append(dependencies[pkgName], singleObj)
    94  			case md.PrimitiveDecode(du, &multiObj) == nil:
    95  				dependencies[pkgName] = append(dependencies[pkgName], multiObj...)
    96  			default:
    97  				log.Tracef("failed to decode poetry lock package dependencies for %s; skipping", pkgName)
    98  			}
    99  		}
   100  		metadata.Packages[i].Dependencies = dependencies
   101  	}
   102  
   103  	var pkgs []pkg.Package
   104  	for _, p := range metadata.Packages {
   105  		pkgs = append(
   106  			pkgs,
   107  			newPackageForIndexWithMetadata(
   108  				ctx,
   109  				plp.licenseResolver,
   110  				p.Name,
   111  				p.Version,
   112  				newPythonPoetryLockEntry(p),
   113  				reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
   114  			),
   115  		)
   116  	}
   117  	return pkgs, nil
   118  }
   119  
   120  func newPythonPoetryLockEntry(p poetryPackage) pkg.PythonPoetryLockEntry {
   121  	return pkg.PythonPoetryLockEntry{
   122  		Index:        extractIndex(p),
   123  		Dependencies: extractPoetryDependencies(p),
   124  		Extras:       extractPoetryExtras(p),
   125  	}
   126  }
   127  
   128  func extractIndex(p poetryPackage) string {
   129  	if p.Source.URL != "" {
   130  		return p.Source.URL
   131  	}
   132  	// https://python-poetry.org/docs/repositories/
   133  	return "https://pypi.org/simple"
   134  }
   135  
   136  func extractPoetryDependencies(p poetryPackage) []pkg.PythonPoetryLockDependencyEntry {
   137  	var deps []pkg.PythonPoetryLockDependencyEntry
   138  	for name, dependencies := range p.Dependencies {
   139  		for _, d := range dependencies {
   140  			deps = append(deps, pkg.PythonPoetryLockDependencyEntry{
   141  				Name:    name,
   142  				Version: d.Version,
   143  				Extras:  d.Extras,
   144  				Markers: d.Markers,
   145  			})
   146  		}
   147  	}
   148  	sort.Slice(deps, func(i, j int) bool {
   149  		return deps[i].Name < deps[j].Name
   150  	})
   151  	return deps
   152  }
   153  
   154  func extractPoetryExtras(p poetryPackage) []pkg.PythonPoetryLockExtraEntry {
   155  	var extras []pkg.PythonPoetryLockExtraEntry
   156  	for name, deps := range p.Extras {
   157  		extras = append(extras, pkg.PythonPoetryLockExtraEntry{
   158  			Name:         name,
   159  			Dependencies: deps,
   160  		})
   161  	}
   162  	sort.Slice(extras, func(i, j int) bool {
   163  		return extras[i].Name < extras[j].Name
   164  	})
   165  	return extras
   166  }