Add additional PHP metadata (#753)

* add php related metadata

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* enable decoding of php metadata for syftjson format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add php metadata to json schema

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2022-01-19 11:42:16 -05:00 committed by GitHub
parent 814f2bf8b9
commit 829e500aa9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 496 additions and 64 deletions

View file

@ -118,6 +118,14 @@ func (p *Package) UnmarshalJSON(b []byte) error {
return err
}
p.Metadata = payload
case pkg.PhpComposerJSONMetadataType:
var payload pkg.PhpComposerJSONMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
default:
log.Warnf("unknown package metadata type=%q for packageID=%q", p.MetadataType, p.ID)
}
return nil

View file

@ -36,6 +36,7 @@ type artifactMetadataContainer struct {
Rpm pkg.RpmdbMetadata
Cargo pkg.CargoPackageMetadata
Go pkg.GolangBinMetadata
Php pkg.PhpComposerJSONMetadata
}
func main() {

View file

@ -662,6 +662,9 @@
{
"$ref": "#/definitions/NpmPackageJSONMetadata"
},
{
"$ref": "#/definitions/PhpComposerJSONMetadata"
},
{
"$ref": "#/definitions/PythonPackageMetadata"
},
@ -674,6 +677,144 @@
"additionalProperties": true,
"type": "object"
},
"PhpComposerAuthors": {
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"homepage": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"PhpComposerExternalReference": {
"required": [
"type",
"url",
"reference"
],
"properties": {
"type": {
"type": "string"
},
"url": {
"type": "string"
},
"reference": {
"type": "string"
},
"shasum": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"PhpComposerJSONMetadata": {
"required": [
"name",
"version",
"source",
"dist"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"source": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PhpComposerExternalReference"
},
"dist": {
"$ref": "#/definitions/PhpComposerExternalReference"
},
"require": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
},
"provide": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
},
"require-dev": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
},
"suggest": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
},
"type": {
"type": "string"
},
"notification-url": {
"type": "string"
},
"bin": {
"items": {
"type": "string"
},
"type": "array"
},
"license": {
"items": {
"type": "string"
},
"type": "array"
},
"authors": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PhpComposerAuthors"
},
"type": "array"
},
"description": {
"type": "string"
},
"homepage": {
"type": "string"
},
"keywords": {
"items": {
"type": "string"
},
"type": "array"
},
"time": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"PomParent": {
"required": [
"groupId",

View file

@ -6,18 +6,12 @@ import (
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
)
type ComposerLock struct {
Packages []Dependency `json:"packages"`
PackageDev []Dependency `json:"packages-dev"`
}
type Dependency struct {
Name string `json:"name"`
Version string `json:"version"`
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.
@ -26,7 +20,7 @@ func parseComposerLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.R
dec := json.NewDecoder(reader)
for {
var lock ComposerLock
var lock composerLock
if err := dec.Decode(&lock); err == io.EOF {
break
} else if err != nil {
@ -36,10 +30,12 @@ func parseComposerLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.R
version := pkgMeta.Version
name := pkgMeta.Name
packages = append(packages, &pkg.Package{
Name: name,
Version: version,
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
Name: name,
Version: version,
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkgMeta,
})
}
}

View file

@ -11,16 +11,98 @@ import (
func TestParseComposerFileLock(t *testing.T) {
expected := []*pkg.Package{
{
Name: "adoy/fastcgi-client",
Version: "1.0.2",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
Name: "adoy/fastcgi-client",
Version: "1.0.2",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{
Name: "adoy/fastcgi-client",
Version: "1.0.2",
Source: pkg.PhpComposerExternalReference{
Type: "git",
URL: "https://github.com/adoy/PHP-FastCGI-Client.git",
Reference: "6d9a552f0206a1db7feb442824540aa6c55e5b27",
},
Dist: pkg.PhpComposerExternalReference{
Type: "zip",
URL: "https://api.github.com/repos/adoy/PHP-FastCGI-Client/zipball/6d9a552f0206a1db7feb442824540aa6c55e5b27",
Reference: "6d9a552f0206a1db7feb442824540aa6c55e5b27",
},
Type: "library",
NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{
{
Name: "Pierrick Charron",
Email: "pierrick@adoy.net",
},
},
Description: "Lightweight, single file FastCGI client for PHP.",
Keywords: []string{
"fastcgi",
"fcgi",
},
Time: "2019-12-11T13:49:21+00:00",
},
},
{
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{
Name: "alcaeus/mongo-php-adapter",
Version: "1.1.11",
Source: pkg.PhpComposerExternalReference{
Type: "git",
URL: "https://github.com/alcaeus/mongo-php-adapter.git",
Reference: "43b6add94c8b4cb9890d662cba4c0defde733dcf",
},
Dist: pkg.PhpComposerExternalReference{
Type: "zip",
URL: "https://api.github.com/repos/alcaeus/mongo-php-adapter/zipball/43b6add94c8b4cb9890d662cba4c0defde733dcf",
Reference: "43b6add94c8b4cb9890d662cba4c0defde733dcf",
},
Require: map[string]string{
"ext-ctype": "*",
"ext-hash": "*",
"ext-mongodb": "^1.2.0",
"mongodb/mongodb": "^1.0.1",
"php": "^5.6 || ^7.0",
},
Provide: map[string]string{
"ext-mongo": "1.6.14",
},
RequireDev: map[string]string{
"phpunit/phpunit": "^5.7.27 || ^6.0 || ^7.0",
"squizlabs/php_codesniffer": "^3.2",
},
Type: "library",
NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{
{
Name: "alcaeus",
Email: "alcaeus@alcaeus.org",
},
{
Name: "Olivier Lechevalier",
Email: "olivier.lechevalier@gmail.com",
},
},
Description: "Adapter to provide ext-mongo interface on top of mongo-php-libary",
Keywords: []string{
"database",
"mongodb",
},
Time: "2019-11-11T20:47:32+00:00",
},
},
}
fixture, err := os.Open("test-fixtures/composer.lock")
@ -33,9 +115,8 @@ func TestParseComposerFileLock(t *testing.T) {
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
differences := deep.Equal(expected, actual)
if differences != nil {
t.Errorf("returned package list differed from expectation: %+v", differences)
}
for _, d := range deep.Equal(expected, actual) {
t.Errorf("diff: %+v", d)
}
}

View file

@ -12,19 +12,19 @@ import (
// Note: composer version 2 introduced a new structure for the installed.json file, so we support both
type installedJSONComposerV2 struct {
Packages []Dependency `json:"packages"`
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
}
func (w *installedJSONComposerV2) UnmarshalJSON(data []byte) error {
type compv2 struct {
Packages []Dependency `json:"packages"`
Packages []pkg.PhpComposerJSONMetadata `json:"packages"`
}
compv2er := new(compv2)
err := json.Unmarshal(data, &compv2er)
if err != nil {
// If we had an err or, we may be dealing with a composer v.1 installed.json
// which should be all arrays
var packages []Dependency
var packages []pkg.PhpComposerJSONMetadata
err := json.Unmarshal(data, &packages)
if err != nil {
return err
@ -55,10 +55,12 @@ func parseInstalledJSON(_ string, reader io.Reader) ([]*pkg.Package, []artifact.
version := pkgMeta.Version
name := pkgMeta.Name
packages = append(packages, &pkg.Package{
Name: name,
Version: version,
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
Name: name,
Version: version,
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkgMeta,
})
}
}

View file

@ -8,21 +8,118 @@ import (
"github.com/go-test/deep"
)
var expectedInstalledJsonPackages = []*pkg.Package{
{
Name: "asm89/stack-cors",
Version: "1.3.0",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{
Name: "asm89/stack-cors",
Version: "1.3.0",
Source: pkg.PhpComposerExternalReference{
Type: "git",
URL: "https://github.com/asm89/stack-cors.git",
Reference: "b9c31def6a83f84b4d4a40d35996d375755f0e08",
},
Dist: pkg.PhpComposerExternalReference{
Type: "zip",
URL: "https://api.github.com/repos/asm89/stack-cors/zipball/b9c31def6a83f84b4d4a40d35996d375755f0e08",
Reference: "b9c31def6a83f84b4d4a40d35996d375755f0e08",
},
Require: map[string]string{
"php": ">=5.5.9",
"symfony/http-foundation": "~2.7|~3.0|~4.0|~5.0",
"symfony/http-kernel": "~2.7|~3.0|~4.0|~5.0",
},
RequireDev: map[string]string{
"phpunit/phpunit": "^5.0 || ^4.8.10",
"squizlabs/php_codesniffer": "^2.3",
},
Time: "2019-12-24T22:41:47+00:00",
Type: "library",
NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{
{
Name: "Alexander",
Email: "iam.asm89@gmail.com",
},
},
Description: "Cross-origin resource sharing library and stack middleware",
Homepage: "https://github.com/asm89/stack-cors",
Keywords: []string{
"cors",
"stack",
},
},
},
{
Name: "behat/mink",
Version: "v1.8.1",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
MetadataType: pkg.PhpComposerJSONMetadataType,
Metadata: pkg.PhpComposerJSONMetadata{
Name: "behat/mink",
Version: "v1.8.1",
Source: pkg.PhpComposerExternalReference{
Type: "git",
URL: "https://github.com/minkphp/Mink.git",
Reference: "07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
},
Dist: pkg.PhpComposerExternalReference{
Type: "zip",
URL: "https://api.github.com/repos/minkphp/Mink/zipball/07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
Reference: "07c6a9fe3fa98c2de074b25d9ed26c22904e3887",
},
Require: map[string]string{
"php": ">=5.3.1",
"symfony/css-selector": "^2.7|^3.0|^4.0|^5.0",
},
RequireDev: map[string]string{
"phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20",
"symfony/debug": "^2.7|^3.0|^4.0",
"symfony/phpunit-bridge": "^3.4.38 || ^5.0.5",
},
Suggest: map[string]string{
"behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
"behat/mink-goutte-driver": "fast headless driver for any app without JS emulation",
"behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
"behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
"dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)",
},
Time: "2020-03-11T15:45:53+00:00",
Type: "library",
NotificationURL: "https://packagist.org/downloads/",
License: []string{
"MIT",
},
Authors: []pkg.PhpComposerAuthors{
{
Name: "Konstantin Kudryashov",
Email: "ever.zet@gmail.com",
Homepage: "http://everzet.com",
},
},
Description: "Browser controller/emulator abstraction for PHP",
Homepage: "http://mink.behat.org/",
Keywords: []string{
"browser",
"testing",
"web",
},
},
},
}
func TestParseInstalledJsonComposerV1(t *testing.T) {
expected := []*pkg.Package{
{
Name: "asm89/stack-cors",
Version: "1.3.0",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
{
Name: "behat/mink",
Version: "v1.8.1",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
}
fixture, err := os.Open("test-fixtures/vendor/composer_1/installed.json")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
@ -33,28 +130,13 @@ func TestParseInstalledJsonComposerV1(t *testing.T) {
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
differences := deep.Equal(expected, actual)
differences := deep.Equal(expectedInstalledJsonPackages, actual)
if differences != nil {
t.Errorf("returned package list differed from expectation: %+v", differences)
}
}
func TestParseInstalledJsonComposerV2(t *testing.T) {
expected := []*pkg.Package{
{
Name: "asm89/stack-cors",
Version: "1.3.0",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
{
Name: "behat/mink",
Version: "v1.8.1",
Language: pkg.PHP,
Type: pkg.PhpComposerPkg,
},
}
fixture, err := os.Open("test-fixtures/vendor/composer_2/installed.json")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
@ -65,7 +147,7 @@ func TestParseInstalledJsonComposerV2(t *testing.T) {
if err != nil {
t.Fatalf("failed to parse requirements: %+v", err)
}
differences := deep.Equal(expected, actual)
differences := deep.Equal(expectedInstalledJsonPackages, actual)
if differences != nil {
t.Errorf("returned package list differed from expectation: %+v", differences)
}

View file

@ -5,6 +5,7 @@ type MetadataType string
const (
// this is the full set of data shapes that can be represented within the pkg.Package.Metadata field
UnknownMetadataType MetadataType = "UnknownMetadata"
ApkMetadataType MetadataType = "ApkMetadata"
DpkgMetadataType MetadataType = "DpkgMetadata"
@ -16,6 +17,7 @@ const (
RustCargoPackageMetadataType MetadataType = "RustCargoPackageMetadata"
KbPackageMetadataType MetadataType = "KbPackageMetadata"
GolangBinMetadataType MetadataType = "GolangBinMetadata"
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
)
var AllMetadataTypes = []MetadataType{
@ -29,4 +31,5 @@ var AllMetadataTypes = []MetadataType{
RustCargoPackageMetadataType,
KbPackageMetadataType,
GolangBinMetadataType,
PhpComposerJSONMetadataType,
}

View file

@ -0,0 +1,67 @@
package pkg
import (
"strings"
"github.com/anchore/packageurl-go"
)
// PhpComposerJSONMetadata represents information found from composer v1/v2 "installed.json" files as well as composer.lock files
type PhpComposerJSONMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
Source PhpComposerExternalReference `json:"source"`
Dist PhpComposerExternalReference `json:"dist"`
Require map[string]string `json:"require,omitempty"`
Provide map[string]string `json:"provide,omitempty"`
RequireDev map[string]string `json:"require-dev,omitempty"`
Suggest map[string]string `json:"suggest,omitempty"`
Type string `json:"type,omitempty"`
NotificationURL string `json:"notification-url,omitempty"`
Bin []string `json:"bin,omitempty"`
License []string `json:"license,omitempty"`
Authors []PhpComposerAuthors `json:"authors,omitempty"`
Description string `json:"description,omitempty"`
Homepage string `json:"homepage,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Time string `json:"time,omitempty"`
}
type PhpComposerExternalReference struct {
Type string `json:"type"`
URL string `json:"url"`
Reference string `json:"reference"`
Shasum string `json:"shasum,omitempty"`
}
type PhpComposerAuthors struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
Homepage string `json:"homepage,omitempty"`
}
func (m PhpComposerJSONMetadata) PackageURL() 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()
}

View file

@ -0,0 +1,51 @@
package pkg
import (
"github.com/anchore/syft/syft/linux"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestPhpComposerJsonMetadata_pURL(t *testing.T) {
tests := []struct {
name string
distro *linux.Release
metadata PhpComposerJSONMetadata
expected string
}{
{
name: "with extractable vendor",
metadata: PhpComposerJSONMetadata{
Name: "ven/name",
Version: "1.0.1",
},
expected: "pkg:composer/ven/name@1.0.1",
}, {
name: "name with slashes (invalid)",
metadata: PhpComposerJSONMetadata{
Name: "ven/name/component",
Version: "1.0.1",
},
expected: "pkg:composer/ven/name-component@1.0.1",
},
{
name: "unknown vendor",
metadata: 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 := test.metadata.PackageURL()
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
})
}
}