port php cataloger to new generic cataloger pattern (#1315)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2022-11-03 13:00:44 -04:00 committed by GitHub
parent bc9740d50a
commit 891f2c576b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 216 deletions

View File

@ -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")
}

View File

@ -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()
}

View File

@ -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))
}
})
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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) {