Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support extracting from uv.lock #314

Merged
merged 10 commits into from
Jan 21, 2025
Empty file.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = 1
requires-python = ">=3.10"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is not valid toml! (I think)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "emoji"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/64/812d7e2ae0ac2ade0d6583f911f99240c80f700afbe8391df10e547f564d/emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca", size = 593932 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/56/4ddf8b36aa4b52077045b17ffb8958f3360b250df4143d1482d9d5bb54d5/emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799", size = 586897 },
]

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "emoji" },
]

[package.metadata]
requires-dist = [{ name = "emoji" }]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "ruff"
version = "0.8.1"
source = { git = "https://github.com/astral-sh/ruff#84748be16341b76e073d117329f7f5f4ee2941ad" }

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "ruff" },
]

[package.metadata]
requires-dist = [{ name = "ruff", git = "https://github.com/astral-sh/ruff" }]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "emoji"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/64/812d7e2ae0ac2ade0d6583f911f99240c80f700afbe8391df10e547f564d/emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca", size = 593932 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/56/4ddf8b36aa4b52077045b17ffb8958f3360b250df4143d1482d9d5bb54d5/emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799", size = 586897 },
]

[[package]]
name = "protobuf"
version = "4.25.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/67/dd/48d5fdb68ec74d70fabcc252e434492e56f70944d9f17b6a15e3746d2295/protobuf-4.25.5.tar.gz", hash = "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584", size = 380315 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/35/1b3c5a5e6107859c4ca902f4fbb762e48599b78129a05d20684fef4a4d04/protobuf-4.25.5-cp310-abi3-win32.whl", hash = "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8", size = 392457 },
{ url = "https://files.pythonhosted.org/packages/a7/ad/bf3f358e90b7e70bf7fb520702cb15307ef268262292d3bdb16ad8ebc815/protobuf-4.25.5-cp310-abi3-win_amd64.whl", hash = "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea", size = 413449 },
{ url = "https://files.pythonhosted.org/packages/51/49/d110f0a43beb365758a252203c43eaaad169fe7749da918869a8c991f726/protobuf-4.25.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173", size = 394248 },
{ url = "https://files.pythonhosted.org/packages/c6/ab/0f384ca0bc6054b1a7b6009000ab75d28a5506e4459378b81280ae7fd358/protobuf-4.25.5-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d", size = 293717 },
{ url = "https://files.pythonhosted.org/packages/05/a6/094a2640be576d760baa34c902dcb8199d89bce9ed7dd7a6af74dcbbd62d/protobuf-4.25.5-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331", size = 294635 },
{ url = "https://files.pythonhosted.org/packages/33/90/f198a61df8381fb43ae0fe81b3d2718e8dcc51ae8502c7657ab9381fbc4f/protobuf-4.25.5-py3-none-any.whl", hash = "sha256:0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41", size = 156467 },
]

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "emoji" },
{ name = "protobuf" },
]

[package.metadata]
requires-dist = [
{ name = "emoji" },
{ name = "protobuf", specifier = ">=3.19.0,<5.0.0.dev0" },
]
149 changes: 149 additions & 0 deletions extractor/filesystem/language/python/uvlock/uvlock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package uvlock extracts uv.lock files.
package uvlock

import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"

"github.com/BurntSushi/toml"
"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/python/internal/pypipurl"
"github.com/google/osv-scalibr/extractor/filesystem/osv"
"github.com/google/osv-scalibr/plugin"
"github.com/google/osv-scalibr/purl"
)

type uvLockPackageSource struct {
Virtual string `toml:"virtual"`
Git string `toml:"git"`
}

type uvLockPackage struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source uvLockPackageSource `toml:"source"`

// uv stores "groups" as a table under "package" after all the packages, which due
// to how TOML works means it ends up being a property on the last package, even
// through in this context it's a global property rather than being per-package
Groups map[string][]uvOptionalDependency `toml:"optional-dependencies"`
}

type uvOptionalDependency struct {
Name string `toml:"name"`
}
type uvLockFile struct {
Version int `toml:"version"`
Packages []uvLockPackage `toml:"package"`
}

// Extractor extracts python packages from uv.lock files.
type Extractor struct{}

// Name of the extractor
func (e Extractor) Name() string { return "python/uvlock" }

// Version of the extractor
func (e Extractor) Version() int { return 0 }

// Requirements of the extractor
func (e Extractor) Requirements() *plugin.Capabilities {
return &plugin.Capabilities{}
}

// FileRequired returns true if the specified file matches uv lockfile patterns
func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
return filepath.Base(api.Path()) == "uv.lock"
}

// Extract extracts packages from uv.lock files passed through the scan input.
func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) {
var parsedLockfile *uvLockFile

_, err := toml.NewDecoder(input.Reader).Decode(&parsedLockfile)

if err != nil {
return []*extractor.Inventory{}, fmt.Errorf("could not extract from %s: %w", input.Path, err)
}

packages := make([]*extractor.Inventory, 0, len(parsedLockfile.Packages))

var groups map[string][]uvOptionalDependency

// uv stores "groups" as a table under "package" after all the packages, which due
// to how TOML works means it ends up being a property on the last package, even
// through in this context it's a global property rather than being per-package
if len(parsedLockfile.Packages) > 0 {
groups = parsedLockfile.Packages[len(parsedLockfile.Packages)-1].Groups
erikvarga marked this conversation as resolved.
Show resolved Hide resolved
}

for _, lockPackage := range parsedLockfile.Packages {
// skip including the root "package", since its name and version are most likely arbitrary
if lockPackage.Source.Virtual == "." {
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
continue
}

_, commit, _ := strings.Cut(lockPackage.Source.Git, "#")

pkgDetails := &extractor.Inventory{
Name: lockPackage.Name,
Version: lockPackage.Version,
Locations: []string{input.Path},
}

if commit != "" {
pkgDetails.SourceCode = &extractor.SourceCodeIdentifier{
Commit: commit,
}
}

depGroupVals := []string{}

for group, deps := range groups {
for _, dep := range deps {
if dep.Name == lockPackage.Name {
depGroupVals = append(depGroupVals, group)
}
}
}

sort.Strings(depGroupVals)

pkgDetails.Metadata = osv.DepGroupMetadata{
DepGroupVals: depGroupVals,
}
packages = append(packages, pkgDetails)
}

return packages, nil
}

// ToPURL converts an inventory created by this extractor into a PURL.
func (e Extractor) ToPURL(i *extractor.Inventory) *purl.PackageURL {
return pypipurl.MakePackageURL(i)
}

// Ecosystem returns the OSV ecosystem ('PyPI') of the software extracted by this extractor.
func (e Extractor) Ecosystem(i *extractor.Inventory) string {
return "PyPI"
}

var _ filesystem.Extractor = Extractor{}
Loading
Loading