From cba5b5723b113e86d67a6a199c9524fa3944bf1f Mon Sep 17 00:00:00 2001 From: Nikita <33390074+Zilborg@users.noreply.github.com> Date: Thu, 19 Aug 2021 17:32:10 +0300 Subject: [PATCH] Added parser for Pipfile.lock to cataloger (#473) * Added parser for Pipfile.lock to cataloger Signed-off-by: Nikita <33390074+Zilborg@users.noreply.github.com> * make lint-fix Signed-off-by: Nikita <33390074+Zilborg@users.noreply.github.com> * Update syft/pkg/cataloger/python/parse_pipfile_lock.go Co-authored-by: Alex Goodman Signed-off-by: Nikita <33390074+Zilborg@users.noreply.github.com> * fix _version Signed-off-by: Nikita <33390074+Zilborg@users.noreply.github.com> * swap method for trimming "==" prefix from pipfile pkg versions Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/python/index_cataloger.go | 1 + .../cataloger/python/parse_pipfile_lock.go | 63 +++++++++++++++++ .../python/parse_pipfile_lock_test.go | 49 +++++++++++++ .../test-fixtures/pipfile-lock/Pipfile.lock | 69 +++++++++++++++++++ 4 files changed, 182 insertions(+) create mode 100644 syft/pkg/cataloger/python/parse_pipfile_lock.go create mode 100644 syft/pkg/cataloger/python/parse_pipfile_lock_test.go create mode 100644 syft/pkg/cataloger/python/test-fixtures/pipfile-lock/Pipfile.lock diff --git a/syft/pkg/cataloger/python/index_cataloger.go b/syft/pkg/cataloger/python/index_cataloger.go index 4c821a289..4ebfe408e 100644 --- a/syft/pkg/cataloger/python/index_cataloger.go +++ b/syft/pkg/cataloger/python/index_cataloger.go @@ -12,6 +12,7 @@ func NewPythonIndexCataloger() *common.GenericCataloger { globParsers := map[string]common.ParserFn{ "**/*requirements*.txt": parseRequirementsTxt, "**/poetry.lock": parsePoetryLock, + "**/Pipfile.lock": parsePipfileLock, "**/setup.py": parseSetup, } diff --git a/syft/pkg/cataloger/python/parse_pipfile_lock.go b/syft/pkg/cataloger/python/parse_pipfile_lock.go new file mode 100644 index 000000000..3a03218a1 --- /dev/null +++ b/syft/pkg/cataloger/python/parse_pipfile_lock.go @@ -0,0 +1,63 @@ +package python + +import ( + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common" +) + +type PipfileLock struct { + Meta struct { + Hash struct { + Sha256 string `json:"sha256"` + } `json:"hash"` + PipfileSpec int `json:"pipfile-spec"` + Requires struct { + PythonVersion string `json:"python_version"` + } `json:"requires"` + Sources []struct { + Name string `json:"name"` + URL string `json:"url"` + VerifySsl bool `json:"verify_ssl"` + } `json:"sources"` + } `json:"_meta"` + Default map[string]Dependency `json:"default"` + Develop map[string]Dependency `json:"develop"` +} + +type Dependency struct { + Version string `json:"version"` +} + +// integrity check +var _ common.ParserFn = parsePipfileLock + +// parsePipfileLock is a parser function for Pipfile.lock contents, returning "Default" python packages discovered. +func parsePipfileLock(_ string, reader io.Reader) ([]pkg.Package, error) { + packages := make([]pkg.Package, 0) + dec := json.NewDecoder(reader) + + for { + var lock PipfileLock + if err := dec.Decode(&lock); err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("failed to parse Pipfile.lock file: %w", err) + } + for name, pkgMeta := range lock.Default { + version := strings.TrimPrefix(pkgMeta.Version, "==") + packages = append(packages, pkg.Package{ + Name: name, + Version: version, + Language: pkg.Python, + Type: pkg.PythonPkg, + }) + } + } + + return packages, nil +} diff --git a/syft/pkg/cataloger/python/parse_pipfile_lock_test.go b/syft/pkg/cataloger/python/parse_pipfile_lock_test.go new file mode 100644 index 000000000..f65864e0e --- /dev/null +++ b/syft/pkg/cataloger/python/parse_pipfile_lock_test.go @@ -0,0 +1,49 @@ +package python + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" +) + +func TestParsePipFileLock(t *testing.T) { + expected := map[string]pkg.Package{ + "aio-pika": { + Name: "aio-pika", + Version: "6.8.0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + "aiodns": { + Name: "aiodns", + Version: "2.0.0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + "aiohttp": { + Name: "aiohttp", + Version: "3.7.4.post0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + "aiohttp-jinja2": { + Name: "aiohttp-jinja2", + Version: "1.4.2", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + } + fixture, err := os.Open("test-fixtures/pipfile-lock/Pipfile.lock") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + actual, err := parsePipfileLock(fixture.Name(), fixture) + if err != nil { + t.Fatalf("failed to parse requirements: %+v", err) + } + + assertPackagesEqual(t, actual, expected) + +} diff --git a/syft/pkg/cataloger/python/test-fixtures/pipfile-lock/Pipfile.lock b/syft/pkg/cataloger/python/test-fixtures/pipfile-lock/Pipfile.lock new file mode 100644 index 000000000..78a6f1038 --- /dev/null +++ b/syft/pkg/cataloger/python/test-fixtures/pipfile-lock/Pipfile.lock @@ -0,0 +1,69 @@ +{ + "_meta": { + "hash": { + "sha256": "a6b2dfd5367688bec81240eb04e7bde7f92b35491be5934fcb4e2e6ca9d275c0" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aio-pika": { + "hashes": [ + "sha256:1d4305a5f78af3857310b4fe48348cdcf6c097e0e275ea88c2cd08570531a369", + "sha256:e69afef8695f47c5d107bbdba21bdb845d5c249acb3be53ef5c2d497b02657c0" + ], + "index": "pypi", + "version": "==6.8.0" + }, + "aiodns": { + "hashes": [ + "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d", + "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "aiohttp": { + "hashes": [ + "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", + "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe" + ], + "index": "pypi", + "version": "==3.7.4.post0" + }, + "aiohttp-jinja2": { + "hashes": [ + "sha256:860da7582efa866744bad5883947557d0f82e457d69903ea65d666b66f8a69ca", + "sha256:9c22a0e48e3b277fc145c67dd8c3b8f609dab36bce9eb337f70dfe716663c9a0" + ], + "index": "pypi", + "version": "==1.4.2" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:6b0ed1af831570e500e2437625979eaa3b36011f66ddfc4ce930128610258ca9", + "sha256:cd80bf957c49765dce6d92c43163ff9d2abc43132ce64d4b1b47717c6d2522df" + ], + "version": "==2.5.2" + }, + "autopep8": { + "hashes": [ + "sha256:5454e6e9a3d02aae38f866eec0d9a7de4ab9f93c10a273fb0340f3d6d09f7514", + "sha256:f01b06a6808bc31698db907761e5890eb2295e287af53f6693b39ce55454034a" + ], + "index": "pypi", + "version": "==1.5.6" + } + } +} \ No newline at end of file