Skip to content

Commit

Permalink
port php cataloger to new generic cataloger pattern (#1315)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <[email protected]>

Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Nov 3, 2022
1 parent bc9740d commit 891f2c5
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 212 deletions.
20 changes: 7 additions & 13 deletions syft/pkg/cataloger/php/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
51 changes: 51 additions & 0 deletions syft/pkg/cataloger/php/package.go
Original file line number Diff line number Diff line change
@@ -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()
}
53 changes: 53 additions & 0 deletions syft/pkg/cataloger/php/package_test.go
Original file line number Diff line number Diff line change
@@ -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/[email protected]",
},
{
name: "name with slashes (invalid)",
metadata: pkg.PhpComposerJSONMetadata{
Name: "ven/name/component",
Version: "1.0.1",
},
expected: "pkg:composer/ven/[email protected]",
},
{
name: "unknown vendor",
metadata: pkg.PhpComposerJSONMetadata{
Name: "name",
Version: "1.0.1",
},
expected: "pkg:composer/[email protected]",
},
}

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))
}
})
}
}
23 changes: 9 additions & 14 deletions syft/pkg/cataloger/php/parse_composer_lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
30 changes: 12 additions & 18 deletions syft/pkg/cataloger/php/parse_composer_lock_test.go
Original file line number Diff line number Diff line change
@@ -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/[email protected]",
Locations: locations,
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Expand Down Expand Up @@ -52,6 +57,8 @@ func TestParseComposerFileLock(t *testing.T) {
{
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Locations: locations,
PURL: "pkg:composer/alcaeus/[email protected]",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Expand Down Expand Up @@ -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)
}
29 changes: 10 additions & 19 deletions syft/pkg/cataloger/php/parse_installed_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -36,34 +39,22 @@ 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 {
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)
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
}
Loading

0 comments on commit 891f2c5

Please sign in to comment.