mirror of
https://github.com/anchore/syft.git
synced 2026-02-14 11:36:42 +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)
|
- Ruby (gem)
|
||||||
- Rust (cargo.lock)
|
- Rust (cargo.lock)
|
||||||
- Swift (cocoapods, swift-package-manager)
|
- Swift (cocoapods, swift-package-manager)
|
||||||
|
- Wordpress plugins
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@ -452,4 +452,12 @@ var commonTestCases = []testCase{
|
|||||||
"glibc": "2.34-210",
|
"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))
|
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))
|
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 {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
|
||||||
for a := range sbom.Artifacts.Packages.Enumerate(c.pkgType) {
|
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)
|
assert.NotEmpty(t, a.Locations.ToSlice(), "package %q has no locations (type=%q)", a.Name, a.Type)
|
||||||
for _, l := range a.Locations.ToSlice() {
|
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 {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
|
||||||
for a := range sbom.Artifacts.Packages.Enumerate(c.pkgType) {
|
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)
|
assert.NotEmpty(t, a.Locations.ToSlice(), "package %q has no locations (type=%q)", a.Name, a.Type)
|
||||||
for _, l := range a.Locations.ToSlice() {
|
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 (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// 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.
|
// 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"
|
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||||
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
sbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/sbom"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/wordpress"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:funlen
|
//nolint:funlen
|
||||||
@ -125,5 +126,6 @@ func DefaultPackageTaskFactories() PackageTaskFactories {
|
|||||||
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel",
|
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel",
|
||||||
),
|
),
|
||||||
newSimplePackageTaskFactory(sbomCataloger.NewCataloger, "sbom"), // note: not evidence of installed packages
|
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"
|
"type": "array"
|
||||||
},
|
},
|
||||||
|
"cpes": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/CPE"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"licenses": {
|
"licenses": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/License"
|
"$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",
|
"$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",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@ -1493,6 +1493,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/$defs/SwiftPackageManagerLockEntry"
|
"$ref": "#/$defs/SwiftPackageManagerLockEntry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/WordpressPluginEntry"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -2268,6 +2271,23 @@
|
|||||||
"revision"
|
"revision"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"WordpressPluginEntry": {
|
||||||
|
"properties": {
|
||||||
|
"pluginInstallDirectory": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"authorUri": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"pluginInstallDirectory"
|
||||||
|
]
|
||||||
|
},
|
||||||
"cpes": {
|
"cpes": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/CPE"
|
"$ref": "#/$defs/CPE"
|
||||||
|
|||||||
@ -60,6 +60,8 @@ func SourceInfo(p pkg.Package) string {
|
|||||||
answer = "acquired package info from resolved Swift package manifest"
|
answer = "acquired package info from resolved Swift package manifest"
|
||||||
case pkg.GithubActionPkg, pkg.GithubActionWorkflowPkg:
|
case pkg.GithubActionPkg, pkg.GithubActionWorkflowPkg:
|
||||||
answer = "acquired package info from GitHub Actions workflow file or composite action file"
|
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:
|
default:
|
||||||
answer = "acquired package info from the following paths"
|
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",
|
"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
|
var pkgTypes []pkg.Type
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|||||||
@ -46,6 +46,7 @@ func AllTypes() []any {
|
|||||||
pkg.RustBinaryAuditEntry{},
|
pkg.RustBinaryAuditEntry{},
|
||||||
pkg.RustCargoLockEntry{},
|
pkg.RustCargoLockEntry{},
|
||||||
pkg.SwiftPackageManagerResolvedEntry{},
|
pkg.SwiftPackageManagerResolvedEntry{},
|
||||||
|
pkg.WordpressPluginEntry{},
|
||||||
pkg.YarnLockEntry{},
|
pkg.YarnLockEntry{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,7 @@ var jsonTypes = makeJSONTypes(
|
|||||||
jsonNames(pkg.SwiftPackageManagerResolvedEntry{}, "swift-package-manager-lock-entry", "SwiftPackageManagerMetadata"),
|
jsonNames(pkg.SwiftPackageManagerResolvedEntry{}, "swift-package-manager-lock-entry", "SwiftPackageManagerMetadata"),
|
||||||
jsonNames(pkg.RustCargoLockEntry{}, "rust-cargo-lock-entry", "RustCargoPackageMetadata"),
|
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
|
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 {
|
func expandLegacyNameVariants(names ...string) []string {
|
||||||
|
|||||||
@ -145,6 +145,7 @@ func FromPackageAttributes(p pkg.Package) []cpe.CPE {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:funlen
|
||||||
func candidateVendors(p pkg.Package) []string {
|
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
|
// 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
|
// 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))
|
vendors.union(candidateVendorsForAPK(p))
|
||||||
case pkg.NpmPackage:
|
case pkg.NpmPackage:
|
||||||
vendors.union(candidateVendorsForJavascript(p))
|
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 "*"]
|
// 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))
|
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)
|
// it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value)
|
||||||
products.removeByValue("")
|
products.removeByValue("")
|
||||||
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"
|
RpmPkg Type = "rpm"
|
||||||
RustPkg Type = "rust-crate"
|
RustPkg Type = "rust-crate"
|
||||||
SwiftPkg Type = "swift"
|
SwiftPkg Type = "swift"
|
||||||
|
WordpressPluginPkg Type = "wordpress-plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AllPkgs represents all supported package types
|
// AllPkgs represents all supported package types
|
||||||
@ -73,6 +74,7 @@ var AllPkgs = []Type{
|
|||||||
RpmPkg,
|
RpmPkg,
|
||||||
RustPkg,
|
RustPkg,
|
||||||
SwiftPkg,
|
SwiftPkg,
|
||||||
|
WordpressPluginPkg,
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageURLType returns the PURL package type for the current package.
|
// PackageURLType returns the PURL package type for the current package.
|
||||||
@ -131,6 +133,8 @@ func (t Type) PackageURLType() string {
|
|||||||
return "cargo"
|
return "cargo"
|
||||||
case SwiftPkg:
|
case SwiftPkg:
|
||||||
return packageurl.TypeSwift
|
return packageurl.TypeSwift
|
||||||
|
case WordpressPluginPkg:
|
||||||
|
return "wordpress-plugin"
|
||||||
default:
|
default:
|
||||||
// TODO: should this be a "generic" purl type instead?
|
// TODO: should this be a "generic" purl type instead?
|
||||||
return ""
|
return ""
|
||||||
@ -201,6 +205,8 @@ func TypeByName(name string) Type {
|
|||||||
return Rpkg
|
return Rpkg
|
||||||
case packageurl.TypeSwift:
|
case packageurl.TypeSwift:
|
||||||
return SwiftPkg
|
return SwiftPkg
|
||||||
|
case "wordpress-plugin":
|
||||||
|
return WordpressPluginPkg
|
||||||
default:
|
default:
|
||||||
return UnknownPkg
|
return UnknownPkg
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,6 +119,7 @@ func TestTypeFromPURL(t *testing.T) {
|
|||||||
expectedTypes.Remove(string(BinaryPkg))
|
expectedTypes.Remove(string(BinaryPkg))
|
||||||
expectedTypes.Remove(string(LinuxKernelModulePkg))
|
expectedTypes.Remove(string(LinuxKernelModulePkg))
|
||||||
expectedTypes.Remove(string(GithubActionPkg), string(GithubActionWorkflowPkg))
|
expectedTypes.Remove(string(GithubActionPkg), string(GithubActionWorkflowPkg))
|
||||||
|
expectedTypes.Remove(string(WordpressPluginPkg))
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(string(test.expected), func(t *testing.T) {
|
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 (
|
const (
|
||||||
// this is the number of packages that should be found in the image-pkg-coverage fixture image
|
// this is the number of packages that should be found in the image-pkg-coverage fixture image
|
||||||
// when analyzed with the squashed scope.
|
// when analyzed with the squashed scope.
|
||||||
coverageImageSquashedPackageCount = 27
|
coverageImageSquashedPackageCount = 28
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPackagesCmdFlags(t *testing.T) {
|
func TestPackagesCmdFlags(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user