mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
Add support for PHP Pear (#2775)
* Add support for PHP Pear and unify PECL with it Signed-off-by: Laurent Goderre <laurent.goderre@docker.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove log statements Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix struct comment Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Laurent Goderre <laurent.goderre@docker.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
78ef2cf53b
commit
529840bfc0
@ -140,11 +140,11 @@ Note that flags using the @<version> can be used for earlier versions of each sp
|
||||
- Linux kernel archives (vmlinz)
|
||||
- Linux kernel modules (ko)
|
||||
- Nix (outputs in /nix/store)
|
||||
- PHP (composer)
|
||||
- PHP (composer, PECL, Pear)
|
||||
- Python (wheel, egg, poetry, requirements.txt)
|
||||
- Red Hat (rpm)
|
||||
- Ruby (gem)
|
||||
- Rust (cargo.lock)
|
||||
- Rust (cargo.lock, auditable binary)
|
||||
- Swift (cocoapods, swift-package-manager)
|
||||
- Wordpress plugins
|
||||
- Terraform providers (.terraform.lock.hcl)
|
||||
|
||||
@ -502,8 +502,8 @@ var commonTestCases = []testCase{
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find php pecl package",
|
||||
pkgType: pkg.PhpPeclPkg,
|
||||
name: "find php pear/pecl package",
|
||||
pkgType: pkg.PhpPearPkg,
|
||||
pkgLanguage: pkg.PHP,
|
||||
pkgInfo: map[string]string{
|
||||
"memcached": "3.2.0",
|
||||
|
||||
@ -85,6 +85,7 @@ func TestPkgCoverageImage(t *testing.T) {
|
||||
definedPkgs.Remove(string(pkg.GithubActionPkg))
|
||||
definedPkgs.Remove(string(pkg.GithubActionWorkflowPkg))
|
||||
definedPkgs.Remove(string(pkg.TerraformPkg))
|
||||
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
|
||||
|
||||
var cases []testCase
|
||||
cases = append(cases, commonTestCases...)
|
||||
@ -227,6 +228,7 @@ func TestPkgCoverageDirectory(t *testing.T) {
|
||||
definedPkgs.Remove(string(pkg.LinuxKernelModulePkg))
|
||||
definedPkgs.Remove(string(pkg.Rpkg))
|
||||
definedPkgs.Remove(string(pkg.UnknownPkg))
|
||||
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
|
||||
|
||||
// for directory scans we should not expect to see any of the following package types
|
||||
definedPkgs.Remove(string(pkg.KbPkg))
|
||||
|
||||
@ -56,6 +56,7 @@ func TestAllPackageCatalogersReachableInTasks(t *testing.T) {
|
||||
// not reachable since they are deprecated
|
||||
"dotnet-portable-executable-cataloger",
|
||||
"dotnet-deps-cataloger",
|
||||
"php-pecl-serialized-cataloger",
|
||||
// not reachable by design
|
||||
"sbom-cataloger",
|
||||
)
|
||||
|
||||
@ -1 +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";}}
|
||||
a:6:{s:4:"name";s:9:"memcached";s:7:"channel";s:12:"pecl.php.net";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";}}
|
||||
@ -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.25"
|
||||
JSONSchemaVersion = "16.0.26"
|
||||
)
|
||||
|
||||
@ -100,7 +100,7 @@ func DefaultPackageTaskFactories() Factories {
|
||||
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"),
|
||||
newSimplePackageTaskFactory(php.NewPearCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, pkgcataloging.ImageTag, "php", "pear"),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
return python.NewPackageCataloger(cfg.PackagesConfig.Python)
|
||||
@ -166,5 +166,6 @@ func DefaultPackageTaskFactories() Factories {
|
||||
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetDepsCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPortableExecutableCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
newSimplePackageTaskFactory(php.NewPeclCataloger, pkgcataloging.DeprecatedTag), // TODO: remove in syft v2.0
|
||||
}
|
||||
}
|
||||
|
||||
2944
schema/json/schema-16.0.26.json
Normal file
2944
schema/json/schema-16.0.26.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.25/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.26/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@ -1872,6 +1872,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/PhpComposerLockEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PhpPearEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/PhpPeclEntry"
|
||||
},
|
||||
@ -2164,11 +2167,38 @@
|
||||
"dist"
|
||||
]
|
||||
},
|
||||
"PhpPearEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"license": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
]
|
||||
},
|
||||
"PhpPeclEntry": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@ -34,6 +34,7 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
pkg.NixStoreEntry{},
|
||||
pkg.NpmPackageLockEntry{},
|
||||
pkg.PhpComposerInstalledEntry{},
|
||||
pkg.PhpPearEntry{},
|
||||
pkg.PhpPeclEntry{},
|
||||
pkg.PortageEntry{},
|
||||
pkg.PythonPipfileLockEntry{},
|
||||
|
||||
@ -40,6 +40,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.PhpPearPkg:
|
||||
answer = "acquired package info from PHP Pear manifest"
|
||||
case pkg.PhpPeclPkg:
|
||||
answer = "acquired package info from PHP Pecl manifest"
|
||||
case pkg.CocoapodsPkg:
|
||||
|
||||
@ -143,6 +143,14 @@ func Test_SourceInfo(t *testing.T) {
|
||||
"from PHP Pecl manifest",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.PhpPearPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from PHP Pear manifest",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.DartPubPkg,
|
||||
|
||||
@ -42,6 +42,7 @@ func AllTypes() []any {
|
||||
pkg.OpamPackage{},
|
||||
pkg.PhpComposerInstalledEntry{},
|
||||
pkg.PhpComposerLockEntry{},
|
||||
pkg.PhpPearEntry{},
|
||||
pkg.PhpPeclEntry{},
|
||||
pkg.PortageEntry{},
|
||||
pkg.PythonPackage{},
|
||||
|
||||
@ -96,6 +96,7 @@ var jsonTypes = makeJSONTypes(
|
||||
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.PhpPearEntry{}, "php-pear-entry"),
|
||||
jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"),
|
||||
jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"),
|
||||
jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"),
|
||||
|
||||
@ -23,8 +23,16 @@ func NewComposerLockCataloger() pkg.Cataloger {
|
||||
WithParserByGlobs(parseComposerLock, "**/composer.lock")
|
||||
}
|
||||
|
||||
// NewPeclCataloger returns a new cataloger for PHP PECL metadata“.
|
||||
// NewPearCataloger returns a new cataloger for PHP Pear metadata (including Pecl metadata).
|
||||
func NewPearCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("php-pear-serialized-cataloger").
|
||||
WithParserByGlobs(parsePear, "**/php/.registry/**/*.reg")
|
||||
}
|
||||
|
||||
// NewPeclCataloger returns a new cataloger for PHP Pecl metadata. Note: this will also catalog Pear metadata so should
|
||||
// not be used in conjunction with the Pear Cataloger.
|
||||
// Deprecated: please use NewPearCataloger instead.
|
||||
func NewPeclCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("php-pecl-serialized-cataloger").
|
||||
WithParserByGlobs(parsePeclSerialized, "**/php/.registry/.channel.*/*.reg")
|
||||
WithParserByGlobs(parsePecl, "**/php/.registry/.channel.*/*.reg")
|
||||
}
|
||||
|
||||
@ -56,6 +56,31 @@ func Test_ComposerLockCataloger_Globs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PearCataloger_Globs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "obtain pear 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, NewPearCataloger())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PeclCataloger_Globs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -63,7 +88,7 @@ func Test_PeclCataloger_Globs(t *testing.T) {
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "obtain pecl files",
|
||||
name: "obtain pear files",
|
||||
fixture: "test-fixtures/glob-paths",
|
||||
expected: []string{
|
||||
"php/.registry/.channel.pecl.php.net/memcached.reg",
|
||||
|
||||
@ -14,7 +14,7 @@ func newComposerLockPackage(pd parsedLockData, indexLocation file.Location) pkg.
|
||||
Version: pd.Version,
|
||||
Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...),
|
||||
PURL: packageURL(pd.Name, pd.Version),
|
||||
PURL: packageURLFromComposer(pd.Name, pd.Version),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpComposerPkg,
|
||||
Metadata: pd.PhpComposerLockEntry,
|
||||
@ -30,7 +30,7 @@ func newComposerInstalledPackage(pd parsedInstalledData, indexLocation file.Loca
|
||||
Version: pd.Version,
|
||||
Locations: file.NewLocationSet(indexLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(indexLocation, pd.License...)...),
|
||||
PURL: packageURL(pd.Name, pd.Version),
|
||||
PURL: packageURLFromComposer(pd.Name, pd.Version),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpComposerPkg,
|
||||
Metadata: pd.PhpComposerInstalledEntry,
|
||||
@ -40,23 +40,39 @@ func newComposerInstalledPackage(pd parsedInstalledData, indexLocation file.Loca
|
||||
return p
|
||||
}
|
||||
|
||||
func newPeclPackage(pd pkg.PhpPeclEntry, indexLocation file.Location) pkg.Package {
|
||||
func newPearPackage(pd peclPearData, 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),
|
||||
PURL: packageURLFromPear(pd.Name, pd.Channel, pd.Version),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpPeclPkg,
|
||||
Metadata: pd,
|
||||
Type: pkg.PhpPearPkg,
|
||||
Metadata: pd.ToPear(),
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
return p
|
||||
}
|
||||
|
||||
func packageURL(name, version string) string {
|
||||
func newPeclPackage(pd peclPearData, 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: packageURLFromPear(pd.Name, pd.Channel, pd.Version),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpPeclPkg,
|
||||
Metadata: pd.ToPecl(),
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
return p
|
||||
}
|
||||
|
||||
func packageURLFromComposer(name, version string) string {
|
||||
var pkgName, vendor string
|
||||
fields := strings.Split(name, "/")
|
||||
switch len(fields) {
|
||||
@ -82,10 +98,15 @@ func packageURL(name, version string) string {
|
||||
return pURL.ToString()
|
||||
}
|
||||
|
||||
func packageURLFromPecl(pkgName, version string) string {
|
||||
func packageURLFromPear(pkgName, channel, version string) string {
|
||||
namespace := channel
|
||||
if namespace == "" {
|
||||
namespace = "pecl.php.net"
|
||||
}
|
||||
|
||||
pURL := packageurl.NewPackageURL(
|
||||
"pecl",
|
||||
"",
|
||||
"pear",
|
||||
namespace,
|
||||
pkgName,
|
||||
version,
|
||||
nil,
|
||||
|
||||
@ -35,7 +35,7 @@ func Test_packageURL(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := packageURL(test.packageName, test.packageVersion)
|
||||
actual := packageURLFromComposer(test.packageName, test.packageVersion)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
@ -45,22 +45,30 @@ func Test_packageURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_packageURLFromPecl(t *testing.T) {
|
||||
func Test_packageURLFromPear(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
channel string
|
||||
version string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "memcached",
|
||||
channel: "pear.php.net",
|
||||
version: "3.2.0",
|
||||
expected: "pkg:pecl/memcached@3.2.0",
|
||||
expected: "pkg:pear/pear.php.net/memcached@3.2.0",
|
||||
},
|
||||
{
|
||||
name: "memcached",
|
||||
channel: "", // important!
|
||||
version: "3.2.0",
|
||||
expected: "pkg:pear/pecl.php.net/memcached@3.2.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := packageURLFromPecl(test.name, test.version)
|
||||
actual := packageURLFromPear(test.name, test.channel, test.version)
|
||||
if actual != test.expected {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
||||
|
||||
112
syft/pkg/cataloger/php/parse_pecl_pear.go
Normal file
112
syft/pkg/cataloger/php/parse_pecl_pear.go
Normal file
@ -0,0 +1,112 @@
|
||||
package php
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/elliotchance/phpserialize"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"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"
|
||||
)
|
||||
|
||||
type peclPearData struct {
|
||||
Name string
|
||||
Channel string
|
||||
Version string
|
||||
License []string
|
||||
}
|
||||
|
||||
func (p *peclPearData) ToPear() pkg.PhpPearEntry {
|
||||
return pkg.PhpPearEntry{
|
||||
Name: p.Name,
|
||||
Channel: p.Channel,
|
||||
Version: p.Version,
|
||||
License: p.License,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *peclPearData) ToPecl() pkg.PhpPeclEntry {
|
||||
return pkg.PhpPeclEntry(p.ToPear())
|
||||
}
|
||||
|
||||
func parsePecl(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
m, err := parsePeclPearSerialized(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if m == nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no pecl package found"))
|
||||
}
|
||||
return []pkg.Package{newPeclPackage(*m, reader.Location)}, nil, nil
|
||||
}
|
||||
|
||||
func parsePear(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
m, err := parsePeclPearSerialized(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if m == nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no pear package found"))
|
||||
}
|
||||
return []pkg.Package{newPearPackage(*m, reader.Location)}, nil, nil
|
||||
}
|
||||
|
||||
// parsePeclPearSerialized is a parser function for Pear metadata contents, returning "Default" php packages discovered.
|
||||
func parsePeclPearSerialized(reader file.LocationReadCloser) (*peclPearData, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
metadata, err := phpserialize.UnmarshalAssociativeArray(
|
||||
data,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse pear metadata file: %w", err)
|
||||
}
|
||||
|
||||
name, ok := metadata["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse pear package name: %w", err)
|
||||
}
|
||||
|
||||
channel, ok := metadata["channel"].(string)
|
||||
if !ok {
|
||||
// this could be the v5 format
|
||||
channel = ""
|
||||
}
|
||||
|
||||
version := readStruct(metadata, "version", "release")
|
||||
license := readStruct(metadata, "license", "_content")
|
||||
|
||||
return &peclPearData{
|
||||
Name: name,
|
||||
Channel: channel,
|
||||
Version: version,
|
||||
License: []string{
|
||||
license,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readStruct(metadata any, fields ...string) string {
|
||||
if len(fields) > 0 {
|
||||
value, ok := metadata.(map[any]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return readStruct(value[fields[0]], fields[1:]...)
|
||||
}
|
||||
value, ok := metadata.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
139
syft/pkg/cataloger/php/parse_pecl_pear_test.go
Normal file
139
syft/pkg/cataloger/php/parse_pecl_pear_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
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 TestParsePear(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expectedPkgs []pkg.Package
|
||||
expectedRelationships []artifact.Relationship
|
||||
}{
|
||||
{
|
||||
name: "v6 format",
|
||||
fixture: "test-fixtures/memcached-v6-format.reg",
|
||||
expectedPkgs: []pkg.Package{
|
||||
{
|
||||
Name: "memcached",
|
||||
Version: "3.2.0",
|
||||
PURL: "pkg:pear/pecl.php.net/memcached@3.2.0",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v6-format.reg")),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v6-format.reg")),
|
||||
),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpPearPkg,
|
||||
Metadata: pkg.PhpPearEntry{
|
||||
Name: "memcached",
|
||||
Channel: "pecl.php.net",
|
||||
Version: "3.2.0",
|
||||
License: []string{"PHP License"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v5 format",
|
||||
fixture: "test-fixtures/memcached-v5-format.reg",
|
||||
expectedPkgs: []pkg.Package{
|
||||
{
|
||||
Name: "memcached",
|
||||
Version: "3.2.0",
|
||||
PURL: "pkg:pear/pecl.php.net/memcached@3.2.0",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v5-format.reg")),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v5-format.reg")),
|
||||
),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpPearPkg,
|
||||
Metadata: pkg.PhpPearEntry{ // important: missing channel
|
||||
Name: "memcached",
|
||||
Version: "3.2.0",
|
||||
License: []string{"PHP License"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgtest.TestFileParser(t, tt.fixture, parsePear, tt.expectedPkgs, tt.expectedRelationships)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePecl(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expectedPkgs []pkg.Package
|
||||
expectedRelationships []artifact.Relationship
|
||||
}{
|
||||
{
|
||||
name: "v6 format",
|
||||
fixture: "test-fixtures/memcached-v6-format.reg",
|
||||
expectedPkgs: []pkg.Package{
|
||||
{
|
||||
Name: "memcached",
|
||||
Version: "3.2.0",
|
||||
PURL: "pkg:pear/pecl.php.net/memcached@3.2.0",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v6-format.reg")),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v6-format.reg")),
|
||||
),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpPeclPkg, // important!
|
||||
Metadata: pkg.PhpPeclEntry{ // important!
|
||||
Name: "memcached",
|
||||
Channel: "pecl.php.net",
|
||||
Version: "3.2.0",
|
||||
License: []string{"PHP License"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v5 format",
|
||||
fixture: "test-fixtures/memcached-v5-format.reg",
|
||||
expectedPkgs: []pkg.Package{
|
||||
{
|
||||
Name: "memcached",
|
||||
Version: "3.2.0",
|
||||
PURL: "pkg:pear/pecl.php.net/memcached@3.2.0",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/memcached-v5-format.reg")),
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations("PHP License", file.NewLocation("test-fixtures/memcached-v5-format.reg")),
|
||||
),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.PhpPeclPkg, // important!
|
||||
Metadata: pkg.PhpPeclEntry{ // important!
|
||||
Name: "memcached",
|
||||
Version: "3.2.0",
|
||||
License: []string{"PHP License"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgtest.TestFileParser(t, tt.fixture, parsePecl, tt.expectedPkgs, tt.expectedRelationships)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_corruptPecl(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromFile(t, "test-fixtures/glob-paths/php/.registry/.channel.pecl.php.net/memcached.reg").
|
||||
WithError().
|
||||
TestParser(t, parseComposerLock)
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
package php
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/elliotchance/phpserialize"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
func Test_corruptPecl(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromFile(t, "test-fixtures/glob-paths/php/.registry/.channel.pecl.php.net/memcached.reg").
|
||||
WithError().
|
||||
TestParser(t, parseComposerLock)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
a:6:{s:4:"name";s:9:"memcached";s:7:"channel";s:12:"pecl.php.net";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";}}
|
||||
@ -37,8 +37,14 @@ type PhpComposerAuthors struct {
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
}
|
||||
|
||||
type PhpPeclEntry struct {
|
||||
// PhpPeclEntry represents a single package entry found within php pecl metadata files.
|
||||
// Deprecated: please use PhpPearEntry instead with the pear cataloger.
|
||||
type PhpPeclEntry PhpPearEntry
|
||||
|
||||
// PhpPearEntry represents a single package entry found within php pear metadata files.
|
||||
type PhpPearEntry struct {
|
||||
Name string `json:"name"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Version string `json:"version"`
|
||||
License []string `json:"license,omitempty"`
|
||||
}
|
||||
|
||||
@ -36,7 +36,8 @@ const (
|
||||
NpmPkg Type = "npm"
|
||||
OpamPkg Type = "opam"
|
||||
PhpComposerPkg Type = "php-composer"
|
||||
PhpPeclPkg Type = "php-pecl"
|
||||
PhpPeclPkg Type = "php-pecl" // Deprecated: will be removed in syft v2.0
|
||||
PhpPearPkg Type = "php-pear"
|
||||
PortagePkg Type = "portage"
|
||||
PythonPkg Type = "python"
|
||||
Rpkg Type = "R-package"
|
||||
@ -78,6 +79,7 @@ var AllPkgs = []Type{
|
||||
OpamPkg,
|
||||
PhpComposerPkg,
|
||||
PhpPeclPkg,
|
||||
PhpPearPkg,
|
||||
PortagePkg,
|
||||
PythonPkg,
|
||||
Rpkg,
|
||||
@ -132,8 +134,8 @@ func (t Type) PackageURLType() string {
|
||||
return packageurl.TypeGeneric
|
||||
case PhpComposerPkg:
|
||||
return packageurl.TypeComposer
|
||||
case PhpPeclPkg:
|
||||
return "pecl"
|
||||
case PhpPearPkg, PhpPeclPkg:
|
||||
return "pear"
|
||||
case PythonPkg:
|
||||
return packageurl.TypePyPi
|
||||
case PortagePkg:
|
||||
@ -192,8 +194,8 @@ func TypeByName(name string) Type {
|
||||
return DebPkg
|
||||
case packageurl.TypeComposer:
|
||||
return PhpComposerPkg
|
||||
case "pecl":
|
||||
return PhpPeclPkg
|
||||
case "pear", "pecl":
|
||||
return PhpPearPkg
|
||||
case packageurl.TypeGolang:
|
||||
return GoModulePkg
|
||||
case packageurl.TypeGem:
|
||||
|
||||
@ -63,8 +63,16 @@ func TestTypeFromPURL(t *testing.T) {
|
||||
expected: PhpComposerPkg,
|
||||
},
|
||||
{
|
||||
purl: "pkg:pecl/memcached@3.2.0",
|
||||
expected: PhpPeclPkg,
|
||||
purl: "pkg:pear/pecl.php.net/memcached@3.2.0", // pecl namespace
|
||||
expected: PhpPearPkg,
|
||||
},
|
||||
{
|
||||
purl: "pkg:pear/pear.php.net/memcached@3.2.0", // pear namespace
|
||||
expected: PhpPearPkg,
|
||||
},
|
||||
{
|
||||
purl: "pkg:pecl/pecl.php.net/memcached@3.2.0", // note: this is an invalid purl, but we will handle it anyway in case folks created the type pre-emptively
|
||||
expected: PhpPearPkg, // we should still consider this a pear package
|
||||
},
|
||||
{
|
||||
purl: "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?type=zip&classifier=dist",
|
||||
@ -124,7 +132,7 @@ func TestTypeFromPURL(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
var pkgTypes []string
|
||||
var pkgTypes = strset.New()
|
||||
var expectedTypes = strset.New()
|
||||
for _, ty := range AllPkgs {
|
||||
expectedTypes.Add(string(ty))
|
||||
@ -141,19 +149,19 @@ func TestTypeFromPURL(t *testing.T) {
|
||||
expectedTypes.Remove(string(WordpressPluginPkg))
|
||||
expectedTypes.Remove(string(TerraformPkg))
|
||||
expectedTypes.Remove(string(GraalVMNativeImagePkg))
|
||||
expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(string(test.expected), func(t *testing.T) {
|
||||
actual := TypeFromPURL(test.purl)
|
||||
|
||||
if actual != "" {
|
||||
pkgTypes = append(pkgTypes, string(actual))
|
||||
pkgTypes.Add(string(actual))
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedTypes.List(), pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
|
||||
|
||||
assert.ElementsMatch(t, expectedTypes.List(), pkgTypes.List(), "missing one or more package types to test against (maybe a package type was added?)")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user