diff --git a/syft/pkg/cataloger/php/cataloger.go b/syft/pkg/cataloger/php/cataloger.go index 6528bac66..35ac46bb3 100644 --- a/syft/pkg/cataloger/php/cataloger.go +++ b/syft/pkg/cataloger/php/cataloger.go @@ -4,23 +4,17 @@ Package php provides a concrete Cataloger implementation for PHP ecosystem files package php import ( - "github.com/anchore/syft/syft/pkg/cataloger/common" + "github.com/anchore/syft/syft/pkg/cataloger/generic" ) // NewPHPComposerInstalledCataloger returns a new cataloger for PHP installed.json files. -func NewPHPComposerInstalledCataloger() *common.GenericCataloger { - globParsers := map[string]common.ParserFn{ - "**/installed.json": parseInstalledJSON, - } - - return common.NewGenericCataloger(nil, globParsers, "php-composer-installed-cataloger") +func NewPHPComposerInstalledCataloger() *generic.Cataloger { + return generic.NewCataloger("php-composer-installed-cataloger"). + WithParserByGlobs(parseInstalledJSON, "**/installed.json") } // NewPHPComposerLockCataloger returns a new cataloger for PHP composer.lock files. -func NewPHPComposerLockCataloger() *common.GenericCataloger { - globParsers := map[string]common.ParserFn{ - "**/composer.lock": parseComposerLock, - } - - return common.NewGenericCataloger(nil, globParsers, "php-composer-lock-cataloger") +func NewPHPComposerLockCataloger() *generic.Cataloger { + return generic.NewCataloger("php-composer-lock-cataloger"). + WithParserByGlobs(parseComposerLock, "**/composer.lock") } diff --git a/syft/pkg/cataloger/php/package.go b/syft/pkg/cataloger/php/package.go new file mode 100644 index 000000000..ee3be7019 --- /dev/null +++ b/syft/pkg/cataloger/php/package.go @@ -0,0 +1,51 @@ +package php + +import ( + "strings" + + "github.com/anchore/packageurl-go" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func newComposerLockPackage(m pkg.PhpComposerJSONMetadata, location ...source.Location) pkg.Package { + p := pkg.Package{ + Name: m.Name, + Version: m.Version, + Locations: source.NewLocationSet(location...), + PURL: packageURL(m), + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + MetadataType: pkg.PhpComposerJSONMetadataType, + Metadata: m, + } + + p.SetID() + return p +} + +func packageURL(m pkg.PhpComposerJSONMetadata) string { + var name, vendor string + fields := strings.Split(m.Name, "/") + switch len(fields) { + case 0: + return "" + case 1: + name = m.Name + case 2: + vendor = fields[0] + name = fields[1] + default: + vendor = fields[0] + name = strings.Join(fields[1:], "-") + } + + pURL := packageurl.NewPackageURL( + packageurl.TypeComposer, + vendor, + name, + m.Version, + nil, + "") + return pURL.ToString() +} diff --git a/syft/pkg/cataloger/php/package_test.go b/syft/pkg/cataloger/php/package_test.go new file mode 100644 index 000000000..9a32df1ff --- /dev/null +++ b/syft/pkg/cataloger/php/package_test.go @@ -0,0 +1,53 @@ +package php + +import ( + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" + + "github.com/anchore/syft/syft/pkg" +) + +func Test_packageURL(t *testing.T) { + tests := []struct { + name string + metadata pkg.PhpComposerJSONMetadata + expected string + }{ + { + name: "with extractable vendor", + metadata: pkg.PhpComposerJSONMetadata{ + Name: "ven/name", + Version: "1.0.1", + }, + expected: "pkg:composer/ven/name@1.0.1", + }, + { + name: "name with slashes (invalid)", + metadata: pkg.PhpComposerJSONMetadata{ + Name: "ven/name/component", + Version: "1.0.1", + }, + expected: "pkg:composer/ven/name-component@1.0.1", + }, + { + name: "unknown vendor", + metadata: pkg.PhpComposerJSONMetadata{ + Name: "name", + Version: "1.0.1", + }, + expected: "pkg:composer/name@1.0.1", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := packageURL(test.metadata) + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(test.expected, actual, true) + t.Errorf("diff: %s", dmp.DiffPrettyText(diffs)) + } + }) + } +} diff --git a/syft/pkg/cataloger/php/parse_composer_lock.go b/syft/pkg/cataloger/php/parse_composer_lock.go index 1ee120e60..999c6b99b 100644 --- a/syft/pkg/cataloger/php/parse_composer_lock.go +++ b/syft/pkg/cataloger/php/parse_composer_lock.go @@ -7,16 +7,20 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" ) +var _ generic.Parser = parseComposerLock + type composerLock struct { Packages []pkg.PhpComposerJSONMetadata `json:"packages"` PackageDev []pkg.PhpComposerJSONMetadata `json:"packages-dev"` } // parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered. -func parseComposerLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { - packages := make([]*pkg.Package, 0) +func parseComposerLock(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + pkgs := make([]pkg.Package, 0) dec := json.NewDecoder(reader) for { @@ -26,19 +30,10 @@ func parseComposerLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.R } else if err != nil { return nil, nil, fmt.Errorf("failed to parse composer.lock file: %w", err) } - for _, pkgMeta := range lock.Packages { - version := pkgMeta.Version - name := pkgMeta.Name - packages = append(packages, &pkg.Package{ - Name: name, - Version: version, - Language: pkg.PHP, - Type: pkg.PhpComposerPkg, - MetadataType: pkg.PhpComposerJSONMetadataType, - Metadata: pkgMeta, - }) + for _, m := range lock.Packages { + pkgs = append(pkgs, newComposerLockPackage(m, reader.Location)) } } - return packages, nil, nil + return pkgs, nil, nil } diff --git a/syft/pkg/cataloger/php/parse_composer_lock_test.go b/syft/pkg/cataloger/php/parse_composer_lock_test.go index b5522d15d..f47049707 100644 --- a/syft/pkg/cataloger/php/parse_composer_lock_test.go +++ b/syft/pkg/cataloger/php/parse_composer_lock_test.go @@ -1,19 +1,24 @@ package php import ( - "os" "testing" - "github.com/go-test/deep" - + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" ) func TestParseComposerFileLock(t *testing.T) { - expected := []*pkg.Package{ + var expectedRelationships []artifact.Relationship + fixture := "test-fixtures/composer.lock" + locations := source.NewLocationSet(source.NewLocation(fixture)) + expectedPkgs := []pkg.Package{ { Name: "adoy/fastcgi-client", Version: "1.0.2", + PURL: "pkg:composer/adoy/fastcgi-client@1.0.2", + Locations: locations, Language: pkg.PHP, Type: pkg.PhpComposerPkg, MetadataType: pkg.PhpComposerJSONMetadataType, @@ -52,6 +57,8 @@ func TestParseComposerFileLock(t *testing.T) { { Name: "alcaeus/mongo-php-adapter", Version: "1.1.11", + Locations: locations, + PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11", Language: pkg.PHP, Type: pkg.PhpComposerPkg, MetadataType: pkg.PhpComposerJSONMetadataType, @@ -106,18 +113,5 @@ func TestParseComposerFileLock(t *testing.T) { }, }, } - fixture, err := os.Open("test-fixtures/composer.lock") - if err != nil { - t.Fatalf("failed to open fixture: %+v", err) - } - - // TODO: no relationships are under test yet - actual, _, err := parseComposerLock(fixture.Name(), fixture) - if err != nil { - t.Fatalf("failed to parse requirements: %+v", err) - } - - for _, d := range deep.Equal(expected, actual) { - t.Errorf("diff: %+v", d) - } + pkgtest.TestFileParser(t, fixture, parseComposerLock, expectedPkgs, expectedRelationships) } diff --git a/syft/pkg/cataloger/php/parse_installed_json.go b/syft/pkg/cataloger/php/parse_installed_json.go index 3afa1f2fe..61a86d635 100644 --- a/syft/pkg/cataloger/php/parse_installed_json.go +++ b/syft/pkg/cataloger/php/parse_installed_json.go @@ -7,9 +7,12 @@ import ( "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/pkg/cataloger/common" + "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/source" ) +var _ generic.Parser = parseComposerLock + // Note: composer version 2 introduced a new structure for the installed.json file, so we support both type installedJSONComposerV2 struct { Packages []pkg.PhpComposerJSONMetadata `json:"packages"` @@ -36,12 +39,9 @@ func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error { return nil } -// integrity check -var _ common.ParserFn = parseComposerLock - -// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered. -func parseInstalledJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) { - packages := make([]*pkg.Package, 0) +// parseInstalledJSON is a parser function for Composer.lock contents, returning "Default" php packages discovered. +func parseInstalledJSON(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + var pkgs []pkg.Package dec := json.NewDecoder(reader) for { @@ -49,21 +49,12 @@ func parseInstalledJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact. if err := dec.Decode(&lock); err == io.EOF { break } else if err != nil { - return nil, nil, fmt.Errorf("failed to parse composer.lock file: %w", err) + return nil, nil, fmt.Errorf("failed to parse installed.json file: %w", err) } for _, pkgMeta := range lock.Packages { - version := pkgMeta.Version - name := pkgMeta.Name - packages = append(packages, &pkg.Package{ - Name: name, - Version: version, - Language: pkg.PHP, - Type: pkg.PhpComposerPkg, - MetadataType: pkg.PhpComposerJSONMetadataType, - Metadata: pkgMeta, - }) + pkgs = append(pkgs, newComposerLockPackage(pkgMeta, reader.Location)) } } - return packages, nil, nil + return pkgs, nil, nil } diff --git a/syft/pkg/cataloger/php/parse_installed_json_test.go b/syft/pkg/cataloger/php/parse_installed_json_test.go index 45c40d6b9..b2788f506 100644 --- a/syft/pkg/cataloger/php/parse_installed_json_test.go +++ b/syft/pkg/cataloger/php/parse_installed_json_test.go @@ -1,156 +1,141 @@ package php import ( - "os" "testing" - "github.com/go-test/deep" - + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" ) -var expectedInstalledJsonPackages = []*pkg.Package{ - { - Name: "asm89/stack-cors", - Version: "1.3.0", - Language: pkg.PHP, - Type: pkg.PhpComposerPkg, - MetadataType: pkg.PhpComposerJSONMetadataType, - Metadata: pkg.PhpComposerJSONMetadata{ - Name: "asm89/stack-cors", - Version: "1.3.0", - Source: pkg.PhpComposerExternalReference{ - Type: "git", - URL: "https://github.com/asm89/stack-cors.git", - Reference: "b9c31def6a83f84b4d4a40d35996d375755f0e08", - }, - Dist: pkg.PhpComposerExternalReference{ - Type: "zip", - URL: "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08", - Reference: "b9c31def6a83f84b4d4a40d35996d375755f0e08", - }, - Require: map[string]string{ - "php": ">=5.5.9", - "symfony/http-foundation": "~2.7|~3.0|~4.0|~5.0", - "symfony/http-kernel": "~2.7|~3.0|~4.0|~5.0", - }, - RequireDev: map[string]string{ - "phpunit/phpunit": "^5.0 || ^4.8.10", - "squizlabs/php_codesniffer": "^2.3", - }, - Time: "2019-12-24T22:41:47+00:00", - Type: "library", - NotificationURL: "https://packagist.org/downloads/", - License: []string{ - "MIT", - }, - Authors: []pkg.PhpComposerAuthors{ - { - Name: "Alexander", - Email: "iam.asm89@gmail.com", - }, - }, - - Description: "Cross-origin resource sharing library and stack middleware", - Homepage: "https://github.com/asm89/stack-cors", - Keywords: []string{ - "cors", - "stack", - }, - }, - }, - { - Name: "behat/mink", - Version: "v1.8.1", - Language: pkg.PHP, - Type: pkg.PhpComposerPkg, - MetadataType: pkg.PhpComposerJSONMetadataType, - Metadata: pkg.PhpComposerJSONMetadata{ - Name: "behat/mink", - Version: "v1.8.1", - Source: pkg.PhpComposerExternalReference{ - Type: "git", - URL: "https://github.com/minkphp/Mink.git", - Reference: "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", - }, - Dist: pkg.PhpComposerExternalReference{ - Type: "zip", - URL: "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887", - Reference: "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", - }, - Require: map[string]string{ - "php": ">=5.3.1", - "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0", - }, - RequireDev: map[string]string{ - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20", - "symfony/debug": "^2.7|^3.0|^4.0", - "symfony/phpunit-bridge": "^3.4.38 || ^5.0.5", - }, - Suggest: map[string]string{ - "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", - "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", - "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", - "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)", - "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)", - }, - Time: "2020-03-11T15:45:53+00:00", - Type: "library", - NotificationURL: "https://packagist.org/downloads/", - License: []string{ - "MIT", - }, - Authors: []pkg.PhpComposerAuthors{ - { - Name: "Konstantin Kudryashov", - Email: "ever.zet@gmail.com", - Homepage: "http://everzet.com", - }, - }, - - Description: "Browser controller/emulator abstraction for PHP", - Homepage: "http://mink.behat.org/", - Keywords: []string{ - "browser", - "testing", - "web", - }, - }, - }, -} - func TestParseInstalledJsonComposerV1(t *testing.T) { - - fixture, err := os.Open("test-fixtures/vendor/composer_1/installed.json") - if err != nil { - t.Fatalf("failed to open fixture: %+v", err) + fixtures := []string{ + "test-fixtures/vendor/composer_1/installed.json", + "test-fixtures/vendor/composer_2/installed.json", } - // TODO: no relationships are under test yet - actual, _, err := parseInstalledJSON(fixture.Name(), fixture) - if err != nil { - t.Fatalf("failed to parse requirements: %+v", err) - } - differences := deep.Equal(expectedInstalledJsonPackages, actual) - if differences != nil { - t.Errorf("returned package list differed from expectation: %+v", differences) - } -} + var expectedRelationships []artifact.Relationship + var expectedPkgs = []pkg.Package{ + { + Name: "asm89/stack-cors", + Version: "1.3.0", + PURL: "pkg:composer/asm89/stack-cors@1.3.0", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + MetadataType: pkg.PhpComposerJSONMetadataType, + Metadata: pkg.PhpComposerJSONMetadata{ + Name: "asm89/stack-cors", + Version: "1.3.0", + Source: pkg.PhpComposerExternalReference{ + Type: "git", + URL: "https://github.com/asm89/stack-cors.git", + Reference: "b9c31def6a83f84b4d4a40d35996d375755f0e08", + }, + Dist: pkg.PhpComposerExternalReference{ + Type: "zip", + URL: "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08", + Reference: "b9c31def6a83f84b4d4a40d35996d375755f0e08", + }, + Require: map[string]string{ + "php": ">=5.5.9", + "symfony/http-foundation": "~2.7|~3.0|~4.0|~5.0", + "symfony/http-kernel": "~2.7|~3.0|~4.0|~5.0", + }, + RequireDev: map[string]string{ + "phpunit/phpunit": "^5.0 || ^4.8.10", + "squizlabs/php_codesniffer": "^2.3", + }, + Time: "2019-12-24T22:41:47+00:00", + Type: "library", + NotificationURL: "https://packagist.org/downloads/", + License: []string{ + "MIT", + }, + Authors: []pkg.PhpComposerAuthors{ + { + Name: "Alexander", + Email: "iam.asm89@gmail.com", + }, + }, -func TestParseInstalledJsonComposerV2(t *testing.T) { - fixture, err := os.Open("test-fixtures/vendor/composer_2/installed.json") - if err != nil { - t.Fatalf("failed to open fixture: %+v", err) + Description: "Cross-origin resource sharing library and stack middleware", + Homepage: "https://github.com/asm89/stack-cors", + Keywords: []string{ + "cors", + "stack", + }, + }, + }, + { + Name: "behat/mink", + Version: "v1.8.1", + PURL: "pkg:composer/behat/mink@v1.8.1", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + MetadataType: pkg.PhpComposerJSONMetadataType, + Metadata: pkg.PhpComposerJSONMetadata{ + Name: "behat/mink", + Version: "v1.8.1", + Source: pkg.PhpComposerExternalReference{ + Type: "git", + URL: "https://github.com/minkphp/Mink.git", + Reference: "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + }, + Dist: pkg.PhpComposerExternalReference{ + Type: "zip", + URL: "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + Reference: "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + }, + Require: map[string]string{ + "php": ">=5.3.1", + "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0", + }, + RequireDev: map[string]string{ + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20", + "symfony/debug": "^2.7|^3.0|^4.0", + "symfony/phpunit-bridge": "^3.4.38 || ^5.0.5", + }, + Suggest: map[string]string{ + "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)", + "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation", + "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)", + "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)", + "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)", + }, + Time: "2020-03-11T15:45:53+00:00", + Type: "library", + NotificationURL: "https://packagist.org/downloads/", + License: []string{ + "MIT", + }, + Authors: []pkg.PhpComposerAuthors{ + { + Name: "Konstantin Kudryashov", + Email: "ever.zet@gmail.com", + Homepage: "http://everzet.com", + }, + }, + + Description: "Browser controller/emulator abstraction for PHP", + Homepage: "http://mink.behat.org/", + Keywords: []string{ + "browser", + "testing", + "web", + }, + }, + }, } - // TODO: no relationships are under test yet - actual, _, err := parseInstalledJSON(fixture.Name(), fixture) - if err != nil { - t.Fatalf("failed to parse requirements: %+v", err) - } - differences := deep.Equal(expectedInstalledJsonPackages, actual) - if differences != nil { - t.Errorf("returned package list differed from expectation: %+v", differences) + for _, fixture := range fixtures { + t.Run(fixture, func(t *testing.T) { + locations := source.NewLocationSet(source.NewLocation(fixture)) + for i := range expectedPkgs { + expectedPkgs[i].Locations = locations + } + pkgtest.TestFileParser(t, fixture, parseInstalledJSON, expectedPkgs, expectedRelationships) + }) } } diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index f3b98e854..2b66bc3da 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -77,19 +77,6 @@ func TestPackageURL(t *testing.T) { }, expected: "pkg:cargo/name@v0.1.0", }, - { - name: "php-composer", - pkg: Package{ - Name: "bad-name", - Version: "bad-v0.1.0", - Type: PhpComposerPkg, - Metadata: PhpComposerJSONMetadata{ - Name: "vendor/name", - Version: "2.0.1", - }, - }, - expected: "pkg:composer/vendor/name@2.0.1", - }, { name: "java", pkg: Package{ @@ -152,6 +139,7 @@ func TestPackageURL(t *testing.T) { expectedTypes.Remove(string(GoModulePkg)) expectedTypes.Remove(string(HackagePkg)) expectedTypes.Remove(string(BinaryPkg)) + expectedTypes.Remove(string(PhpComposerPkg)) for _, test := range tests { t.Run(test.name, func(t *testing.T) {