mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
Add PDM parser (#4234)
Signed-off-by: Pavel Buchart <pavel@buchart.cz> Signed-off-by: Keith Zantow <kzantow@gmail.com> Co-authored-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
parent
0c98a364d5
commit
e923db2a94
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
.python-version
|
||||||
|
|
||||||
# app configuration
|
# app configuration
|
||||||
/.syft.yaml
|
/.syft.yaml
|
||||||
|
|||||||
@ -3,5 +3,5 @@ package internal
|
|||||||
const (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||||
JSONSchemaVersion = "16.0.40"
|
JSONSchemaVersion = "16.0.41"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -51,6 +51,7 @@ func AllTypes() []any {
|
|||||||
pkg.PhpPeclEntry{},
|
pkg.PhpPeclEntry{},
|
||||||
pkg.PortageEntry{},
|
pkg.PortageEntry{},
|
||||||
pkg.PythonPackage{},
|
pkg.PythonPackage{},
|
||||||
|
pkg.PythonPdmLockEntry{},
|
||||||
pkg.PythonPipfileLockEntry{},
|
pkg.PythonPipfileLockEntry{},
|
||||||
pkg.PythonPoetryLockEntry{},
|
pkg.PythonPoetryLockEntry{},
|
||||||
pkg.PythonRequirementsEntry{},
|
pkg.PythonRequirementsEntry{},
|
||||||
|
|||||||
@ -102,6 +102,7 @@ var jsonTypes = makeJSONTypes(
|
|||||||
jsonNames(pkg.PhpPearEntry{}, "php-pear-entry"),
|
jsonNames(pkg.PhpPearEntry{}, "php-pear-entry"),
|
||||||
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
|
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
|
||||||
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
|
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
|
||||||
|
jsonNames(pkg.PythonPdmLockEntry{}, "python-pdm-lock-entry"),
|
||||||
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),
|
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),
|
||||||
jsonNames(pkg.PythonPoetryLockEntry{}, "python-poetry-lock-entry", "PythonPoetryLockMetadata"),
|
jsonNames(pkg.PythonPoetryLockEntry{}, "python-poetry-lock-entry", "PythonPoetryLockMetadata"),
|
||||||
jsonNames(pkg.PythonRequirementsEntry{}, "python-pip-requirements-entry", "PythonRequirementsMetadata"),
|
jsonNames(pkg.PythonRequirementsEntry{}, "python-pip-requirements-entry", "PythonRequirementsMetadata"),
|
||||||
|
|||||||
4011
schema/json/schema-16.0.41.json
Normal file
4011
schema/json/schema-16.0.41.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "anchore.io/schema/syft/json/16.0.40/document",
|
"$id": "anchore.io/schema/syft/json/16.0.41/document",
|
||||||
"$ref": "#/$defs/Document",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@ -2549,6 +2549,9 @@
|
|||||||
{
|
{
|
||||||
"$ref": "#/$defs/PythonPackage"
|
"$ref": "#/$defs/PythonPackage"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/PythonPdmLockEntry"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/$defs/PythonPipRequirementsEntry"
|
"$ref": "#/$defs/PythonPipRequirementsEntry"
|
||||||
},
|
},
|
||||||
@ -3131,6 +3134,35 @@
|
|||||||
],
|
],
|
||||||
"description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)."
|
"description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)."
|
||||||
},
|
},
|
||||||
|
"PythonPdmLockEntry": {
|
||||||
|
"properties": {
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Summary provides a description of the package"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/PythonFileRecord"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"description": "Files are the package files with their paths and hash digests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"description": "Dependencies are the dependency specifications, without environment qualifiers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"summary",
|
||||||
|
"files",
|
||||||
|
"dependencies"
|
||||||
|
],
|
||||||
|
"description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file."
|
||||||
|
},
|
||||||
"PythonPipRequirementsEntry": {
|
"PythonPipRequirementsEntry": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
|||||||
pkg.PhpPeclEntry{},
|
pkg.PhpPeclEntry{},
|
||||||
pkg.PortageEntry{},
|
pkg.PortageEntry{},
|
||||||
pkg.PythonPipfileLockEntry{},
|
pkg.PythonPipfileLockEntry{},
|
||||||
|
pkg.PythonPdmLockEntry{},
|
||||||
pkg.PythonRequirementsEntry{},
|
pkg.PythonRequirementsEntry{},
|
||||||
pkg.PythonPoetryLockEntry{},
|
pkg.PythonPoetryLockEntry{},
|
||||||
pkg.PythonUvLockEntry{},
|
pkg.PythonUvLockEntry{},
|
||||||
@ -342,6 +343,25 @@ func Test_OriginatorSupplier(t *testing.T) {
|
|||||||
originator: "Person: auth (auth@auth.gov)",
|
originator: "Person: auth (auth@auth.gov)",
|
||||||
supplier: "Person: auth (auth@auth.gov)",
|
supplier: "Person: auth (auth@auth.gov)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "from python PDM lock",
|
||||||
|
input: pkg.Package{
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Summary: "A test package",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
originator: "",
|
||||||
|
supplier: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "from r -- maintainer > author",
|
name: "from r -- maintainer > author",
|
||||||
input: pkg.Package{
|
input: pkg.Package{
|
||||||
|
|||||||
@ -30,7 +30,8 @@ func NewPackageCataloger(cfg CatalogerConfig) pkg.Cataloger {
|
|||||||
WithParserByGlobs(parsePoetryLock, "**/poetry.lock").
|
WithParserByGlobs(parsePoetryLock, "**/poetry.lock").
|
||||||
WithParserByGlobs(parsePipfileLock, "**/Pipfile.lock").
|
WithParserByGlobs(parsePipfileLock, "**/Pipfile.lock").
|
||||||
WithParserByGlobs(parseSetup, "**/setup.py").
|
WithParserByGlobs(parseSetup, "**/setup.py").
|
||||||
WithParserByGlobs(parseUvLock, "**/uv.lock")
|
WithParserByGlobs(parseUvLock, "**/uv.lock").
|
||||||
|
WithParserByGlobs(parsePdmLock, "**/pdm.lock")
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInstalledPackageCataloger returns a new cataloger for python packages within egg or wheel installation directories.
|
// NewInstalledPackageCataloger returns a new cataloger for python packages within egg or wheel installation directories.
|
||||||
|
|||||||
@ -454,6 +454,7 @@ func Test_IndexCataloger_Globs(t *testing.T) {
|
|||||||
"src/poetry.lock",
|
"src/poetry.lock",
|
||||||
"src/Pipfile.lock",
|
"src/Pipfile.lock",
|
||||||
"src/uv.lock",
|
"src/uv.lock",
|
||||||
|
"src/pdm.lock",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
140
syft/pkg/cataloger/python/parse_pdm_lock.go
Normal file
140
syft/pkg/cataloger/python/parse_pdm_lock.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package python
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/unknown"
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pdmLock struct {
|
||||||
|
Metadata struct {
|
||||||
|
Groups []string `toml:"groups"`
|
||||||
|
Strategy []string `toml:"strategy"`
|
||||||
|
LockVersion string `toml:"lock_version"`
|
||||||
|
ContentHash string `toml:"content_hash"`
|
||||||
|
} `toml:"metadata"`
|
||||||
|
Package []pdmLockPackage `toml:"package"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pdmLockPackage struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
Version string `toml:"version"`
|
||||||
|
RequiresPython string `toml:"requires_python"`
|
||||||
|
Summary string `toml:"summary"`
|
||||||
|
Dependencies []string `toml:"dependencies"`
|
||||||
|
Files []pdmLockPackageFile `toml:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pdmLockPackageFile struct {
|
||||||
|
File string `toml:"file"`
|
||||||
|
Hash string `toml:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ generic.Parser = parsePdmLock
|
||||||
|
|
||||||
|
// parsePdmLock is a parser function for pdm.lock contents, returning python packages discovered.
|
||||||
|
func parsePdmLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var lock pdmLock
|
||||||
|
_, err := toml.NewDecoder(reader).Decode(&lock)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to parse pdm.lock file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pkgs []pkg.Package
|
||||||
|
for _, p := range lock.Package {
|
||||||
|
var files []pkg.PythonFileRecord
|
||||||
|
for _, f := range p.Files {
|
||||||
|
if colonIndex := strings.Index(f.Hash, ":"); colonIndex != -1 {
|
||||||
|
algorithm := f.Hash[:colonIndex]
|
||||||
|
value := f.Hash[colonIndex+1:]
|
||||||
|
|
||||||
|
files = append(files, pkg.PythonFileRecord{
|
||||||
|
Path: f.File,
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: algorithm,
|
||||||
|
Value: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only store used part of the dependency information
|
||||||
|
var deps []string
|
||||||
|
for _, dep := range p.Dependencies {
|
||||||
|
// remove environment markers (after semicolon)
|
||||||
|
dep = strings.Split(dep, ";")[0]
|
||||||
|
dep = strings.TrimSpace(dep)
|
||||||
|
if dep != "" {
|
||||||
|
deps = append(deps, dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pythonPkgMetadata := pkg.PythonPdmLockEntry{
|
||||||
|
Files: files,
|
||||||
|
Summary: p.Summary,
|
||||||
|
Dependencies: deps,
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgs = append(pkgs, newPackageForIndexWithMetadata(
|
||||||
|
p.Name,
|
||||||
|
p.Version,
|
||||||
|
pythonPkgMetadata,
|
||||||
|
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
relationships := buildPdmRelationships(pkgs)
|
||||||
|
|
||||||
|
return pkgs, relationships, unknown.IfEmptyf(pkgs, "unable to determine packages")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPdmRelationships(pkgs []pkg.Package) []artifact.Relationship {
|
||||||
|
pkgMap := make(map[string]pkg.Package, len(pkgs))
|
||||||
|
for _, p := range pkgs {
|
||||||
|
pkgMap[p.Name] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
var relationships []artifact.Relationship
|
||||||
|
for _, p := range pkgs {
|
||||||
|
meta, ok := p.Metadata.(pkg.PythonPdmLockEntry)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect unique dependencies
|
||||||
|
added := strset.New()
|
||||||
|
|
||||||
|
for _, depName := range meta.Dependencies {
|
||||||
|
// Handle version specifiers
|
||||||
|
depName = strings.Split(depName, "<")[0]
|
||||||
|
depName = strings.Split(depName, ">")[0]
|
||||||
|
depName = strings.Split(depName, "=")[0]
|
||||||
|
depName = strings.Split(depName, "~")[0]
|
||||||
|
depName = strings.TrimSpace(depName)
|
||||||
|
|
||||||
|
if depName == "" || added.Has(depName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
added.Add(depName)
|
||||||
|
|
||||||
|
if dep, exists := pkgMap[depName]; exists {
|
||||||
|
relationships = append(relationships, artifact.Relationship{
|
||||||
|
From: dep,
|
||||||
|
To: p,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
363
syft/pkg/cataloger/python/parse_pdm_lock_test.go
Normal file
363
syft/pkg/cataloger/python/parse_pdm_lock_test.go
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
package python
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePdmLock(t *testing.T) {
|
||||||
|
|
||||||
|
fixture := "test-fixtures/pdm-lock/pdm.lock"
|
||||||
|
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||||
|
expectedPkgs := []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "certifi",
|
||||||
|
Version: "2025.1.31",
|
||||||
|
PURL: "pkg:pypi/certifi@2025.1.31",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "Python package for providing Mozilla's CA Bundle.",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "chardet",
|
||||||
|
Version: "3.0.4",
|
||||||
|
PURL: "pkg:pypi/chardet@3.0.4",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "Universal encoding detector for Python 2 and 3",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "charset-normalizer",
|
||||||
|
Version: "2.0.12",
|
||||||
|
PURL: "pkg:pypi/charset-normalizer@2.0.12",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet.",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "colorama",
|
||||||
|
Version: "0.3.9",
|
||||||
|
PURL: "pkg:pypi/colorama@0.3.9",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "Cross-platform colored terminal text.",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "idna",
|
||||||
|
Version: "2.7",
|
||||||
|
PURL: "pkg:pypi/idna@2.7",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "Internationalized Domain Names in Applications (IDNA)",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "py",
|
||||||
|
Version: "1.4.34",
|
||||||
|
PURL: "pkg:pypi/py@1.4.34",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "library with cross-python path, ini-parsing, io, code, log facilities",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pytest",
|
||||||
|
Version: "3.2.5",
|
||||||
|
PURL: "pkg:pypi/pytest@3.2.5",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "pytest: simple powerful testing with Python",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: []string{
|
||||||
|
"argparse",
|
||||||
|
"colorama",
|
||||||
|
"ordereddict",
|
||||||
|
"py>=1.4.33",
|
||||||
|
"setuptools",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "requests",
|
||||||
|
Version: "2.27.1",
|
||||||
|
PURL: "pkg:pypi/requests@2.27.1",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "Python HTTP for Humans.",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dependencies: []string{
|
||||||
|
"certifi>=2017.4.17",
|
||||||
|
"chardet<5,>=3.0.2",
|
||||||
|
"charset-normalizer~=2.0.0",
|
||||||
|
"idna<3,>=2.5",
|
||||||
|
"idna<4,>=2.5",
|
||||||
|
"urllib3<1.27,>=1.21.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "setuptools",
|
||||||
|
Version: "39.2.0",
|
||||||
|
PURL: "pkg:pypi/setuptools@39.2.0",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "Easily download, build, install, upgrade, and uninstall Python packages",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "urllib3",
|
||||||
|
Version: "1.26.20",
|
||||||
|
PURL: "pkg:pypi/urllib3@1.26.20",
|
||||||
|
Locations: locations,
|
||||||
|
Language: pkg.Python,
|
||||||
|
Type: pkg.PythonPkg,
|
||||||
|
Metadata: pkg.PythonPdmLockEntry{
|
||||||
|
Summary: "HTTP library with thread-safe connection pooling, file post, and more.",
|
||||||
|
Files: []pkg.PythonFileRecord{
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "",
|
||||||
|
Digest: &pkg.PythonFileDigest{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Value: "40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map for easy lookup of packages by name
|
||||||
|
pkgMap := make(map[string]pkg.Package)
|
||||||
|
for _, p := range expectedPkgs {
|
||||||
|
pkgMap[p.Name] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRelationships := []artifact.Relationship{
|
||||||
|
// pytest dependencies
|
||||||
|
{
|
||||||
|
From: pkgMap["colorama"],
|
||||||
|
To: pkgMap["pytest"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: pkgMap["py"],
|
||||||
|
To: pkgMap["pytest"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: pkgMap["setuptools"],
|
||||||
|
To: pkgMap["pytest"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
// requests dependencies
|
||||||
|
{
|
||||||
|
From: pkgMap["certifi"],
|
||||||
|
To: pkgMap["requests"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: pkgMap["chardet"],
|
||||||
|
To: pkgMap["requests"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: pkgMap["charset-normalizer"],
|
||||||
|
To: pkgMap["requests"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: pkgMap["urllib3"],
|
||||||
|
To: pkgMap["requests"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: pkgMap["idna"],
|
||||||
|
To: pkgMap["requests"],
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgtest.TestFileParser(t, fixture, parsePdmLock, expectedPkgs, expectedRelationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_corruptPdmLock(t *testing.T) {
|
||||||
|
pkgtest.NewCatalogTester().
|
||||||
|
FromFile(t, "test-fixtures/glob-paths/src/pdm.lock").
|
||||||
|
WithError().
|
||||||
|
TestParser(t, parsePdmLock)
|
||||||
|
}
|
||||||
1
syft/pkg/cataloger/python/test-fixtures/glob-paths/src/pdm.lock
generated
Normal file
1
syft/pkg/cataloger/python/test-fixtures/glob-paths/src/pdm.lock
generated
Normal file
@ -0,0 +1 @@
|
|||||||
|
bogus
|
||||||
137
syft/pkg/cataloger/python/test-fixtures/pdm-lock/pdm.lock
generated
Normal file
137
syft/pkg/cataloger/python/test-fixtures/pdm-lock/pdm.lock
generated
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# This file is @generated by PDM.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
groups = ["default", "security", "tests"]
|
||||||
|
strategy = ["inherit_metadata", "static_urls"]
|
||||||
|
lock_version = "4.5.0"
|
||||||
|
content_hash = "sha256:2584886ac58a0ae70aa36bc0318b62c3e2c89acc9c21ebb9aee74147c0a9dc06"
|
||||||
|
|
||||||
|
[[metadata.targets]]
|
||||||
|
requires_python = ">=3.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.1.31"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
groups = ["security"]
|
||||||
|
marker = "python_version >= \"3.6\""
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chardet"
|
||||||
|
version = "3.0.4"
|
||||||
|
summary = "Universal encoding detector for Python 2 and 3"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "os_name == \"nt\""
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "2.0.12"
|
||||||
|
requires_python = ">=3.5.0"
|
||||||
|
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
groups = ["security"]
|
||||||
|
marker = "python_version >= \"3.6\""
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.3.9"
|
||||||
|
summary = "Cross-platform colored terminal text."
|
||||||
|
groups = ["tests"]
|
||||||
|
marker = "sys_platform == \"win32\""
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "2.7"
|
||||||
|
summary = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
groups = ["default", "security"]
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/4b/2a/0276479a4b3caeb8a8c1af2f8e4355746a97fab05a372e4a2c6a6b876165/idna-2.7-py2.py3-none-any.whl", hash = "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz", hash = "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py"
|
||||||
|
version = "1.4.34"
|
||||||
|
summary = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||||
|
groups = ["tests"]
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/53/67/9620edf7803ab867b175e4fd23c7b8bd8eba11cb761514dcd2e726ef07da/py-1.4.34-py2.py3-none-any.whl", hash = "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/68/35/58572278f1c097b403879c1e9369069633d1cbad5239b9057944bb764782/py-1.4.34.tar.gz", hash = "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "3.2.5"
|
||||||
|
summary = "pytest: simple powerful testing with Python"
|
||||||
|
groups = ["tests"]
|
||||||
|
dependencies = [
|
||||||
|
"argparse; python_version == \"2.6\"",
|
||||||
|
"colorama; sys_platform == \"win32\"",
|
||||||
|
"ordereddict; python_version == \"2.6\"",
|
||||||
|
"py>=1.4.33",
|
||||||
|
"setuptools",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/1f/f8/8cd74c16952163ce0db0bd95fdd8810cbf093c08be00e6e665ebf0dc3138/pytest-3.2.5.tar.gz", hash = "sha256:6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/ef/41/d8a61f1b2ba308e96b36106e95024977e30129355fd12087f23e4b9852a1/pytest-3.2.5-py2.py3-none-any.whl", hash = "sha256:241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.27.1"
|
||||||
|
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
summary = "Python HTTP for Humans."
|
||||||
|
groups = ["security"]
|
||||||
|
marker = "python_version >= \"3.6\""
|
||||||
|
dependencies = [
|
||||||
|
"certifi>=2017.4.17",
|
||||||
|
"chardet<5,>=3.0.2; python_version < \"3\"",
|
||||||
|
"charset-normalizer~=2.0.0; python_version >= \"3\"",
|
||||||
|
"idna<3,>=2.5; python_version < \"3\"",
|
||||||
|
"idna<4,>=2.5; python_version >= \"3\"",
|
||||||
|
"urllib3<1.27,>=1.21.1",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "39.2.0"
|
||||||
|
requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
|
||||||
|
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
|
groups = ["tests"]
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip", hash = "sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl", hash = "sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.20"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||||
|
summary = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
groups = ["security"]
|
||||||
|
marker = "python_version >= \"3.6\""
|
||||||
|
files = [
|
||||||
|
{url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"},
|
||||||
|
{url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"},
|
||||||
|
]
|
||||||
@ -79,6 +79,16 @@ func (m PythonPackage) OwnedFiles() (result []string) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PythonPdmLockEntry represents a single package entry within a pdm.lock file.
|
||||||
|
type PythonPdmLockEntry struct {
|
||||||
|
// Summary provides a description of the package
|
||||||
|
Summary string `mapstructure:"summary" json:"summary" toml:"summary"`
|
||||||
|
// Files are the package files with their paths and hash digests
|
||||||
|
Files []PythonFileRecord `mapstructure:"files" json:"files" toml:"files"`
|
||||||
|
// Dependencies are the dependency specifications, without environment qualifiers
|
||||||
|
Dependencies []string `mapstructure:"dependencies" json:"dependencies" toml:"dependencies"`
|
||||||
|
}
|
||||||
|
|
||||||
// PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file.
|
// PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file.
|
||||||
type PythonPipfileLockEntry struct {
|
type PythonPipfileLockEntry struct {
|
||||||
// Hashes are the package file hash values in the format "algorithm:digest" for integrity verification.
|
// Hashes are the package file hash values in the format "algorithm:digest" for integrity verification.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user