mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
feat: Add Wordpress cataloger (#2218)
* Closes #1911 Wordpress cataloger Signed-off-by: disc <a.hacicheant@gmail.com> * Fixed a few unit tests and static analizer notices Signed-off-by: disc <a.hacicheant@gmail.com> * Updated `README.md` Signed-off-by: disc <a.hacicheant@gmail.com> * Fixed `golangci-lint` notices Added integration test for `wordpress-plugin` Signed-off-by: disc <a.hacicheant@gmail.com> * Fixed `gosimports` notices Signed-off-by: disc <a.hacicheant@gmail.com> * Updated `json schema` version Signed-off-by: disc <a.hacicheant@gmail.com> * Fixed CLI tests, increased expected package count Signed-off-by: disc <a.hacicheant@gmail.com> * Read first 4Kb of a plugins file's content Signed-off-by: disc <a.hacicheant@gmail.com> * replace JSON schema version Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * change wording on source info for wordpress packages Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * Minor changes after a huge refactoring Signed-off-by: disc <a.hacicheant@gmail.com> * Removed unused files Signed-off-by: disc <a.hacicheant@gmail.com> * Updated schema Signed-off-by: disc <a.hacicheant@gmail.com> * Fixed integration tests Signed-off-by: disc <a.hacicheant@gmail.com> * fix integration tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * Renamed `metadata.Name` to `metadata.PluginInstallDirectory` Signed-off-by: disc <a.hacicheant@gmail.com> * rename fields to be compliant with json conventions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: disc <a.hacicheant@gmail.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
98b700e83c
commit
96ee2db875
@ -54,6 +54,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
|
||||
- Ruby (gem)
|
||||
- Rust (cargo.lock)
|
||||
- Swift (cocoapods, swift-package-manager)
|
||||
- Wordpress plugins
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@ -452,4 +452,12 @@ var commonTestCases = []testCase{
|
||||
"glibc": "2.34-210",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "find wordpress plugins",
|
||||
pkgType: pkg.WordpressPluginPkg,
|
||||
pkgLanguage: pkg.PHP,
|
||||
pkgInfo: map[string]string{
|
||||
"Akismet Anti-spam: Spam Protection": "5.3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -123,7 +123,6 @@ func TestPkgCoverageImage(t *testing.T) {
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", pkgCount, len(c.pkgInfo))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@ -211,7 +210,6 @@ func TestPkgCoverageDirectory(t *testing.T) {
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", actualPkgCount, len(test.pkgInfo))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@ -249,7 +247,6 @@ func TestPkgCoverageImage_HasEvidence(t *testing.T) {
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
||||
for a := range sbom.Artifacts.Packages.Enumerate(c.pkgType) {
|
||||
assert.NotEmpty(t, a.Locations.ToSlice(), "package %q has no locations (type=%q)", a.Name, a.Type)
|
||||
for _, l := range a.Locations.ToSlice() {
|
||||
@ -259,7 +256,6 @@ func TestPkgCoverageImage_HasEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@ -279,7 +275,6 @@ func TestPkgCoverageDirectory_HasEvidence(t *testing.T) {
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
||||
for a := range sbom.Artifacts.Packages.Enumerate(c.pkgType) {
|
||||
assert.NotEmpty(t, a.Locations.ToSlice(), "package %q has no locations (type=%q)", a.Name, a.Type)
|
||||
for _, l := range a.Locations.ToSlice() {
|
||||
@ -289,7 +284,6 @@ func TestPkgCoverageDirectory_HasEvidence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Akismet
|
||||
*/
|
||||
/*
|
||||
Plugin Name:Akismet Anti-spam: Spam Protection
|
||||
Plugin URI: https://akismet.com/
|
||||
Description: Used by millions, Akismet is quite possibly the best way in the world to <strong>protect your blog from spam</strong>. Akismet Anti-spam keeps your site protected even while you sleep. To get started: activate the Akismet plugin and then go to your Akismet Settings page to set up your API key.
|
||||
Version: 5.3
|
||||
Requires at least: 5.8
|
||||
Requires PHP: 5.6.20
|
||||
Author: Automattic - Anti-spam Team
|
||||
Author URI: https://automattic.com/wordpress-plugins/
|
||||
License: GPLv2 or later
|
||||
Text Domain: akismet
|
||||
*/
|
||||
// rest of plugin's code ...
|
||||
@ -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.3"
|
||||
JSONSchemaVersion = "16.0.4"
|
||||
)
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/wordpress"
|
||||
)
|
||||
|
||||
//nolint:funlen
|
||||
@ -125,5 +126,6 @@ func DefaultPackageTaskFactories() PackageTaskFactories {
|
||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel",
|
||||
),
|
||||
newSimplePackageTaskFactory(sbomCataloger.NewCataloger, "sbom"), // note: not evidence of installed packages
|
||||
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
|
||||
}
|
||||
}
|
||||
|
||||
2023
schema/json/schema-11.0.2.json
Normal file
2023
schema/json/schema-11.0.2.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -2242,6 +2242,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"cpes": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/CPE"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"licenses": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/License"
|
||||
|
||||
2304
schema/json/schema-16.0.4.json
Normal file
2304
schema/json/schema-16.0.4.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.3/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.4/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@ -1493,6 +1493,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/SwiftPackageManagerLockEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/WordpressPluginEntry"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -2268,6 +2271,23 @@
|
||||
"revision"
|
||||
]
|
||||
},
|
||||
"WordpressPluginEntry": {
|
||||
"properties": {
|
||||
"pluginInstallDirectory": {
|
||||
"type": "string"
|
||||
},
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"authorUri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pluginInstallDirectory"
|
||||
]
|
||||
},
|
||||
"cpes": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/CPE"
|
||||
|
||||
@ -60,6 +60,8 @@ func SourceInfo(p pkg.Package) string {
|
||||
answer = "acquired package info from resolved Swift package manifest"
|
||||
case pkg.GithubActionPkg, pkg.GithubActionWorkflowPkg:
|
||||
answer = "acquired package info from GitHub Actions workflow file or composite action file"
|
||||
case pkg.WordpressPluginPkg:
|
||||
answer = "acquired package info from found wordpress plugin PHP source files"
|
||||
default:
|
||||
answer = "acquired package info from the following paths"
|
||||
}
|
||||
|
||||
@ -263,6 +263,14 @@ func Test_SourceInfo(t *testing.T) {
|
||||
"from GitHub Actions workflow file or composite action file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.WordpressPluginPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"acquired package info from found wordpress plugin PHP source files",
|
||||
},
|
||||
},
|
||||
}
|
||||
var pkgTypes []pkg.Type
|
||||
for _, test := range tests {
|
||||
|
||||
@ -46,6 +46,7 @@ func AllTypes() []any {
|
||||
pkg.RustBinaryAuditEntry{},
|
||||
pkg.RustCargoLockEntry{},
|
||||
pkg.SwiftPackageManagerResolvedEntry{},
|
||||
pkg.WordpressPluginEntry{},
|
||||
pkg.YarnLockEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +102,7 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.SwiftPackageManagerResolvedEntry{}, "swift-package-manager-lock-entry", "SwiftPackageManagerMetadata"),
|
||||
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
|
||||
jsonNamesWithoutLookup(pkg.RustBinaryAuditEntry{}, "rust-cargo-audit-entry", "RustCargoPackageMetadata"), // the legacy value is split into two types, where the other is preferred
|
||||
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
|
||||
)
|
||||
|
||||
func expandLegacyNameVariants(names ...string) []string {
|
||||
|
||||
@ -145,6 +145,7 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE {
|
||||
return result
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func candidateVendors(p pkg.Package) []string {
|
||||
// in ecosystems where the packaging metadata does not have a clear field to indicate a vendor (or a field that
|
||||
// could be interpreted indirectly as such) the project name tends to be a common stand in. Examples of this
|
||||
@ -184,6 +185,9 @@ func candidateVendors(p pkg.Package) []string {
|
||||
vendors.union(candidateVendorsForAPK(p))
|
||||
case pkg.NpmPackage:
|
||||
vendors.union(candidateVendorsForJavascript(p))
|
||||
case pkg.WordpressPluginEntry:
|
||||
vendors.clear()
|
||||
vendors.union(candidateVendorsForWordpressPlugin(p))
|
||||
}
|
||||
|
||||
// We should no longer be generating vendor candidates with these values ["" and "*"]
|
||||
@ -243,6 +247,11 @@ func candidateProducts(p pkg.Package) []string {
|
||||
products.union(candidateProductsForAPK(p))
|
||||
}
|
||||
|
||||
if _, hasWordpressMetadata := p.Metadata.(pkg.WordpressPluginEntry); hasWordpressMetadata {
|
||||
products.clear()
|
||||
products.union(candidateProductsForWordpressPlugin(p))
|
||||
}
|
||||
|
||||
// it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value)
|
||||
products.removeByValue("")
|
||||
products.removeByValue("*")
|
||||
|
||||
56
syft/pkg/cataloger/internal/cpegenerate/wordpress.go
Normal file
56
syft/pkg/cataloger/internal/cpegenerate/wordpress.go
Normal file
@ -0,0 +1,56 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var (
|
||||
vendorFromURLRegexp = regexp.MustCompile(`^https?://(www.)?(?P<vendor>.+)\.\w/?`)
|
||||
)
|
||||
|
||||
func candidateVendorsForWordpressPlugin(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.WordpressPluginEntry)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
if metadata.AuthorURI != "" {
|
||||
matchMap := internal.MatchNamedCaptureGroups(vendorFromURLRegexp, metadata.AuthorURI)
|
||||
if vendor, ok := matchMap["vendor"]; ok && vendor != "" {
|
||||
vendors.addValue(vendor)
|
||||
}
|
||||
} else {
|
||||
// add plugin_name + _project as a vendor if no Author URI found
|
||||
vendors.addValue(fmt.Sprintf("%s_project", normalizeWordpressPluginName(p.Name)))
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
func candidateProductsForWordpressPlugin(p pkg.Package) fieldCandidateSet {
|
||||
metadata, ok := p.Metadata.(pkg.WordpressPluginEntry)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
products := newFieldCandidateSet()
|
||||
|
||||
products.addValue(normalizeWordpressPluginName(p.Name))
|
||||
products.addValue(normalizeWordpressPluginName(metadata.PluginInstallDirectory))
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
func normalizeWordpressPluginName(name string) string {
|
||||
name = strings.TrimSpace(strings.ToLower(name))
|
||||
for _, value := range []string{" "} {
|
||||
name = strings.ReplaceAll(name, value, "_")
|
||||
}
|
||||
return name
|
||||
}
|
||||
135
syft/pkg/cataloger/internal/cpegenerate/wordpress_test.go
Normal file
135
syft/pkg/cataloger/internal/cpegenerate/wordpress_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_candidateVendorsForWordpressPlugin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Akismet Anti-spam: Spam Protection",
|
||||
pkg: pkg.Package{
|
||||
Name: "Akismet Anti-spam: Spam Protection",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "akismet",
|
||||
Author: "Automattic - Anti-spam Team",
|
||||
AuthorURI: "https://automattic.com/wordpress-plugins/",
|
||||
},
|
||||
},
|
||||
expected: []string{"automattic"},
|
||||
},
|
||||
{
|
||||
name: "All-in-One WP Migration",
|
||||
pkg: pkg.Package{
|
||||
Name: "All-in-One WP Migration",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "all-in-one-wp-migration",
|
||||
AuthorURI: "https://servmask.com",
|
||||
},
|
||||
},
|
||||
expected: []string{"servmask"},
|
||||
},
|
||||
{
|
||||
name: "Booking Ultra Pro Appointments Booking Calendar",
|
||||
pkg: pkg.Package{
|
||||
Name: "Booking Ultra Pro Appointments Booking Calendar",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "booking-ultra-pro",
|
||||
Author: "Booking Ultra Pro",
|
||||
AuthorURI: "https://bookingultrapro.com/",
|
||||
},
|
||||
},
|
||||
expected: []string{"bookingultrapro"},
|
||||
},
|
||||
{
|
||||
name: "Coming Soon Chop Chop",
|
||||
pkg: pkg.Package{
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "cc-coming-soon",
|
||||
Author: "Chop-Chop.org",
|
||||
AuthorURI: "https://www.chop-chop.org",
|
||||
},
|
||||
},
|
||||
expected: []string{"chop-chop"},
|
||||
},
|
||||
{
|
||||
name: "Access Code Feeder",
|
||||
pkg: pkg.Package{
|
||||
Name: "Access Code Feeder",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "access-code-feeder",
|
||||
},
|
||||
},
|
||||
// When a plugin as no `Author URI` use plugin_name + _project as a vendor
|
||||
expected: []string{"access_code_feeder_project"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := candidateVendorsForWordpressPlugin(test.pkg).uniqueValues()
|
||||
assert.ElementsMatch(t, test.expected, actual, "different vendors")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_candidateProductsWordpressPlugin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "All-in-One WP Migration",
|
||||
pkg: pkg.Package{
|
||||
Name: "All-in-One WP Migration",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "all-in-one-wp-migration",
|
||||
},
|
||||
},
|
||||
expected: []string{"all-in-one_wp_migration", "all-in-one-wp-migration"},
|
||||
},
|
||||
{
|
||||
name: "Akismet Anti-spam: Spam Protection",
|
||||
pkg: pkg.Package{
|
||||
Name: "Akismet Anti-spam: Spam Protection",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "akismet",
|
||||
},
|
||||
},
|
||||
expected: []string{"akismet_anti-spam:_spam_protection", "akismet"},
|
||||
},
|
||||
{
|
||||
name: "Access Code Feeder",
|
||||
pkg: pkg.Package{
|
||||
Name: "Access Code Feeder",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "access-code-feeder",
|
||||
},
|
||||
},
|
||||
expected: []string{"access_code_feeder", "access-code-feeder"},
|
||||
},
|
||||
{
|
||||
name: "CampTix Event Ticketing",
|
||||
pkg: pkg.Package{
|
||||
Name: "CampTix Event Ticketing",
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "camptix",
|
||||
},
|
||||
},
|
||||
expected: []string{"camptix_event_ticketing", "camptix"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, candidateProductsForWordpressPlugin(test.pkg).uniqueValues(), "different products")
|
||||
})
|
||||
}
|
||||
}
|
||||
16
syft/pkg/cataloger/wordpress/cataloger.go
Normal file
16
syft/pkg/cataloger/wordpress/cataloger.go
Normal file
@ -0,0 +1,16 @@
|
||||
package wordpress
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
const (
|
||||
catalogerName = "wordpress-plugins-cataloger"
|
||||
wordpressPluginsGlob = "**/wp-content/plugins/*/*.php"
|
||||
)
|
||||
|
||||
func NewWordpressPluginCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger(catalogerName).
|
||||
WithParserByGlobs(parseWordpressPluginFiles, wordpressPluginsGlob)
|
||||
}
|
||||
33
syft/pkg/cataloger/wordpress/cataloger_test.go
Normal file
33
syft/pkg/cataloger/wordpress/cataloger_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package wordpress
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_WordpressPlugin_Globs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "obtain wordpress plugin files",
|
||||
fixture: "test-fixtures/glob-paths",
|
||||
expected: []string{
|
||||
"wp-content/plugins/akismet/akismet.php",
|
||||
"wp-content/plugins/all-in-one-wp-migration/all-in-one-wp-migration.php",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, test.fixture).
|
||||
ExpectsResolverContentQueries(test.expected).
|
||||
TestCataloger(t, NewWordpressPluginCataloger())
|
||||
})
|
||||
}
|
||||
}
|
||||
31
syft/pkg/cataloger/wordpress/package.go
Normal file
31
syft/pkg/cataloger/wordpress/package.go
Normal file
@ -0,0 +1,31 @@
|
||||
package wordpress
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func newWordpressPluginPackage(name, version string, m pluginData, location file.Location) pkg.Package {
|
||||
meta := pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: m.PluginInstallDirectory,
|
||||
Author: m.Author,
|
||||
AuthorURI: m.AuthorURI,
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Language: pkg.PHP,
|
||||
Type: pkg.WordpressPluginPkg,
|
||||
Metadata: meta,
|
||||
}
|
||||
|
||||
if len(m.Licenses) > 0 {
|
||||
p.Licenses = pkg.NewLicenseSet(pkg.NewLicense(m.Licenses[0]))
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
}
|
||||
98
syft/pkg/cataloger/wordpress/parse_plugin.go
Normal file
98
syft/pkg/cataloger/wordpress/parse_plugin.go
Normal file
@ -0,0 +1,98 @@
|
||||
package wordpress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"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"
|
||||
)
|
||||
|
||||
const contentBufferSize = 4096
|
||||
|
||||
var patterns = map[string]*regexp.Regexp{
|
||||
// match example: "Plugin Name: WP Migration" ---> WP Migration
|
||||
"name": regexp.MustCompile(`(?i)plugin name:\s*(?P<name>.+)`),
|
||||
|
||||
// match example: "Version: 5.3" ---> 5.3
|
||||
"version": regexp.MustCompile(`(?i)version:\s*(?P<version>[\d.]+)`),
|
||||
|
||||
// match example: "License: GPLv3" ---> GPLv3
|
||||
"license": regexp.MustCompile(`(?i)license:\s*(?P<license>\w+)`),
|
||||
|
||||
// match example: "Author: MonsterInsights" ---> MonsterInsights
|
||||
"author": regexp.MustCompile(`(?i)author:\s*(?P<author>.+)`),
|
||||
|
||||
// match example: "Author URI: https://servmask.com/" ---> https://servmask.com/
|
||||
"author_uri": regexp.MustCompile(`(?i)author uri:\s*(?P<author_uri>.+)`),
|
||||
}
|
||||
|
||||
type pluginData struct {
|
||||
Licenses []string `mapstructure:"licenses" json:"licenses,omitempty"`
|
||||
pkg.WordpressPluginEntry `mapstructure:",squash" json:",inline"`
|
||||
}
|
||||
|
||||
func parseWordpressPluginFiles(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
var pkgs []pkg.Package
|
||||
var fields = make(map[string]interface{})
|
||||
buffer := make([]byte, contentBufferSize)
|
||||
|
||||
_, err := reader.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read %s file: %w", reader.Location.Path(), err)
|
||||
}
|
||||
|
||||
fileContent := string(buffer)
|
||||
for field, pattern := range patterns {
|
||||
matchMap := internal.MatchNamedCaptureGroups(pattern, fileContent)
|
||||
if value := matchMap[field]; value != "" {
|
||||
fields[field] = value
|
||||
}
|
||||
}
|
||||
|
||||
name, nameOk := fields["name"]
|
||||
version, versionOk := fields["version"]
|
||||
|
||||
// get a plugin name from a plugin's directory name
|
||||
pluginInstallDirectory := filepath.Base(filepath.Dir(reader.RealPath))
|
||||
|
||||
if nameOk && name != "" && versionOk && version != "" {
|
||||
var metadata pluginData
|
||||
|
||||
metadata.PluginInstallDirectory = pluginInstallDirectory
|
||||
|
||||
author, authorOk := fields["author"]
|
||||
if authorOk && author != "" {
|
||||
metadata.Author = author.(string)
|
||||
}
|
||||
|
||||
authorURI, authorURIOk := fields["author_uri"]
|
||||
if authorURIOk && authorURI != "" {
|
||||
metadata.AuthorURI = authorURI.(string)
|
||||
}
|
||||
|
||||
license, licenseOk := fields["license"]
|
||||
if licenseOk && license != "" {
|
||||
licenses := make([]string, 0)
|
||||
licenses = append(licenses, license.(string))
|
||||
metadata.Licenses = licenses
|
||||
}
|
||||
|
||||
pkgs = append(
|
||||
pkgs,
|
||||
newWordpressPluginPackage(
|
||||
name.(string),
|
||||
version.(string),
|
||||
metadata,
|
||||
reader.Location,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return pkgs, nil, nil
|
||||
}
|
||||
32
syft/pkg/cataloger/wordpress/parse_plugin_test.go
Normal file
32
syft/pkg/cataloger/wordpress/parse_plugin_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package wordpress
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func TestParseWordpressPluginFiles(t *testing.T) {
|
||||
fixture := "test-fixtures/glob-paths/wp-content/plugins/akismet/akismet.php"
|
||||
locations := file.NewLocationSet(file.NewLocation(fixture))
|
||||
|
||||
var expectedPkg = pkg.Package{
|
||||
Name: "Akismet Anti-spam: Spam Protection",
|
||||
Version: "5.3",
|
||||
Locations: locations,
|
||||
Type: pkg.WordpressPluginPkg,
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocations("GPLv2"),
|
||||
),
|
||||
Language: pkg.PHP,
|
||||
Metadata: pkg.WordpressPluginEntry{
|
||||
PluginInstallDirectory: "akismet",
|
||||
Author: "Automattic - Anti-spam Team",
|
||||
AuthorURI: "https://automattic.com/wordpress-plugins/",
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestFileParser(t, fixture, parseWordpressPluginFiles, []pkg.Package{expectedPkg}, nil)
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// stub file
|
||||
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* @package Akismet
|
||||
*/
|
||||
/*
|
||||
Plugin Name:Akismet Anti-spam: Spam Protection
|
||||
Plugin URI: https://akismet.com/
|
||||
Description: Used by millions, Akismet is quite possibly the best way in the world to <strong>protect your blog from spam</strong>. Akismet Anti-spam keeps your site protected even while you sleep. To get started: activate the Akismet plugin and then go to your Akismet Settings page to set up your API key.
|
||||
Version: 5.3
|
||||
Requires at least: 5.8
|
||||
Requires PHP: 5.6.20
|
||||
Author: Automattic - Anti-spam Team
|
||||
Author URI: https://automattic.com/wordpress-plugins/
|
||||
License: GPLv2 or later
|
||||
Text Domain: akismet
|
||||
*/
|
||||
// rest of plugin's code ...
|
||||
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: All-in-One WP Migration
|
||||
* Plugin URI: https://servmask.com/
|
||||
* Description: Migration tool for all your blog data. Import or Export your blog content with a single click.
|
||||
* Author: ServMask
|
||||
* Author URI: https://servmask.com/
|
||||
* Version: 7.78
|
||||
*/
|
||||
// rest of plugin's code ...
|
||||
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// stub file
|
||||
@ -40,6 +40,7 @@ const (
|
||||
RpmPkg Type = "rpm"
|
||||
RustPkg Type = "rust-crate"
|
||||
SwiftPkg Type = "swift"
|
||||
WordpressPluginPkg Type = "wordpress-plugin"
|
||||
)
|
||||
|
||||
// AllPkgs represents all supported package types
|
||||
@ -73,6 +74,7 @@ var AllPkgs = []Type{
|
||||
RpmPkg,
|
||||
RustPkg,
|
||||
SwiftPkg,
|
||||
WordpressPluginPkg,
|
||||
}
|
||||
|
||||
// PackageURLType returns the PURL package type for the current package.
|
||||
@ -131,6 +133,8 @@ func (t Type) PackageURLType() string {
|
||||
return "cargo"
|
||||
case SwiftPkg:
|
||||
return packageurl.TypeSwift
|
||||
case WordpressPluginPkg:
|
||||
return "wordpress-plugin"
|
||||
default:
|
||||
// TODO: should this be a "generic" purl type instead?
|
||||
return ""
|
||||
@ -201,6 +205,8 @@ func TypeByName(name string) Type {
|
||||
return Rpkg
|
||||
case packageurl.TypeSwift:
|
||||
return SwiftPkg
|
||||
case "wordpress-plugin":
|
||||
return WordpressPluginPkg
|
||||
default:
|
||||
return UnknownPkg
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@ func TestTypeFromPURL(t *testing.T) {
|
||||
expectedTypes.Remove(string(BinaryPkg))
|
||||
expectedTypes.Remove(string(LinuxKernelModulePkg))
|
||||
expectedTypes.Remove(string(GithubActionPkg), string(GithubActionWorkflowPkg))
|
||||
expectedTypes.Remove(string(WordpressPluginPkg))
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(string(test.expected), func(t *testing.T) {
|
||||
|
||||
8
syft/pkg/wordpress.go
Normal file
8
syft/pkg/wordpress.go
Normal file
@ -0,0 +1,8 @@
|
||||
package pkg
|
||||
|
||||
// WordpressPluginEntry represents all metadata parsed from the wordpress plugin file
|
||||
type WordpressPluginEntry struct {
|
||||
PluginInstallDirectory string `mapstructure:"pluginInstallDirectory" json:"pluginInstallDirectory"`
|
||||
Author string `mapstructure:"author" json:"author,omitempty"`
|
||||
AuthorURI string `mapstructure:"authorUri" json:"authorUri,omitempty"`
|
||||
}
|
||||
@ -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 = 27
|
||||
coverageImageSquashedPackageCount = 28
|
||||
)
|
||||
|
||||
func TestPackagesCmdFlags(t *testing.T) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user