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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2637 additions and 3 deletions

View File

@ -460,4 +460,12 @@ var commonTestCases = []testCase{
"Akismet Anti-spam: Spam Protection": "5.3",
},
},
{
name: "find php pecl package",
pkgType: pkg.PhpPeclPkg,
pkgLanguage: pkg.PHP,
pkgInfo: map[string]string{
"memcached": "3.2.0",
},
},
}

View File

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

1
go.mod
View File

@ -31,6 +31,7 @@ require (
github.com/distribution/reference v0.6.0
github.com/docker/docker v26.0.0+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/elliotchance/phpserialize v1.4.0
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.2.0
github.com/gkampitakis/go-snaps v0.5.2

2
go.sum
View File

@ -242,6 +242,8 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY=
github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

View File

@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.6"
JSONSchemaVersion = "16.0.7"
)

View File

@ -80,6 +80,7 @@ func DefaultPackageTaskFactories() PackageTaskFactories {
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "javascript", "node", "npm",
),
newSimplePackageTaskFactory(php.NewComposerLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "php", "composer"),
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, pkgcataloging.ImageTag, "php", "pecl"),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return python.NewPackageCataloger(cfg.PackagesConfig.Python)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.6/document",
"$id": "anchore.io/schema/syft/json/16.0.7/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -1496,6 +1496,9 @@
{
"$ref": "#/$defs/PhpComposerLockEntry"
},
{
"$ref": "#/$defs/PhpPeclEntry"
},
{
"$ref": "#/$defs/PortageDbEntry"
},
@ -1779,6 +1782,27 @@
"dist"
]
},
"PhpPeclEntry": {
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"license": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object",
"required": [
"name",
"version"
]
},
"PortageDbEntry": {
"properties": {
"installedSize": {

View File

@ -36,6 +36,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from rust cargo manifest"
case pkg.PhpComposerPkg:
answer = "acquired package info from PHP composer manifest"
case pkg.PhpPeclPkg:
answer = "acquired package info from PHP Pecl manifest"
case pkg.CocoapodsPkg:
answer = "acquired package info from installed cocoapods manifest file"
case pkg.ConanPkg:

View File

@ -127,6 +127,14 @@ func Test_SourceInfo(t *testing.T) {
"from PHP composer manifest",
},
},
{
input: pkg.Package{
Type: pkg.PhpPeclPkg,
},
expected: []string{
"from PHP Pecl manifest",
},
},
{
input: pkg.Package{
Type: pkg.DartPubPkg,

View File

@ -35,6 +35,7 @@ func AllTypes() []any {
pkg.NpmPackageLockEntry{},
pkg.PhpComposerInstalledEntry{},
pkg.PhpComposerLockEntry{},
pkg.PhpPeclEntry{},
pkg.PortageEntry{},
pkg.PythonPackage{},
pkg.PythonPipfileLockEntry{},

View File

@ -91,6 +91,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.YarnLockEntry{}, "javascript-yarn-lock-entry", "YarnLockJsonMetadata"),
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred
jsonNames(pkg.PhpPeclEntry{}, "php-pecl-entry", "PhpPeclMetadata"),
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),

View File

@ -200,6 +200,11 @@ func TestReflectTypeFromJSONName_LegacyValues(t *testing.T) {
input: "PythonRequirementsMetadata",
expected: reflect.TypeOf(pkg.PythonRequirementsEntry{}),
},
{
name: "map pkg.PhpPeclEntry struct type",
input: "PhpPeclMetadata",
expected: reflect.TypeOf(pkg.PhpPeclEntry{}),
},
{
name: "map pkg.ErlangRebarLockEntry struct type",
input: "RebarLockMetadataType",
@ -414,6 +419,12 @@ func Test_JSONName_JSONLegacyName(t *testing.T) {
expectedJSONName: "php-composer-installed-entry",
expectedLegacyName: "PhpComposerJsonMetadata", // note: maps to multiple entries (v11-12 breaking change)
},
{
name: "PhpPeclMetadata",
metadata: pkg.PhpPeclEntry{},
expectedJSONName: "php-pecl-entry",
expectedLegacyName: "PhpPeclMetadata",
},
{
name: "PortageMetadata",
metadata: pkg.PortageEntry{},

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

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

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

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

View File

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

View File

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

View File

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

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"`
}

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:

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,

View File

@ -9,7 +9,7 @@ import (
const (
// this is the number of packages that should be found in the image-pkg-coverage fixture image
// when analyzed with the squashed scope.
coverageImageSquashedPackageCount = 28
coverageImageSquashedPackageCount = 29
)
func TestPackagesCmdFlags(t *testing.T) {