feat: cataloger for PHP Pecl and PEAR packages (#2604)

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
This commit is contained in:
Laurent Goderre
2024-04-02 11:55:56 -04:00
committed by GitHub
parent e0f5b5a787
commit e0233625cb
25 changed files with 2637 additions and 3 deletions
+6
View File
@@ -22,3 +22,9 @@ func NewComposerLockCataloger() pkg.Cataloger {
return generic.NewCataloger("php-composer-lock-cataloger").
WithParserByGlobs(parseComposerLock, "**/composer.lock")
}
// NewPeclCataloger returns a new cataloger for PHP PECL metadata“.
func NewPeclCataloger() pkg.Cataloger {
return generic.NewCataloger("php-pecl-serialized-cataloger").
WithParserByGlobs(parsePeclSerialized, "**/php/.registry/.channel.*/*.reg")
}
+25
View File
@@ -55,3 +55,28 @@ func Test_ComposerLockCataloger_Globs(t *testing.T) {
})
}
}
func Test_PeclCataloger_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
expected []string
}{
{
name: "obtain pecl files",
fixture: "test-fixtures/glob-paths",
expected: []string{
"php/.registry/.channel.pecl.php.net/memcached.reg",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
ExpectsResolverContentQueries(test.expected).
TestCataloger(t, NewPeclCataloger())
})
}
}
+27
View File
@@ -40,6 +40,22 @@ func newComposerInstalledPackage(pd parsedInstalledData, indexLocation file.Loca
return p
}
func newPeclPackage(pd pkg.PhpPeclEntry, indexLocation file.Location) pkg.Package {
p := pkg.Package{
Name: pd.Name,
Version: pd.Version,
Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...),
PURL: packageURLFromPecl(pd.Name, pd.Version),
Language: pkg.PHP,
Type: pkg.PhpPeclPkg,
Metadata: pd,
}
p.SetID()
return p
}
func packageURL(name, version string) string {
var pkgName, vendor string
fields := strings.Split(name, "/")
@@ -65,3 +81,14 @@ func packageURL(name, version string) string {
"")
return pURL.ToString()
}
func packageURLFromPecl(pkgName, version string) string {
pURL := packageurl.NewPackageURL(
"pecl",
"",
pkgName,
version,
nil,
"")
return pURL.ToString()
}
+25
View File
@@ -44,3 +44,28 @@ func Test_packageURL(t *testing.T) {
})
}
}
func Test_packageURLFromPecl(t *testing.T) {
tests := []struct {
name string
version string
expected string
}{
{
name: "memcached",
version: "3.2.0",
expected: "pkg:pecl/memcached@3.2.0",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := packageURLFromPecl(test.name, test.version)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
@@ -0,0 +1,72 @@
package php
import (
"context"
"fmt"
"io"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/elliotchance/phpserialize"
)
// parsePeclSerialized is a parser function for PECL metadata contents, returning "Default" php packages discovered.
func parsePeclSerialized(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
data, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to read file: %w", err)
}
metadata, err := phpserialize.UnmarshalAssociativeArray(
data,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse pecl metadata file: %w", err)
}
name, ok := metadata["name"].(string)
if !ok {
return nil, nil, fmt.Errorf("failed to parse pecl package name: %w", err)
}
version := readStruct(metadata, "version", "release")
license := readStruct(metadata, "license", "_content")
pkgs = append(
pkgs,
newPeclPackage(
pkg.PhpPeclEntry{
Name: name,
Version: version,
License: []string{
license,
},
},
reader.Location,
),
)
return pkgs, nil, nil
}
func readStruct(metadata any, fields ...string) string {
if len(fields) > 0 {
value, ok := metadata.(map[any]any)
if !ok {
log.Tracef("unable to read '%s' from: %v", fields[0], metadata)
return ""
}
return readStruct(value[fields[0]], fields[1:]...)
}
value, ok := metadata.(string)
if !ok {
log.Tracef("unable to read value from: %v", metadata)
}
return value
}
@@ -0,0 +1,35 @@
package php
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParsePeclSerialized(t *testing.T) {
var expectedRelationships []artifact.Relationship
fixture := "test-fixtures/memcached.reg"
locations := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "memcached",
Version: "3.2.0",
PURL: "pkg:pecl/memcached@3.2.0",
Locations: locations,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("PHP License", file.NewLocation(fixture)),
),
Language: pkg.PHP,
Type: pkg.PhpPeclPkg,
Metadata: pkg.PhpPeclEntry{
Name: "memcached",
Version: "3.2.0",
License: []string{"PHP License"},
},
},
}
pkgtest.TestFileParser(t, fixture, parsePeclSerialized, expectedPkgs, expectedRelationships)
}
@@ -0,0 +1 @@
a:5:{s:4:"name";s:9:"memcached";s:4:"date";s:10:"2022-01-11";s:4:"time";s:8:"15:23:47";s:7:"version";a:2:{s:7:"release";s:5:"3.2.0";s:3:"api";s:5:"3.2.0";}s:7:"license";a:2:{s:7:"attribs";a:1:{s:3:"uri";s:26:"http://www.php.net/license";}s:8:"_content";s:11:"PHP License";}}
+6
View File
@@ -36,3 +36,9 @@ type PhpComposerAuthors struct {
Email string `json:"email,omitempty"`
Homepage string `json:"homepage,omitempty"`
}
type PhpPeclEntry struct {
Name string `json:"name"`
Version string `json:"version"`
License []string `json:"license,omitempty"`
}
+6
View File
@@ -34,6 +34,7 @@ const (
NixPkg Type = "nix"
NpmPkg Type = "npm"
PhpComposerPkg Type = "php-composer"
PhpPeclPkg Type = "php-pecl"
PortagePkg Type = "portage"
PythonPkg Type = "python"
Rpkg Type = "R-package"
@@ -68,6 +69,7 @@ var AllPkgs = []Type{
NixPkg,
NpmPkg,
PhpComposerPkg,
PhpPeclPkg,
PortagePkg,
PythonPkg,
Rpkg,
@@ -117,6 +119,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypeGeneric
case PhpComposerPkg:
return packageurl.TypeComposer
case PhpPeclPkg:
return "pecl"
case PythonPkg:
return packageurl.TypePyPi
case PortagePkg:
@@ -169,6 +173,8 @@ func TypeByName(name string) Type {
return JavaPkg
case packageurl.TypeComposer:
return PhpComposerPkg
case "pecl":
return PhpPeclPkg
case packageurl.TypeGolang:
return GoModulePkg
case packageurl.TypeNPM:
+4
View File
@@ -59,6 +59,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:composer/laravel/laravel@5.5.0",
expected: PhpComposerPkg,
},
{
purl: "pkg:pecl/memcached@3.2.0",
expected: PhpPeclPkg,
},
{
purl: "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?type=zip&classifier=dist",
expected: JavaPkg,