split python package catalogers by image vs directory

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2020-10-20 11:35:05 -04:00
parent beb6afff36
commit 0ce8701e73
No known key found for this signature in database
GPG Key ID: 5CB45AE22BAB7EA7
17 changed files with 96 additions and 124 deletions

View File

@ -122,7 +122,7 @@ validate-cyclonedx-schema:
.PHONY: unit .PHONY: unit
unit: fixtures ## Run unit tests (with coverage) unit: fixtures ## Run unit tests (with coverage)
$(call title,Running unit tests) $(call title,Running unit tests)
go test -coverprofile $(COVER_REPORT) ./... go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL) @go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
@echo "Coverage: $$(cat $(COVER_TOTAL))" @echo "Coverage: $$(cat $(COVER_TOTAL))"
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi @if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi

View File

@ -32,7 +32,7 @@ type Cataloger interface {
func ImageCatalogers() []Cataloger { func ImageCatalogers() []Cataloger {
return []Cataloger{ return []Cataloger{
ruby.NewGemSpecCataloger(), ruby.NewGemSpecCataloger(),
python.NewPythonCataloger(), // TODO: split and replace me python.NewPythonPackageCataloger(),
javascript.NewJavascriptPackageCataloger(), javascript.NewJavascriptPackageCataloger(),
deb.NewDpkgdbCataloger(), deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(), rpmdb.NewRpmdbCataloger(),
@ -46,7 +46,8 @@ func ImageCatalogers() []Cataloger {
func DirectoryCatalogers() []Cataloger { func DirectoryCatalogers() []Cataloger {
return []Cataloger{ return []Cataloger{
ruby.NewGemFileLockCataloger(), ruby.NewGemFileLockCataloger(),
python.NewPythonCataloger(), // TODO: split and replace me python.NewPythonIndexCataloger(),
python.NewPythonPackageCataloger(),
javascript.NewJavascriptLockCataloger(), javascript.NewJavascriptLockCataloger(),
deb.NewDpkgdbCataloger(), deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(), rpmdb.NewRpmdbCataloger(),

View File

@ -7,15 +7,23 @@ import (
"github.com/anchore/syft/syft/cataloger/common" "github.com/anchore/syft/syft/cataloger/common"
) )
// NewPythonCataloger returns a new Python cataloger object. // NewPythonPackageCataloger returns a new cataloger for python packages within egg or wheel installation directories.
func NewPythonCataloger() *common.GenericCataloger { func NewPythonPackageCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/*egg-info/PKG-INFO": parseEggMetadata, "**/*egg-info/PKG-INFO": parseWheelOrEggMetadata,
"**/*dist-info/METADATA": parseWheelMetadata, "**/*dist-info/METADATA": parseWheelOrEggMetadata,
"**/*requirements*.txt": parseRequirementsTxt,
"**/poetry.lock": parsePoetryLock,
"**/setup.py": parseSetup,
} }
return common.NewGenericCataloger(nil, globParsers, "python-cataloger") return common.NewGenericCataloger(nil, globParsers, "python-package-cataloger")
}
// NewPythonIndexCataloger returns a new cataloger for python packages referenced from poetry lock files, requirements.txt files, and setup.py files.
func NewPythonIndexCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/*requirements*.txt": parseRequirementsTxt,
"**/poetry.lock": parsePoetryLock,
"**/setup.py": parseSetup,
}
return common.NewGenericCataloger(nil, globParsers, "python-index-cataloger")
} }

View File

@ -1,10 +1,11 @@
package python package python
import ( import (
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
"os" "os"
"testing" "testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
) )
func TestParsePoetryLock(t *testing.T) { func TestParsePoetryLock(t *testing.T) {
@ -13,28 +14,28 @@ func TestParsePoetryLock(t *testing.T) {
Name: "added-value", Name: "added-value",
Version: "0.14.2", Version: "0.14.2",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PoetryPkg, Type: pkg.PythonPkg,
Licenses: nil, Licenses: nil,
}, },
{ {
Name: "alabaster", Name: "alabaster",
Version: "0.7.12", Version: "0.7.12",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PoetryPkg, Type: pkg.PythonPkg,
Licenses: nil, Licenses: nil,
}, },
{ {
Name: "appnope", Name: "appnope",
Version: "0.1.0", Version: "0.1.0",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PoetryPkg, Type: pkg.PythonPkg,
Licenses: nil, Licenses: nil,
}, },
{ {
Name: "asciitree", Name: "asciitree",
Version: "0.3.3", Version: "0.3.3",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PoetryPkg, Type: pkg.PythonPkg,
Licenses: nil, Licenses: nil,
}, },
} }

View File

@ -47,7 +47,7 @@ func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
Name: name, Name: name,
Version: version, Version: version,
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonRequirementsPkg, Type: pkg.PythonPkg,
}) })
default: default:
continue continue

View File

@ -13,14 +13,14 @@ func TestParseRequirementsTxt(t *testing.T) {
Name: "foo", Name: "foo",
Version: "1.0.0", Version: "1.0.0",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonRequirementsPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
"flask": { "flask": {
Name: "flask", Name: "flask",
Version: "4.0.0", Version: "4.0.0",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonRequirementsPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
} }

View File

@ -41,7 +41,7 @@ func parseSetup(_ string, reader io.Reader) ([]pkg.Package, error) {
Name: strings.Trim(name, "'\""), Name: strings.Trim(name, "'\""),
Version: strings.Trim(version, "'\""), Version: strings.Trim(version, "'\""),
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonSetupPkg, Type: pkg.PythonPkg,
}) })
} }
} }

View File

@ -13,35 +13,35 @@ func TestParseSetup(t *testing.T) {
Name: "pathlib3", Name: "pathlib3",
Version: "2.2.0", Version: "2.2.0",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonSetupPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
"mypy": { "mypy": {
Name: "mypy", Name: "mypy",
Version: "v0.770", Version: "v0.770",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonSetupPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
"mypy1": { "mypy1": {
Name: "mypy1", Name: "mypy1",
Version: "v0.770", Version: "v0.770",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonSetupPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
"mypy2": { "mypy2": {
Name: "mypy2", Name: "mypy2",
Version: "v0.770", Version: "v0.770",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonSetupPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
"mypy3": { "mypy3": {
Name: "mypy3", Name: "mypy3",
Version: "v0.770", Version: "v0.770",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonSetupPkg, Type: pkg.PythonPkg,
Licenses: []string{}, Licenses: []string{},
}, },
} }

View File

@ -11,39 +11,17 @@ import (
) )
// integrity check // integrity check
var _ common.ParserFn = parseWheelMetadata var _ common.ParserFn = parseWheelOrEggMetadata
var _ common.ParserFn = parseEggMetadata
// parseWheelMetadata is a parser function for individual Python Wheel metadata file contents, returning all Python
// packages listed.
func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.WheelPkg
}
return packages, err
}
// parseEggMetadata is a parser function for individual Python Egg metadata file contents, returning all Python
// packages listed.
func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.EggPkg
}
return packages, err
}
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes), // parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed. // returning all Python packages listed.
func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) { func parseWheelOrEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
fields := make(map[string]string) fields := make(map[string]string)
var key string var key string
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
line = strings.TrimRight(line, "\n") line = strings.TrimRight(line, "\n")
// empty line indicates end of entry // empty line indicates end of entry
@ -90,6 +68,7 @@ func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
Name: fields["Name"], Name: fields["Name"],
Version: fields["Version"], Version: fields["Version"],
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PythonPkg,
} }
if license, ok := fields["License"]; ok && license != "" { if license, ok := fields["License"]; ok && license != "" {

View File

@ -52,7 +52,7 @@ func TestParseEggMetadata(t *testing.T) {
Name: "requests", Name: "requests",
Version: "2.22.0", Version: "2.22.0",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.EggPkg, Type: pkg.PythonPkg,
Licenses: []string{"Apache 2.0"}, Licenses: []string{"Apache 2.0"},
}, },
} }
@ -61,7 +61,7 @@ func TestParseEggMetadata(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err) t.Fatalf("failed to open fixture: %+v", err)
} }
actual, err := parseEggMetadata(fixture.Name(), fixture) actual, err := parseWheelOrEggMetadata(fixture.Name(), fixture)
if err != nil { if err != nil {
t.Fatalf("failed to parse egg-info: %+v", err) t.Fatalf("failed to parse egg-info: %+v", err)
} }
@ -75,7 +75,7 @@ func TestParseWheelMetadata(t *testing.T) {
Name: "Pygments", Name: "Pygments",
Version: "2.6.1", Version: "2.6.1",
Language: pkg.Python, Language: pkg.Python,
Type: pkg.WheelPkg, Type: pkg.PythonPkg,
Licenses: []string{"BSD License"}, Licenses: []string{"BSD License"},
}, },
} }
@ -84,7 +84,7 @@ func TestParseWheelMetadata(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err) t.Fatalf("failed to open fixture: %+v", err)
} }
actual, err := parseWheelMetadata(fixture.Name(), fixture) actual, err := parseWheelOrEggMetadata(fixture.Name(), fixture)
if err != nil { if err != nil {
t.Fatalf("failed to parse dist-info: %+v", err) t.Fatalf("failed to parse dist-info: %+v", err)
} }

View File

@ -16,6 +16,6 @@ func (p PoetryMetadataPackage) Pkg() pkg.Package {
Name: p.Name, Name: p.Name,
Version: p.Version, Version: p.Version,
Language: pkg.Python, Language: pkg.Python,
Type: pkg.PoetryPkg, Type: pkg.PythonPkg,
} }
} }

View File

@ -25,7 +25,7 @@ func TestPackage_pURL(t *testing.T) {
pkg: Package{ pkg: Package{
Name: "name", Name: "name",
Version: "v0.1.0", Version: "v0.1.0",
Type: WheelPkg, Type: PythonPkg,
}, },
expected: "pkg:pypi/name@v0.1.0", expected: "pkg:pypi/name@v0.1.0",
}, },
@ -33,7 +33,7 @@ func TestPackage_pURL(t *testing.T) {
pkg: Package{ pkg: Package{
Name: "name", Name: "name",
Version: "v0.1.0", Version: "v0.1.0",
Type: EggPkg, Type: PythonPkg,
}, },
expected: "pkg:pypi/name@v0.1.0", expected: "pkg:pypi/name@v0.1.0",
}, },
@ -41,7 +41,7 @@ func TestPackage_pURL(t *testing.T) {
pkg: Package{ pkg: Package{
Name: "name", Name: "name",
Version: "v0.1.0", Version: "v0.1.0",
Type: PythonSetupPkg, Type: PythonPkg,
}, },
expected: "pkg:pypi/name@v0.1.0", expected: "pkg:pypi/name@v0.1.0",
}, },
@ -49,7 +49,7 @@ func TestPackage_pURL(t *testing.T) {
pkg: Package{ pkg: Package{
Name: "name", Name: "name",
Version: "v0.1.0", Version: "v0.1.0",
Type: PythonRequirementsPkg, Type: PythonPkg,
}, },
expected: "pkg:pypi/name@v0.1.0", expected: "pkg:pypi/name@v0.1.0",
}, },

View File

@ -6,32 +6,25 @@ import "github.com/package-url/packageurl-go"
type Type string type Type string
const ( const (
UnknownPkg Type = "UnknownPackage" UnknownPkg Type = "UnknownPackage"
ApkPkg Type = "apk" ApkPkg Type = "apk"
GemPkg Type = "gem" GemPkg Type = "gem"
DebPkg Type = "deb" DebPkg Type = "deb"
EggPkg Type = "egg" RpmPkg Type = "rpm"
RpmPkg Type = "rpm" NpmPkg Type = "npm"
WheelPkg Type = "wheel" PythonPkg Type = "python"
PoetryPkg Type = "poetry" JavaPkg Type = "java-archive"
NpmPkg Type = "npm" JenkinsPluginPkg Type = "jenkins-plugin"
PythonRequirementsPkg Type = "python-requirements" GoModulePkg Type = "go-module"
PythonSetupPkg Type = "python-setup"
JavaPkg Type = "java-archive"
JenkinsPluginPkg Type = "jenkins-plugin"
GoModulePkg Type = "go-module"
) )
var AllPkgs = []Type{ var AllPkgs = []Type{
ApkPkg, ApkPkg,
GemPkg, GemPkg,
DebPkg, DebPkg,
EggPkg,
RpmPkg, RpmPkg,
WheelPkg,
NpmPkg, NpmPkg,
PythonRequirementsPkg, PythonPkg,
PythonSetupPkg,
JavaPkg, JavaPkg,
JenkinsPluginPkg, JenkinsPluginPkg,
GoModulePkg, GoModulePkg,
@ -45,7 +38,7 @@ func (t Type) PackageURLType() string {
return packageurl.TypeGem return packageurl.TypeGem
case DebPkg: case DebPkg:
return "deb" return "deb"
case EggPkg, WheelPkg, PythonRequirementsPkg, PythonSetupPkg: case PythonPkg:
return packageurl.TypePyPi return packageurl.TypePyPi
case NpmPkg: case NpmPkg:
return packageurl.TypeNPM return packageurl.TypeNPM

View File

@ -26,6 +26,17 @@ var imageOnlyTestCases = []testCase{
"npm": "6.14.6", "npm": "6.14.6",
}, },
}, },
{
name: "find python egg & wheel packages",
pkgType: pkg.PythonPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"Pygments": "2.6.1",
"requests": "2.22.0",
"somerequests": "3.22.0",
"someotherpkg": "3.19.0",
},
},
} }
var dirOnlyTestCases = []testCase{ var dirOnlyTestCases = []testCase{
@ -96,6 +107,26 @@ var dirOnlyTestCases = []testCase{
"get-stdin": "8.0.0", "get-stdin": "8.0.0",
}, },
}, },
{
name: "find python requirements.txt & setup.py package references",
pkgType: pkg.PythonPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
// dir specific test cases
"flask": "4.0.0",
"python-dateutil": "2.8.1",
"python-swiftclient": "3.8.1",
"pytz": "2019.3",
"jsonschema": "2.6.0",
"passlib": "1.7.2",
"mypy": "v0.770",
// common to image and directory
"Pygments": "2.6.1",
"requests": "2.22.0",
"somerequests": "3.22.0",
"someotherpkg": "3.19.0",
},
},
} }
var commonTestCases = []testCase{ var commonTestCases = []testCase{
@ -131,46 +162,6 @@ var commonTestCases = []testCase{
"example-jenkins-plugin": "1.0-SNAPSHOT", "example-jenkins-plugin": "1.0-SNAPSHOT",
}, },
}, },
{
name: "find python wheel packages",
pkgType: pkg.WheelPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"Pygments": "2.6.1",
"requests": "2.10.0",
},
},
{
name: "find python egg packages",
pkgType: pkg.EggPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"requests": "2.22.0",
"otherpkg": "2.19.0",
},
},
{
name: "find python requirements.txt packages",
pkgType: pkg.PythonRequirementsPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"flask": "4.0.0",
"python-dateutil": "2.8.1",
"python-swiftclient": "3.8.1",
"pytz": "2019.3",
"jsonschema": "2.6.0",
"passlib": "1.7.2",
"pathlib": "1.0.1",
},
},
{
name: "find python setup.py packages",
pkgType: pkg.PythonSetupPkg,
pkgLanguage: pkg.Python,
pkgInfo: map[string]string{
"mypy": "v0.770",
},
},
{ {
name: "find apkdb packages", name: "find apkdb packages",

View File

@ -1,3 +1,2 @@
jsonschema==2.6.0 jsonschema==2.6.0
passlib==1.7.2 passlib==1.7.2
pathlib==1.0.1

View File

@ -1,6 +1,6 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: otherpkg Name: someotherpkg
Version: 2.19.0 Version: 3.19.0
Summary: Python HTTP for Humans. Summary: Python HTTP for Humans.
Home-page: http://python-requests.org Home-page: http://python-requests.org
Author: Kenneth Reitz Author: Kenneth Reitz

View File

@ -1,6 +1,6 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: requests Name: somerequests
Version: 2.10.0 Version: 3.22.0
Summary: stuff Summary: stuff
Home-page: stuff Home-page: stuff
Author: Georg Brandl Author: Georg Brandl