mirror of
https://github.com/anchore/syft.git
synced 2025-11-18 08:53:15 +01:00
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:
parent
bc9740d50a
commit
891f2c576b
@ -4,23 +4,17 @@ Package php provides a concrete Cataloger implementation for PHP ecosystem files
|
|||||||
package php
|
package php
|
||||||
|
|
||||||
import (
|
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.
|
// NewPHPComposerInstalledCataloger returns a new cataloger for PHP installed.json files.
|
||||||
func NewPHPComposerInstalledCataloger() *common.GenericCataloger {
|
func NewPHPComposerInstalledCataloger() *generic.Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
return generic.NewCataloger("php-composer-installed-cataloger").
|
||||||
"**/installed.json": parseInstalledJSON,
|
WithParserByGlobs(parseInstalledJSON, "**/installed.json")
|
||||||
}
|
|
||||||
|
|
||||||
return common.NewGenericCataloger(nil, globParsers, "php-composer-installed-cataloger")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPHPComposerLockCataloger returns a new cataloger for PHP composer.lock files.
|
// NewPHPComposerLockCataloger returns a new cataloger for PHP composer.lock files.
|
||||||
func NewPHPComposerLockCataloger() *common.GenericCataloger {
|
func NewPHPComposerLockCataloger() *generic.Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
return generic.NewCataloger("php-composer-lock-cataloger").
|
||||||
"**/composer.lock": parseComposerLock,
|
WithParserByGlobs(parseComposerLock, "**/composer.lock")
|
||||||
}
|
|
||||||
|
|
||||||
return common.NewGenericCataloger(nil, globParsers, "php-composer-lock-cataloger")
|
|
||||||
}
|
}
|
||||||
|
|||||||
51
syft/pkg/cataloger/php/package.go
Normal file
51
syft/pkg/cataloger/php/package.go
Normal 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()
|
||||||
|
}
|
||||||
53
syft/pkg/cataloger/php/package_test.go
Normal file
53
syft/pkg/cataloger/php/package_test.go
Normal 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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,16 +7,20 @@ import (
|
|||||||
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"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 {
|
type composerLock struct {
|
||||||
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
|
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
|
||||||
PackageDev []pkg.PhpComposerJSONMetadata `json:"packages-dev"`
|
PackageDev []pkg.PhpComposerJSONMetadata `json:"packages-dev"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseComposerLock is a parser function for Composer.lock contents, returning "Default" php packages discovered.
|
// 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) {
|
func parseComposerLock(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
packages := make([]*pkg.Package, 0)
|
pkgs := make([]pkg.Package, 0)
|
||||||
dec := json.NewDecoder(reader)
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -26,19 +30,10 @@ func parseComposerLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.R
|
|||||||
} else if err != nil {
|
} 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 composer.lock file: %w", err)
|
||||||
}
|
}
|
||||||
for _, pkgMeta := range lock.Packages {
|
for _, m := range lock.Packages {
|
||||||
version := pkgMeta.Version
|
pkgs = append(pkgs, newComposerLockPackage(m, reader.Location))
|
||||||
name := pkgMeta.Name
|
|
||||||
packages = append(packages, &pkg.Package{
|
|
||||||
Name: name,
|
|
||||||
Version: version,
|
|
||||||
Language: pkg.PHP,
|
|
||||||
Type: pkg.PhpComposerPkg,
|
|
||||||
MetadataType: pkg.PhpComposerJSONMetadataType,
|
|
||||||
Metadata: pkgMeta,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages, nil, nil
|
return pkgs, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,24 @@
|
|||||||
package php
|
package php
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"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) {
|
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",
|
Name: "adoy/fastcgi-client",
|
||||||
Version: "1.0.2",
|
Version: "1.0.2",
|
||||||
|
PURL: "pkg:composer/adoy/fastcgi-client@1.0.2",
|
||||||
|
Locations: locations,
|
||||||
Language: pkg.PHP,
|
Language: pkg.PHP,
|
||||||
Type: pkg.PhpComposerPkg,
|
Type: pkg.PhpComposerPkg,
|
||||||
MetadataType: pkg.PhpComposerJSONMetadataType,
|
MetadataType: pkg.PhpComposerJSONMetadataType,
|
||||||
@ -52,6 +57,8 @@ func TestParseComposerFileLock(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "alcaeus/mongo-php-adapter",
|
Name: "alcaeus/mongo-php-adapter",
|
||||||
Version: "1.1.11",
|
Version: "1.1.11",
|
||||||
|
Locations: locations,
|
||||||
|
PURL: "pkg:composer/alcaeus/mongo-php-adapter@1.1.11",
|
||||||
Language: pkg.PHP,
|
Language: pkg.PHP,
|
||||||
Type: pkg.PhpComposerPkg,
|
Type: pkg.PhpComposerPkg,
|
||||||
MetadataType: pkg.PhpComposerJSONMetadataType,
|
MetadataType: pkg.PhpComposerJSONMetadataType,
|
||||||
@ -106,18 +113,5 @@ func TestParseComposerFileLock(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
fixture, err := os.Open("test-fixtures/composer.lock")
|
pkgtest.TestFileParser(t, fixture, parseComposerLock, expectedPkgs, expectedRelationships)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,12 @@ import (
|
|||||||
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"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
|
// Note: composer version 2 introduced a new structure for the installed.json file, so we support both
|
||||||
type installedJSONComposerV2 struct {
|
type installedJSONComposerV2 struct {
|
||||||
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
|
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
|
||||||
@ -36,12 +39,9 @@ func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// integrity check
|
// parseInstalledJSON is a parser function for Composer.lock contents, returning "Default" php packages discovered.
|
||||||
var _ common.ParserFn = parseComposerLock
|
func parseInstalledJSON(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
var pkgs []pkg.Package
|
||||||
// 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)
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -49,21 +49,12 @@ func parseInstalledJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.
|
|||||||
if err := dec.Decode(&lock); err == io.EOF {
|
if err := dec.Decode(&lock); err == io.EOF {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} 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 {
|
for _, pkgMeta := range lock.Packages {
|
||||||
version := pkgMeta.Version
|
pkgs = append(pkgs, newComposerLockPackage(pkgMeta, reader.Location))
|
||||||
name := pkgMeta.Name
|
|
||||||
packages = append(packages, &pkg.Package{
|
|
||||||
Name: name,
|
|
||||||
Version: version,
|
|
||||||
Language: pkg.PHP,
|
|
||||||
Type: pkg.PhpComposerPkg,
|
|
||||||
MetadataType: pkg.PhpComposerJSONMetadataType,
|
|
||||||
Metadata: pkgMeta,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages, nil, nil
|
return pkgs, nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,26 @@
|
|||||||
package php
|
package php
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"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",
|
Name: "asm89/stack-cors",
|
||||||
Version: "1.3.0",
|
Version: "1.3.0",
|
||||||
|
PURL: "pkg:composer/asm89/stack-cors@1.3.0",
|
||||||
Language: pkg.PHP,
|
Language: pkg.PHP,
|
||||||
Type: pkg.PhpComposerPkg,
|
Type: pkg.PhpComposerPkg,
|
||||||
MetadataType: pkg.PhpComposerJSONMetadataType,
|
MetadataType: pkg.PhpComposerJSONMetadataType,
|
||||||
@ -62,6 +70,7 @@ var expectedInstalledJsonPackages = []*pkg.Package{
|
|||||||
{
|
{
|
||||||
Name: "behat/mink",
|
Name: "behat/mink",
|
||||||
Version: "v1.8.1",
|
Version: "v1.8.1",
|
||||||
|
PURL: "pkg:composer/behat/mink@v1.8.1",
|
||||||
Language: pkg.PHP,
|
Language: pkg.PHP,
|
||||||
Type: pkg.PhpComposerPkg,
|
Type: pkg.PhpComposerPkg,
|
||||||
MetadataType: pkg.PhpComposerJSONMetadataType,
|
MetadataType: pkg.PhpComposerJSONMetadataType,
|
||||||
@ -117,40 +126,16 @@ 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: no relationships are under test yet
|
for _, fixture := range fixtures {
|
||||||
actual, _, err := parseInstalledJSON(fixture.Name(), fixture)
|
t.Run(fixture, func(t *testing.T) {
|
||||||
if err != nil {
|
locations := source.NewLocationSet(source.NewLocation(fixture))
|
||||||
t.Fatalf("failed to parse requirements: %+v", err)
|
for i := range expectedPkgs {
|
||||||
|
expectedPkgs[i].Locations = locations
|
||||||
}
|
}
|
||||||
differences := deep.Equal(expectedInstalledJsonPackages, actual)
|
pkgtest.TestFileParser(t, fixture, parseInstalledJSON, expectedPkgs, expectedRelationships)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,19 +77,6 @@ func TestPackageURL(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: "pkg:cargo/name@v0.1.0",
|
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",
|
name: "java",
|
||||||
pkg: Package{
|
pkg: Package{
|
||||||
@ -152,6 +139,7 @@ func TestPackageURL(t *testing.T) {
|
|||||||
expectedTypes.Remove(string(GoModulePkg))
|
expectedTypes.Remove(string(GoModulePkg))
|
||||||
expectedTypes.Remove(string(HackagePkg))
|
expectedTypes.Remove(string(HackagePkg))
|
||||||
expectedTypes.Remove(string(BinaryPkg))
|
expectedTypes.Remove(string(BinaryPkg))
|
||||||
|
expectedTypes.Remove(string(PhpComposerPkg))
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user