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 }