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,18 +1,26 @@
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{
func TestParseInstalledJsonComposerV1(t *testing.T) {
fixtures := []string{
"test-fixtures/vendor/composer_1/installed.json",
"test-fixtures/vendor/composer_2/installed.json",
}
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,
@ -62,6 +70,7 @@ var expectedInstalledJsonPackages = []*pkg.Package{
{
Name: "behat/mink",
Version: "v1.8.1",
PURL: "pkg:composer/behat/mink@v1.8.1",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
@ -119,38 +128,14 @@ var expectedInstalledJsonPackages = []*pkg.Package{
},
}
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)
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
}
// 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)
}
}
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)
}
// 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)
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) {