github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/misc/wordpress/plugins/plugins.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package plugins extracts packages from installed Wordpress plugins.
    16  package plugins
    17  
    18  import (
    19  	"bufio"
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"strings"
    24  
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    28  	"github.com/google/osv-scalibr/inventory"
    29  	"github.com/google/osv-scalibr/plugin"
    30  	"github.com/google/osv-scalibr/purl"
    31  	"github.com/google/osv-scalibr/stats"
    32  )
    33  
    34  const (
    35  	// Name is the unique name of this extractor.
    36  	Name = "wordpress/plugins"
    37  )
    38  
    39  // Config is the configuration for the Wordpress extractor.
    40  type Config struct {
    41  	// Stats is a stats collector for reporting metrics.
    42  	Stats stats.Collector
    43  	// MaxFileSizeBytes is the maximum file size this extractor will unmarshal. If
    44  	// `FileRequired` gets a bigger file, it will return false,
    45  	MaxFileSizeBytes int64
    46  }
    47  
    48  // DefaultConfig returns the default configuration for the Wordpress extractor.
    49  func DefaultConfig() Config {
    50  	return Config{
    51  		Stats:            nil,
    52  		MaxFileSizeBytes: 10 * units.MiB,
    53  	}
    54  }
    55  
    56  // Extractor structure for plugins files.
    57  type Extractor struct {
    58  	stats            stats.Collector
    59  	maxFileSizeBytes int64
    60  }
    61  
    62  // New returns a Wordpress extractor.
    63  //
    64  // For most use cases, initialize with:
    65  // ```
    66  // e := New(DefaultConfig())
    67  // ```
    68  func New(cfg Config) *Extractor {
    69  	return &Extractor{
    70  		stats:            cfg.Stats,
    71  		maxFileSizeBytes: cfg.MaxFileSizeBytes,
    72  	}
    73  }
    74  
    75  // NewDefault returns an extractor with the default config settings.
    76  func NewDefault() filesystem.Extractor {
    77  	return New(DefaultConfig())
    78  }
    79  
    80  // Config returns the configuration of the extractor.
    81  func (e Extractor) Config() Config {
    82  	return Config{
    83  		Stats:            e.stats,
    84  		MaxFileSizeBytes: e.maxFileSizeBytes,
    85  	}
    86  }
    87  
    88  // Name of the extractor.
    89  func (e Extractor) Name() string { return Name }
    90  
    91  // Version of the extractor.
    92  func (e Extractor) Version() int { return 0 }
    93  
    94  // Requirements of the extractor.
    95  func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
    96  
    97  // FileRequired returns true if the specified file matches the /wp-content/plugins/ pattern.
    98  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
    99  	path := api.Path()
   100  	if !strings.HasSuffix(path, ".php") || !strings.Contains(path, "wp-content/plugins/") {
   101  		return false
   102  	}
   103  
   104  	fileinfo, err := api.Stat()
   105  	if err != nil {
   106  		return false
   107  	}
   108  
   109  	if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes {
   110  		e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded)
   111  		return false
   112  	}
   113  
   114  	e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK)
   115  	return true
   116  }
   117  
   118  func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) {
   119  	if e.stats == nil {
   120  		return
   121  	}
   122  	e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{
   123  		Path:          path,
   124  		Result:        result,
   125  		FileSizeBytes: fileSizeBytes,
   126  	})
   127  }
   128  
   129  // Extract parses the PHP file to extract Wordpress package.
   130  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
   131  	pkg, err := parsePHPFile(input.Reader)
   132  	if err != nil {
   133  		return inventory.Inventory{}, err
   134  	}
   135  
   136  	if pkg == nil {
   137  		return inventory.Inventory{}, nil
   138  	}
   139  
   140  	return inventory.Inventory{Packages: []*extractor.Package{&extractor.Package{
   141  		Name:      pkg.Name,
   142  		Version:   pkg.Version,
   143  		PURLType:  purl.TypeWordpress,
   144  		Locations: []string{input.Path},
   145  	}}}, nil
   146  }
   147  
   148  type wpPackage struct {
   149  	Name    string
   150  	Version string
   151  }
   152  
   153  func parsePHPFile(r io.Reader) (*wpPackage, error) {
   154  	scanner := bufio.NewScanner(r)
   155  	var name, version string
   156  
   157  	for scanner.Scan() {
   158  		line := scanner.Text()
   159  
   160  		if strings.Contains(line, "Plugin Name:") {
   161  			name = strings.TrimSpace(strings.Split(line, "Plugin Name:")[1])
   162  		}
   163  
   164  		if strings.Contains(line, "Version:") {
   165  			version = strings.TrimSpace(strings.Split(line, ":")[1])
   166  		}
   167  
   168  		if name != "" && version != "" {
   169  			break
   170  		}
   171  	}
   172  
   173  	if err := scanner.Err(); err != nil {
   174  		return nil, fmt.Errorf("failed to read PHP file: %w", err)
   175  	}
   176  
   177  	if name == "" || version == "" {
   178  		return nil, nil
   179  	}
   180  
   181  	return &wpPackage{Name: name, Version: version}, nil
   182  }