diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 096d51d76..bac9fa5db 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -36,6 +36,7 @@ func ImageCatalogers() []Cataloger { return []Cataloger{ ruby.NewGemSpecCataloger(), python.NewPythonPackageCataloger(), + php.NewPHPComposerInstalledCataloger(), javascript.NewJavascriptPackageCataloger(), deb.NewDpkgdbCataloger(), rpmdb.NewRpmdbCataloger(), @@ -51,7 +52,7 @@ func DirectoryCatalogers() []Cataloger { ruby.NewGemFileLockCataloger(), python.NewPythonIndexCataloger(), python.NewPythonPackageCataloger(), - php.NewPHPIndexCataloger(), + php.NewPHPComposerLockCataloger(), javascript.NewJavascriptLockCataloger(), deb.NewDpkgdbCataloger(), rpmdb.NewRpmdbCataloger(), diff --git a/syft/pkg/cataloger/php/cataloger.go b/syft/pkg/cataloger/php/cataloger.go new file mode 100644 index 000000000..6528bac66 --- /dev/null +++ b/syft/pkg/cataloger/php/cataloger.go @@ -0,0 +1,26 @@ +/* +Package php provides a concrete Cataloger implementation for PHP ecosystem files. +*/ +package php + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/common" +) + +// 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") +} + +// 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") +} diff --git a/syft/pkg/cataloger/php/index_cataloger.go b/syft/pkg/cataloger/php/index_cataloger.go deleted file mode 100644 index e70374b7f..000000000 --- a/syft/pkg/cataloger/php/index_cataloger.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Package python provides a concrete Cataloger implementation for Python ecosystem files (egg, wheel, requirements.txt). -*/ -package php - -import ( - "github.com/anchore/syft/syft/pkg/cataloger/common" -) - -// NewPythonIndexCataloger returns a new cataloger for python packages referenced from poetry lock files, requirements.txt files, and setup.py files. -func NewPHPIndexCataloger() *common.GenericCataloger { - globParsers := map[string]common.ParserFn{ - "**/composer.lock": parseComposerLock, - } - - return common.NewGenericCataloger(nil, globParsers, "php-index-cataloger") -} diff --git a/syft/pkg/cataloger/php/parse_installed_json.go b/syft/pkg/cataloger/php/parse_installed_json.go new file mode 100644 index 000000000..63272d3f2 --- /dev/null +++ b/syft/pkg/cataloger/php/parse_installed_json.go @@ -0,0 +1,67 @@ +package php + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common" +) + +// Note: composer version 2 introduced a new structure for the installed.json file, so we support both +type installedJSONComposerV2 struct { + Packages []Dependency `json:"packages"` +} + +func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error { + type compv2 struct { + Packages []Dependency `json:"packages"` + } + compv2er := new(compv2) + err := json.Unmarshal(data, &compv2er) + if err != nil { + // If we had an err or, we may be dealing with a composer v.1 installed.json + // which should be all arrays + var packages []Dependency + err := json.Unmarshal(data, &packages) + if err != nil { + return err + } + w.Packages = packages + return nil + } + w.Packages = compv2er.Packages + 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) + dec := json.NewDecoder(reader) + + for { + var lock installedJSONComposerV2 + 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) + } + 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, + }) + } + } + + return packages, nil, nil +} diff --git a/syft/pkg/cataloger/php/parse_installed_json_test.go b/syft/pkg/cataloger/php/parse_installed_json_test.go new file mode 100644 index 000000000..bc9221548 --- /dev/null +++ b/syft/pkg/cataloger/php/parse_installed_json_test.go @@ -0,0 +1,73 @@ +package php + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/go-test/deep" +) + +func TestParseInstalledJsonComposerV1(t *testing.T) { + expected := []pkg.Package{ + { + Name: "asm89/stack-cors", + Version: "1.3.0", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + }, + { + Name: "behat/mink", + Version: "v1.8.1", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + }, + } + fixture, err := os.Open("test-fixtures/vendor/composer_1/installed.json") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + // 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(expected, actual) + if differences != nil { + t.Errorf("returned package list differed from expectation: %+v", differences) + } + +} + +func TestParseInstalledJsonComposerV2(t *testing.T) { + expected := []pkg.Package{ + { + Name: "asm89/stack-cors", + Version: "1.3.0", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + }, + { + Name: "behat/mink", + Version: "v1.8.1", + Language: pkg.PHP, + Type: pkg.PhpComposerPkg, + }, + } + fixture, err := os.Open("test-fixtures/vendor/composer_2/installed.json") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + // 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(expected, actual) + if differences != nil { + t.Errorf("returned package list differed from expectation: %+v", differences) + } + +} diff --git a/syft/pkg/cataloger/php/test-fixtures/vendor/composer_1/installed.json b/syft/pkg/cataloger/php/test-fixtures/vendor/composer_1/installed.json new file mode 100644 index 000000000..2f7274ada --- /dev/null +++ b/syft/pkg/cataloger/php/test-fixtures/vendor/composer_1/installed.json @@ -0,0 +1,127 @@ +[ + { + "name": "asm89/stack-cors", + "version": "1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/asm89/stack-cors.git", + "reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08", + "reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08", + "shasum": "" + }, + "require": { + "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" + }, + "require-dev": { + "phpunit/phpunit": "^5.0 || ^4.8.10", + "squizlabs/php_codesniffer": "^2.3" + }, + "time": "2019-12-24T22:41:47+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Asm89\\Stack\\": "src/Asm89/Stack/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "description": "Cross-origin resource sharing library and stack middleware", + "homepage": "https://github.com/asm89/stack-cors", + "keywords": [ + "cors", + "stack" + ], + "support": { + "issues": "https://github.com/asm89/stack-cors/issues", + "source": "https://github.com/asm89/stack-cors/tree/1.3.0" + } + }, + { + "name": "behat/mink", + "version": "v1.8.1", + "version_normalized": "1.8.1.0", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "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": { + "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", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "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": [ + "browser", + "testing", + "web" + ], + "support": { + "issues": "https://github.com/minkphp/Mink/issues", + "source": "https://github.com/minkphp/Mink/tree/v1.8.1" + } + } +] diff --git a/syft/pkg/cataloger/php/test-fixtures/vendor/composer_2/installed.json b/syft/pkg/cataloger/php/test-fixtures/vendor/composer_2/installed.json new file mode 100644 index 000000000..eb903f214 --- /dev/null +++ b/syft/pkg/cataloger/php/test-fixtures/vendor/composer_2/installed.json @@ -0,0 +1,135 @@ +{ + "packages": [ + { + "name": "asm89/stack-cors", + "version": "1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/asm89/stack-cors.git", + "reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08", + "reference": "b9c31def6a83f84b4d4a40d35996d375755f0e08", + "shasum": "" + }, + "require": { + "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" + }, + "require-dev": { + "phpunit/phpunit": "^5.0 || ^4.8.10", + "squizlabs/php_codesniffer": "^2.3" + }, + "time": "2019-12-24T22:41:47+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Asm89\\Stack\\": "src/Asm89/Stack/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexander", + "email": "iam.asm89@gmail.com" + } + ], + "description": "Cross-origin resource sharing library and stack middleware", + "homepage": "https://github.com/asm89/stack-cors", + "keywords": [ + "cors", + "stack" + ], + "support": { + "issues": "https://github.com/asm89/stack-cors/issues", + "source": "https://github.com/asm89/stack-cors/tree/1.3.0" + }, + "install-path": "../asm89/stack-cors" + }, + { + "name": "behat/mink", + "version": "v1.8.1", + "version_normalized": "1.8.1.0", + "source": { + "type": "git", + "url": "https://github.com/minkphp/Mink.git", + "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + "reference": "07c6a9fe3fa98c2de074b25d9ed26c22904e3887", + "shasum": "" + }, + "require": { + "php": ">=5.3.1", + "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "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": { + "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", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Behat\\Mink\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "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": [ + "browser", + "testing", + "web" + ], + "support": { + "issues": "https://github.com/minkphp/Mink/issues", + "source": "https://github.com/minkphp/Mink/tree/v1.8.1" + }, + "install-path": "../behat/mink" + } + ], + "dev": true, + "dev-package-names": [ + "behat/mink" + ] +} diff --git a/test/cli/packages_cmd_test.go b/test/cli/packages_cmd_test.go index f77d68fb1..afa3c0ada 100644 --- a/test/cli/packages_cmd_test.go +++ b/test/cli/packages_cmd_test.go @@ -62,7 +62,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "squashed-scope-flag", args: []string{"packages", "-o", "json", "-s", "squashed", request}, assertions: []traitAssertion{ - assertPackageCount(17), + assertPackageCount(20), assertSuccessfulReturnCode, }, }, @@ -70,7 +70,7 @@ func TestPackagesCmdFlags(t *testing.T) { name: "all-layers-scope-flag", args: []string{"packages", "-o", "json", "-s", "all-layers", request}, assertions: []traitAssertion{ - assertPackageCount(19), + assertPackageCount(22), assertSuccessfulReturnCode, }, }, @@ -81,7 +81,7 @@ func TestPackagesCmdFlags(t *testing.T) { "SYFT_PACKAGE_CATALOGER_SCOPE": "all-layers", }, assertions: []traitAssertion{ - assertPackageCount(19), + assertPackageCount(22), assertSuccessfulReturnCode, }, }, diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index 18f15fac2..82aa81a26 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -41,6 +41,16 @@ var imageOnlyTestCases = []testCase{ "someotherpkg": "3.19.0", }, }, + { + name: "find PHP composer installed.json packages", + pkgType: pkg.PhpComposerPkg, + pkgLanguage: pkg.PHP, + pkgInfo: map[string]string{ + "nikic/fast-route": "v1.3.0", + "psr/container": "2.0.2", + "psr/http-factory": "1.0.1", + }, + }, { // When the image is build lib overwrites pkgs/lib causing there to only be two packages name: "find apkdb packages", diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index a4a092ac1..1fe38baee 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -60,7 +60,6 @@ func TestPkgCoverageImage(t *testing.T) { // for image scans we should not expect to see any of the following package types definedLanguages.Remove(pkg.Go.String()) definedLanguages.Remove(pkg.Rust.String()) - definedLanguages.Remove(pkg.PHP.String()) observedPkgs := internal.NewStringSet() definedPkgs := internal.NewStringSet() @@ -72,7 +71,6 @@ func TestPkgCoverageImage(t *testing.T) { definedPkgs.Remove(string(pkg.KbPkg)) definedPkgs.Remove(string(pkg.GoModulePkg)) definedPkgs.Remove(string(pkg.RustPkg)) - definedPkgs.Remove(string(pkg.PhpComposerPkg)) var cases []testCase cases = append(cases, commonTestCases...) diff --git a/test/integration/test-fixtures/image-pkg-coverage/pkgs/php/vendor/composer/installed.json b/test/integration/test-fixtures/image-pkg-coverage/pkgs/php/vendor/composer/installed.json new file mode 100644 index 000000000..39af3b5de --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/pkgs/php/vendor/composer/installed.json @@ -0,0 +1,173 @@ +{ + "packages": [ + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "version_normalized": "1.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "time": "2018-02-13T20:26:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "install-path": "../nikic/fast-route" + }, + { + "name": "psr/container", + "version": "2.0.2", + "version_normalized": "2.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "time": "2021-11-05T16:47:00+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "install-path": "../psr/container" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "time": "2019-04-30T12:38:16+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "install-path": "../psr/http-factory" + } + ], + "dev": true, + "dev-package-names": [] +}