From 23f2f7938d38eea4761dbecbd094fa98176c091d Mon Sep 17 00:00:00 2001 From: Federico Loi Date: Mon, 23 Dec 2024 11:53:24 +0100 Subject: [PATCH 1/5] Implementation of Wordpress plugins extractor --- docs/supported_inventory_types.md | 4 +- .../language/wordpress/plugins/plugins.go | 193 +++++++++++++ .../wordpress/plugins/plugins_test.go | 253 ++++++++++++++++++ .../language/wordpress/plugins/testdata/empty | 0 .../wordpress/plugins/testdata/invalid | 2 + .../language/wordpress/plugins/testdata/valid | 17 ++ extractor/filesystem/list/list.go | 5 + purl/purl.go | 3 + 8 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 extractor/filesystem/language/wordpress/plugins/plugins.go create mode 100644 extractor/filesystem/language/wordpress/plugins/plugins_test.go create mode 100644 extractor/filesystem/language/wordpress/plugins/testdata/empty create mode 100644 extractor/filesystem/language/wordpress/plugins/testdata/invalid create mode 100644 extractor/filesystem/language/wordpress/plugins/testdata/valid diff --git a/docs/supported_inventory_types.md b/docs/supported_inventory_types.md index 63bc8d2f..bbd9eea4 100644 --- a/docs/supported_inventory_types.md +++ b/docs/supported_inventory_types.md @@ -52,7 +52,9 @@ SCALIBR supports extracting software package information from a variety of OS an * Lockfiles: Gemfile.lock (OSV) * Rust * Cargo.lock - +* Wordpress plugins + * Installed plugins + ## Container inventory * Containerd container images that are running on host diff --git a/extractor/filesystem/language/wordpress/plugins/plugins.go b/extractor/filesystem/language/wordpress/plugins/plugins.go new file mode 100644 index 00000000..f0d6fc56 --- /dev/null +++ b/extractor/filesystem/language/wordpress/plugins/plugins.go @@ -0,0 +1,193 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package wordpress extracts packages from installed plugins. +package plugins + +import ( + "bufio" + "context" + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/internal/units" + "github.com/google/osv-scalibr/log" + "github.com/google/osv-scalibr/plugin" + "github.com/google/osv-scalibr/purl" + "github.com/google/osv-scalibr/stats" +) + +const ( + // Name is the unique name of this extractor. + Name = "wordpress/plugins" +) + +// Config is the configuration for the Wordpress extractor. +type Config struct { + // Stats is a stats collector for reporting metrics. + Stats stats.Collector + // MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If + // `FileRequired` gets a bigger file, it will return false, + MaxFileSizeBytes int64 +} + +// DefaultConfig returns the default configuration for the Wordpress extractor. +func DefaultConfig() Config { + return Config{ + Stats: nil, + MaxFileSizeBytes: 10 * units.MiB, + } +} + +// Extractor structure for plugins files. +type Extractor struct { + stats stats.Collector + maxFileSizeBytes int64 +} + +// New returns a Wordpress extractor. +// +// For most use cases, initialize with: +// ``` +// e := New(DefaultConfig()) +// ``` +func New(cfg Config) *Extractor { + return &Extractor{ + stats: cfg.Stats, + maxFileSizeBytes: cfg.MaxFileSizeBytes, + } +} + +// Config returns the configuration of the extractor. +func (e Extractor) Config() Config { + return Config{ + Stats: e.stats, + MaxFileSizeBytes: e.maxFileSizeBytes, + } +} + +// Name of the extractor. +func (e Extractor) Name() string { return Name } + +// Version of the extractor. +func (e Extractor) Version() int { return 0 } + +// Requirements of the extractor. +func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} } + +// FileRequired returns true if the specified file matches the /wp-content/plugins/ pattern. +func (e Extractor) FileRequired(api filesystem.FileAPI) bool { + path := api.Path() + log.Error(path) + // Check if the file path is under /wp-content/plugins/ + if !strings.Contains(path, "wp-content/plugins/") || filepath.Ext(path) != ".php" { + return false + } + + fileinfo, err := api.Stat() + if err != nil { + return false + } + + if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes { + e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded) + return false + } + + e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK) + return true +} + +func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) { + if e.stats == nil { + return + } + e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{ + Path: path, + Result: result, + FileSizeBytes: fileSizeBytes, + }) +} + +// Extract parses the PHP file to extract Wordpress package. +func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) { + pkgs, err := parsePHPFile(input.Reader) + if err != nil { + return nil, err + } + + var inventories []*extractor.Inventory + for _, pkg := range pkgs { + inventories = append(inventories, &extractor.Inventory{ + Name: pkg.Name, + Version: pkg.Version, + Locations: []string{input.Path}, + }) + } + + return inventories, nil +} + +type Package struct { + Name string + Version string +} + +func parsePHPFile(r io.Reader) ([]Package, error) { + scanner := bufio.NewScanner(r) + var pkgs []Package + + var name, version string + for scanner.Scan() { + line := scanner.Text() + + if strings.Contains(line, "Plugin Name:") { + name = strings.TrimSpace(strings.Split(line, "Plugin Name:")[1]) + } + + if strings.Contains(line, "Version:") { + version = strings.TrimSpace(strings.Split(line, ":")[1]) + } + + if name != "" && version != "" { + pkgs = append(pkgs, Package{Name: name, Version: version}) + break + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to read PHP file: %w", err) + } + + return pkgs, nil +} + +// ToPURL converts an inventory created by this extractor into a PURL. +func (e Extractor) ToPURL(i *extractor.Inventory) *purl.PackageURL { + return &purl.PackageURL{ + Type: purl.TypeWordpress, + Name: i.Name, + Version: i.Version, + } +} + +// Ecosystem returns the OSV Ecosystem of the software extracted by this extractor. +func (e Extractor) Ecosystem(_ *extractor.Inventory) string { + // wordpress ecosystem does not exist in OSV + return "" +} diff --git a/extractor/filesystem/language/wordpress/plugins/plugins_test.go b/extractor/filesystem/language/wordpress/plugins/plugins_test.go new file mode 100644 index 00000000..ca4a7f99 --- /dev/null +++ b/extractor/filesystem/language/wordpress/plugins/plugins_test.go @@ -0,0 +1,253 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugins_test + +import ( + "context" + "io/fs" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/osv-scalibr/extractor" + "github.com/google/osv-scalibr/extractor/filesystem" + "github.com/google/osv-scalibr/extractor/filesystem/internal/units" + "github.com/google/osv-scalibr/extractor/filesystem/language/wordpress/plugins" + "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" + scalibrfs "github.com/google/osv-scalibr/fs" + "github.com/google/osv-scalibr/purl" + "github.com/google/osv-scalibr/stats" + "github.com/google/osv-scalibr/testing/fakefs" + "github.com/google/osv-scalibr/testing/testcollector" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + cfg plugins.Config + wantCfg plugins.Config + }{ + { + name: "default", + cfg: plugins.DefaultConfig(), + wantCfg: plugins.Config{ + MaxFileSizeBytes: 10 * units.MiB, + }, + }, + { + name: "custom", + cfg: plugins.Config{ + MaxFileSizeBytes: 10, + }, + wantCfg: plugins.Config{ + MaxFileSizeBytes: 10, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := plugins.New(tt.cfg) + if !reflect.DeepEqual(got.Config(), tt.wantCfg) { + t.Errorf("New(%+v).Config(): got %+v, want %+v", tt.cfg, got.Config(), tt.wantCfg) + } + }) + } +} + +func TestFileRequired(t *testing.T) { + tests := []struct { + name string + path string + fileSizeBytes int64 + maxFileSizeBytes int64 + wantRequired bool + wantResultMetric stats.FileRequiredResult + }{ + { + name: "wp-content/plugins/foo/test.php file", + path: "wp-content/plugins/foo/test.php", + wantRequired: true, + wantResultMetric: stats.FileRequiredResultOK, + }, + { + name: "foo/test/bar/wp-content/plugins/foo/test.php file", + path: "foo/test/bar/wp-content/plugins/foo/test.php", + wantRequired: true, + wantResultMetric: stats.FileRequiredResultOK, + }, + { + name: "file not required", + path: "test.php", + wantRequired: false, + }, + { + name: "wp-content/plugins/foo/test.php file required if file size < max file size", + path: "wp-content/plugins/foo/test.php", + fileSizeBytes: 100 * units.KiB, + maxFileSizeBytes: 1000 * units.KiB, + wantRequired: true, + wantResultMetric: stats.FileRequiredResultOK, + }, + { + name: "wp-content/plugins/foo/test.php file required if file size == max file size", + path: "wp-content/plugins/foo/test.php", + fileSizeBytes: 1000 * units.KiB, + maxFileSizeBytes: 1000 * units.KiB, + wantRequired: true, + wantResultMetric: stats.FileRequiredResultOK, + }, + { + name: "wp-content/plugins/foo/test.php file not required if file size > max file size", + path: "wp-content/plugins/foo/test.php", + fileSizeBytes: 1000 * units.KiB, + maxFileSizeBytes: 100 * units.KiB, + wantRequired: false, + wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, + }, + { + name: "wp-content/plugins/foo/test.php file required if max file size set to 0", + path: "wp-content/plugins/foo/test.php", + fileSizeBytes: 100 * units.KiB, + maxFileSizeBytes: 0, + wantRequired: true, + wantResultMetric: stats.FileRequiredResultOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + collector := testcollector.New() + var e filesystem.Extractor = plugins.New(plugins.Config{ + Stats: collector, + MaxFileSizeBytes: tt.maxFileSizeBytes, + }) + + fileSizeBytes := tt.fileSizeBytes + if fileSizeBytes == 0 { + fileSizeBytes = 1000 + } + + isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ + FileName: filepath.Base(tt.path), + FileMode: fs.ModePerm, + FileSize: fileSizeBytes, + })) + if isRequired != tt.wantRequired { + t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) + } + + gotResultMetric := collector.FileRequiredResult(tt.path) + if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric { + t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) + } + }) + } +} + +func TestExtract(t *testing.T) { + tests := []struct { + name string + path string + osrelease string + cfg plugins.Config + wantInventory []*extractor.Inventory + wantErr error + wantResultMetric stats.FileExtractedResult + }{ + { + name: "valid plugin file", + path: "testdata/valid", + wantInventory: []*extractor.Inventory{ + { + Name: "Akismet Anti-spam: Spam Protection", + Version: "5.3", + Locations: []string{"testdata/valid"}, + }, + }, + wantResultMetric: stats.FileExtractedResultSuccess, + }, + { + name: "wordpress plugin file not valid", + path: "testdata/invalid", + wantErr: cmpopts.AnyError, + wantResultMetric: stats.FileExtractedResultErrorUnknown, + }, + { + name: "wordpress plugin file empty", + path: "testdata/empty", + wantErr: cmpopts.AnyError, + wantResultMetric: stats.FileExtractedResultErrorUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + collector := testcollector.New() + var e filesystem.Extractor = plugins.New(plugins.Config{ + Stats: collector, + MaxFileSizeBytes: 100, + }) + + d := t.TempDir() + + // Opening and Reading the Test File + r, err := os.Open(tt.path) + defer func() { + if err = r.Close(); err != nil { + t.Errorf("Close(): %v", err) + } + }() + if err != nil { + t.Fatal(err) + } + + info, err := os.Stat(tt.path) + if err != nil { + t.Fatalf("Failed to stat test file: %v", err) + } + + input := &filesystem.ScanInput{ + FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info, + } + + got, err := e.Extract(context.Background(), input) + + if diff := cmp.Diff(tt.wantInventory, got); diff != "" { + t.Errorf("Inventory mismatch (-want +got):\n%s", diff) + } + }) + } +} +func TestToPURL(t *testing.T) { + e := plugins.Extractor{} + i := &extractor.Inventory{ + Name: "Name", + Version: "1.2.3", + Locations: []string{"location"}, + } + want := &purl.PackageURL{ + Type: purl.TypeWordpress, + Name: "Name", + Version: "1.2.3", + } + got := e.ToPURL(i) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("ToPURL(%v) (-want +got):\n%s", i, diff) + } +} diff --git a/extractor/filesystem/language/wordpress/plugins/testdata/empty b/extractor/filesystem/language/wordpress/plugins/testdata/empty new file mode 100644 index 00000000..e69de29b diff --git a/extractor/filesystem/language/wordpress/plugins/testdata/invalid b/extractor/filesystem/language/wordpress/plugins/testdata/invalid new file mode 100644 index 00000000..347ba93e --- /dev/null +++ b/extractor/filesystem/language/wordpress/plugins/testdata/invalid @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/extractor/filesystem/language/wordpress/plugins/testdata/valid b/extractor/filesystem/language/wordpress/plugins/testdata/valid new file mode 100644 index 00000000..f70330b4 --- /dev/null +++ b/extractor/filesystem/language/wordpress/plugins/testdata/valid @@ -0,0 +1,17 @@ +protect your blog from spam. 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 ... \ No newline at end of file diff --git a/extractor/filesystem/list/list.go b/extractor/filesystem/list/list.go index f5933557..9a70819e 100644 --- a/extractor/filesystem/list/list.go +++ b/extractor/filesystem/list/list.go @@ -51,6 +51,7 @@ import ( "github.com/google/osv-scalibr/extractor/filesystem/language/ruby/gemfilelock" "github.com/google/osv-scalibr/extractor/filesystem/language/ruby/gemspec" "github.com/google/osv-scalibr/extractor/filesystem/language/rust/cargolock" + "github.com/google/osv-scalibr/extractor/filesystem/language/wordpress/plugins" "github.com/google/osv-scalibr/extractor/filesystem/os/apk" "github.com/google/osv-scalibr/extractor/filesystem/os/cos" "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg" @@ -117,6 +118,8 @@ var ( PHP []filesystem.Extractor = []filesystem.Extractor{&composerlock.Extractor{}} // Containers extractors. Containers []filesystem.Extractor = []filesystem.Extractor{containerd.New(containerd.DefaultConfig())} + // Wordpress extractors. + Wordpress []filesystem.Extractor = []filesystem.Extractor{plugins.New(plugins.DefaultConfig())} // OS extractors. OS []filesystem.Extractor = []filesystem.Extractor{ @@ -148,6 +151,7 @@ var ( Ruby, Rust, Dotnet, + Wordpress, SBOM, OS, Containers, @@ -167,6 +171,7 @@ var ( "dotnet": Dotnet, "php": PHP, "rust": Rust, + "wordpress": Wordpress, "sbom": SBOM, "os": OS, diff --git a/purl/purl.go b/purl/purl.go index b9fac335..d08c872a 100644 --- a/purl/purl.go +++ b/purl/purl.go @@ -93,6 +93,8 @@ const ( TypeSwift = "swift" // TypeGooget is pkg:googet purl TypeGooget = "googet" + // TypeWordpress is pkg:wordpress purl + TypeWordpress = "wordpress" ) // PackageURL is the struct representation of the parts that make a package url. @@ -184,6 +186,7 @@ func validType(t string) bool { TypeRPM: true, TypeSwift: true, TypeGooget: true, + TypeWordpress: true, } // purl type is case-insensitive, canonical form is lower-case From 9f6354ef3e52d0ffa407a44c8e3fa1d2eca3ac1f Mon Sep 17 00:00:00 2001 From: Federico Loi Date: Wed, 8 Jan 2025 09:42:51 +0100 Subject: [PATCH 2/5] Fix issues --- extractor/filesystem/language/wordpress/plugins/plugins.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/extractor/filesystem/language/wordpress/plugins/plugins.go b/extractor/filesystem/language/wordpress/plugins/plugins.go index f0d6fc56..dc9b2256 100644 --- a/extractor/filesystem/language/wordpress/plugins/plugins.go +++ b/extractor/filesystem/language/wordpress/plugins/plugins.go @@ -20,13 +20,11 @@ import ( "context" "fmt" "io" - "path/filepath" "strings" "github.com/google/osv-scalibr/extractor" "github.com/google/osv-scalibr/extractor/filesystem" "github.com/google/osv-scalibr/extractor/filesystem/internal/units" - "github.com/google/osv-scalibr/log" "github.com/google/osv-scalibr/plugin" "github.com/google/osv-scalibr/purl" "github.com/google/osv-scalibr/stats" @@ -93,9 +91,7 @@ func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabili // FileRequired returns true if the specified file matches the /wp-content/plugins/ pattern. func (e Extractor) FileRequired(api filesystem.FileAPI) bool { path := api.Path() - log.Error(path) - // Check if the file path is under /wp-content/plugins/ - if !strings.Contains(path, "wp-content/plugins/") || filepath.Ext(path) != ".php" { + if !strings.Contains(path, "wp-content/plugins/") || !strings.HasSuffix(path, ".php") { return false } From b3f80facc5d342424d4d392ad6e4e8a6563baacd Mon Sep 17 00:00:00 2001 From: Federico Loi Date: Mon, 20 Jan 2025 11:03:44 +0100 Subject: [PATCH 3/5] fix tests --- .../wordpress/plugins/plugins_test.go | 76 +++++++------------ 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/extractor/filesystem/language/wordpress/plugins/plugins_test.go b/extractor/filesystem/language/wordpress/plugins/plugins_test.go index ca4a7f99..b84f0dba 100644 --- a/extractor/filesystem/language/wordpress/plugins/plugins_test.go +++ b/extractor/filesystem/language/wordpress/plugins/plugins_test.go @@ -17,9 +17,7 @@ package plugins_test import ( "context" "io/fs" - "os" "path/filepath" - "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -29,9 +27,9 @@ import ( "github.com/google/osv-scalibr/extractor/filesystem/internal/units" "github.com/google/osv-scalibr/extractor/filesystem/language/wordpress/plugins" "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" - scalibrfs "github.com/google/osv-scalibr/fs" "github.com/google/osv-scalibr/purl" "github.com/google/osv-scalibr/stats" + "github.com/google/osv-scalibr/testing/extracttest" "github.com/google/osv-scalibr/testing/fakefs" "github.com/google/osv-scalibr/testing/testcollector" ) @@ -63,8 +61,8 @@ func TestNew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := plugins.New(tt.cfg) - if !reflect.DeepEqual(got.Config(), tt.wantCfg) { - t.Errorf("New(%+v).Config(): got %+v, want %+v", tt.cfg, got.Config(), tt.wantCfg) + if diff := cmp.Diff(tt.wantCfg, got.Config()); diff != "" { + t.Errorf("New(%+v).Config(): (-want +got):\n%s", tt.cfg, diff) } }) } @@ -161,79 +159,59 @@ func TestFileRequired(t *testing.T) { } func TestExtract(t *testing.T) { - tests := []struct { - name string - path string - osrelease string - cfg plugins.Config - wantInventory []*extractor.Inventory - wantErr error - wantResultMetric stats.FileExtractedResult - }{ + tests := []extracttest.TestTableEntry{ { - name: "valid plugin file", - path: "testdata/valid", - wantInventory: []*extractor.Inventory{ + Name: "valid plugin file", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/valid", + }, + WantInventory: []*extractor.Inventory{ { Name: "Akismet Anti-spam: Spam Protection", Version: "5.3", Locations: []string{"testdata/valid"}, }, }, - wantResultMetric: stats.FileExtractedResultSuccess, }, { - name: "wordpress plugin file not valid", - path: "testdata/invalid", - wantErr: cmpopts.AnyError, - wantResultMetric: stats.FileExtractedResultErrorUnknown, + Name: "wordpress plugin file not valid", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/invalid", + }, }, { - name: "wordpress plugin file empty", - path: "testdata/empty", - wantErr: cmpopts.AnyError, - wantResultMetric: stats.FileExtractedResultErrorUnknown, + Name: "wordpress plugin file empty", + InputConfig: extracttest.ScanInputMockConfig{ + Path: "testdata/empty", + }, }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(tt.Name, func(t *testing.T) { collector := testcollector.New() var e filesystem.Extractor = plugins.New(plugins.Config{ Stats: collector, MaxFileSizeBytes: 100, }) - d := t.TempDir() - - // Opening and Reading the Test File - r, err := os.Open(tt.path) - defer func() { - if err = r.Close(); err != nil { - t.Errorf("Close(): %v", err) - } - }() - if err != nil { - t.Fatal(err) - } + scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) + defer extracttest.CloseTestScanInput(t, scanInput) - info, err := os.Stat(tt.path) - if err != nil { - t.Fatalf("Failed to stat test file: %v", err) - } + got, err := e.Extract(context.Background(), &scanInput) - input := &filesystem.ScanInput{ - FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info, + if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { + t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) + return } - got, err := e.Extract(context.Background(), input) - - if diff := cmp.Diff(tt.wantInventory, got); diff != "" { - t.Errorf("Inventory mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(tt.WantInventory, got, cmpopts.SortSlices(extracttest.InventoryCmpLess)); diff != "" { + t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", e.Name(), tt.InputConfig.Path, diff) } }) } } + func TestToPURL(t *testing.T) { e := plugins.Extractor{} i := &extractor.Inventory{ From 981e8398c8d95c526be9d5328fd02ebf0c544c9a Mon Sep 17 00:00:00 2001 From: Federico Loi Date: Mon, 20 Jan 2025 13:55:46 +0100 Subject: [PATCH 4/5] making lint happy --- purl/purl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/purl/purl.go b/purl/purl.go index da4994b8..2a3cb75f 100644 --- a/purl/purl.go +++ b/purl/purl.go @@ -201,7 +201,7 @@ func validType(t string) bool { TypeRPM: true, TypeSwift: true, TypeGooget: true, - TypeWordpress: true, + TypeWordpress: true, } // purl type is case-insensitive, canonical form is lower-case From d97e32b36533ec7086e5dfa188bfc77e652a8a21 Mon Sep 17 00:00:00 2001 From: Federico Loi Date: Wed, 5 Feb 2025 17:24:09 +0100 Subject: [PATCH 5/5] add fixes --- docs/supported_inventory_types.md | 11 ++++-- .../language/wordpress/plugins/plugins.go | 37 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/docs/supported_inventory_types.md b/docs/supported_inventory_types.md index 6e52e1f6..fe13d7e3 100644 --- a/docs/supported_inventory_types.md +++ b/docs/supported_inventory_types.md @@ -51,7 +51,9 @@ SCALIBR supports extracting software package information from a variety of OS an * Lockfiles: pom.xml, gradle.lockfile, verification-metadata.xml * Javascript * Installed NPM packages (package.json) - * Lockfiles: package-lock.json, yarn.lock, pnpm-lock.yaml + * Lockfiles: package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock +* ObjectiveC + * Podfile.lock * PHP: * Composer * Python @@ -65,11 +67,12 @@ SCALIBR supports extracting software package information from a variety of OS an * Lockfiles: Gemfile.lock (OSV) * Rust * Cargo.lock -* Wordpress plugins - * Installed plugins + * Rust binaries * Swift * Podfile.lock * Package.resolved +* Wordpress plugins + * Installed plugins ## Container inventory @@ -80,4 +83,4 @@ SCALIBR supports extracting software package information from a variety of OS an * SPDX SBOM descriptors * CycloneDX SBOM descriptors -If you're a SCALIBR user and are interested in having it support new inventory types we're happy to accept contributions. See the docs on [how to add a new Extractor](/docs/new_extractor.md). +If you're a SCALIBR user and are interested in having it support new inventory types we're happy to accept contributions. See the docs on [how to add a new Extractor](/docs/new_extractor.md). \ No newline at end of file diff --git a/extractor/filesystem/language/wordpress/plugins/plugins.go b/extractor/filesystem/language/wordpress/plugins/plugins.go index dc9b2256..2a73e9d2 100644 --- a/extractor/filesystem/language/wordpress/plugins/plugins.go +++ b/extractor/filesystem/language/wordpress/plugins/plugins.go @@ -91,7 +91,7 @@ func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabili // FileRequired returns true if the specified file matches the /wp-content/plugins/ pattern. func (e Extractor) FileRequired(api filesystem.FileAPI) bool { path := api.Path() - if !strings.Contains(path, "wp-content/plugins/") || !strings.HasSuffix(path, ".php") { + if !strings.HasSuffix(path, ".php") || !strings.Contains(path, "wp-content/plugins/") { return false } @@ -122,21 +122,26 @@ func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result s // Extract parses the PHP file to extract Wordpress package. func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) { - pkgs, err := parsePHPFile(input.Reader) + if input == nil || input.Reader == nil { + return nil, fmt.Errorf("invalid input: nil reader") + } + + pkg, err := parsePHPFile(input.Reader) if err != nil { return nil, err } - var inventories []*extractor.Inventory - for _, pkg := range pkgs { - inventories = append(inventories, &extractor.Inventory{ - Name: pkg.Name, - Version: pkg.Version, - Locations: []string{input.Path}, - }) + if pkg == nil { + return nil, nil + } + + inventory := &extractor.Inventory{ + Name: pkg.Name, + Version: pkg.Version, + Locations: []string{input.Path}, } - return inventories, nil + return []*extractor.Inventory{inventory}, nil } type Package struct { @@ -144,11 +149,10 @@ type Package struct { Version string } -func parsePHPFile(r io.Reader) ([]Package, error) { +func parsePHPFile(r io.Reader) (*Package, error) { scanner := bufio.NewScanner(r) - var pkgs []Package - var name, version string + for scanner.Scan() { line := scanner.Text() @@ -161,7 +165,6 @@ func parsePHPFile(r io.Reader) ([]Package, error) { } if name != "" && version != "" { - pkgs = append(pkgs, Package{Name: name, Version: version}) break } } @@ -170,7 +173,11 @@ func parsePHPFile(r io.Reader) ([]Package, error) { return nil, fmt.Errorf("failed to read PHP file: %w", err) } - return pkgs, nil + if name == "" || version == "" { + return nil, nil + } + + return &Package{Name: name, Version: version}, nil } // ToPURL converts an inventory created by this extractor into a PURL.